базовые концепции ООП — конструкторыbaseline OOP concepts — constructors

КонструкторыConstructors

Конструктор — специальный метод, вызываемый ровно один раз при создании объекта, приводящий его в валидное начальное состояние. В C++ существует несколько разновидностей (по умолчанию, копирования, перемещения, с параметрами), а ключевое слово explicit и делегирование между конструкторами закрывают конкретные, часто спрашиваемые на собеседованиях грабли неявных преобразований.A constructor is a special method, called exactly once when an object is created, that brings it into a valid initial state. C++ has several kinds (default, copy, move, parameterized), and the explicit keyword plus delegation between constructors close off specific, frequently interview-asked traps around implicit conversions.

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

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

Конструктор — метод с именем, совпадающим с именем класса, без возвращаемого типа, вызываемый компилятором автоматически в момент создания объекта. Его задача — установить инвариант класса (то, что должно быть истинным для любого валидного объекта этого типа) прежде, чем объект станет доступен для использования где-либо ещё.A constructor is a method whose name matches the class name, has no return type, and is invoked automatically by the compiler at the moment an object is created. Its job is to establish the class's invariant (whatever must hold true for any valid object of that type) before the object becomes accessible anywhere else.

class Fraction {
    int num, den;
public:
    Fraction(int n, int d) : num(n), den(d) {
        if (den == 0) throw std::invalid_argument("zero denominator");
        // инвариант "den != 0" гарантирован до того, как объект где-либо используется// the "den != 0" invariant is guaranteed before the object is used anywhere
    }
};

Fraction half(1, 2);   // конструктор вызывается ровно один раз, в момент создания// the constructor runs exactly once, at creation time

Конструктор по умолчаниюThe default constructor

Конструктор, не принимающий аргументов (или у которого все параметры имеют значения по умолчанию). Компилятор генерирует его автоматически, но только если класс не объявляет вообще никаких других конструкторов — как только появляется хотя бы один пользовательский конструктор, неявный конструктор по умолчанию исчезает, и создание объекта без аргументов начинает требовать явного = default или собственной реализации.A constructor that takes no arguments (or where every parameter has a default value). The compiler generates one automatically, but only if the class declares no other constructors at all — the moment even one user-defined constructor appears, the implicit default constructor disappears, and creating an object with no arguments starts requiring an explicit = default or a hand-written implementation.

struct A {
    A(int x) {}     // как только это объявлено, A() исчезает// once this is declared, A() disappears
};
// A a; — ошибка компиляции: нет подходящего конструктора// A a; — compile error: no matching constructor

struct B {
    B() = default;   // явно просим сгенерированную компилятором реализацию обратно// explicitly ask for the compiler-generated implementation back
    B(int x) {}
};

explicit на одноаргументных конструкторахon single-argument constructors

Конструктор, принимающий ровно один аргумент (не считая параметров со значением по умолчанию), без explicit становится путём для неявного преобразования типа — компилятор может молча вызвать его там, где ожидается объект класса, а передано значение другого типа. explicit запрещает такое неявное использование, оставляя только прямой вызов конструктора.A constructor taking exactly one argument (not counting parameters with a default value), without explicit, becomes a path for implicit type conversion — the compiler can silently call it wherever an object of the class is expected but a value of another type is passed instead. explicit forbids that implicit use, leaving only a direct constructor call.

class Meters {
    double value;
public:
    Meters(double v) : value(v) {}   // НЕ explicit — опасно// NOT explicit — dangerous
};
void setDistance(Meters m);

setDistance(5.0);   // компилируется! 5.0 неявно превращается в Meters(5.0) — вероятно, не то, что имелось в виду// compiles! 5.0 silently becomes Meters(5.0) — probably not what was intended

class Seconds {
    double value;
public:
    explicit Seconds(double v) : value(v) {}
};
void setDuration(Seconds s);
// setDuration(5.0); — ошибка компиляции, ровно то поведение, которое нужно// setDuration(5.0); — compile error, exactly the behavior wanted
setDuration(Seconds(5.0));   // только явный вызов проходит// only an explicit call goes through

Практическое правило: одноаргументные конструкторы почти всегда должны быть explicit, если только сознательно не проектируется неявная конвертация как часть публичного API типа (как, например, у std::string, который намеренно допускает неявное построение из const char*).The practical rule: single-argument constructors should almost always be explicit, unless implicit conversion is being deliberately designed as part of the type's public API (as with std::string, which intentionally allows implicit construction from a const char*).

Делегирующие конструкторыDelegating constructors

С C++11 один конструктор может вызвать другой конструктор того же класса из своего списка инициализации, избегая дублирования логики установки инварианта между несколькими перегрузками конструктора.Since C++11, one constructor can call another constructor of the same class from its own init list, avoiding duplicated invariant-setup logic across multiple constructor overloads.

class Rectangle {
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {
        if (w <= 0 || h <= 0) throw std::invalid_argument("non-positive size");
    }
    Rectangle(double side) : Rectangle(side, side) {}
    // квадрат делегирует полному конструктору — проверка инварианта не дублируется// the square delegates to the full constructor — the invariant check isn't duplicated
};

До C++11 та же цель достигалась через приватный вспомогательный метод init(), вызываемый из каждого конструктора, — рабочий, но менее прямой обходной путь, поскольку такой метод можно случайно вызвать повторно на уже сконструированном объекте.Before C++11, the same goal was achieved via a private helper method init() called from every constructor — a working but less direct workaround, since such a method could accidentally be called again on an already-constructed object.

ГраблиGotchas

Most Vexing Parse — объявление выглядит как вызов конструктора без аргументовThe Most Vexing Parse — a declaration that looks like a zero-argument constructor call

Widget w(); не создаёт объект w — компилятор разбирает это как объявление функции w, не принимающей аргументов и возвращающей Widget. Правильный способ вызвать конструктор по умолчанию явно — Widget w; (без скобок) или Widget w{}; (uniform initialization, C++11), которая от этой неоднозначности не страдает.Widget w(); doesn't create an object w — the compiler parses it as declaring a function named w that takes no arguments and returns a Widget. The correct way to explicitly call the default constructor is Widget w; (no parens) or Widget w{}; (uniform initialization, C++11), which doesn't suffer from this ambiguity.

Виртуальный вызов из конструктора не диспетчеризуется полиморфноA virtual call from a constructor doesn't dispatch polymorphically

Подробно разобрано на странице про полиморфизм: пока выполняется конструктор, vptr объекта указывает на vtable именно текущего класса, а не производного — переопределение в наследнике ещё не «активно».Covered in detail on the polymorphism page: while a constructor is running, the object's vptr points at the vtable of the currently-executing class, not the derived one — the override in the descendant isn't "active" yet.

Список инициализации выполняется в порядке объявления полей, не в порядке в спискеThe init list runs in field-declaration order, not the list's own order

Подробно разобрано на странице про определение классов — частый источник неопределённого поведения при инициализации одного поля значением другого.Covered in detail on the class definition page — a common source of undefined behavior when one field is initialized from another.