Программа курса← Программирование на языке C++
1Введение в язык C++
2Как выполняются программы на C++
3Структуры и классы
4Объектно-ориентированное программирование
5Дополнительные возможности C++
6Шаблоны
7Методические материалы

Структуры и методы

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

Структура – составной тип

struct Point {
    double x;
    double y;
};

Point p;              // поля не инициализированы: там мусор
Point q = {1.5, 2.5}; // инициализация списком
Point r{1.5, 2.5};    // то же самое, современный синтаксис

std::cout << q.x;     // доступ через точку

Объявление структуры не выделяет память – это описание типа. Память занимает экземпляр.

Структура – полноценный тип: её можно передавать в функции, возвращать, класть в массив, делать полем другой структуры.

struct Segment {
    Point from;
    Point to;
};

Segment s{{0, 0}, {3, 4}};
std::cout << s.to.x;   // 3

Размер и выравнивание

sizeof структуры – не всегда сумма размеров полей:

struct Bad  { char a; int b; char c; };   // обычно 12 байт
struct Good { int b; char a; char c; };   // обычно 8 байт

Процессор читает данные эффективнее, когда адрес кратен размеру типа, поэтому компилятор вставляет между полями выравнивающие пропуски (padding). Порядок полей на это влияет: если сложить мелкие поля рядом, дырок будет меньше.

В прикладном коде за этим не гоняются – читаемость важнее восьми байт. Но в структуре, которой создаются миллионы экземпляров, или в той, что описывает формат данных на диске или в сети, порядок полей становится вопросом по существу. Во втором случае к нему добавляется ещё и порядок байт, и тогда структуру не отображают на буфер напрямую, а разбирают полями явно.

Указатель на структуру

Point p{1, 2};
Point* ptr = &p;

std::cout << (*ptr).x;   // работает, но скобки обязательны и мешают
std::cout << ptr->x;     // то же самое, читаемо

-> – сокращение для (*p).. Правило простое: объект и ссылка – точка, указатель – стрелка.

Передача в функцию подчиняется тем же правилам, что и для любого другого типа:

void ByValue(Point p);        // копия всей структуры
void ByRef(Point& p);         // оригинал, можно менять
void ByConstRef(const Point& p);  // оригинал, менять нельзя
void ByPtr(Point* p);         // может быть nullptr

Массив-поле копируется вместе со структурой, хотя отдельно взятый массив в функцию копироваться не стал бы. А вот поле-указатель копируется как адрес: у копии и оригинала он будет один, и данные окажутся общими. Это поверхностное копирование – источник половины ошибок в классах, управляющих памятью. К нему мы вернёмся, когда дойдём до конструктора копирования.

Методы

Функция, работающая только с одной структурой, логично живёт внутри неё:

struct Point {
    double x;
    double y;

    double Length() const {
        return std::sqrt(x * x + y * y);
    }

    void Move(double dx, double dy) {
        x += dx;
        y += dy;
    }
};

Point p{3, 4};
std::cout << p.Length();   // 5
p.Move(1, 1);

Внутри метода поля доступны просто по имени. Никакой магии здесь нет: метод – это обычная функция, которой компилятор добавляет скрытый первый параметр – указатель на объект, у которого её вызвали.

this

Этот скрытый параметр называется this:

struct Point {
    double x;
    double y;

    void SetX(double x) {
        this->x = x;      // без this поле было бы затенено параметром
    }

    Point& Move(double dx, double dy) {
        x += dx;
        y += dy;
        return *this;     // возвращаем сам объект – можно строить цепочки
    }
};

p.Move(1, 0).Move(0, 1);

Писать this-> без необходимости не нужно – поля и так видны. Он нужен в двух случаях: когда имя параметра совпадает с именем поля и когда нужен сам объект – вернуть его или сравнить адрес.

Константные методы

Метод, помеченный const, обещает не менять объект:

double Length() const;   // не меняет
void Move(double, double);  // меняет

Обещание проверяет компилятор: попытка присвоить полю внутри const-метода – ошибка.

Практическое следствие важнее формального: у константного объекта можно вызвать только константные методы.

const Point p{3, 4};
p.Length();     // ок
p.Move(1, 1);   // ошибка компиляции

Поскольку в функции объекты почти всегда приходят как const T&, метод без const там недоступен. Поэтому правило: всё, что не меняет объект, помечайте const сразу. Дописать потом – значит пройтись по всей цепочке вызовов.

Объявление и определение

Метод можно определить прямо в структуре или вынести наружу – тогда в определении указывают, к какому типу он относится:

// point.hpp
struct Point {
    double x;
    double y;

    double Length() const;    // объявление
};

// point.cpp
double Point::Length() const {   // определение
    return std::sqrt(x * x + y * y);
}

Разделение нужно по той же причине, что и для обычных функций: заголовок описывает интерфейс и подставляется всем, кто им пользуется, а реализация компилируется один раз. Правку тела метода, определённого в заголовке, оплачивают пересборкой все, кто этот заголовок включает.

Короткие методы всё же обычно оставляют в заголовке – чтобы компилятор мог их встроить. Разница между «встроить» и «вызвать» для метода из двух строк бывает кратной.