Skip to content

Latest commit

 

History

History
1034 lines (791 loc) · 20.3 KB

04.classes.md

File metadata and controls

1034 lines (791 loc) · 20.3 KB

Структуры и классы

Информация о пользователе:

  1. Имя
  2. email
std::string name;
std::string email;
Агрегируем данные

Для упрощения программы, логически связанные данные можно объединить.

struct User
{
    std::string name;
    std::string email;
};

const User user = 
    { "Bob", "[email protected]" };
    
std::cout << user.name;

name, email - поля структуры

Много пользователей (array of structs)
User users[N];
Много пользователей (struct of arrays)
struct Users
{
    std::string name[N];
    std::string email[N];
};

Модификаторы доступа

struct A
{
public:
    int x; // Доступно всем
protected:
    int y; // Наследникам и объектам класса
private:
    int z; // Только объектам класса
};

A a;
a.x = 1; // ок
a.y = 1; // ошибка
a.z = 1; // ошибка

Объект - сущность в адресном пространстве компьютера, появляющаяся при создании класса.

struct vs class

В С++ struct от class отличаются только модификатором доступа по умолчанию. По умолчанию содержимое struct доступно извне (public), а содержимое class - нет (private).

class A
{
    int x; // private
};

struct B
{
    int x; // public
}

Методы класса

struct User
{
    void serialize(Stream& out)
    {
        out.write(name);
        out.write(email);
    }

private:
    std::string name;
    std::string email;
};

serialize - метод класса

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

Классы в С

struct File
{
    int descriptor;
    char buffer[BufferSize];
};

File* openFile(const char* fileName)
{
    File* file = (File*) malloc(sizeof(File));
    file->descriptor = open(fileName, O_CREAT);
    return file;
}

void write(File* file, const char* data, size_t size)
{
    ...
}

void close(File* file)
{
    close(file->descriptor);
    free(file);
}

File* file = openFile("some_file.dat");
write(file, data, size);
close(file);
class File
{
public:
    File(const char* fileName)
    {
        descriptor = open(fileName, O_CREAT);
    }
    
    void write(const char* data, size_t size)
    {
        ...
    }
    
    ~File()
    {
        close(descriptor);
    }
    
private:
    int descriptor;
    char buffer[BufferSize];
};

File file("some_file.dat");
file.write(data, size);

Декорирование методов класса

struct A
{
    void foo(); // _ZN1A3fooEv
};

void bar(); // _Z3barv

Указатель на экземпляр класса

void write([File* this], const char* data, size_t size)
{
    this->descriptor ...
}

Метод класса - обычная функция, которая неявно получает указатель на объект класса (this)

struct A
{
    void foo() { std::cout << "ok"; }
    void bar() { std::cout << x; }
    
    int x;
};

A* a = nullptr;
a->foo(); // Ок
a->bar(); // Разыменование нулевого указателя
void foo([A* this])
{
    std::cout << "ok";
}

void bar([A* this])
{
    std::cout << [this->]x;
}

Конструктор (ctor)

Служит для инициализации объекта.

Если конструктор не написан явно, С++ гарантирует, что будет создан конструктор по умолчанию.

struct A
{
    A() {}
};
Конструктор вызывается автоматически при создании объекта
// Выделение памяти в куче + вызов конструктора
A* x = new A();

// Выделение памяти на стеке + вызов конструктора
A y;

Деструктор (dtor)

Если деструктор не написан явно, С++ гарантирует, что будет создан деструктор по умолчанию.

struct A
{
    ~A() {}
};

Служит для деинициализации объекта, гарантированно вызыватся при удалении объекта.

{
    A* x = new A();
    A y;
} // Выход из области видимости: 
  // вызов деструктора + освобождение 
  // памяти на стеке
  // Для х это означает, что
  // будет освобождена только память
  // занятая указателем, но та,
  // на которую он указывает
{
    A* x = new A();
    A y;
    delete x;
}

RAII (Resource Acquire Is Initialization)

Захват ресурса есть инициализация.

В конструкторе объект получает доступ к какому либо ресурсу (например, открывается файл), а при вызове деструктура этот ресурс освобождается (закрывается файл).

class File
{
public:
    File(const char* fileName)
    {
        descriptor = open(fileName, O_CREAT);
    }

    ~File()
    {
        close(descriptor);
    }
};

Можно использовать не только для управления ресурсами

struct Profiler
{
    Profiler() { // получаем текущее время }
    ~Profiler() { // сохраняем время между
        // выховами конструктора и деструктора }
};

void someFunction()
{
    Profiler p;
    if (...) return;
    ...
    if (...) return;
    ...
}

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

struct A
{
    int x;
};

A a;
a.x = 3; // Ок

const A b;
b.x = 3; // Ошибка, константный
         // объект нельзя изменять

const A* c = &a;
c->x = 3; // Ошибка, константный
         // объект нельзя изменять

Любые методы кроме конструктора и деструктора могут быть константными.

class User
{
    using Year = uint32_t;
    Year age;
public:
    void setAge(Year value)
    {
        age = value;
    }

    bool canBuyAlcohol() const
    {
        return age >= 21;
    }
};

class UserDb
{
public:
    const User* getReadOnlyUser(
        const std::string& name) const
    {
        return db.find(name);
    }
};

const User* user = userDb.getReadOnlyUser("Bob");
user->setAge(21); // Ошибка
if (user->canBuyAlcohol()) // Ок
void User_setAge([User* const this], Year value)
{
    [this->]age = value;
}

bool User_canBuyAlcohol([const User* const this]) const
{
    return [this->]age >= 21;
}

mutable

class Log
{
    void write(const std::string& text);
};

class UserDb
{
    mutable Log log;
public:
    const User& getReadOnlyUser(
        const std::string& name) const
    {
        log.write("...");
        return db.find(name);
    }
};
const User& UserDb_getReadOnlyUser(
    [const UserDb* const this],
    const std::string& name) const
{
    [this->]log.write("...");
    // Вызываем Log_write с const Log* const
}

void Log_write([Log* const this], const std::string& text)
{
    ...
}

Наследование

Возможность порождать класс на основе другого с сохранением всех свойств класса-предка.

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

class Shape
{
protected:
    int x;
    int y;
};

class Circle
    : public Shape
{
    int radius;
};

Наследование моделирует отношение «является».

Требуется для создания иерархичности – свойства реального мира.

Представление в памяти при наследовании

Инструменты для исследования

  • sizeof(T) - размер типа в байтах
  • offsetof(T, M) - смещение поля M от начала типа T
struct A
{
    double x;
};

struct B
    : public A
{
    double y;
};

struct C
    : public B
{
    double z;
};

std::cout << sizeof(A) << std::endl; // 8
std::cout << sizeof(B) << std::endl; // 16
std::cout << sizeof(C) << std::endl; // 24

std::cout << offsetof(C, x) << std::endl; // 0
std::cout << offsetof(C, y) << std::endl; // 8
std::cout << offsetof(C, z) << std::endl; // 16
Поле Смещение Доступность в типах
x 0 A, B, C
y 8 B, C
z 16 C
C* c = new C();
c->x; // Ок
c->y; // Ок
c->z; // Ок

B* b = (B*) c;
b->x; // Ок
b->y; // Ок
b->z; // Ошибка компиляции

A* a = (A*) c;
a->x; // Ок
a->y; // Ошибка компиляции
a->z; // Ошибка компиляции

Приведение вверх и вниз по иерархии

Приведение вверх (к базовому классу) всегда безопасно.
void foo(A& a) {}

C c;
foo(c);
Приведение вниз может быть опасным
struct A {};
struct B : public A {};
struct C : public A {};

B* b = new B();
A* a = b;
C* c = a; // Ошибка компиляции
C* c = static_cast<C*>(b); // Ошибка компиляции
C* c = static_cast<C*>(a); // !!!

Сохраняйте тип, пусть компилятор помогает писать корректный код!

Общий базовый тип - плохая идея

Композиция

class Car
{
    Engine engine;
    Wheels wheels[4];
};

Композиция моделирует отношение «содержит/является частью»

Агрегация

class Car
{
    Driver* driver_;
};

При агрегации класс не контролирует время жизни своей части.

Унифицированный язык моделирования (Unified Modeling Language, UML)

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

UML редактор: https://www.draw.io/

Диаграмма классов (Class diagram)

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

Классы

Видимость:

+ Публичный метод (public)
# Защищенный метод (protected)
- Приватный метод (private)

Ассоциация

Показывает, что объекты связаны, бывает однонаправленной и двунаправленной.

Композиция

Моделирует отношение «содержит/является частью».

При композиции класс явно контролирует время жизни своей составной части.

Агрегация

Моделирует отношение «содержит/является частью».

При агрегации класс не контролирует время жизни своей части.

Наследование

Моделирует отношение «является».

Конструирование объекта

Порядок конструирования:

  1. Выделяется память под объект
  2. Если есть базовые классы, то конструирование начинается с них в порядке их очередности в списке наследования
  3. Инициализируются поля класса в том порядке, в котором они объявлены в классе
  4. Происходит вызов конструктора
class A
{
public:
    A() {} // 3
    ~A() {}

private:
    int x; // 1
    int y; // 2
};

class B
    : public A
{
public:
    B() {} // 5
    ~B() {}

private:
    int z; // 4
};

Порядок уничтожения:

  1. Происходит вызов деструктора
  2. Вызываются деструкторы для полей класса в обратном порядке их объявления в классе
  3. Уничтожаются базовые классы в порядке обратном списку наследования
class A
{
public:
    A() {}
    ~A() {} // 3

private:
    int x; // 5
    int y; // 4
};

class B
    : public A
{
public:
    B() {}
    ~B() {} // 1

private:
    int z; // 2
};

Списки инициализации

class A
{
    A()
        : x(5)
        , y(6)
    {
        z = 7;
    }
    
    int x;
    int y;
    int z;
};

Распространенная ошибка:

class A
{
    A()
        : y(5) // Инициализация в порядке объявления в классе!
        , x(y)
    {
    }
    
    int x;
    int y;
};

Инициализация в объявлении

class A
{
    int x = 3;
};

Выравнивание полей

В целях повышения быстродействия данные в памяти должны быть выровнены, то есть размещены определенным образом.

Предпочтительное выравнивание можно узнать:

std::cout << alignof(char) << std::endl; // 1
std::cout << alignof(double) << std::endl; // 8
Гранулярность памяти

Инструменты для исследования

  • sizeof(T) - размер типа в байтах
  • offsetof(T, M) - смещение поля M от начала типа T
struct S
{
    char m1;
    double m2;
};
sizeof(char) == 1
sizeof(double) == 8
sizeof(S) == 16
offsetof(S, m1) == 0
offsetof(S, m2) == 8
[         char         ][        double        ]
[c][.][.][.][.][.][.][.][d][d][d][d][d][d][d][d]

Выравниванием можно управлять:

#pragma pack(push, 1)
class S
{
public:
    char m1;
    double m2;
};
#pragma pack(pop)

offsetof(S, m1) == 0
offsetof(S, m2) == 1
sizeof(S) == 9

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

Оптимизация размера POD структур

struct POD
{
    int x;
    double y;
    int z;
};

std::cout << sizeof(POD) << std::endl; // 24
struct POD
{
    double y;
    int x;
    int z;
};

std::cout << sizeof(POD) << std::endl; // 16

Предсказуемое размещение в памяти

Порядок размещения полей класса/структуры в памяти в порядке объявления гарантирован только для простых типов (POD).

Простые типы (POD, Plain Old Data)

  1. Скалярные типы (bool, числа, указатели, перечисления (enum), nullptr_t)
  2. class или struct которые:
    • Имеют только тривиальные (сгенерированные компилятором) конструктор, деструктор, конструктор копирования
    • Нет виртуальных функций и базового класса
    • Все нестатические поля с модификатором доступа public
    • Не содержит статических полей не POD типа

Примеры

class NotPOD
{
public:
    NotPOD(int x)
    {
    }
};
class NotPOD
    : public Base
{
};
class NotPOD
{
    virtual void f()
    {
    }
};
class NotPOD
{
    int x;
};
class POD
{
public:
    NotPOD m1;
    int m2;
    static double m3;
private:
    void f() {}
};

Копирование простого типа - memcpy

Простые типы можно использовать для передачи из программы в программу, записи на диск и т.д. Но только на одной и той же платформе!

struct POD
{
    int x;
    double y;
    
    void serialize(File& file) const
    {
        file.write(this, sizeof(POD));
    }
};

Инициализация POD типов

struct POD
{
    int x;
    double y;
};

Инициализация нулем (zero-initialization):

POD p1 = POD();
POD p2 {};
POD* p3 = new POD();

// x == 0
// y == 0

Инициализация по умолчанию (default-initialization):

POD p1;
POD* p2 = new POD;

// x, y содержат мусор

Рекомендуемое разделение на заголовочные файлы и файлы с реализацией

a.h
#pragma once

struct A
{
    void foo();
};
a.cpp
#include "a.h"

void A::foo()
{
}

Защита от повторного включения

buffer.h
class Buffer
{
    ...
};
text_processor.h
#include "buffer.h"
...
main.cpp
#include "buffer.h"
#include "text_processor.h"

В одной единице трансляции два объявления класса Buffer, компилятор не знает какое использовать.

buffer.h
#ifndef BUFFER_H
#define BUFFER_H

class Buffer
{
    ...
};

#endif

Или просто #pragma once

Циклическое включение

a.h
#include "b.h"

class A
{
    B* b;
};
b.h
#include "a.h"

class B
{
    A* a;
};

Предварительное объявление (forward declarations)

a.h
class B;

class A
{
    B* b;
};
a.cpp
#include "b.h"
#include "a.h"

...
b.h
class A;

class B
{
    A* a;
};

Практическая часть

Написать свой аллокатор со стратегией линейного выделения памяти со следующим интерфейсом:

class LinearAllocator
{
public:
    LinearAllocator(size_t maxSize);
    char* alloc(size_t size);
    void reset();
};

Аллокатор при создании аллоцирует указанный размер, после чего при вызове alloc возвращает указатель на блок запрошенного размера или nullptr, если места недостаточно. После вызова reset аллокатор позволяет использовать свою память снова.

EOF