12.6 – Списки инициализаторов членов в конструкторах

Добавлено 3 июля 2021 в 20:58

В предыдущем уроке для простоты мы инициализировали члены данных нашего класса в конструкторе с помощью оператора присваивания. Например:

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. Не инициализируйте переменные-члены таким образом, чтобы они зависели от других инициализируемых переменных-членов (другими словами, убедитесь, что ваши переменные-члены будут правильно инициализированы, даже если порядок инициализации будет отличаться).
  2. Инициализируйте переменные в списке инициализаторов в том же порядке, в котором они объявлены в вашем классе. Это не обязательно, если выполняются предыдущая рекомендация, но ваш компилятор может выдать вам предупреждение, если вы так не сделаете и у вас включены все предупреждения.

Резюме

Списки инициализаторов членов позволяют нам инициализировать наши члены, а не присваивать им значения. Это единственный способ инициализировать члены, которым требуются значения при инициализации, например, константные или ссылочные члены, и это может быть более производительным, чем присваивание значений в теле конструктора. Списки инициализаторов членов работают как с базовыми типами, так и с членами, которые сами являются классами.

Небольшой тест

Вопрос 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;
}

Теги

C++ / CppLearnCppДля начинающихКласс (программирование)Конструктор / Constructor / ctor (программирование)ОбучениеПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.