На этом уроке мы рассмотрим, как инициализировать переменные-члены класса с помощью списка инициализации в языке С++, а также особенности и нюансы, которые при этом могут возникнуть.
Списки инициализации членов класса
На предыдущем уроке мы инициализировали члены нашего класса в конструкторе через оператор присваивания:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Values { private: int m_value1; double m_value2; char m_value3; public: Values() { // Это всё операции присваивания, а не инициализация m_value1 = 3; m_value2 = 4.5; m_value3 = 'd'; } }; |
Сначала создаются m_value1
, m_value2
и m_value3
. Затем выполняется тело конструктора, где этим переменным присваиваются значения. Аналогичен код в не объектно-ориентированном C++:
1 2 3 4 5 6 7 |
int m_value1; double m_value2; char m_value3; m_value1 = 3; m_value2 = 4.5; m_value3 = 'd'; |
Хотя в плане синтаксиса языка C++ вопросов никаких нет — всё корректно, но более эффективно — использовать инициализацию, а не присваивание после объявления.
Как мы уже знаем из предыдущих уроков, некоторые типы данных (например, константы и ссылки) должны быть инициализированы сразу. Рассмотрим следующий пример:
1 2 3 4 5 6 7 8 9 10 11 |
class Values { private: const int m_value; public: Values() { m_value = 3; // ошибка: константам нельзя присваивать значения } }; |
Аналогичен код в не объектно-ориентированном C++:
1 2 |
const int m_value; // ошибка: константы должны быть инициализированы значениями m_value = 7; // ошибка: константам нельзя присваивать значения |
Для решения этой проблемы в C++ добавили метод инициализации переменных-членов класса через список инициализации членов, вместо присваивания им значений после объявления. Не путайте этот список с аналогичным списком инициализаторов, который используется для инициализации массивов.
Из урока №28 мы уже знаем, что инициализировать переменные можно тремя способами: через копирующую инициализацию, прямую инициализацию или uniform-инициализацию.
1 2 3 |
int value1 = 3; // копирующая инициализация double value2(4.5); // прямая инициализация char value3 {'d'} // uniform-инициализация |
Использование списка инициализации почти идентично выполнению прямой инициализации (или uniform-инициализации в C++11).
Чтобы было понятнее, рассмотрим пример. Вот код с присваиванием значений переменным-членам класса в конструкторе:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Values { private: int m_value1; double m_value2; char m_value3; public: Values() { // Это всё операции присваивания, а не инициализация m_value1 = 3; m_value2 = 4.5; m_value3 = 'd'; } }; |
Теперь давайте перепишем этот код, но уже с использованием списка инициализации:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include <iostream> class Values { private: int m_value1; double m_value2; char m_value3; public: Values() : m_value1(3), m_value2(4.5), m_value3('d') // напрямую инициализируем переменные-члены класса { // Нет необходимости использовать присваивание } void print() { std::cout << "Values(" << m_value1 << ", " << m_value2 << ", " << m_value3 << ")\n"; } }; int main() { Values value; value.print(); return 0; } |
Результат выполнения программы:
Values(3, 4.5, d)
Список инициализации членов находится сразу же после параметров конструктора. Он начинается с двоеточия (:
), а затем значение для каждой переменной указывается в круглых скобках. Больше не нужно выполнять операции присваивания в теле конструктора. Также обратите внимание, что список инициализации членов не заканчивается точкой с запятой.
Можно также добавить возможность caller-у передавать значения для инициализации:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <iostream> class Values { private: int m_value1; double m_value2; char m_value3; public: Values(int value1, double value2, char value3='d') : m_value1(value1), m_value2(value2), m_value3(value3) // напрямую инициализируем переменные-члены класса { // Нет необходимости использовать присваивание } void print() { std::cout << "Values(" << m_value1 << ", " << m_value2 << ", " << m_value3 << ")\n"; } }; int main() { Values value(3, 4.5); // value1 = 3, value2 = 4.5, value3 = 'd' (значение по умолчанию) value.print(); return 0; } |
Результат выполнения программы:
Values(3, 4.5, d)
Мы можем использовать параметры по умолчанию для предоставления значений по умолчанию, если пользователь их не предоставил. Например, класс, который имеет константную переменную-член:
1 2 3 4 5 6 7 8 9 10 |
class Values { private: const int m_value; public: Values(): m_value(7) // напрямую инициализируем константную переменную-член { } }; |
Это работает, поскольку нам разрешено инициализировать константные переменные (но не присваивать им значения после объявления!).
Правило: Используйте списки инициализации членов, вместо операций присваивания, для инициализации переменных-членов вашего класса.
uniform-инициализация в C++11
В C++11 вместо прямой инициализации можно использовать uniform-инициализацию:
1 2 3 4 5 6 7 8 9 10 |
class Values { private: const int m_value; public: Values(): m_value { 7 } // используем uniform-инициализацию { } }; |
Настоятельно рекомендуется использовать этот синтаксис (даже если вы не используете константы или ссылки в качестве переменных-членов вашего класса), поскольку списки инициализации членов необходимы при композиции и наследовании (это рассмотрим несколько позже).
Правило: Используйте uniform-инициализацию вместо прямой инициализации в C++11.
Инициализация массивов в классе
Рассмотрим класс с массивом в качестве переменной-члена:
1 2 3 4 5 6 |
class Values { private: const int m_array[7]; }; |
До C++11 мы могли только обнулить массив через список инициализации:
1 2 3 4 5 6 7 8 9 10 11 12 |
class Values { private: const int m_array[7]; public: Values(): m_array {} // обнуляем массив { // Если мы хотим, чтобы массив имел значения, то мы должны здесь использовать присваивание } }; |
Однако в C++11 вы можете полностью инициализировать массив, используя uniform-инициализацию:
1 2 3 4 5 6 7 8 9 10 11 |
class Values { private: const int m_array[7]; public: Values(): m_array { 3, 4, 5, 6, 7, 8, 9 } // используем uniform-инициализацию для инициализации массива { } }; |
Инициализация переменных-членов, которые являются классами
Список инициализации членов также может использоваться для инициализации членов, которые являются классами:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <iostream> class A { public: A(int a) { std::cout << "A " << a << "\n"; } }; class B { private: A m_a; public: B(int b) : m_a(b-1) // вызывается конструктор A(int) для инициализации члена m_a { std::cout << "B " << b << "\n"; } }; int main() { B b(7); return 0; } |
Результат выполнения программы:
A 6
B 7
При создании переменной b
вызывается конструктор B(int)
со значением 7
. До того, как тело конструктора выполнится, инициализируется m_a
, вызывая конструктор A(int)
со значением 6
. Таким образом, выведется A 6
. Затем управление возвратится обратно к конструктору B(), и тогда уже он выполнится и выведется B 7
.
Использование списков инициализации
Если список инициализации помещается на той же строке, что и имя конструктора, то лучше всё разместить в одной строке:
1 2 3 4 5 6 7 8 9 10 11 12 |
class Values { private: int m_value1; double m_value2; char m_value3; public: Values() : m_value1(3), m_value2(4.5), m_value3('d') // всё находится в одной строке { } }; |
Если список инициализации членов не помещается в строке с именем конструктора, то на следующей строке (используя перенос) инициализаторы должны быть с отступом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Values { private: int m_value1; double m_value2; char m_value3; public: Values(int value1, double value2, char value3='d') // на этой строке уже и так много чего, : m_value1(value1), m_value2(value2), m_value3(value3) // поэтому переносим инициализаторы на новую строку (не забываем использовать отступ) { } }; |
Если все инициализаторы не помещаются на одной строке, то вы можете выделить для каждого инициализатора отдельную строку:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Values { private: int m_value1; double m_value2; char m_value3; float m_value4; public: Values(int value1, double value2, char value3='d', float value4=17.5) // на этой строке уже и так много чего, : m_value1(value1), // поэтому выделяем каждому инициализатору отдельную строку, не забывая о запятой в конце m_value2(value2), m_value3(value3), m_value4(value4) { } }; |
Порядок выполнения в списке инициализации
Удивительно, но переменные в списке инициализации не инициализируются в том порядке, в котором они указаны. Вместо этого они инициализируются в том порядке, в котором объявлены в классе, поэтому следует соблюдать следующие рекомендации:
Не инициализируйте переменные-члены таким образом, чтобы они зависели от других переменных-членов, которые инициализируются первыми (другими словами, убедитесь, что все ваши переменные-члены правильно инициализируются, даже если порядок в списке инициализации отличается).
Инициализируйте переменные в списке инициализации в том порядке, в котором они объявлены в классе.
Заключение
Списки инициализации членов позволяют инициализировать члены, а не присваивать им значения. Это единственный способ инициализации констант и ссылок, которые являются переменными-членами вашего класса. Во многих случаях использование списка инициализации может быть более результативным, чем присваивание значений переменным-членам в теле конструктора. Списки инициализации работают как с переменными фундаментальных типов данных, так и с членами, которые сами являются классами.
Тест
Напишите класс с именем RGBA, который содержит 4 переменные-члены типа std::uint8_t (подключите заголовочный файл cstdint для доступа к типу std::uint8_t):
m_red
;
m_green
;
m_blue
;
m_alpha
.
Присвойте 0
в качестве значения по умолчанию для m_red
, m_green
и m_blue
, и 255
для m_alpha
. Создайте конструктор со списком инициализации членов, который позволит пользователю передавать значения для m_red
,m_green
, m_blue
и m_alpha
. Напишите функцию print(), которая будет выводить значения переменных-членов.
Подсказка: Если функция print() работает некорректно, то убедитесь, что вы конвертировали std::uint8_t в int.
Следующий код функции main():
1 2 3 4 5 6 7 |
int main() { RGBA color(0, 135, 135); color.print(); return 0; } |
Должен выдавать следующий результат:
r=0 g=135 b=135 a=255
Ответ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include <iostream> #include <cstdint> // для std::uint8 class RGBA { private: std::uint8_t m_red; std::uint8_t m_green; std::uint8_t m_blue; std::uint8_t m_alpha; public: RGBA(std::uint8_t red=0, std::uint8_t green=0, std::uint8_t blue=0, std::uint8_t 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 color(0, 135, 135); color.print(); return 0; } |
Почему std::cout некорректно выводит std::uint8_t? С чем это связано?
Читать до конца надо)(а если честно, то из-за не корректного преобразования)
Т.е. программа, приведенная выше, обрабатывает
myint
как переменную типа char.https://ravesli.com/urok-32-fiksirovannyj-razmer-integers-spor-naschet-unsigned/#toc-3
Спасибо за вашу работу ! Помогает в обучении !
Пожалуйста)
Немножко не понравилось слово "обнуляем" массив. Скользкое слово, и его можно понимать по-разному. Я сначала думал, что "обнулить", это значит ликвидировать массив, сделать его под ноль. А потом возникла версия, что обнулить — это заполнить массив нулями. Семь членов — семь нулей…
Да, есть такое. Режет слух или бьет по психологии.
Тут думаю, что можно было и без этого "обнулить" обойтись. Объявили массив на 7 и хватит.
А зачем в тестовом задании используется тип переменной std::uint8_t? Просто так?
Я так понял, что uint8_t может принять максимум 2^8 = 256 байт информации. Каналы RGB как раз находятся в диапазоне 0-255, поэтому использовать unsigned int нет смысла (только лишняя потеря памяти), так как значение все равно не будет больше 255.
uint8_t — это беззнаковая целая переменная длиной 1 байт (8 бит). Соответственно, может принимать значения 0..255
Спасибо Юрий! Тема даётся очень трудно. Для меня это действительно сложно. Но я вам признателен за вашу большую работу.
Пожалуйста. Если не получается — не зацикливайтесь, продолжайте изучение со следующих уроков.
спасибо большое за статьи, благодаря вам, я наконец начал понимать ООП!
Спасибо, что читаете 🙂
Фейнман говорил о квантовой физике… думаю, применимо и к ООП:
— Если Вам кажется, что Вы поняли квантовую физику, значит Вы ничего не понимаете в квантовой физике
🙂
Ануар, ничего, это ничего.
Главное не сдавайтесь и все получиться.
Я тоже было месяц-полтора назад запутался с void функцией, задание с падающим мячиком.
Карты те же, сложно было, но уже тут и пусть немного неуверенно, но я понимаю как использовать списки инициализации в конструкторах, а это старт для большего.
Все мы с чего-то начинали.