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

Привет! Ниже речь пойдет об известных всем операторах 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)

что и следовало ожидать.
Поделиться публикацией

Похожие публикации

Комментарии 40
    +20
    Есть замечание по терминологии.
    Для начала опеределимся с терминологией — далее именуемый в стандарте new-expression я буду называть «оператор new» (...), а operator new я так и буду называть operator new. (...)

    оператор new — это оператор языка, такой же как if, while и т.д. (хотя if, while и т.д. все же именуются как statement, но отбросим лирику)

    Во-первых, отличить на глаз «оператор» от «operator» в потоке текста очень сложно (особенно тем, кто много читает на английском). Во-вторых, раз уж вы пытаетесь рассмотреть разницу между этими понятиями, называть их одним и тем же словом неразумно. Можно окончательно запутать читателя. Конечно, и statement можно перевести как «оператор», но лучше не надо. Это не «лирика», а важное различие на уровне компилятора. (Пишу не в личные сообщения, потому что вопрос спорный и подлежит обсуждению.)
      +1
      Можно было, например, опустить слово «оператор/operator». Все равно дальше по тексту одна из форм всегда используется со скобками, другая — без них.
        0
        Согласен, исправлю.
          +5
          В наиболее распространенном русском переводе «The C++ Programming Language» этой теме был посвящен довольно-таки большой параграф во вступлении от переводчиков. В той книге они использовали для перевода «statement» слово «инструкция». Я думаю, достаточно большое количество русскоговорящих разработчиков на C++ начинали именно с этой книги, или, во всяком случае, читали её на каком-то этапе — так что в плане терминологии её можно использовать как некоторого рода стандарт.
          +9
          Статья конечно может «как-то» помочь новичкам.
          Но! Не знаю о каких книгах вы говорите (судя по всему о дейтелах и им подобных), но в Мейерсе, Саттере, Дьюхерсте, Вилсоне и даже в Джосютиссе (не представляю какие еще книги по С++ можно читать, ну кроме Страуструпа конечно) четко разделяется оператор new от выражения new.
          Думаю (это критика, которую вы хотели) вы просто только что открыли для себя Америку и хотели раксрыть «заговор» всему миру, но мир еще десять лет назад знал об этом ка свои пять пальцев.

          P. S. Ставлю плюс, чтоб не разбить стремление к sharing-у знаний.
            +4
            >Не знаю о каких книгах вы говорите
            Я же написал «в книгах для начинающих»
            За критику спасибо, но не каждый С++'ник может «на пальцах» объяснить где и как вызывается конструктор при использовании new, поэтому
            >мир еще десять лет назад знал об этом
            считаю не совсем уместным. Мир то знал, а вот некоторые программисты до сих пор не знают)
              0
              :) Теперь будут знать, и еще будут знать какие книги читать ;)
                +1
                не каждый С++'ник может «на пальцах» объяснить где и как вызывается конструктор при использовании new, поэтому

                Этими знаниями можно блеснуть на собеседовании (но мне такой вопрос ни разу не задавали и я кажется знаю почему), но какой от них практический толк?

                Ну вот не знаю я (или просто забыл) что operator new != keyword new, но я примерно помню (читал не раз) как переопределяется свой operator new. А теперь я знаю (вспомнил) что operator new != keyword new… и… ничего не изменилось, я все еще не знаю накой мне это может понадобится и тем более на какие грабли при этом я встану (я ведь знания как правильно переопределять operator new гораздо важнее чем знания, что operator new != keyword new).

                Какой профит?
                  –2
                  Какой профит?

                  В прикладном плане никакого. Но неужели не хочется знать чуть больше, чем необходимо? :)
                    0
                    А если включить мозг? И вспомнить, что и глобальный оператор можно только заменить, а локальный перегрузить как угодно?
                      0
                      А какое отношение это имеет к написанному в статье?
                        +1
                        Трампарарам., вы говорите не знаете как применить на практике различие межу кейвордом и оператором. А я уже говорю, что оператор можно перегрузить как угодно. Уже только из этого вытекает масса различным применений.

                        А если заменить реализацию кейворда на свою, а оператор не трогать? А если сделать и то и другое?
                          0
                          Как вы кейворд переопределите? Единственный способ — это взять исходники компилятора, подправить там код обработки new и скомпилировать свой компилятор.
                +2
                > но мир еще десять лет назад знал об этом ка свои пять пальцев

                тридцать лет назад :-)
                  +2
                  Время летит… :)
                  –2
                  Можно Джефферсона еще почитать) А об авторах, которых Вы перечислили даже не слышал, ну кроме Страуступа, конечно)
                  +1
                  Имхо проще в русском тексте так и писать — «new-expression» и «operator new» вместо «оператор» и «operator».
                    0
                    Да, подумал и исправил именно на этот вариант.
                    +1
                    Советую к прочтению: www.amse.ru/courses/cpp2/2011_03_21.html
                      +3
                      Прочитав про две разные сущности, ожидал дальше увидеть два разных примера применения каждой из них. Но не увидел. Как можно воспользоваться этим различием между new-оператором и new-ключевым словом в процессе разработки?
                        0
                        Не помню где точно, но вроде бы на Хабре проскакивала эта ссылка — там описывают разницу между «оператором new» и «new оператором» :)
                          0
                          Прочитав про две разные сущности, ожидал дальше увидеть два разных примера применения каждой из них.

                          Эм, здесь речь немного не об этом, пример реализации operator new() можно найти в любой книге, поэтому я не стал его включать в статью.
                          Как можно воспользоваться этим различием… в процессе разработки?

                          В процессе разработки, думаю, никак. Просто такие вещи полезно знать и понимать хотя бы для самого себя.
                            +2
                            Как это нигде? А как же placement new?
                            char buf[10]; Foo * f = new(buf) Foo(123); — как раз и будет пример new-expression.
                          +1
                          Ну не знаю, я бы привёл реализацию operator new (msvc10):
                          _C_LIB_DECL
                          int __cdecl _callnewh(size_t size) _THROW1(_STD bad_alloc);
                          _END_C_LIB_DECL
                          
                          void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
                                  {       // try to allocate size bytes
                                  void *p;
                                  while ((p = malloc(size)) == 0)
                                          if (_callnewh(size) == 0)
                                          {       // report no memory
                                          static const std::bad_alloc nomem;
                                          _RAISE(nomem);
                                          }
                          
                                  return (p);
                                  }
                          

                          становится ясно, что operator new просто выделяет память, причём тем-же malloc — при неудачном выделении памяти — вызывает new_handler функцию и только потом (если ничего не изменилось) бросает исключение.
                            +2
                            1. Нигде не разделяют new key-word языка С++ и оператор new, везде о них говорят как об одной сущности.
                            Как это нигде, Мейерс разделяет, стандарт разделяет :-)
                              –1
                              Ну и мы с вами :)
                              Под «нигде» я подразумевал книги для начинающих.
                              +1
                              Кстати, похожая ситуация с operator ->() и оператором ->, который для пользовательских типов преобразуется в:
                              (object.operator ->())->member
                                +2
                                Читаем «C++ For Real Programmers» (Jeff Alger) и ещё много нового о работе с памятью.
                                  0
                                  А при чем тут работа с памятью?
                                    0
                                    Для гибкой работы с памятью дали возможность перегрузить operator new
                                    habrahabr.ru/post/148657/
                                      0
                                      Ну это да, как и delete. Просто в статье речь не о работе с памятью и не об использовании new / delete.
                                        0
                                        Ну, new и operator new() предназначены — внезапно — для управления памятью. А в книге, которую я порекомендовал, помимо озвученного в статье материала, есть ещё куча информации об этих сущностях, а также об управлении памятью в C++ вообще. Информация в статье интересная, но сама по себе бесполезная, вот книга и даст остальную информацию, необходимую для того, чтобы, например, реализовать сборку мусора и дефрагментацию памяти.
                                  0
                                  Вроде банальный момент, который сразу всплывает когда в коде встречаются 'new int' и 'new Foo'.
                                    +1
                                    Да неужели?

                                    int* p = new int(10);//как бе сконструировали инт
                                    Foo* p_foo = (Foo*)malloc(sizeof(Foo));//ниче не конструировали
                                    new(p_foo) Foo(19, false,«description»);//вау — обьект сконструировался!
                                    0
                                    у кого-нибудь корректно получалось в своей жизни хоть раз использовать placement syntax new для массивов?
                                    у меня он так и не стал добавлять нужные оверхеды и пришлось везде юзать обычный placement syntax внутри цикла.
                                      0
                                      А в чем проблема? У меня вроде все работало.
                                        0
                                        грубо говоря как то так работало:
                                        T* pNew = (T*)m_BasicCallback->malloc_cb(sizeof(T)*size,1);
                                        //copy old elements
                                        T* raw_i = pNew;
                                        int i=0;
                                        for(;i<m_Size;++i,++raw_i){
                                        raw_i = new (raw_i) T(m_pData[i]);
                                        }

                                        а вот какую-то запись которая бы и память корректно выделила и конструкторы для всех экземпляров массива вызвала не получилось. вариант для массивов памяти выделял почему-то меньше чем было нужно.
                                          0
                                          Хм, вот такой пример:
                                           char *ptr = (char*) malloc(10);
                                              memset(ptr, 1, 10);
                                          
                                              char* ptrPlacement = new(ptr) char[10];
                                              ptrPlacement[9]=10;
                                              free(ptr);
                                          

                                          Проблем в упор не вижу (Ну кроме того, что если вместо char будет класс, то придется делать прямой вызов деструктора, а потом делать free)
                                            0
                                            Для динамически созданной группы обьектов нельзя применять конструктор с параметрами.
                                        0
                                        Спасибо большое за статью! Для меня этот вопрос с оператором new создал путаницу в голове. Теперь все стало ясно.

                                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                        Самое читаемое