Skip to the content.

Структуры

Структура позволяет синтаксически сгруппировать логически связанные данные.

Создание структуры

struct Person
{
    // поля структуры
    string name;
    int age;
    double weight;
    string country = "Russia"; // значение по умолчанию C++11
};

Переменная типа Person - экземпляр структуры

Person man;

Еще один способ создания экземпляра

struct Person
{
    ...
} man;

struct Person
{
    ...
} man = {"John", 12, 29.1};

Доступ к полям структуры

Через оператор “.”

man.name = "Bjarne";
man.age = 45;
man.weight = 84.7;
man.country = "Denmark";

Используя список инициализаторов

Person women = {"Jeanne", 27, 41.6};

Uniform-инициализация C++11

Person student {"Andry", 17, 69.2};

Если в списке инициализаторов не указать поле, то ему присвоиться значение по умолчаню (если не указать при описании структуры, то - 0).

Значения по умолчанию появились в С++11, однако они не совместима с списками инициализаторов, проблема была решена в С++14.

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

void birthday(Person * p)
{
    p->age++; // аналогично: (*p).age++;
}

Методы

Одним из отличий структур в яп С и С++ является возможность определения методов.

struct Person
{
    string name;
    int age;
    double weight;
    string country = "Russia";
  
    void birthday()
    {
        ++age;
    }
};

Person man = {"John", 34, 91.4};
man.birthday();

Методы реализованы точно так же как и функции, однако в методах есть неявный параметр this - указатель на текущий экземпляр структуры.

struct Person
{
    double weight;
  
    void new_weight(/* Person * const this */
                    double weight)
    {
        this->weight += weight;
    }
};

Методы можно разделять на объявление и определение. Стоит обратить внимание, что структура автоматически создаёт одноименное пространство имён:

struct Person
{
    double weight;
  
    void new_weight(double weight); // объявление (в заголовочный файл)
};

// определение (в один из .срр)
void Person::new_weight(double weight)
{
    this->weight += weight;
}

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

Специальные типы методов: Конструкторы и Деструкторы

Конструктор - метод для инициализации структур.

struct Person
{
    int age;
    double weight;
    
    Person()
    {
        age = 0;
        weight = 0;
    }
    
    Person(int age, double weight)
    {
        this->age = age;
        this->weight = weight;
    }
}

Person man; // вызов конструктора по умолчанию (без параметров)
Person woman(28, 31.2); // вызов второго конструктора

Если для структуры не было объявлено конструкторов, то компилятор создаст конструктор по умолчанию (без параметров), если же мы описали хотя бы один конструктор, то конструктор по умолчанию создаваться уже не будет и если он нам понадобиться его нужно будет описать.

Списки инициализации - позволяют проинициализировать поля до входа в конструктор. Это предпочтительный способ инициализации полей из-за производительности: экономим пару операций присваивания, ведь в реализации выше всеравно вначале отработает список инициализации для полей, а потом в теле конструктора мы сделаем присваивания.

Стоит так же помнить что инициализация всегда происходит в порядке объявления полей (вначале age потом weight), это важно в случае зависимости между полями. Так происходит потому что деструктор должен удалять в порядке, строго противоположном, порядку конструирования, а если каждый конструктор будет в своем порядке создавать, то как быть деструктору? Для этого было решено ввести для всех конструкторов строгий порядок инициализации, не связанный с их кодом.

struct Person
{
    int age;
    double weight;
    
    Person() : weight(0), age(0) // всеравно вначале проинициализирует age потом weight
    {}
    
    Person(int age, double weight) : age(age), weight(weight)
    {}
}

Параметры функций по умолчанию

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

struct Person
{
    int age;
    double weight;
    
    Person(int age = 0, double weight = 0) : age(age), weight(weight)
    {}
}

Person man; // 0, 0
Person woman(31); // age = 31, weight = 0
Person kid(3, 15.1);

Конструкторы с одним параметром выделяются на фоне других

struct Person
{
    int age;
    Person(int age = 0) : age(age)
    {}
}

// Экземпляры могут быть объявлены двумя способами:
Person kid(5);
Person man = 35; // будет неявно преобразовано в верхнюю инструкцию

Однако если указать ключевое слово explicit:

explicit Person(int age = 0) : age(age)
{}

Экземпляры можно будет объявить только явной инициализацией:
Person kid(5);

Это относится не только к конструкторам от одного параметра, но так же к конструкторам которые могут принимать один параметр (т.е конструкторы с параметрами больше одного, однако для них указны значения по умолчанию)

Приколы синтаксиса С++

Если что-то похоже на объявление функции, то это и есть объявление функции

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

Point p1; // экземпляр
Point p2(); // объявление функции, которая возвращает тип Point и не принимает параметров

Аналогично:

double k = 5.1;
Point p3(int(k)); // объявление функции
Point p4((int)k); // экземпляр

Деструктор

Деструктор - метод, который вызывается при удалении экземпляра структуры (по умолчанию генерируется компилятором, однако можно переопределить).

struct Array
{
    // конструктор: создать динамический массив размера size
    explicit IntArray(size_t size) : size(size), data(new int[size]) 
    {}
    
    // деструктор
    ~IntArray()
    {
        delete [] data; 
    } // соответственно без переопределения деструктора будет утечка памяти
    
    // объявлять поля после методов, которые их используют можно, поскольку компилятор всеравно вначале поработает с полями, а потом с методами
    size_t size;
    int * data;
}

Время жизни - это временной интервал между вызовами конструктора и деструктора.

void func()
{
    IntArray a1(10); // Вызов конструктора для а1
    IntArray a2(15); // Вызов конструктора для а2
    
    for (int i = 0; i != 30; ++i) 
    {
        IntArray a3(30); // Вызов конструктора для а3
        ...
    } // Вызов деструктора а3
} // Вызов деструктора а2, потом а1 (в обратном порядке)

Динамическая память

Экземпляры структуры, так же как и значения встроенных типов можно создавать в динамической памяти. Важное отличие оператора new от функции malloc в том, что new вызывает конструктор, а malloc только выделяет память:

// Выделить память и создать экземпляр (вызов конструктора с значением 10)
IntArray * p1 = new IntArray(10);

// Вызов деструктора и освобождение памяти
delete [] p1; 
// Выделить память и создать массив: 10 экземпляров (полюбому будет вызов конструктора по умолчанию)
IntArray * p2 = new IntArray[10];
delete [] p2;
// Только выделить память
IntArray * p3 = (IntArray *)malloc(sizeof(IntArray));

free(p3);

Placment new

Особая форма оператора new, которая принимает адрес по которому должен быть создан объект. Такой new не выделяет никакой памяти, а просто вызывает конструктор по адресу который ему передали. Такая форма может быть полезна если у нас есть своя функция выделения памяти:

myalloc - самописная функция, которая умеет выделять память заданного размера и возвращает указатель на область памяти

myfree - самописная функция освобождения памяти

// выделение памяти по своему
void *p = myalloc(sizeof(IntArray));

// создание экземпляра по адресу p
IntArray * a = new (p) IntArray(10);

// явный вызов деструктора!
a->~IntArray();

// освобождение памяти по своему
myfree(p);

Стоит отметить, что использование placement new может вызвать проблемы с выравниванием.

Когда использовать геттеры и сеттеры


Геттеры\сеттеры + приватные поля стоит делать только если пользовательский код способен сломать состояние класса поменяв их. Например, это могут быть связанные данные: размер экрана и буфер пикселей - эти два поля категорически нельзя менять по отдельности, только вместе. А значит нужно сделать такой сеттер в котором при изменении одного из этих полей так же изменится второе. А вот расположение окна можно менять как угодно, оно просто перерисуется, значит это поле можно оставить публичным.