Разница между "new T()" и "new T"

Начнем с new T().

Стандарт говорит нам, что если Т является POD-классом (не объектно-ориентированной сущностью), то объект будет инициализирован значением по умолчанию (обычно, например, для арифметических типов это 0), а если это не POD-класс (явная объектно-ориентированная сущность), то для него вызовется конструктор по умолчанию (либо явный, либо созданный компилятором). Если конструктор по умолчанию задан явно, то будет вызван только он, и вся ответственность за инициализацию ляжет на него. Никой инициализации по умолчанию больше не будет. Если же конструктор по умолчанию не задан явно, и компилятор создал его сам, и в этом случае все члены класса будут проинициализированы неявно: POD-объекты будут проинициализированы нулем, а для не-POD объектов будет проведена инициализация по умолчанию (включая всех его дочерних составляющих — рекурсивный обход всех подобъектов и их инициализация по такому же принципу).

Теперь new T.

В этом случае для POD-объектов вообще не будет никакой инициализации (что было в памяти на момент распределения, то и будет). Для не POD-объекта просто будет вызван конструктор по умолчанию (либо явный, ли заданный компилятором по умолчанию), и не будет проводиться никакой инициализации POD-составляющих этого объекта.

Для простоты, POD-типами (Plain Old Data) является все наследие языка С в С++. Везде, где есть объектно-ориентированная примесь — это уже не POD-класс. Для не POD-классов нельзя делать никаких предположений о внутренней структуре, расположению в памяти и т.д.

Забавно, структура:

struct A {
  int b;
};

является POD-типом, а вот если добавить в нее, например, слово public:

struct A {
public:
  int b;
};

то по стандарту это не POD-объект, и его нельзя уже трогать на уровне внутреннего представления, например обнулить через memset. Хотя многие компиляторы разрешают такие “игры” с не POD-объектами и, программа может в принципе работать, но это против стандарта, и, конечно, против переносимости программы.

Итак, описание различий весьма путанное, поэтому лучше рассмотреть пример.

Для чистоты эксперимента я буду использовать так называемое распределение памяти с размещением. То есть я вручную указываю, в каком месте памяти должен будет создаваться объект. Это позволит контролировать “непредсказуемые” значения неинициализированной памяти.

Итак, первый пример:

#include <iostream>
#include <cstdlib>

class T {
public:
  // Для простоты экспериментируем на однобайтовом типе.
  unsigned char n;
};

int main() {
  // "Случайная" память для создания объекта.
  // Берем с запасом, чтобы уж точно вместить объект класса T.
  char p[10240];

  // Заполняем память числом 170 (0xAA)
  std::memset(p, 170 /* 0xAA */, sizeof(p));
  // Создаем объект явно в памяти, заполненной числом 170.
  T* a = new (p) T;
  std::cout << "new T: T.n = " << (int)a->n << std::endl;

  // Заполняем память числом 171 (0xAB)
  std::memset(p, 171 /* 0xAB */, sizeof(p));

  // Создаем объект явно в памяти, заполненной числом 171.
  T* b = new (p) T();
  std::cout << "new T(): T.n = " << (int)b->n << std::endl;

  return 0;
}

Данный пример выведет:

new T: T.n = 170
new T(): T.n = 0

Видно, что для new T элемент T.n так остался неинициализированным и равным числу 170, которые заполнили память заранее. Для new T() же в свою очередь элемент T.n стал равны нулю, то есть он был проинициализирован. Все, как сказано в стандарте.

Теперь изменим одну маленькую деталь — добавим в класс Т явный конструктор:

class T {
public:
  // Явный конструктор.
  T() {}
  // Для простоты экспериментируем на однобайтовом типе.
  unsigned char n;
};

Теперь нас ждет сюрприз. Теперь программа будет выводить следующее:

new T: T.n = 170
new T(): T.n = 171

Получается, что даже при new T() элемент T.n не был более инициализирован. Почему? А потому, что стандарт гласит: если задан явный конструктор класса, то никакие инициализации по умолчанию для POD-объектов не производятся. Раз программист задал конструктор явно, значит он знает что делает, и вся ответственность за инициализацию теперь на его плечах.

Лично для себя я всегда предпочитаю писать new T() хотя бы для единообразия вызова конструкторов. Также я всегда явно инициализирую все POD-объекты вручную в конструкторе или в его списке инициализации. Отсутствие предположений о значении POD-типов по умолчанию и инициализация их принудительно позволяет избежать сюрпризов при смене компилятора.

Другие посты по теме:


Оригинальный пост | Disclaimer

Комментарии