C++ Course
  • Описание курса
  • Материалы лекций
  • Задания
  • Об авторе
  • Гайды

On this page

  • Общая модель памяти – Указатели
  • Ссылки
    • Использование ссылок в цикле
  • Простая работа с файлами
    • Текстовые файлы
    • Двоичные файлы

Ссылки, указатели, работа с файлами

Как мы уже знаем, C++ является языком низкого уровня и предоставляет прямой доступ к памяти, поэтому при разработке программ на это языке важно помнить и понимать, как используется память программы. Сложную работу с памятью (ее выделение и использование), разделение памяти по видам мы будем разбирать в следующих главах, сейчас ограничимся базовым использование ссылок и указателей.

Общая модель памяти – Указатели

Когда мы думаем о памяти, стоит представлять себе однородное пространство, разбитое на “ячейки” – байты. Байт – наименьшая адресуемая единица памяти.

Битность системы – это размер регистра процессора, который совпадает с размером адреса в памяти: 32 бита для 32-битной системы, 64 бита для 64-битной.

Адрес в памяти – некоторый “номер” байта, по которому можно найти наш массив, переменную или иной объект. На самом деле номера, которые мы будем видеть, не являются настоящими аппаратными номера – это адреса и виртуального пространства, которое операционная система уже конвертирует в реальный номер. В частности, это позволяет создавать несколько виртуальных пространств: для RAM, накопителей, устройств, датчиков.

Модель памяти компьютера. Адресация

На иллюстрации выше показан фрагмент такого пространства, адреса.

Указатель – переменная особого типа, которая хранит номер байта. Компилятор считает, что с этого байта начинает объект.

Например, int* – указатель на переменную типа int – номер на первый из 4 байт, в которых хранится целое число. Этот случай показан на рисунке выше.

  • &a – получение “адреса”, где лежит переменная a, возвращает указатель на тип переменной a. Например, int*

  • *ptr – разыменование указателя ptr, так получаем значение, которое записано в памяти по сохраненному адресу

Пример работы с указателями:

int main() {
    int x = 42;
    int* ptr = &x;  // сохраняем адрес в памяти переменной x в указатель ptr

    ++x;  // увеличим x на единицу
    std::cout << *ptr << "\n";  // 43
}

Указатель ptr в программе выше продолжает указывать на один и тот же блок в памяти. При изменении переменной ее расположение в памяти остается прежним, ее можно найти по тому же адресу

Где и почему создаются переменные, будем разбираться чуть позже, пока же отметим интересный факт:

#include <iostream>

int main() {
    int x = 1;
    int y = 2;
    int z = 3;
    std::cout << &x << "\n"; // 0x7ffdfee3188c
    std::cout << &y << "\n"; // 0x7ffdfee31888
    std::cout << &z << "\n"; // 0x7ffdfee31884
}

Во-первых, заметим, что переменные, созданные позже, обладают меньшими адресами. А во-вторых, найдем разницу между адресами:

\(\text{7ffdfee3188c}_{16} - \text{7ffdfee31888}_{16} = \text{C}_{16} - \text{8}_{16} = 12 - 8 = 4\)

Это должно совпадать с нашими ожиданиями: действительно, ведь каждый intзанимает в 4 байтах.

Следующий пример показывать работу с указателями. Им можно присваивать различные адреса переменных, все будет работать корректно.

#include <iostream>

int main() {
    int x = 42, y = 13;
    int* ptr;  // по умолчанию не инициализируется, тут лежит «случайный» адрес
    ptr = nullptr;  // «нулевой» указатель
    ptr = &x;  // теперь в ptr лежит адрес переменной x
    std::cout << *ptr << "\n";  // 42
    ptr = &y;  // можно поменять адрес, записанный в ptr
    std::cout << *ptr << "\n";  // 13
}

Здесь стоит обратить внимание на nullptr – это нулевой адрес, его нельзя разыменовать. Используется значение неопределенного указателя, который пока никуда не указывает.

Ссылки

C++ позволяет создавать “псевдонимы” для переменных. Тогда можно производить операции с двумя именами, а результат будет отражаться на одной и той же памяти:

#include <iostream>

int main() {
    int x = 42;
    int& ref = x;  // ссылка на x

    ++x;
    std::cout << ref << "\n";  // 43
    std::cout << x << "\n";  // 43

    ++ref;
    std::cout << ref << "\n";  // 44
    std::cout << x << "\n";  // 44
}

На одну и ту же память ссылается 2 объекта: переменные x и ref. Инкремент любой из них увеличит число в памяти на 1, значит, и результат этой операции можно посмотреть в обоих переменных.

В отличие от указатель, ссылки нельзя перепривязать, ведь попытка присвоить ей новую переменную приведет к изменению значения:

#include <iostream>

int main() {
    int x = 42, y = 13;
    int& ref = x;  // OK
    ref = y;  // ссылка останется привязанной к x, значение x поменяется
    std::cout << x << '\n'; // 13 -- теперь значение переменной x
}

Использование ссылок в цикле

Вот такой пример был разобран в главе про std::vector:

#include <iostream>
#include <vector>
#include <string>

int main() {
    std::vector<std::string> data = {"Just", "some", "random", "words"};
    for (std::string &word: data) {
        std::cout << word << ' ';
    }
    std::cout << '\n';
}

Здесь используется & для обозначения того, что строка wordбудет не копироваться, а браться по ссылке. Таким образом, во время каждой итерации word указывает на некоторый элемент вектора.

Это существенно более оптимально, потому что копирование является очень сложной по времени операции, его стоит избегать везде, где это возможно.

Рассмотрим UB (undefined behavior) ситуацию, которая может возникнуть при использовании ссылок – dangling reference или “висячие ссылки”. Она возникает, когда время жизни объекта уже истекло, а ссылка или указатель еще существует:

Пример с искусственным скоупом – местом жизни локальных переменных:

#include <iostream>

int main() {
    int* ptr = nullptr;

    {
        int x = 42;
        ptr = &x;
    }

    // обращаться к памяти, в которой жила переменная x, уже нельзя:
    std::cout << *ptr << "\n";  // undefined behavior
}

Аналогичную неопределенную ситуацию можно создать для ссылки:

#include <iostream>
#include <vector>

int main() {
    std::vector<std::string> words = {"one", "two", "three"};

    std::string& ref = words[0];  // псевдоним для начального элемента вектора

    words.clear(); // перестали существовать все элементы вектора

    // обращаться к ссылке ref уже нельзя!
    std::cout << ref << "\n";  // undefined behavior
}

Простая работа с файлами

Пути до файлов могут быть:

  • абсолютными – от корня файловой системы. Например, C://Users/dfbakin/Documents/Project/src/test.txt или /home/dfbakin/project/src/test.txt

  • относительным – от некоторого расположение, например, от расположения программы. Если в программе написать data/test.txt, то система будет искать в соседней с прогаммой папке data

Текстовые файлы

Рассмотрим простую работу с файлами. C++ предоставляет возможность открыть поток чтения из файла или записи в него, использование таких потоков совпадает с std::cin и std::cout

#include <iostream>
#include <fstream>

int main() {
    std::ofstream out;          // поток для записи
    out.open("hello.txt");      // открываем файл для записи
    if (out.is_open()) {
        out << "Hello World!" << std::endl;
    }
    out.close();
    std::cout << "File has been written" << std::endl;
}

Программа выше откроет файл в режиме записи, создаст его при необходимости. Стоит обратить внимание, что при возникновении ошибки при открытии файла программа продолжит исполнение, поэтому нужно проверять запись "Hello World!" на корректность – правда ли, что файл был действительно открыт.

Следующий код позволяет прочитать неограниченное количество строк из текстового файла. Это необходимо в случае, если оно неизвестно нам заранее:

#include <iostream>
#include <fstream>
#include <string>     // для std::getline

int main() {
    std::string line;

    std::ifstream in("hello.txt"); // окрываем файл для чтения
    if (in.is_open()) {
        while (std::getline(in, line)) {
            std::cout << line << std::endl;
        }
    }
    in.close();     // закрываем файл
}

Двоичные файлы

В некоторых ситуациях может возникнуть желание читать и записывать в файл конкретное количество конкретных байт.

#include <fstream>
#include <iostream>
#include <vector>
#include <iomanip>

int main() {
    std::vector<float> data = {
        0.00001, 0.00002, 0.00003, 0.00004, 0.00005, 0.00006, 0.00007, 0.00008};
    std::ofstream output_file("foo.bin", std::ios::out | std::ios::binary);
    for (int i = 0; i < data.size(); ++i) {
        char* data_ptr = reinterpret_cast<char*>(&data[i]);
        output_file.write(data_ptr, sizeof(float));
    }

    // Alternative to write the data
    // output_file.write(reinterpret_cast<char*>(data.data()), data.size() * sizeof(float));

    output_file.close();

    std::vector<float> input_data(data.size());
    std::ifstream input_file("foo.bin", std::ios::in | std::ios::binary);
    std::cout << "File data is\n";
    for (int i = 0; i < 8; i++) {
        char* data_ptr = reinterpret_cast<char*>(&input_data[i]);
        input_file.read(data_ptr, sizeof(float));
    }

    // Alternative to write the data
    // input_file.write(reinterpret_cast<char*>(input_data.data()), input_data.size() *
    // sizeof(float));

    for (int i = 0; i < input_data.size(); i++) {
        std::cout << std::fixed << input_data[i] << ", \n";
    }
    std::cout << "\nFinished reading\n";
    input_file.close();

    remove("foo.bin");
}
Denis Bakin ©

Build on Quart Academic Website Template adapted by Dr. Gang He