Инициализация в C++: разница между =, () и {} с примерами

Инициализация в C++: разница между =, () и {} с примерами

Инициализация в C++: разница между =, () и {} с примерами

Запрос «инициализация в C++: = () {}» часто встречается у начинающих и продолжающих разработчиков. Разные формы инициализации влияют на то, какие конструкторы вызываются, как проверяются ошибки и даже на то, будет ли объект корректно создан. Ниже — практическое руководство с примерами и советами, которое поможет уверенно выбирать нужный синтаксис.

Что такое инициализация в C++ и почему это важно

Инициализация — это способ задать начальное состояние объекта. От выбранной формы зависят:

  • какой конструктор или правило инициализации будет применено;
  • произойдёт ли проверка сужения (narrowing);
  • не попадём ли мы в ловушки парсинга (например, «most vexing parse»);
  • как будут инициализироваться агрегаты и контейнеры.
  • Копирующая, прямая и списковая инициализация: =, (), {}

    Базовые формы на примере простых типов:

    int a = 1;     // copy initialization (копирующая)
    int b(1);      // direct initialization (прямая)
    int c{1};      // direct list initialization (списковая)
    int d = {1};   // copy list initialization (списковая через =)
    
    int z{};       // value/zero initialization: z == 0
    

    Ключевые отличия:

  • = вызывает копирующую инициализацию; не участвуют explicit-конструкторы;
  • () вызывает прямую инициализацию; explicit-конструкторы допускаются;
  • {} включает списковую инициализацию, даёт защиту от сужения и может выбирать перегрузки с std::initializer_list.
  • Проверка сужения (narrowing) только для {}

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

    int i1 = 3.14; // допустимо: 3.14 будет усечено до 3 (часто лишь предупреждение)
    int i2(3.14);  // допустимо, но также приведёт к усечению
    int i3{3.14};  // ОШИБКА компиляции: narrowing при {} запрещён
    

    Вывод: для безопасной инициализации чисел предпочитайте {} — компилятор раньше поймает опасные преобразования.

    std::initializer_list и выбор перегрузок при {}

    Когда у класса есть перегрузка конструктора с std::initializer_list, форма {} обычно выбирает именно её. Это важно для контейнеров и собственных типов.

    #include <initializer_list>
    #include <iostream>
    
    struct Widget {
        Widget(int a, int b) { std::cout << "(int,int)n"; }
        Widget(std::initializer_list<int> l) { std::cout << "initializer_list size=" << l.size() << "n"; }
    };
    
    int main() {
        Widget w1(1, 2);   // (int,int)
        Widget w2{1, 2};   // initializer_list
        Widget w3 = {1, 2}; // initializer_list
    }
    

    С контейнерами различие особенно заметно:

    #include <vector>
    
    std::vector<int> v1(5, 10); // пять элементов, каждый равен 10
    std::vector<int> v2{5, 10}; // два элемента: 5 и 10
    

    Совет: если хотите «пять десяток» — используйте (), если «список значений» — {}.

    explicit-конструкторы и выбор формы

    Ключевое слово explicit запрещает неявные преобразования при копирующей инициализации (=). Прямая и списковая инициализация explicit-конструкторы допускают.

    struct X {
        explicit X(int) {}
    };
    
    X a = 1;   // ОШИБКА: explicit не допускает copy init
    X b(1);    // ОК: direct init
    X c{1};    // ОК: direct list init
    

    Most vexing parse и нулевая инициализация

    Скобки () могут быть разобраны компилятором как объявление функции, а не объекта. Классический пример:

    #include <vector>
    
    std::vector<int> v(); // Объявление функции v, возвращающей vector<int>, а НЕ объект!
    std::vector<int> v1{}; // Объект: пустой вектор
    int x{};               // x == 0 (value/zero init)
    

    Используйте {} для инициализации по умолчанию и избежания неоднозначностей.

    Типы инициализации в терминах стандарта

  • Zero-initialization: обнуление фундаментальных типов при определённых контекстах (например, int x{}; new int{}).
  • Default-initialization: вызывает конструктор по умолчанию (например, T x; внутри функции — без обнуления примитивов).
  • Value-initialization: T x{}; создаёт «значение по умолчанию» (для примитивов — ноль).
  • Copy initialization: T x = expr; не использует explicit-конструкторы.
  • Direct initialization: T x(expr); допускает explicit-конструкторы.
  • List initialization: T x{…}; даёт защиту от narrowing и может выбирать initializer_list.
  • Агрегаты, {} и назначенные инициализаторы (C++20)

    Агрегаты (простые структуры без пользовательских конструкторов) удобно инициализировать через {}:

    struct Point { int x; int y; };
    
    Point p1{1, 2};     // aggregate init
    Point p2{};         // x=0, y=0
    

    С C++20 доступны назначенные инициализаторы (designated initializers):

    Point p3{ .y = 2, .x = 1 }; // порядок произвольный
    

    Поддержка в компиляторах уже широкая, но проверяйте свой стандарт (-std=c++20) и версию компилятора, особенно под Windows.

    Практические советы и чек-лист

  • Для чисел и простых типов по умолчанию используйте {} — получите нулевую/безопасную инициализацию и защиту от narrowing.
  • Если нужна конкретная перегрузка с количеством/значениями — выбирайте () или {} осознанно (см. пример с std::vector).
  • Если конструктор explicit — избегайте формы с =; применяйте () или {}.
  • Для агрегатов используйте {} и, при необходимости, назначенные инициализаторы (C++20).
  • Чтобы избежать «most vexing parse», не пишите T obj(); используйте T obj{};
  • Помните: при {} конструктор с std::initializer_list имеет приоритет. Это может неожиданно выбрать «списковую» перегрузку.
  • Для безопасного обнуления динамической памяти используйте new T{}, а не new T().
  • Мини-практикум

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

    #include <iostream>
    #include <vector>
    
    struct Demo {
        Demo(int, int) { std::cout << "Demo(int,int)n"; }
        Demo(std::initializer_list<int>) { std::cout << "Demo(init-list)n"; }
    };
    
    int main() {
        int a{};            // ?
        int b{3.7};         // ? (подсказка: narrowing)
        Demo d1(1, 2);      // ?
        Demo d2{1, 2};      // ?
        std::vector<int> v1(3, 7); // ?
        std::vector<int> v2{3, 7}; // ?
    }
    

    Заключение

    Форма инициализации в C++ — это не просто стиль, а механика, влияющая на выбор конструктора, безопасность преобразований и понятность кода. Запомните простое правило: для безопасного старта — {}. Для точного выбора перегрузки — осознанно используйте () и {}. Для запрета неявных преобразований — explicit и избегайте =.

    Если хотите системно прокачать базу по C++ и закрыть пробелы по инициализации, классам, памяти и стандартной библиотеке, рекомендую посмотреть курс «C++ с Нуля до Гуру: от синтаксиса к реальным проектам». Он практикоориентированный и отлично дополняет материал статьи.

    Источник

    НЕТ КОММЕНТАРИЕВ

    Оставить комментарий