базовые концепции ООП — классы и инициализацияbaseline OOP concepts — classes and initialization

Определение классов, инициализацияClass definition & initialization

class и struct в C++ — практически одна и та же конструкция с разным доступом по умолчанию, а не два разных языковых понятия, как в некоторых других языках. Настоящая тонкость в другом: список инициализации в конструкторе выглядит как последовательность присваиваний, но реально исполняется в порядке объявления полей в классе — не в порядке, в котором они перечислены в списке.class and struct in C++ are practically the same construct with a different default access level, not two distinct language concepts like in some other languages. The real subtlety lies elsewhere: a constructor's init list looks like a sequence of assignments, but actually runs in the order fields are declared in the class — not the order they're listed in the init list.

частый вопрос на собеседованииcommon interview question

ОпределениеDefinition

Класс — определяемый пользователем тип, объединяющий данные (поля) и операции над ними (методы) в одну единицу. class и struct объявляют абсолютно одинаковую сущность на уровне языка; единственная разница — уровень доступа по умолчанию: private для class, public для struct. Соглашение (не требование языка) — использовать struct для простых наборов данных без инвариантов, class — когда есть инкапсуляция и логика, которую нужно защищать от прямого доступа.A class is a user-defined type that bundles data (fields) and operations on that data (methods) into one unit. class and struct declare exactly the same entity at the language level; the only difference is the default access level: private for class, public for struct. The convention (not a language requirement) is to use struct for plain data bags with no invariants, and class when there's encapsulation and logic that needs protecting from direct access.

struct Point {          // поля public по умолчанию// fields are public by default
    double x, y;
};

class BankAccount {    // поля private по умолчанию// fields are private by default
    double balance = 0;   // доступ только через методы — инвариант "баланс не уходит в минус"// access only via methods — the "balance never goes negative" invariant
public:
    void deposit(double amount) { balance += amount; }
};

Point p{1.0, 2.0};        // прямой доступ к полям — это нормально для Point// direct field access — perfectly fine for Point
// p.x — ок, но account.balance — ошибка компиляции, balance приватен// p.x is fine, but account.balance is a compile error — balance is private

Member initializer listThe member initializer list

Список инициализации после двоеточия в конструкторе — не синтаксический сахар для присваивания в теле, а единственный способ инициализировать (а не присвоить значение уже созданному объекту) поля-ссылки, поля-константы и поля без конструктора по умолчанию. Присваивание в теле конструктора для таких полей просто не скомпилируется.The colon-delimited initializer list in a constructor isn't syntactic sugar for assignment inside the body — it's the only way to initialize (rather than assign to an already-constructed object) reference fields, const fields, and fields with no default constructor. Assignment inside the constructor body for such fields simply won't compile.

class Logger {
    const std::string prefix;
    std::ostream& out;
public:
    Logger(std::string p, std::ostream& o) : prefix(std::move(p)), out(o) {}
    // Logger(std::string p, std::ostream& o) { prefix = p; out = o; } — не скомпилируется:// Logger(std::string p, std::ostream& o) { prefix = p; out = o; } — won't compile:
    // prefix — const, out — ссылка, ни то ни другое нельзя переприсвоить после создания// prefix is const, out is a reference, neither can be reassigned after construction
};

Даже для обычных полей без const/ссылок список инициализации эффективнее: без него поле сначала конструируется конструктором по умолчанию, а затем оператором присваивания получает нужное значение — два вызова вместо одного. Для типов вроде std::string/std::vector это заметная лишняя работа при частом создании объектов.Even for ordinary fields with no const/references, the init list is more efficient: without it, the field is first default-constructed and then given its real value via assignment — two calls instead of one. For types like std::string/std::vector, that's measurable extra work when objects are created frequently.

Порядок инициализации — по объявлению, а не по спискуInitialization order — by declaration, not by the list

Ключевое правило, которое ловит почти всех: поля инициализируются в порядке их объявления в теле класса, а не в порядке, в котором они перечислены в списке инициализации конструктора. Порядок в списке — просто вопрос читаемости кода, компилятор его игнорирует при генерации фактического порядка вызовов.The key rule that trips up almost everyone: fields are initialized in the order they're declared inside the class body, not the order they're listed in the constructor's init list. The list's order is purely a readability choice — the compiler ignores it when generating the actual call order.

struct Rect {
    int width;
    int height;      // объявлен после width — инициализируется после width, всегда// declared after width — initialized after width, always

    Rect(int w) : height(w * 2), width(w) {}
    // список говорит "height, потом width" — компилятор всё равно// the list says "height, then width" — the compiler still
    // инициализирует width первым (он объявлен первым), height — вторым// initializes width first (declared first), height second
};

В этом примере результат случайно совпадает с ожиданием, потому что height не зависит от значения width на момент своей инициализации — но если бы height(width * 2) пытался прочитать width, это было бы обращение к ещё не инициализированному полю (неопределённое поведение), несмотря на то, что в списке width написан раньше. Хорошие компиляторы (GCC/Clang с -Wreorder) предупреждают именно об этом несоответствии порядка в списке и порядка объявления.In this example the result happens to match expectations because height doesn't depend on width's value at the moment it's initialized — but if height(width * 2) tried to read width, that would be reading an as-yet-uninitialized field (undefined behavior), despite width appearing earlier in the list. Good compilers (GCC/Clang with -Wreorder) warn specifically about this mismatch between the list's order and the declaration order.

ГраблиGotchas

Инициализация поля значением другого поля того же объекта — риск чтения мусораInitializing a field from another field of the same object risks reading garbage

Если поле, объявленное раньше, в своей инициализации ссылается на поле, объявленное позже (в противоречии с намерением, выраженным порядком в списке), оно прочитает неинициализированную память — компилятор не переставляет порядок объявления ради того, чтобы список инициализации «сработал» так, как написан.If a field declared earlier references, in its own initialization, a field declared later (contradicting the intent expressed by the list's order), it reads uninitialized memory — the compiler never reorders declarations to make the init list "work" as written.

in-class default member initializer переопределяется списком, а не суммируется с нимis overridden by the list, not combined with it

int count = 0; прямо в теле класса задаёт значение по умолчанию, применяемое только если конструктор не упоминает count в своём списке инициализации — если упоминает, значение из списка полностью заменяет значение по умолчанию, они не складываются и не комбинируются.int count = 0; right in the class body sets a default value, applied only when a constructor doesn't mention count in its own init list — if it does, the list's value fully replaces the default, they aren't added together or combined.

class с template параметром без аргументов по умолчанию имеет иные правила наследования доступаthe default access level applies the same way to template classes

Правило «private по умолчанию для class, public для struct» одинаково распространяется и на шаблонные классы/структуры, и на обычное наследование (class D : Base — приватное наследование по умолчанию, struct D : Base — публичное) — источник тихих ошибок при смене struct на class без ревизии списка наследования.The "private by default for class, public for struct" rule applies identically to template classes/structs and to plain inheritance (class D : Base defaults to private inheritance, struct D : Base to public) — a source of silent bugs when switching struct to class without revisiting the inheritance list.