Топ контрибуторов
loading
loading
Знаете ли Вы, что

После прохождения теста можно комментировать вопросы теста, а Ваши комментарии увидят модераторы теста и пользователи, которым когда-либо эти вопросы попадались.

Лента обновлений
ссылка Oct 22 23:24
Комментарий от alexcei88:
В вопросе переменная s даже не выводиться, а выводитьс...
ссылка Oct 22 17:46
Комментарий от AlexFurm:
Это UB, так можно вызывать только статические функции ч...
ссылка Oct 22 17:43
Комментарий от AlexFurm:
Любые битовые операции с signed это UB
ссылка Oct 21 20:30
Комментарий от yoori:
Любой вариант скомпилируется если компилировать не в конеч...
ссылка Oct 21 16:53
Добавлен вопрос в тест QA (Quality Assurance)
Статистика

Тестов: 153, вопросов: 8596. Пройдено: 443367 / 2177507.

Порядок инициализации объектов в C++

head tail Статья
категория
C++
дата27.07.2009
авторanalizer
голосов18

Что было раньше, курица или яйцо?

В данной статье я постараюсь раскрыть некоторые моменты связанные с порядком инициализации объектов и их полей в C++. Частично (очень частично) данная статься повторяет Основы C++ (part 1), тем не менее повторяющиеся части я решил оставить, дабы сохранить полноту картины и попытаться более полно раскрыть суть.

Итак, приступим. Инициализацию объекта можно разбить на четые последовательных этапа:

  1. Инициализация виртуальных базовых классов
  2. Инициализация прямых базовых классов
  3. Инициализации полей объекта
    1. Константные поля, поля-ссылки и оператор копирования
    2. Инициализация статических полей
    3. Порядок инициализации полей
  4. Выполнение тела конструктора

При первом знакомстве с C++ возникает ошибочное мнение что инициализация некоего поля объекта - это присвоение ему значения внутри конструктора, например так:

struct A
{
	int a;
	A();
};

A::A()
{
	a = 1;
}

На самом деле, инициализация полей объекта будет завершена к моменту выполнения входа в конструктор, т.е. инициализация полей произойдёт где-то между A::A() и {, а в строчке a = 1; будет всего лишь произведено присваивание уже проинициализированной переменой. Данный способ не позволит инициализировать заданными значениями базовые классы, константные поля, а также поля-ссылки.

Для инициализации полей, а также базовых классов используется список инициализации. Список инициализации отделён от заголовка класса двоеточием и имеет вид "имя поля или базового класса (значение)".

Инициализация виртуальных базовых классов

Виртуальные базовые классы инициализируются в том порядке, в котором они находятся в дереве наследования, если бы мы обходили его "слева-направо". Т.е вне зависимости от того как они будут перечислены в списке инициализации. Таким образом имея иерархию классов приведённую ниже, при создании объекта класса G, порядок инициализации виртуальных классов будет C, D, A, B, причём конструктор каждого такого класса будет вызван лишь единожды, несмотря на повторение включения класса C:

class A {};
class B {};
class C {};
class D {};
class E : virtual A, virtual B, virtual C {};
class F : virtual C, virtual D {};
class G : F, E {};

Также важной особенностью виртуального наследования является тот факт, что конструирование виртуальных базовых классов производит конструктор того класса, объект которого мы создаём, а не конструкторы тех классов которые производят виртуальное наследование. Т.е.:

struct A
{
	A(int i);
};

struct B : virtual A
{
	B();
};

struct C : public B
{
	C() : A(int()){}
};

На данной особенности можно построить эмуляцию ключевых слов final из языка Java или sealed из C#, которые запрещают наследование от данного класса:


class A_finalizer
{
	A_finalizer(){}
	friend class A;
};

class A : virtual A_finalizer
{};

class B : A
{};

int main()
{
	A a;
	B b;
}

Инициализация прямых базовых классов

Инициализация прямых базовых классов происходит в той же последовательности в которой они указаны в списке наследования класса, вне зависимости от того как они будут перечислены в списке инициализации.

Инициализации полей объекта

Как было сказано выше, без использования списка инициализации невозможно задать значения полей-констант и полей ссылок. В следующем примере конструктор класса A инициализирует константное поле i_ переданным значением, а в классе-потомке B происходит инициализация поля j_ и поля-ссылки rj_:

struct A{
	const int i_;
	A(int i)
		: i_(i)
	{}
};

struct B : A
{
	int j_;
	int& rj_;
	B(int i, int j)
		: A(i), j_(j), rj_(j_)
	{}
};

Среди прочего стоит также отметить тот факт, к полям-ссылкам (а также к mutable и статическим полям) не применяется ключевое слово const применённое к объекту. Т.е. появляется возможность записи в поля объекта, который является константным. Таким образом следующий код выведет на экран "3", хотя использование данного приёма, если подходить формально, влечёт за собой undefined behavior:


#include <iostream>
int main()
{
	const B b(1,2);
	b.rj_ = 3;
	std::cout<<b.j_<<std::endl;
}

Константные поля, поля-ссылки и оператор копирования

A::A(const A& a){ *this = a; }
долгое время я считал такой код нормальным

Итак, поля всевозможные иницализировать мы умеем, классы получаются аккуратные, всё поля которые не изменяются на протяжении жизни объекта объявлены константными и прочее. Но ВНЕЗАПНО приходит осознание того, что архитектура программы требует наличия у объектов оператора копирования. Как поступить? Убирать у полей спецификаторы const и чем-то заменять ссылки? Довольно спорное решение. Всё можно сделать куда проще. Что собой являет оператор копирования в большинстве случаев? Освобождение уже захваченных ресурсов, а затем захват новых с новыми параметрами. Фактически то же что и последовательный вызов деструктора, а затем конструктора. C++ позволяет это сделать. Например следующим образом:

#include <new>
struct A
{
	const int i_;
	A(int i)
		: i_(i)
	{}
	A& operator = (const A& other)
	{
		if ( this != &other )
		{
			this->~A();
			new (this) A(other);
		}
		return *this;
	}
};

Т.е., как и требовалось, мы сначала уничтожаем существующий объект, а затем на его месте создаём новый, при помощи конструктора копирования, в котором можно поместить все необходимые инициализации.

Инициализация статических полей

Прежде всего, статические поля класса или структуры должны быть объявлены (помимо объявления класса) в глобальной (или namespace) области в одной из следующих форм (где T - тип поля, А - имя класса или структуры, x - имя поля):

  • T A::x;
    Если T - POD-тип, то поле будет проинициализировано нулём, в противном случае будет вызван деструктор по-умолчанию, если же он недоступен, то получим ошибку при компиляции
  • T A::x = a;
    Произойдёт явная инициализация объекта значением a. Следует отметить, что автоматического приведения типа a к типу который принимает конструктор T не произойдёт. Т.е. если конструктор T принимает const std::string&, а тип a - const char*, то будем иметь ошибку компиляции типа no suitable constructor exists
  • T A::x(b);
    Тоже что и предыдущий вариант, но с неявным приведением типов, т.е. const char* будет приведён к объекту типа const std::string&, который и будет передан в конструктор
  • T A::x = { c };
    Разрешено только для скалярных типов, эквивалентен T A::x = c;

Очевидно, что если одно из таких объявлений расположено в заголовочном файле, который включён (возможно ненапрямую) в несколько модулей трансляции, то при линковке вы получите ошибку связанную со множественным определением объекта. Поэтому объявления статических полей как правило располагают непосредственно в *.cpp файлах. Инициализация всех статических полей будет выполнена до первого вызова функции или использования объекта определённого в том же модуле трансляции. Все статические объекты объявленные в одном модуле трансляции будут проинициализированы в том же порядке в котором они появляются в данном модуле. Исключение составляют объекты POD-типов, инициализированные константными выражениями, которые будут проинициализированы ещё раньше.

Также особенностью инициализации статических полей является тот факт, что инициализация происходит в области имён класса. Т.е.:

struct A
{
	static int i_;
private:
	static int f()
	{
		return 5;
	}
};

int A::i_=f();

Прошу заметить, что мало того что метод A::f(), доступен без указания имени класса, но также он доступен для вызова несмотря на спецификатор доступа private.

Порядок инициализации полей

С порядком инициализации полей дела обстоят куда проще чем с самой инициализацией - поля инициализируются строго в том порядке в котором они объявлены в классе. Не зависимо от порядка в котором они записаны в списке инициализации. Т.е. следующий код выведет на экран "123", а не "132":

#include <iostream>
struct A
{
	A(int i)
	{
		std::cout<<i
	}
};

struct B
{
	A a1;
	A a2;
	A a3;
	B()
		: a1(1), a3(3), a2(2)
	{}
};

B b;

----

Дмитрий analizer Потапов, июль 2009

Если Вам понравилась статья, проголосуйте за нее

Голосов: 18  loading...
admin   yohan   globus   maximus   alexis112   valyala   Check047   Rastamanka   m1chael   sava_vodka   aleksandru2005   Probleskovy   viperff   ksune4ka_00x   qVlad   SunDrop   MegaByte   mad_rus