Как оператор new в C++ понимает, сколько памяти выделять

Ответ

Оператор new в C++ не «понимает» размер динамически. Размер вычисляется на этапе компиляции на основе типа и передается в низкоуровневую функцию выделения памяти.

Механизм работы new T:

  1. Компилятор вычисляет sizeof(T) с учетом выравнивания (alignof(T)).
  2. Генерируется вызов глобальной функции void* operator new(std::size_t count), куда передается вычисленный размер.
  3. Эта функция запрашивает count байт у менеджера памяти ОС (например, через malloc или системный вызов).
  4. Если тип T имеет нетривиальный конструктор, компилятор затем вызывает его для инициализации памяти.

Пример для массива new T[n]:

MyClass* arr = new MyClass[10];

Компилятор вычислит общий размер как: total_size = sizeof(MyClass) * 10 + возможный_оверхед. Оверхед может потребоваться для хранения размера массива (чтобы delete[] знал, сколько деструкторов вызвать).

На практике:

int* p1 = new int;           // Выделяет sizeof(int) байт (например, 4)
double* p2 = new double[5];  // Выделяет 5 * sizeof(double) байт (например, 5*8=40)

struct alignas(16) Vec4 { float x, y, z, w; };
Vec4* vec = new Vec4; // Выделяет 16 байт с выравниванием 16

Важные детали:

  • Размер известен компилятору: sizeof — это оператор времени компиляции.
  • Выравнивание: Память выделяется с учетом alignof(T). Неправильное выравнивание может привести к падению производительности (на x86) или к ошибке (на ARM).
  • Placement new: new (buffer) T() — не выделяет память, а только конструирует объект в предоставленном буфере buffer. Размер типа все равно известен компилятору.
  • Пользовательский operator new: Можно переопределить, чтобы управлять выделением, но ему все равно передается готовый размер.

Ответ 18+ 🔞

Давай разберём эту тему, а то некоторые думают, что оператор new — это такой магический ящик, который сам всё знает. Хуй с горы! Всё куда прозаичнее.

Вот смотри, когда ты пишешь new T, компилятор — этот хитрая жопа — не бегает в рантайме с линейкой, засовывая её в каждый тип. Нет, ёпта! Он всё вычисляет на этапе компиляции, пока ты кофе пьёшь. Он смотрит на T, берёт оператор sizeof(T), учитывает выравнивание (alignof(T)), получает число байтов и — хуй! — передаёт это число в низкоуровневую функцию выделения памяти. Всё, овердохуища магии.

Как это работает по шагам, если грубо:

  1. Компилятор: «Так, T. sizeof(T) будет столько-то, alignof(T) — столько. Складываем, получаем count».
  2. Компилятор генерирует вызов: «Эй, operator new, на, выдели count байтов, вот тебе размер!».
  3. Функция operator new идёт к менеджеру памяти ОС (чаще всего через malloc) и говорит: «Дашь count байтов?».
  4. Если у T есть конструктор (нетривиальный), компилятор потом дёргает его, чтобы проинициализировать эту свежевыделенную память. Без этого — просто кусок сырой памяти.

Пример с массивом, чтобы вообще всё стало ясно:

MyClass* arr = new MyClass[10];

Что делает компилятор? Он считает: общий_размер = sizeof(MyClass) * 10. Но часто ещё приплюсовывает возможный оверхед — несколько лишних байтов, чтобы delete[] потом знал, сколько объектов разрушать. А то как он иначе узнает, насколько далеко прыгать, вызывая деструкторы? Вот именно, ни хуя себе!

Практика, чтобы закрепить:

int* p1 = new int;           // Выделит sizeof(int) байт. Скорее всего, 4.
double* p2 = new double[5];  // Выделит 5 * sizeof(double). Допустим, 5*8=40.

struct alignas(16) Vec4 { float x, y, z, w; }; // Специальное выравнивание!
Vec4* vec = new Vec4; // Выделит 16 байт, и адрес будет кратен 16. Иначе на некоторых процессорах будет пиздец производительности или вообще ошибка.

Важные детали, которые надо помнить, чтобы не выглядеть ебаньком:

  • Размер известен на этапе компиляции: sizeof — это не функция, а оператор. Всё решается, когда код компилируется. Никакой магии в рантайме.
  • Выравнивание — важно! Память выделяется с учётом alignof(T). Если проигнорировать, на x86 просто будешь терять производительность, а на ARM можешь получить мгновенный краш. Хуй моржовый!
  • Placement new — особый случай: new (buffer) T() — это вообще не про выделение памяти. Это команда: «Вот тут, в готовом буфере buffer, позови конструктор для T». Размер типа компилятор и тут знает, просто память ему уже дали.
  • Можно переопределить operator new: Да, ты можешь написать свою функцию, которая будет выделять память. Но ей всё равно передадут готовый, вычисленный компилятором размер. Твоя задача — просто найти столько байтов и вернуть указатель. Доверия к тому, что ты сделаешь это правильно, ебать ноль, но возможность есть.

Короче, new — это не волшебник, а просто посредник. Вся арифметика делается до запуска программы. Э бошка думай!