Что вы знаете об ограничениях размера стека вызовов в C++?

«Что вы знаете об ограничениях размера стека вызовов в C++?» — вопрос из категории C++ Core, который задают на 25% собеседований C/C++ Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Стек вызовов — это область памяти, выделяемая для каждого потока, где хранятся локальные переменные, аргументы функций и адреса возврата. Его размер жестко ограничен и обычно невелик.

Ключевые аспекты:

  • Типичный размер: По умолчанию в Linux — 8 МБ (можно проверить и изменить командой ulimit -s). В Windows для основного потока — 1 МБ, для создаваемых потоков — по умолчанию тоже 1 МБ, но его можно задать в std::thread.
  • Причины переполнения (Stack Overflow):
    • Глубокая или бесконечная рекурсия.
    • Создание больших объектов на стеке (например, std::array<int, 1000000> или char buffer[1024 * 1024]).
    • Много вложенных вызовов функций с большим количеством локальных переменных.

Пример проблемного кода:

void recursiveFunction(int depth) {
    int localArray[1000]; // Выделяет 4 КБ на стеке при каждом вызове
    // ... какая-то работа
    if (depth > 0) {
        recursiveFunction(depth - 1); // При depth > 2000 почти гарантированно переполнит стек
    }
}

Стратегии предотвращения и решения:

  1. Избегать глубокой рекурсии. Переписывать рекурсивные алгоритмы на итеративные, если глубина может быть большой.
  2. Выделять большие данные в куче. Использовать std::vector, std::make_unique или new.
  3. Явно увеличивать размер стека потока. Например, в POSIX с помощью pthread_attr_setstacksize, а в C++11 — передавая нужный размер в конструктор std::thread.
  4. Использовать -fstack-usage (GCC/Clang) для анализа использования стека функциями.

Практический пример замены рекурсии на итерацию:

// Рекурсивный обход (опасен при большой глубине)
void traverseTreeRecursive(TreeNode* node) {
    if (!node) return;
    process(node);
    traverseTreeRecursive(node->left);
    traverseTreeRecursive(node->right);
}

// Итеративный обход с использованием стека в куче (std::stack)
void traverseTreeIterative(TreeNode* root) {
    std::stack<TreeNode*> nodeStack;
    nodeStack.push(root);
    while (!nodeStack.empty()) {
        TreeNode* current = nodeStack.top();
        nodeStack.pop();
        if (current) {
            process(current);
            nodeStack.push(current->right); // Сначала кладем правый
            nodeStack.push(current->left);  // Затем левый
        }
    }
}