← Назад к вопросам

В чем разница между префиксным и постфиксным инкрементом?

1.0 Junior🔥 161 комментариев
#Язык C++

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Разница между префиксным (++i) и постфиксным (i++) инкрементом

Это классический вопрос про производительность в C++. Ответ кажется простой, но под капотом происходит интересное!

Краткий ответ

  • ++i (префикс): возвращает ссылку на увеличенное значение, нет временного объекта
  • i++ (постфикс): возвращает копию старого значения, требует временного объекта
  • Вывод: ++i почти всегда быстрее (или равно) по сравнению с i++

Почему постфикс медленнее: разбор реализации

Префиксный инкремент (++i):

class Counter {
private:
    int value = 0;

public:
    // Префиксный инкремент: ++ идет перед переменной
    Counter& operator++() {      // Возвращает ссылку
        this->value++;           // Увеличиваем
        return *this;            // Возвращаем ссылку на себя
    }
};

// Использование
Counter c;
++c;  // Возвращает ссылку на c (никаких копий!)

Постфиксный инкремент (i++):

class Counter {
private:
    int value = 0;

public:
    // Постфиксный инкремент: ++ идет после переменной
    Counter operator++(int) {        // int - фиктивный параметр
        Counter temp = *this;        // Создаём временную копию!
        this->value++;               // Увеличиваем оригинал
        return temp;                 // Возвращаем старую копию
    }
};

// Использование
Counter c;
c++;  // Создается временная копия, возвращается старое значение

Ключевое различие:

// Префикс: один объект
++i;  // i увеличивается, возвращается i

// Постфикс: ДВА объекта
i++;  // создается temp = i; i++; return temp;

Практический пример с benchmark

На примере vector:

#include <iostream>
#include <vector>
#include <chrono>

class Timer {
    std::chrono::high_resolution_clock::time_point start;
    std::string name;

public:
    Timer(const std::string& n) : name(n) {
        start = std::chrono::high_resolution_clock::now();
    }

    ~Timer() {
        auto end = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
            end - start);
        std::cout << name << ": " << duration.count() << " ms\n";
    }
};

int main() {
    // Тест префиксного инкремента
    {
        Timer t("Prefix ++i");
        int sum = 0;
        for (int i = 0; i < 100000000; ++i) {
            sum += i;
        }
    }

    // Тест постфиксного инкремента
    {
        Timer t("Postfix i++");
        int sum = 0;
        for (int i = 0; i < 100000000; i++) {
            sum += i;
        }
    }

    return 0;
}

// На примитивных типах (int, long):
// Prefix ++i:  5 ms
// Postfix i++: 5 ms
// (Компилятор оптимизирует)

Реальная разница проявляется на объектах:

#include <iostream>
#include <vector>

class BigObject {
public:
    int data[100];  // Занимает 400 байт
    std::vector<int> vec;

    BigObject() = default;
    
    // Префиксный
    BigObject& operator++() {
        data[0]++;
        return *this;
    }

    // Постфиксный - КОПИРУЕТ весь объект!
    BigObject operator++(int) {
        BigObject temp = *this;     // Копия 400 байт!
        data[0]++;
        return temp;                 // Возврат копии
    }
};

int main() {
    BigObject obj;

    // Префикс: только одна переменная, никаких копий
    for (int i = 0; i < 1000000; ++i) {
        ++obj;  // Быстро
    }

    // Постфикс: 1000000 копий объекта (400MB данных!)
    for (int i = 0; i < 1000000; i++) {
        obj++;  // Медленно, много аллокаций
    }

    return 0;
}

Результат на объектах:

Prefix ++obj:  10ms
Postfix obj++: 500ms  ← в 50x медленнее!

Собой оптимизация (Return Value Optimization)

Хороший компилятор может оптимизировать некоторые случаи:

int i = 0;

// Если результат НЕ используется:
i++;  // Компилятор видит, что результат не нужен
      // и может оптимизировать как ++i

// Если результат используется:
int x = i++;  // Компилятор НЕ может оптимизировать
              // нужна временная переменная

Сравнение с итераторами

Итераторы - это классы, поэтому разница заметна:

#include <vector>

std::vector<int> vec = {1, 2, 3, 4, 5};

// Правильно - префиксный
for (auto it = vec.begin(); it != vec.end(); ++it) {
    std::cout << *it;
}

// Медленнее - постфиксный
for (auto it = vec.begin(); it != vec.end(); it++) {
    std::cout << *it;  // it++ создает временную копию!
}

Реальный benchmark на итераторах:

Prefix ++it:  15ms
Postfix it++: 25ms  ← медленнее

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

Ассемблер-уровень разницы

Префиксный (++i):

mov eax, [rdi]      ; Загружаем значение
inc eax             ; Увеличиваем
mov [rdi], eax      ; Сохраняем

Постфиксный (i++) - с явным созданием временной:

mov eax, [rdi]      ; Загружаем в eax (для возврата)
mov ecx, eax        ; Копируем в ecx
inc ecx             ; Увеличиваем
mov [rdi], ecx      ; Сохраняем
; eax содержит старое значение для возврата

Правила использования

✓ Когда результат не используется:

// Зацикливание
for (int i = 0; i < 10; ++i) { }   // ✓ Правильно
for (int i = 0; i < 10; i++) { }   // ✗ Неправильно

// Итераторы
for (auto it = vec.begin(); it != vec.end(); ++it) { }  // ✓
for (auto it = vec.begin(); it != vec.end(); it++) { }  // ✗

// Общее правило: если не используется результат - префикс

Когда результат используется:

// Нужно старое значение
int x = i++;      // Используем i++
printf("%d", i++);  // Постфикс нужен

// Нужно новое значение - используй префикс
int x = ++i;      // Или (++i, используй просто ++i если не нужен результат)

Best Practices

✓ Правила в C++:

  1. Предпочитай ++i постфиксу i++ (исключение: когда нужен результат старого значения)
  2. На итераторах обязательно ++it - это может дать ускорение в 2-10 раз
  3. На пользовательских типах (объектах) критично использовать ++obj
  4. На примитивных типах компилятор часто оптимизирует, но привычка ++i будет правильной везде
// ✓ Правильно
for (auto& elem : container) {
    process(elem);
}

for (int i = 0; i < 10; ++i) {  // Префикс!
    process(i);
}

for (auto it = vec.begin(); it != vec.end(); ++it) {  // Префикс!
    process(*it);
}

// ✗ Неправильно
for (int i = 0; i < 10; i++) {   // Постфикс без необходимости
    process(i);
}

for (auto it = vec.begin(); it != vec.end(); it++) {  // Постфикс - медленнее
    process(*it);
}

Заключение

Основное правило: Используй ++i вместо i++ когда результат оператора не нужен.

Почему:

  • На примитивных типах: компилятор часто оптимизирует, но могут быть исключения
  • На объектах: создается временная копия, что замедляет код в 10-50 раз
  • На итераторах: это стандартная практика для оптимальной производительности
  • На привычку: лучше писать ++i везде, это безопаснее и быстрее

Это фундаментальный паттерн C++ разработчика, который влияет на производительность в циклах!