*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***

пятница, 20 февраля 2009 г.

Разница между 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-типов по умолчанию и инициализация их принудительно позволяет избежать сюрпризов при смене компилятора.


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

3 комментария:

  1. > Лично для себя я всегда предпочитаю писать new T() хотя бы для единообразия вызова конструкторов

    Одна проблема:
    T x();

    это не локальный объект x типа T, а прототип функции.

    ОтветитьУдалить
  2. cd-riper: Согласен. При вызове конструктора без использования new есть неоднозначность в грамматике, когда выражение может выглядеть как и вызов конструктора и как декларация функции, и по стандарту в этом случае компилятор рассматривает это как декларацию функции.

    Я немного покопался в этой теме недавно - http://easy-coding.blogspot.com/2009/02/c.html.

    Благо Мейерс и Саттер все детально разжевали этот кислый вопрос.

    Уточню свой подход: для "new" я обычно пишу "new T()", ну а для автоматического объявления приходится писать просто "T x".

    ОтветитьУдалить
  3. Александр, Вы пишете:
    >Если же конструктор по умолчанию не задан явно, и компилятор создал его сам, и в этом случае все члены класса будут проинициализированы неявно: POD-объекты будут проинициализированы нулем, а для не-POD объектов будет проведена инициализация по умолчанию (включая всех его дочерних составляющих — рекурсивный обход всех подобъектов и их инициализация по такому же принципу).
    Это полностью соответствует стандарту (5.3.4/15), но его не выполняет ни один из компиляторов, на которых я тестировал эту проблему, а именно VS разных версий и gcc(g++). Для того, чтобы убедиться в этом, достаточно добавить не-POD поле в структуру T. В обоих случаях в поле n будет мусор. То есть для не-POD данных new T() аналогично new T. По поводу VS есть официальный баг: http://connect.microsoft.com/VisualStudio/feedback/details/100744/value-initialization-in-new-expression , но он "Closed as Won't Fix". Так что, видимо, правильнее будет придерживаться Вашего совета:
    >Также я всегда явно инициализирую все POD-объекты вручную в конструкторе или в его списке инициализации.

    ОтветитьУдалить