본문 바로가기

전위 증가(++it)와 후위 증가(it++)의 차이

@iamrain2025. 11. 27. 10:47

1. 이론

1.1. 기본 개념: Fundamental Types (e.g., int)

가장 기본적인 자료형인 int를 예로 들어 두 연산자의 차이를 이해해 보겠습니다.

  • 전위 증가 (Pre-increment): ++i
    1. i의 값을 1 증가시킵니다.
    2. 증가된 값을 반환(return)합니다.
    int i = 5;
    int a = ++i; // i는 6이 되고, a에도 6이 할당됩니다.
  • 후위 증가 (Post-increment): i++
    1. i현재 값(증가 전)을 복사하여 임시 공간에 저장합니다.
    2. i의 값을 1 증가시킵니다.
    3. 복사해 두었던 원래 값을 반환합니다.
    int i = 5;
    int a = i++; // i의 원래 값인 5가 a에 할당되고, 그 후에 i는 6이 됩니다.

이 차이로 인해 후위 증가는 값을 임시로 저장하기 위한 추가적인 메모리 공간과 연산이 필요합니다. 원시 자료형에서는 컴파일러 최적화로 인해 성능 차이가 거의 없거나 무시할 수 있는 수준이지만, 이 기본 동작 원리가 복잡한 사용자 정의 타입(클래스, 이터레이터 등)에서 중요한 성능 차이를 만들어냅니다.

1.2. 사용자 정의 타입 (User-Defined Types)과 연산자 오버로딩

C++에서는 classstruct 같은 사용자 정의 타입에 대해 연산자를 오버로딩(overloading)하여 동작을 재정의할 수 있습니다. 이터레이터(iterator) 역시 클래스 객체이므로, 증가 연산자가 오버로딩되어 있습니다.

  • 전위 증가 오버로딩: operator++()
    • 시그니처: T& operator++()
    • 동작: 객체의 상태를 직접 변경하고, 변경된 자기 자신을 참조(*this)로 반환합니다. 임시 객체를 생성하지 않습니다.
    // 개념적 구현
    MyClass& MyClass::operator++() {
        // 1. 내부 상태를 증가시킨다.
        _value++;
        // 2. 변경된 자기 자신의 참조를 반환한다.
        return *this;
    }
  • 후위 증가 오버로딩: operator++(int)
    • 시그니처: T operator++(int)
    • int 매개변수는 전위 증가와 후위 증가를 구분하기 위해 사용되는 더미(dummy) 매개변수입니다. 실제 값이 전달되지는 않습니다.
    • 동작:
      1. 현재 상태를 복사한 임시 객체를 생성합니다. (MyClass temp = *this;)
      2. 원본 객체의 상태를 변경(증가)합니다. (++(*this); 또는 _value++;)
      3. 복사해 두었던 임시 객체를 값으로 반환합니다.
    // 개념적 구현
    MyClass MyClass::operator++(int) {
        // 1. 변경 전 상태를 저장할 임시 객체를 생성 (복사 생성자 호출)
        MyClass temp = *this;
        // 2. 원본 객체의 상태를 변경 (전위 증가 연산자 호출을 통해 코드 재사용 가능)
        ++(*this);
        // 3. 저장해 두었던 임시 객체를 반환 (임시 객체 반환에 따른 비용 발생)
        return temp;
    }

1.3. 이터레이터(Iterator)에서의 차이와 성능

STL 컨테이너의 이터레이터는 내부적으로 포인터나 복잡한 자료구조를 통해 컨테이너의 원소를 가리키는 객체입니다. 이터레이터에 증가 연산자를 사용할 때, 위에서 설명한 연산자 오버로딩 규칙이 그대로 적용됩니다.

  • ++it (전위 증가): 이터레이터가 가리키는 위치를 다음 원소로 이동시키고, 이동된 이터레이터 자신을 참조로 반환합니다. 임시 객체 생성 및 복사 비용이 없습니다.
  • it++ (후위 증가): 이터레이터의 현재 상태를 복사하여 임시 이터레이터 객체를 만듭니다. 그 다음 원본 이터레이터를 다음 원소로 이동시키고, 복사해 두었던 임시 이터레이터를 반환합니다. 이 과정에서 객체의 복사 생성과 소멸이 일어나므로 전위 증가에 비해 비효율적입니다.

특히 반복문에서 이터레이터를 사용할 때 이 차이는 더욱 중요해집니다.

// 좋은 예: 전위 증가 사용
for (std::vector<int>::iterator it = myVec.begin(); it != myVec.end(); ++it) {
    // 루프가 반복될 때마다 ++it는 임시 객체를 생성하지 않는다.
}

// 나쁜 예: 후위 증가 사용
for (std::vector<int>::iterator it = myVec.begin(); it != myVec.end(); it++) {
    // 루프가 반복될 때마다 it++는 불필요한 임시 이터레이터 객체를 생성하고 파괴한다.
    // 이는 컨테이너가 크고 이터레이터가 복잡할수록 눈에 띄는 성능 저하를 유발한다.
}

1.4. 비교: 전위 증가 vs. 후위 증가

구분 전위 증가 (++it) 후위 증가 (it++)
반환 값 증가 후의 객체 증가 전의 객체 (복사본)
구현 객체 상태를 직접 변경 후 *this 참조 반환 임시 객체에 원본을 복사 후, 원본을 변경하고 임시 객체를 값으로 반환
성능 우수함. 임시 객체 생성/소멸 비용이 없음. 비효율적일 수 있음. 임시 객체 생성, 복사 생성자 호출, 소멸자 호출 비용 발생.
메모리 추가 메모리 사용 없음 임시 객체를 저장할 추가 메모리 사용
주 사용처 값의 증가만 필요할 때. 특히 반복문 조건에서 강력히 권장됨. 증가 전의 원본 값이 반드시 필요한 특별한 경우.

결론: 언제 무엇을 사용해야 하는가?

  • 증가 연산 후의 값이 필요 없고, 단순히 객체의 상태를 증가시키는 것이 목적이라면 항상 전위 증가(++it)를 사용하세요. 이는 C++ 프로그래밍의 중요한 컨벤션 중 하나입니다. 특히 for 반복문에서 이터레이터를 증가시킬 때는 습관적으로 전위 증가를 사용하는 것이 좋습니다.
  • 증가시키기 전의 원본 값이 반드시 필요한 매우 드문 경우에만 후위 증가(it++)를 사용하세요.

1.5. 동작 계층 구조

  1. 사용자 코드 계층: 개발자가 for(...; ...; ++it) 또는 it++ 코드를 작성합니다.
  2. 컴파일러 계층: 컴파일러는 연산자를 분석합니다.
    • ++it: it.operator++() 함수 호출 코드를 생성합니다.
    • it++: it.operator++(0) 함수 호출 코드를 생성합니다. (0은 더미 값)
  3. 라이브러리/사용자 정의 클래스 계층: 이터레이터 클래스에 구현된 해당 operator++ 멤버 함수가 호출됩니다.
    • operator++(): 내부 포인터나 상태 값을 직접 증가시키고 *this를 반환합니다.
    • operator++(int): 임시 객체를 스택에 생성(복사 생성자), 원본 객체의 상태를 변경, 스택에 생성된 임시 객체를 반환합니다.
  4. 런타임/메모리 계층:
    • 전위 증가: 기존 객체의 메모리만 수정됩니다.
    • 후위 증가: 스택에 임시 객체를 위한 새로운 메모리가 할당되고, 작업이 끝나면 해제됩니다. 이 과정에서 생성자/소멸자 코드가 실행됩니다.
  5. 기계어 계층:
    • 전위 증가: 주소 값을 증가시키는 더 적은 수의 명령어로 변환될 수 있습니다.
    • 후위 증가: 값을 복사하고, 원래 주소를 증가시키고, 복사된 값을 사용하는 등 더 많은 명령어가 필요합니다.

1.6. 요약

전위 증가와 후위 증가는 원시 타입에서는 성능 차이가 미미하지만, 이터레이터와 같은 객체에서는 중요한 차이를 보입니다.

전위 증가(++it)는 객체의 값을 먼저 증가시킨 후, 변경된 자기 자신의 참조를 반환합니다. 이 과정에서 불필요한 임시 객체가 생성되지 않아 매우 효율적입니다.

반면, 후위 증가(it++)는 값의 변경 전 상태를 반환해야 하므로, 객체의 현재 상태를 복사한 임시 객체를 먼저 만듭니다. 그 다음 원본 객체의 값을 증가시키고, 마지막으로 만들어 두었던 임시 객체를 반환합니다. 이 때문에 임시 객체를 만들고 소멸시키는 과정에서 복사 생성자와 소멸자가 호출되어 성능 저하가 발생합니다.

따라서 C++에서는 반복문 등에서 이터레이터를 증가시킬 때, 증가 전의 값이 굳이 필요하지 않은 이상 항상 성능이 좋은 전위 증가(++it)를 사용하는 것이 표준적인 관례입니다.


2. 실습 코드: 게임 인벤토리 순회

Inventory.h

#pragma once

#include <iostream>
#include <string>
#include <vector>

// 게임에 등장하는 아이템을 표현하는 구조체
struct Item {
    std::string name;
    int id;
};

// 아이템 목록을 관리하는 인벤토리 클래스
class Inventory {
public:
    // 인벤토리 순회를 위한 커스텀 이터레이터
    class InventoryIterator {
    public:
        // 이터레이터가 필수로 정의해야 하는 타입들
        using value_type = Item;
        using pointer = Item*;
        using reference = Item&;
        using iterator_category = std::forward_iterator_tag;
        using difference_type = std::ptrdiff_t;

    private:
        pointer m_ptr; // 현재 아이템을 가리키는 포인터

    public:
        // 생성자
        InventoryIterator(pointer ptr) : m_ptr(ptr) {}

        // 역참조 연산자: 현재 가리키는 아이템을 반환
        reference operator*() const { return *m_ptr; }
        pointer operator->() { return m_ptr; }

        // 전위 증가 연산자
        InventoryIterator& operator++() {
            m_ptr++;
            return *this;
        }

        // 후위 증가 연산자
        InventoryIterator operator++(int) {
            InventoryIterator temp = *this;
            ++(*this);
            return temp;
        }

        // 비교 연산자
        friend bool operator==(const InventoryIterator& a, const InventoryIterator& b) { return a.m_ptr == b.m_ptr; }
        friend bool operator!=(const InventoryIterator& a, const InventoryIterator& b) { return a.m_ptr != b.m_ptr; }
    };

private:
    // 아이템을 저장할 std::vector
    std::vector<Item> m_items;

public:
    // 인벤토리에 아이템을 추가하는 함수
    void AddItem(const std::string& name, int id) {
        m_items.push_back({name, id});
    }

    // 순회를 위한 시작 이터레이터를 반환
    InventoryIterator begin() { return InventoryIterator(m_items.data()); }
    // 순회를 위한 끝 이터레이터를 반환
    InventoryIterator end() { return InventoryIterator(m_items.data() + m_items.size()); }
};

main.cpp

#include "Inventory.h"

int main() {
    // 인벤토리 객체 생성
    Inventory playerInventory;

    // 인벤토리에 아이템 추가
    playerInventory.AddItem("Health Potion", 101);
    playerInventory.AddItem("Sword of Destiny", 205);
    playerInventory.AddItem("Magic Shield", 302);

    std::cout << "--- 모든 아이템 ID에 1000을 더하는 업데이트 (전위 증가 사용) ---" << std::endl;

    for (Inventory::InventoryIterator it = playerInventory.begin(); it != playerInventory.end(); ++it) {
        // 이터레이터가 가리키는 아이템의 ID를 업데이트
        it->id += 1000;
        // 업데이트된 아이템 정보 출력
        std::cout << "Updated Item: " << it->name << ", New ID: " << it->id << std::endl;
    }

    std::cout << "\n--- 업데이트 후 아이템 목록 재확인 ---" << std::endl;
    // 범위 기반 for문을 사용한 순회 (내부적으로 begin(), end(), ++, != 연산 사용)
    for (const auto& item : playerInventory) {
        std::cout << "Item: " << item.name << ", ID: " << item.id << std::endl;
    }

    return 0;
}

'C++' 카테고리의 다른 글

std::list가 sort를 멤버 함수로 제공하는 이유  (0) 2025.12.03
C++ lvalue, rvalue  (0) 2025.12.01
템플릿(Template)과 매크로(Macro)  (0) 2025.11.21
객체 지향 프로그래밍  (0) 2025.11.20
RTTI와 RAII  (0) 2025.11.20
iamrain
@iamrain :: Annals of Unreal

iamrain 님의 블로그 입니다.

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차