Pull to refresh

[C++] Всё ли мы знаем об операторах new и delete?

Reading time 4 min
Views 81K
Привет! Ниже речь пойдет об известных всем операторах new и delete, точнее о том, о чем не пишут в книгах (по крайней мере в книгах для начинающих).
На написание данной статьи меня побудило часто встречаемое заблуждение по поводу new и delete, которое я постоянно вижу на форумах и даже(!!!) в некоторых книгах.
Все ли мы знаем, что такое на самом деле new и delete? Или только думаем, что знаем?
Эта статья поможет вам разобраться с этим (ну, а те, кто знают, могут покритиковать:))

Note: ниже пойдет речь исключительно об операторе new, для других форм оператора new и для всех форм оператора delete все ниженаписанное также является правдой и применимо по аналогии.

Итак, начнем с того, что обычно пишут в книгах для начинающих, когда описывают new (текст взят «с потолка», но вцелом соответствует правде):
Оператор new выделяет память больше или равную требуемому размеру и, в отличие от функций языка С, вызывает конструктор(ы) для объекта(ов), под которые память выделена… вы можете перегрузить (где-то пишут реализовать) оператор new под свои нужды.

И для примера показывают примитивную перегрузку (реализацию) оператора new, прототип которого выглядит так
void* operator new (std::size_t size) throw (std::bad_alloc);

На что хочется обратить внимание:
1. Нигде не разделяют new key-word языка С++ и оператор new, везде о них говорят как об одной сущности.
2. Везде пишут, что new вызывает конструктор(ы) для объекта(ов).
И первое и второе является распространенным заблуждением.

Но не будем надеяться на книги для начинающих, обратимся к Стандарту, а именно к разделу 5.3.4 и к 18.6.1, в которых собственно и раскрывается (точнее приоткрывается) тема данной статьи.
5.3.4
The new-expression attempts to create an object of the type-id (8.1) or new-type-id to which it is applied. /*дальше нам не интересно*/
18.6.1
void* operator new(std::size_t size) throw(std::bad_alloc);
Effects: The allocation function called by a new-expression (5.3.4) to allocate size bytes of
storage suitably aligned to represent any object of that size /*дальше нам не интересно*/


Тут мы уже видим, что в первом случае new именуется как expression, а во втором он объявлен как operator. И это действительно 2 разные сущности!
Попробуем разобраться почему так, для этого нам понадобятся ассемблерные листинги, полученные после компиляции кода, использующего new. Ну, а теперь обо все по порядку.

new-expression — это оператор языка, такой же как if, while и т.д. (хотя if, while и т.д. все же именуются как statement, но отбросим лирику) Т.е. встречая его в листинге компилятор генерирует определенный код, соответствующий этому оператору. Так же new — это одно из key-words языка С++, что еще раз подтверждает его общность с if'ами, for'ами и т.п. А operator new() в свою очередь — это просто одноименная функция языка С++, поведение которой можно переопределить. ВАЖНОoperator new() НЕ вызывает конструктор(ы) для объекта(ов), под который(ые) выделяется память. Он просто выделяет память нужного размера и все. Его отличие от сишных функций в том, что он может бросить исключение и его можно переопределить, а так же сделать оператором для отдельно взятого класса, тем самым переопределить его только для этого класса (остальное вспомните сами:)).
А вот new-expression как раз и вызывает конструктор(ы) объекта(ов). Хотя правильней сказать, что он тоже ничего не вызывает, просто, встречая его, компилятор генерирует код вызова конструктора(ов).

Для полноты картины рассмотрим следующий пример:

#include <iostream>

class Foo
{
public:
    Foo() 
    {
        std::cout << "Foo()" << std::endl;
    }
};

int main ()
{
    Foo *bar = new Foo;
}


после исполнения данного кода, как и ожидалось, будет напечатано «Foo()». Разберемся почему, для этого понадобится заглянуть в ассемблер, который я немного прокомментировал для удобства.
(код получен компилятором cl, используемым в MSVS 2012, хотя в основном я использую gcc, но это к делу не относится)
/Foo *bar = new Foo;
push        1  ; размер в байтах для объекта Foo
call        operator new (02013D4h)  ; вызываем operator new
pop         ecx 
mov         dword ptr [ebp-0E0h],eax  ; записываем указатель, вернувшийся из new, в bar
and         dword ptr [ebp-4],0  
cmp         dword ptr [ebp-0E0h],0  ; проверяем не 0 ли записался в bar
je          main+69h (0204990h)  ; если 0, то уходим отсюда (возможно вообще из main или в какой-то обработчик, в данном случае неважно)
mov         ecx,dword ptr [ebp-0E0h]  ; кладем указатель на выделенную память в ecx (MSVS всегда передает this в ecx(rcx))
call        Foo::Foo (02011DBh)  ; и вызываем конструктор
; дальше не интересно

Для тех, кто ничего не понял, вот (почти) аналог того, что получилось на сиподобном псевдокоде (т.е. не надо пробовать это компилировать :))
Foo *bar = operator new (1); // где 1 - требуемый размер
bar->Foo(); // вызываем конструктор


Приведенный код подтверждает все, написанное выше, а именно:
1. оператор (языка) new и operator new() — это НЕ одно и тоже.
2. operator new() НЕ вызывает конструктор(ы)
3. вызов конструктора(ов) генерирует компилятор, встречая в коде key-word «new»

Итог: надеюсь, эта статья помогла вам понять разницу между new-expressionи operator new() или даже узнать, что она (эта разница) вообще существует, если кто-то не знал.

P.S. оператор delete и operator delete() имеют аналогичное различие, поэтому в начале статьи я сказал, что не буду его описывать. Думаю, теперь вы поняли, почему его описание не имеет смысла и сможете самостоятельно проверить справедливость написанного выше для delete.

Update:
Хабражитель с ником khim в личной переписке предложил следующий код, который хорошо демонстрирует суть написанного выше.
#include <iostream>

class Test {
public:
    Test() {
        std::cout << "Test::Test()" << std::endl;
    }

    void* operator new (std::size_t size) throw (std::bad_alloc) {
        std::cout << "Test::operator new(" << size << ")" << std::endl;
        return ::operator new(size);
    }
};

int main() {
    Test *t = new Test();
    void *p = Test::operator new(100); // 100 для различия в выводе
}

Этот код выведет следующее
Test::operator new(1)
Test::Test()
Test::operator new(100)

что и следовало ожидать.
Tags:
Hubs:
+62
Comments 40
Comments Comments 40

Articles