Pull to refresh

Порядок инициализации в конструкторах

Reading time 2 min
Views 52K
Итак, вот небольшая программа на C++:

#include <iostream>

class A {
private:
  int a;
  int b;
public:
  A(int x) : b(x), a(b) {}
  void dump() {
    std::cout << "a=" << a << " b=" << b << std::endl;
  }
};

int main() {
  A a(42);
  a.dump();
  return 0;
}

Если вы считаете, что она выдаст

a=42 b=42


То вы обманываетесь, она выдаст что-то вроде

a=4379 b=42

Это произойдёт потому, что компилятор будет инициализировать переменные не в том порядке, в котором они перечислены в строке

A(int x) : b(x), a(b)

а сперва будет инициализирована переменная «a», и лишь потом переменная «b». Так как на момент инициализации «a», переменная «b» ещё имеет неопределённое значение, то и «a» получит неопределённое значение.

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

А в каком же порядке идёт инициализация?


На самом деле, порядок инициализации никак не зависит от порядка в строке

A(int x) : b(x), a(b)

Всё определяется порядком деклараций:

  int a;
  int b;

Если переставить эти две строчки местами, то и порядок инициализации изменится, и конструктор станет работать правильно.

Вы можете убедиться в этом, поигравшись с вот таким примером

#include <iostream>

class S {
private:
  int data;
public:
  S(int x) {
    std::cout << "S(int x) x=" << x << std::endl;
    data = x;
  }
  S(S& x) {
    std::cout << "S(S& x) x.data=" << x.data << std::endl;
    data = x.data;
  }
  int dump() {
    return data;
  }
  ~S() {
    std::cout << "~S() x.data=" << this->data << std::endl;
  }
};

class A {
private:
  S a;   // попробуйте переставить местами
  S b;   // эти две декларации
public:
  A(int x) : b(x), a(b) {}
  void dump() {
    std::cout << "a=" << a.dump() << " b=" << b.dump() << std::endl;
  }
};

int main() {
  A a(1);
  a.dump();
  return 0;
}

У меня но выдал вот такой результат:

S(S& x) x.data=134515845
S(int x) x=1
a=134515845 b=1
~S() x.data=1
~S() x.data=134515845

Обратите внимание, что сперва был выполнен конструктор «S(S& x)». Если же переставить местам декларации, то всё будет работать правильно.

Что это? Баг в С++?


Нет, конечно!

Дело в том, что при удалении объекта, все разрушительные действия должны выполняться в порядке, строго противоположном, порядку конструирования. Вместе с тем, C++ допускает сосуществование нескольких конструкторов. В каком же порядке разрушать части объекта, если порядок инициализации в разных конструкторах различен? Помнить, как именно был создан объект, может оказаться весьма дорого. Остаётся только одно — ввести для всех конструкторов строгий порядок инициализации, не связанный с их кодом.

Что и было сделано.

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

Всем успехов!

upd: на это есть стандарт тут пункт 12.6.2 #5 стр 197.
upd2: или http://www.kuzbass.ru:8086/docs/isocpp/special.html#class.base.init (спасибо EntropiouS)
Tags:
Hubs:
+61
Comments 74
Comments Comments 74

Articles