Память в C/C++ под наши объекты, как известно, можно выделять на стеке, а можно в куче. На стеке она обычнно выделяется автоматически и нам об этом сильно беспокоиться не надо.
int32_t foo(int32_t x, int32_t y) {
int32_t z = x + y;
return z;
}
При вызове функции foo
на стеке, после аргументов (хотя не факт что аргументы будут переданы через стек), будет выделено (просто вершина стека будет сдвинута) еще 4 байта (а может быть и больше, кто ж знает, что там настроено у компилятора!) под переменную z. А может быть и не будет выделено (например, если компилятор оптимизирует переменную и сложит результат сразу в регистр rax
). Дикая природа удивительна, неправда ли?
Освобождается память со стека тоже автоматически. Причем уже не обычно, а всегда. Если только, конечно, вы случайно не сломали стек, не сделали чудовищную ассемблерную вставку и теперь адрес возврата не ведет куда-то не туда или не используете attribute ( ( naked ) )
. Но, мне кажется, в этих случаях у вас куда более серьезные проблемы... Во всех остальных случаях память со стека освобожается автоматически. Потому, как известно, вот такой код порождает висячий указатель
int32_t* foo(int32_t x, int32_t y) {
int32_t z = x + y;
return &z;
}
Обычно для выделения чего-то на стеке размер этого чего-то должен быть заранее известен на этапе компиляции. Обычно, но не всегда...
Однажды один большой и сложный HTTP сервер внезапно упал. Упал он, как ни странно, с моим любимым сообщением segmentation fault (core dumped). К этому все впрочем уже привыкли, ведь HTTP сервер был написан на чистом и прекрасном C. Так что падение -- это что-то само собой разумеюшееся.
Содержимое core файла было загадочным: строчка, на которую указывал dump, не делала ничего страшного. Она не разменовывала указатель, не писала в массив, не читала из массива, не освобождала память, не выделяля память... Ничего. Она просто пыталась вызвать функцию и передать в нее параметры. Но что-то пошло не так.
У нее закончился стэк.
Но как же так?! В core дампе было всего от силы 40 стэк фреймов! Как он мог закончится? Там же 10 мегабайт под Linux!
Путешествуя по этому стэк трейсу, я поднялся на пять стэк фреймов выше. Все они были довольно небольшого размера. 40 байт, 180, килобайт... А вот шестой фрейм оказался невероятно большим! 8 мегабайт!
Открывши соответствующий исходник, я обнаружил:
int encoded_len = request->content_len * 4 / 3 + 1;
char encoded_buffer[encoded_len];
encoded_len = encode_base64(request->content, content_len, encoded_buffer, encoded_len);
process(encoded_buffer, enconded_len);
Знакомьтесь, variable length array (VLA)! Прекрасная фича языка C. Существует в языке C++ как нестандартное расширение (MSVC не поддердживает, в GCC и Clang компилируется).
Это массив переменной длины на стеке. Причина падения была ясна.
VLA -- концептуально, фича довольно полезная. Но крайне небезопасная.
Вам нужен буффер, но его длину вы узнаете только в runtime? Пожалуйста, VLA! Не нужен никакой malloc
/new
-- просто объяви массив и укажи длину! К тому же это в среднем намного быстрее чем malloc
и не утечет, автоматически освободится! Ну и конечно же,
что может быть лучше чем получить segfault вместо out-of-memory?
Лучше VLA может быть только прямое использование функции alloca()
. Ведь в отличие от VLA, у нее намного больше вариативности по отрыванию ног
void fill(char* ptr, int n) {
for (int i =0;i<n;++i) {
ptr[i] = i * i;
}
}
int use_alloca(int n) {
char* ptr = (char*)alloca(n);
fill(ptr, n);
return ptr[n-1];
}
int main() {
int n = 0;
for (int i = 1; i < 10000; ++i) {
n += use_alloca(i);
}
return n ? 0 : 1;
}
Тут каждый вызов alloca
не приводит к переполнению стека сам по себе. Но
если use_alloca
будет заинлайнена компилятором по какой-либо причине, мы получим SIGSEGV
Использование alloca
и VLA крайне не рекомендуется. man упоминает случай, когда их использование может быть оправдано: ваш код полагается на setjmp/longjump и нормальный менеджмент динамически выделенной памяти можеь быть осложнен, а стек все равно будет очищен даже при longjmp. Не буду спрашивать, зачем оно вам...
alloca и vla действительно в среднем быстрее чем динамическая аллокация. Но если уж нужно действительно быстро, то вариант с преаллоцированным массивом или массивом фиксированной длины получше будет
А В C++ (без расширений) VLA нет. Там есть шаблоны, а они не дружат с VLA.
#include <iostream>
template <size_t N>
void test_array(int (&arr)[N]) {
std::cout << sizeof(arr) << "\n";
}
int main(int argc, char* argv[]) {
int fixed[15];
int vla[argc];
test_array(fixed);
test_array(vla); // compilation error
}