Согласно определению, инициализация – это приведение объекта или устройства в состояние готовности к использованию. Если мы говорим о программировании, инициализация – это задание начального значения, без чего невозможно использование переменной или константы. Если переменной не задано значение, результат работы программы не определён. Поэтому инициализация является важным действием при разработке программ. Константы вообще нельзя создать без инициализации, поскольку их нельзя изменить в дальнейшем.
Обратите внимание, что присваивание и инициализация – это разные понятия. Присваивание подразумевает запись значения в ячейку памяти. Для переменных инициализация выполняется также. А вот константы могут быть реализованы без использования оперативной памяти, компилятор связывает имя и значение, проверяет соответствие типов при использовании константы и подставляет в команду необходимое значение.
В настоящее время в языке С++ существует много способов инициализации переменной. К сожалению, это вносит некоторую путаницу, но разобраться с инициализацией необходимо. Впрочем, если использовать некоторые правила, то проблем быть не должно.
В языке С++ существуют следующие способы инициализации.
Рассмотрим примеры инициализации переменной типа int.
int i1; // Инициализация по умолчанию
int i2 = 5; // Копирующая инициализация
int i3(5); // Прямая инициализация
int i4(); // Ошибка! Это прототип функции, а не объявление переменной
int i5 = int(); // Инициализация значением, i5 = 0
int i6{5}; // Универсальная инициализация (выполняется прямая инициализация)
int i7 = {5}; // Универсальная инициализация (выполняется копирующая инициализация)
int i8{}; // Универсальная инициализация, i8 = 0
Для переменных базовых типов принципиальной разницы между приведёнными примерами нет. Конечно, инициализацию по умолчанию следует использовать с осторожностью. Также не очень наглядными кажутся инициализация значением и универсальная инициализация с пустыми фигурными скобками.
Следует обратить внимание на то, что универсальная инициализация, в отличие от копирующей и прямой, не позволяет делать сужающие преобразования (т.е. преобразования из вещественного значения в целое, из целого значения большего размера к целому значению меньшего размера).
int i1 = 5.5; // Предупреждение
int i2{5.5}; // Ошибка!
С одной стороны, это более безопасный вариант. С другой стороны, 1e15 формально является вещественным литералом, но что мешает нам присвоить его в целую переменную? Такая запись короче и понятнее, чем целый литерал с пятнадцатью нулями. Предупреждения компилятора тоже надо учитывать, так что для внимательного программиста использование копирующей инициализации не приведёт к проблемам.
long long int lli = 1e15;
// Использование явного преобразования подавляет предупреждения компилятора
long long int lli = static_cast<long long int>(1e15);
Для массивов может быть использована инициализация по умолчанию и агрегатная инициализация.
int m1[10]; // Инициализация по умолчанию
int m2[10] = {0}; // Все элементы массива инициализируются нулём
int m3[] = {1, 2, 4}; // Количество элементов массива равно количеству элементов инициализатора
int m4[10] = {}; // В этом случае все элементы массива также инициализируются нулём
int m5[10] {0};
int m6[] {1, 2, 4};
int m7[10] {};
int m8[] {}; // Ошибка!
Во всех примерах, кроме, естественно, первого, используется агрегатная инициализация. Отсутствие знака равенства – это лишь вариант синтаксиса, универсальная инициализация не применяется, соответственно, при сужающих преобразованиях выдаётся предупреждение.
Количество элементов в инициализаторе может быть меньше, чем количество элементов массива. В этом случае указанными значениями инициализируются первые элементы массива, остальные элементы инициализируются нулями. Количество элементов в инициализаторе не может быть больше, чем количество элементов массива.
При наличии непустого инициализатора можно опускать размер массива (но только по первой размерности). В этом случае количество элементов массива определяется по количеству элементов в инициализаторе. При пустом инициализаторе возникает ошибка, т.к. не может быть массива нулевого размера. Однако при указании количества элементов массива использование пустого инициализатора не приводит к возникновению ошибки, при этом все элементы массива инициализируются нулями.
При инициализации двумерных массивов лучше использовать два уровня фигурных скобок, чтобы избежать разночтений, если количество элементов в инициализаторе меньше, чем количество элементов массива.
int mt1[2][3] = {{1, 2}, {3, 4}};
int mt2[2][3] = {1, 2, 3, 4};
int mt3[2][3] = {};
В первом случае первые два элемента каждой строки инициализируются указанными значениями, а третий элемент – нулём. Во втором случае первые три числа будут использованы для инициализации элементов первой строки, а вторая строка будет проинициализирована четвёртым числом и двумя нулями. В третьем случае все элементы матрицы инициализируются нулями.
Для агрегатных объектов может быть использована инициализация по умолчанию, копирующая инициализация, агрегатная инициализация, прямая инициализация, инициализация значением и назначенная инициализация.
struct S { int a; double b; int c; };
S s1; // Инициализация по умолчанию
S s2 = s1; // Копирующая инициализация
S s3 = {1, 5.5, 7}; // Агрегатная инициализация
S s4 {1, 5.5}; // Агрегатная инициализация
S s5 {}; // Агрегатная инициализация, все поля инициализируются нулями
S s6(1, 5.5, 7); // Прямая инициализация
S s7(1, 5.5); // Прямая инициализация
S s8(); // Это опять прототип функции
S s9 = S(); // Инициализация значением
S s10 {.a = 1, .c = 7}; // Назначенная инициализация
S s11 = {.b = 5.5, .c = 7}; // Назначенная инициализация
S s12 = {.c = 7, .a = 1}; // Ошибка!
Инициализация по умолчанию, как обычно, может приводить к неопределённому поведению. При копирующей инициализации в примере использовалась неинициализированная переменная, в реальной программе такого, конечно же, не должно быть, но здесь просто демонстрируется использование копирующей инициализации. При агрегатной инициализации можно использовать как синтаксис со знаком равенства, так и синтаксис без знака равенства. Пропущенные поля инициализируются нулями, то же происходит и при прямой инициализации. Инициализация значением приводит к инициализации всех полей нулями, т.к. в агрегатных классах нет конструкторов. При назначенной инициализации можно пропускать любые поля, а не только последние, но порядок полей должен быть сохранён.
Принципиальной разницы между агрегатной, прямой и назначенной инициализацией нет, немного отличается лишь синтаксис. Для инициализации полей нулями удобно использовать синтаксис с пустыми фигурными скобками. Копирующая инициализация может быть использована при необходимости скопировать значения из другой переменной.
Для инициализации объектов классов может быть использована инициализация по умолчанию, копирующая инициализация, прямая инициализация, инициализация значением и универсальная инициализация.
class X
{ private:
int a; double b; int c;
public:
X() : a(1), b(1.1), c(1) { }; // Конструктор умолчания
explicit X(int _a) : a(_a), b(2.2), c(2) { }; // Явный конструктор с 1 параметром
X(double _b) : a(2), b(_b), c(2) { }; // Конструктор с 1 параметром
X(int _a, double _b, int _c) : a(_a), b(_b), c(_c) { }; // Конструктор с 3 параметрами
};
X x1; // Инициализация по умолчанию
X x2 = x1; // Копирующая инициализация
X x3 = 5; // Копирующая инициализация
X x4(5); // Прямая инициализация
X x5(1.1, 5.5, 7); // Прямая инициализация
X x6(1, 5.5); // Ошибка – нет конструктора с двумя параметрами
X x7 = X(); // Инициализация значением
X x8 = {1, 5.5, 7}; // Универсальная инициализация
X x9 {1, 5.5, 7}; // Универсальная инициализация
X x10 {}; // Универсальная инициализация
Переменная x1 по форме инициализируется по умолчанию. Однако для класса определён конструктор умолчания, который используется в данном случае, т.е. поля объекта класса будут правильно проинициализированы. Если в классе нет никаких конструкторов, то компилятор генерирует конструктор умолчания, который, однако, ничего не делает, в этом случае поля класса ничем не инициализируется, и возникает неопределённое поведение.
Для переменной x2 выполняется копирующая инициализация, используется конструктор копирования. Для переменной x3 по форме мы тоже имеет копирующую инициализацию, но инициализатор имеет другой тип, поэтому необходим вызов конструктора с один параметром подходящего типа. Если такого конструктора не будет, возникнет ошибка. В данном случае в классе есть два конструктора с одним параметром, но конструктор с параметром типа int объявлен с ключевым словом explicit, поэтому не может быть использован при копирующей инициализации. В данном случае используется конструктор с параметром типа double, несмотря на то, что он хуже подходит по типу. А вот для переменной x4 при прямой инициализации вызывается конструктор с параметром типа int.
Обратите внимание, что в отличие от инициализации агрегатных классов, при прямой и универсальной инициализации невозможно менять количество параметров. Для агрегатных классов поля, для которых не указаны значения, инициализировались нулями. Для обычных классов требуется точное соответствие количества параметров.
При инициализации значением и при универсальной инициализации с пустыми фигурными скобками используется конструктор умолчания, т.е. поля класса будут проинициализированы не нулями, а значениями, указанными в конструкторе умолчания. Если конструктора умолчания в классе нет, возникнет ошибка.
При универсальной инициализации как обычно не выполняются сужающие преобразования, поэтому для переменных x8 и x9 невозможно подставить вещественное значение для целочисленного поля, как это сделано при инициализации переменной x5.
Динамически выделяемые переменные по умолчанию ничем не инициализируются. С операцией new можно использовать прямую и универсальную инициализацию для инициализации выделенной памяти.
int *p1 = new int; // Инициализация по умолчанию, неопределённое поведение
int *p2 = new int (5); // Прямая инициализация
int *p3 = new int {5}; // Универсальная инициализация
Аналогично работает инициализация и для динамически создаваемых массивов. Причём для массивов даже работает возможность выведения количества элементов массива по количеству элементов в инициализаторе.
int *p4 = new int[3] (1, 2, 3); // Прямая инициализация
int *p5 = new int[3] {1, 2, 3}; // Универсальная инициализация
int *p6 = new int[5] {1, 2, 3}; // В инициализаторе меньше элементов, чем в массива
int *p7 = new int[ ] {1, 2, 3}; // Размер массива определяется по инициализатору
Если в инициализаторе элементов меньше, чем в массиве, то оставшиеся элементы (при любой инициализации) будут инициализированы по умолчанию, т.е. для элементов базовых типов будет использоваться значение 0, а для объектов классов будет вызываться конструктор умолчания. Если конструктора умолчания нет, то возникает ошибка.
Несмотря на то, что приведённые выше инструкции синтаксические корректны, подобный подход представляется странным. Зачем динамически выделять память, если мы знаем количество элементов массива? В этом случае удобнее использовать статический массив. Кроме того, если в инициализаторе окажется элементов больше, чем нужно, возникнет ошибка.
int n;
int *p8 = new int[n] {1, 2, 3};
Если в приведённом выше примере, значение переменной n окажется меньше 3, то возникнет ошибка, причём возникнет она на этапе выполнения программы, поскольку, компилятор, естественно, не может до запуска программы проверить, что n больше или равно 3.
Однако использование пустых фигурных скобок для инициализации по умолчанию представляется разумным и полезным решением.
int n;
int *p9 = new int[n] {};
Несомненно, в языке С++ существует много вариантов инициализации, в которых можно запутаться без привычки. Но, с другой стороны, можно выделить предпочтительные способы инициализации для разных случаев.
Для переменных базовых типов копирующая инициализация является удобным и привычным способом инициализации. Хотя можно использовать и универсальную инициализацию.
Для объектов классов можно использовать прямую инициализацию, которая означает вызов подходящего конструктора, или универсальную инициализацию. При инициализации по умолчанию следует обращать внимание на происхождение конструктора умолчания – генерируемый компилятором конструктор умолчания существует лишь формально и на самом деле поля объекта класса не инициализирует. Но инициализацию по умолчанию, конечно, лучше не использовать, более удачным вариантом является инициализация значением, при этом поля объекта класса инициализируются нулями, даже если конструктор умолчания сгенерирован компилятором.
В остальных случаях лучшим вариантом является универсальная инициализация, которую можно использовать как с указанием конкретных значений, так и для инициализации значениями по умолчанию при использовании пустых фигурных скобок.