базовые концепции ООП — классы и инициализацияbaseline OOP concepts — classes and 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.
Класс — определяемый пользователем тип, объединяющий данные (поля) и операции над ними (методы) в одну единицу. 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
Список инициализации после двоеточия в конструкторе — не синтаксический сахар для присваивания в теле, а единственный способ инициализировать (а не присвоить значение уже созданному объекту) поля-ссылки, поля-константы и поля без конструктора по умолчанию. Присваивание в теле конструктора для таких полей просто не скомпилируется.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.
Ключевое правило, которое ловит почти всех: поля инициализируются в порядке их объявления в теле класса, а не в порядке, в котором они перечислены в списке инициализации конструктора. Порядок в списке — просто вопрос читаемости кода, компилятор его игнорирует при генерации фактического порядка вызовов.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.
Если поле, объявленное раньше, в своей инициализации ссылается на поле, объявленное позже (в противоречии с намерением, выраженным порядком в списке), оно прочитает неинициализированную память — компилятор не переставляет порядок объявления ради того, чтобы список инициализации «сработал» так, как написан.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.
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.
Правило «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.