12.6 – Списки инициализаторов членов в конструкторах
В предыдущем уроке для простоты мы инициализировали члены данных нашего класса в конструкторе с помощью оператора присваивания. Например:
class Something
{
private:
int m_value1;
double m_value2;
char m_value3;
public:
Something()
{
// Это всё присваивания, а не инициализации
m_value1 = 1;
m_value2 = 2.2;
m_value3 = 'c';
}
};
Когда выполняется конструктор класса, создаются m_value1
, m_value2
и m_value3
. Затем запускается тело конструктора, в котором переменным-членам данных присваиваются значения. Это похоже на выполнение следующего кода в не объектно-ориентированном C++:
int m_value1;
double m_value2;
char m_value3;
m_value1 = 1;
m_value2 = 2.2;
m_value3 = 'c';
Хотя это допустимо в рамках синтаксиса языка C++, но это не демонстрирует хороший стиль (и может быть менее эффективным, чем инициализация).
Однако, как вы узнали из предыдущих уроков, некоторые типы данных (например, константные и ссылочные переменные) должны быть инициализированы в строке, в которой они объявлены. Рассмотрим следующий пример:
class Something
{
private:
const int m_value;
public:
Something()
{
m_value = 1; // ошибка: переменным const не может
// быть выполнено присваивание
}
};
Это создает код, подобный следующему:
// ошибка: переменные const должны быть инициализированы значением
const int m_value;
// ошибка: переменные const не может быть выполнено присваивание
m_value = 5;
Присваивание значений константным или ссылочным переменным-членам в теле конструктора в некоторых случаях невозможно.
Списки инициализаторов членов
Чтобы решить эту проблему, C++ предоставляет метод инициализации переменных-членов класса (вместо присваивания им значений после их создания) через список инициализаторов членов (часто называемый «списком инициализации членов»). Не путайте их с похоже называющимся списком инициализаторов, который мы можем использовать для присваивания значений массивам.
В уроке «1.4 – Присваивание и инициализация переменных» вы узнали, что переменные можно инициализировать тремя способами: через копирующую, прямую и унифицированную инициализацию.
int value1 = 1; // копирующая инициализация
double value2(2.2); // прямая инициализация
char value3 {'c'}; // унифицированная инициализация
Использование списка инициализации почти идентично выполнению прямой инициализации или унифицированной инициализации.
Это лучше всего изучить на примере. Вернемся к нашему коду, который выполняет присваивания в теле конструктора:
class Something
{
private:
int m_value1;
double m_value2;
char m_value3;
public:
Something()
{
// Это всё присваивания, а не инициализации
m_value1 = 1;
m_value2 = 2.2;
m_value3 = 'c';
}
};
Теперь давайте напишем тот же код, используя список инициализации:
class Something
{
private:
int m_value1;
double m_value2;
char m_value3;
public:
Something() : m_value1{ 1 }, m_value2{ 2.2 }, m_value3{ 'c' } // Инициализируем наши переменные-члены
{
// Здесь нет необходимости в присваивании
}
void print()
{
std::cout << "Something(" << m_value1 << ", " << m_value2 << ", " << m_value3 << ")\n";
}
};
int main()
{
Something something{};
something.print();
return 0;
}
Эта программа печатает:
Something(1, 2.2, c)
Список инициализаторов членов класса вставляется после параметров конструктора. Он начинается с двоеточия (:
), а затем перечисляет через запятые все иницализируемые переменные вместе со значениями этих переменных.
Обратите внимание, что нам больше не нужно выполнять присваивание в теле конструктора, поскольку список инициализаторов заменяет эту функцию. Также обратите внимание, что список инициализаторов не заканчивается точкой с запятой.
Конечно, конструкторы более полезны, когда мы позволяем вызывающему передавать значения инициализации:
#include <iostream>
class Something
{
private:
int m_value1;
double m_value2;
char m_value3;
public:
Something(int value1, double value2, char value3='c')
: m_value1{ value1 }, m_value2{ value2 }, m_value3{ value3 } // напрямую инициализирует наши переменные-члены
{
// Здесь нет необходимости в присваивании
}
void print()
{
std::cout << "Something(" << m_value1 << ", " << m_value2 << ", " << m_value3 << ")\n";
}
};
int main()
{
// value1 = 1, value2=2.2, value3 получает значение по умолчанию value 'c'
Something something{ 1, 2.2 };
something.print();
return 0;
}
Эта программа печатает:
Something(1, 2.2, c)
Обратите внимание, что вы можете использовать параметры по умолчанию, чтобы указать значение по умолчанию, если пользователь его не предоставил.
Вот пример класса с константной переменной-членом:
class Something
{
private:
const int m_value;
public:
Something(): m_value{ 5 } // напрямую инициализируем нашу константную переменную-член
{
}
};
Это работает, потому что нам разрешено инициализировать константные переменные (но не присваивать им значения!).
Правило
Для инициализации переменных-членов вашего класса вместо присваивания используйте списки инициализаторов членов.
Инициализация элементов массива списками инициализаторов членов
Рассмотрим класс с членом-массивом:
class Something
{
private:
const int m_array[5];
};
До C++11 член-массив с помощью списка инициализации членов класса можно было только обнулить:
class Something
{
private:
const int m_array[5];
public:
Something(): m_array {} // обнуляем член-массив
{
}
};
Однако, начиная с C++11, вы можете полностью инициализировать член-массив, используя унифицированную инициализацию:
class Something
{
private:
const int m_array[5];
public:
Something(): m_array { 1, 2, 3, 4, 5 } // используем унифицированную инициализацию
// для инициализации нашего члена-массива
{
}
};
Инициализация переменных-членов, которые являются классами
Список инициализации членов также может использоваться для инициализации членов, которые являются классами.
#include <iostream>
class A
{
public:
A(int x) { std::cout << "A " << x << '\n'; }
};
class B
{
private:
A m_a;
public:
B(int y)
: m_a{ y-1 } // вызов конструктора A(int) для инициализации члена m_a
{
std::cout << "B " << y << '\n';
}
};
int main()
{
B b{ 5 };
return 0;
}
Эта программа печатает:
A 4
B 5
Когда создается переменная b
, конструктор B(int)
вызывается со значением 5. Перед выполнением тела конструктора инициализируется m_a
, вызывая конструктор A(int)
со значением 4. Это печатает "A 4". Затем управление возвращается конструктору B
, и тело конструктора B
выполняется с выводом "B 5".
Форматирование списков инициализаторов
C++ дает вам большую гибкость в том, как форматировать списки инициализаторов, и вам решать, как вы хотите действовать. Но вот несколько рекомендаций:
Если список инициализаторов умещается в той же строке, что и имя функции, то можно разместить всё в одной строке:
class Something
{
private:
int m_value1;
double m_value2;
char m_value3;
public:
Something() : m_value1{ 1 }, m_value2{ 2.2 }, m_value3{ 'c' } // всё в одной строке
{
}
};
Если список инициализаторов не помещается в той же строке, что и имя функции, он должен быть помещен с отступом на следующей строке.
class Something
{
private:
int m_value1;
double m_value2;
char m_value3;
public:
Something(int value1, double value2, char value3='c') // в этой строке уже много чего есть
: m_value1{ value1 }, m_value2{ value2 }, m_value3{ value3 } // поэтому мы можем поместить
// всё с отступом в следующую строку
{
}
};
Если все инициализаторы не помещаются в одну строку (или инициализаторы нетривиальны), вы можете разделить их, по одному на строку:
class Something
{
private:
int m_value1;
double m_value2;
char m_value3;
float m_value4;
public:
// в первой строке уже много чего есть
Something(int value1, double value2, char value3='c', float value4=34.6f)
: m_value1{ value1 }, // по одному в строке, запятые в конце каждой строки
m_value2{ value2 },
m_value3{ value3 },
m_value4{ value4 }
{
}
};
Порядок в списке инициализаторов
Возможно, удивительно, что переменные в списке инициализаторов не инициализируются в том порядке, в котором они указаны в списке инициализаторов. Вместо этого они инициализируются в том порядке, в котором они объявлены в классе.
Для достижения наилучших результатов следует соблюдать следующие рекомендации:
- Не инициализируйте переменные-члены таким образом, чтобы они зависели от других инициализируемых переменных-членов (другими словами, убедитесь, что ваши переменные-члены будут правильно инициализированы, даже если порядок инициализации будет отличаться).
- Инициализируйте переменные в списке инициализаторов в том же порядке, в котором они объявлены в вашем классе. Это не обязательно, если выполняются предыдущая рекомендация, но ваш компилятор может выдать вам предупреждение, если вы так не сделаете и у вас включены все предупреждения.
Резюме
Списки инициализаторов членов позволяют нам инициализировать наши члены, а не присваивать им значения. Это единственный способ инициализировать члены, которым требуются значения при инициализации, например, константные или ссылочные члены, и это может быть более производительным, чем присваивание значений в теле конструктора. Списки инициализаторов членов работают как с базовыми типами, так и с членами, которые сами являются классами.
Небольшой тест
Вопрос 1
Напишите класс с именем RGBA
, который содержит 4 переменных-члена типа std::uint_fast8_t
с именами m_red
, m_green
, m_blue
и m_alpha
(включите через #include
заголовок cstdint
для доступа к типу std::uint_fast8_t
). Присвойте значения по умолчанию 0 для m_red
, m_green
и m_blue
и 255 для m_alpha
. Создайте конструктор, который использует список инициализаторов членов, который позволяет пользователю инициализировать значения m_red
, m_blue
, m_green
и m_alpha
. Напишите функцию print()
, которая выводит значения переменных-членов.
Если вам нужно напоминание о том, как использовать целые числа фиксированной ширины, просмотрите урок «4.6 – Целочисленные типы фиксированной ширины и size_t».
Подсказка: если ваша функция print()
работает некорректно, убедитесь, что вы приводите uint_fast8_t
к типу int
.
Должен запуститься следующий код:
int main()
{
RGBA teal{ 0, 127, 127 };
teal.print();
return 0;
}
и выдать следующий результат:
r=0 g=127 b=127 a=255
Ответ
#include <iostream> #include <cstdint> // для std::uint_fast8 class RGBA { public: // Псевдоним типа избавляет нас от набора текста и упрощает поддержку класса using component_type = std::uint_fast8_t; private: component_type m_red; component_type m_green; component_type m_blue; component_type m_alpha; public: RGBA(component_type red=0, component_type green=0, component_type blue=0, component_type alpha=255) : m_red{ red }, m_green{ green }, m_blue{ blue }, m_alpha{ alpha } { } void print() { std::cout << "r=" << static_cast<int>(m_red) << " g=" << static_cast<int>(m_green) << " b=" << static_cast<int>(m_blue) << " a=" << static_cast<int>(m_alpha) << '\n'; } }; int main() { RGBA teal{ 0, 127, 127 }; teal.print(); return 0; }