В чем разница между преинкрементом (++i) и постинкрементом (i++) в C++?

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

Ответ

Разница заключается в значении, которое возвращает оператор, и в потенциальной эффективности.

Семантика

  • Преинкремент (++i): "Увеличить, затем использовать".
    1. Значение i увеличивается на 1.
    2. Возвращается ссылка на уже изменённый объект i.
  • Постинкремент (i++): "Использовать, затем увеличить".
    1. Сохраняется копия исходного значения i.
    2. Значение i увеличивается на 1.
    3. Возвращается сохранённая копия (старое значение).

Наглядный пример с встроенными типами:

int i = 5;
int a = ++i; // Шаг 1: i = 6. Шаг 2: a = i (т.е. 6).
// Результат: i == 6, a == 6

int j = 5;
int b = j++; // Шаг 1: сохранить старое j (5). Шаг 2: j = 6. Шаг 3: b = старое значение (5).
// Результат: j == 6, b == 5

Вопрос эффективности (для пользовательских типов)

Для встроенных типов (int, char) современные компиляторы генерируют идентичный оптимизированный код, и разницы в скорости нет.

Однако для типов с перегруженными операторами (например, итераторы) ++i обычно предпочтительнее:

class MyIterator {
    // Преинкремент: работает с текущим объектом, возвращает ссылку.
    MyIterator& operator++() {
        // ... логика инкремента ...
        return *this;
    }
    // Постинкремент: должен создать копию ДО изменения, вернуть её.
    MyIterator operator++(int) { // int — фиктивный параметр для отличия сигнатур
        MyIterator temp = *this; // ВОТ ОНА, ДОПОЛНИТЕЛЬНАЯ КОПИЯ!
        ++(*this);               // Используем уже реализованный преинкремент
        return temp;             // Возвращаем старую копию
    }
};

Создание временного объекта в operator++(int) может быть накладным для сложных типов.

Рекомендация (best practice):

  • В циклах for всегда используйте ++i. Это привычка, которая гарантирует максимальную эффективность для любых типов.
    for (auto it = vec.begin(); it != vec.end(); ++it) { // Хорошо
    // for (...; ...; it++) { // Менее оптимально для не-POD типов
  • Используйте i++ только тогда, когда вам действительно нужно старое значение в выражении.
    auto old_value = counter++; // Логично: сохраняем старое значение, счётчик растёт.
    process(ptr++);             // Обработать текущий элемент, затем сдвинуть указатель.