본문 바로가기
개발/C++ (98,03,11,14,17,20,23)

Before Classic C++ : 레퍼런스(&)와 포인터(*)

by snowoods 2025. 11. 1.

Before Classic C++

 

10. 레퍼런스(&)와 포인터(*)

 

개요

레퍼런스(reference)와 포인터(pointer)는 C++에서 메모리에 있는 기존 변수에 간접적으로 접근하는 방법을 제공합니다. 포인터는 변수의 메모리 주소를 저장하는 변수이고, 레퍼런스는 변수의 또 다른 이름(별칭)입니다.

 

C++ 버전별 주요 키워드 도입 시기

  • C++98: 포인터는 C에서 계승되었고, 레퍼런스(L-value 레퍼런스)는 C++ 초기부터 도입되어 객체를 효율적으로 전달하는 핵심 기능으로 사용되었습니다.
  • C++11: R-value 레퍼런스(&&)가 도입되어, 임시 객체를 효율적으로 처리하는 이동 시맨틱(move semantics)과 완벽한 전달(perfect forwarding)이 가능해졌습니다. 또한, 0이나 NULL 대신 nullptr 키워드를 사용하여 널 포인터를 명확하게 표현하는 것이 권장됩니다.

 

내용 설명

차이점

구분 포인터 (Pointer) 레퍼런스 (Reference)
선언 int* p = &var; int& ref = var;
초기화 선언 후 다른 변수의 주소를 할당할 수 있음 선언 시 반드시 초기화해야 함
재할당 가리키는 대상을 변경할 수 있음 다른 변수를 가리키도록 변경할 수 없음
Null nullptr를 가리킬 수 있음 (아무것도 가리키지 않음) null이 될 수 없음 (항상 유효한 객체를 참조)
접근 역참조 연산자(*)를 사용 (*p) 변수 이름처럼 직접 사용 (ref)
본질 주소를 저장하는 별도의 변수 기존 변수의 또 다른 이름 (별칭)

함수 인자로서의 활용

  • 포인터 전달(Pass-by-pointer): 함수의 인자로 변수의 주소를 전달합니다. 함수 내에서 포인터를 통해 원본 변수를 수정할 수 있습니다. nullptr 전달이 가능하므로, 함수 내에서 유효성 검사가 필요할 수 있습니다.
  • 레퍼런스 전달(Pass-by-reference): 함수에 변수 자체를 전달하는 것처럼 보이지만, 실제로는 원본 변수의 별칭이 전달됩니다. 함수 내에서 레퍼런스를 통해 원본 변수를 수정할 수 있으며, null이 아니므로 더 안전하고 구문이 간결합니다.

함수 반환 값으로서의 활용

  • 값 반환(Return-by-value): 객체의 복사본을 반환합니다. 원본 데이터는 변경되지 않지만, 객체가 클 경우 복사 비용이 발생할 수 있습니다.
  • 레퍼런스 반환(Return-by-reference): 객체의 레퍼런스를 반환하여 복사를 피하고, 반환된 레퍼런스를 통해 원본 객체를 직접 수정할 수 있습니다. 단, 함수가 종료된 후에도 존재하는 전역 변수나 정적 변수, 동적 할당된 객체를 반환해야 하며, 지역 변수의 레퍼런스를 반환하면 안 됩니다 (댕글링 레퍼런스).

 

예제 코드

#include <iostream>
#include <string>

struct Person {
    std::string name;
    int age;
};

// Pass-by-reference
void updateAgeByRef(Person& p, int newAge) {
    p.age = newAge; // 원본 객체의 age가 변경됨
}

// Pass-by-pointer
void updateAgeByPtr(Person* p, int newAge) {
    if (p != nullptr) { // 항상 null 체크를 하는 것이 안전
        p->age = newAge; // 원본 객체의 age가 변경됨
    }
}

// 전역 변수 (레퍼런스 반환 예제를 위해)
Person globalPerson = {"Global", 99};

// Return-by-reference
Person& getGlobalPerson() {
    return globalPerson; // 전역 변수의 레퍼런스 반환
}

int main() {
    Person p1 = {"Alice", 30};
    Person p2 = {"Bob", 25};

    std::cout << "--- Initial State ---" << std::endl;
    std::cout << p1.name << " is " << p1.age << " years old." << std::endl;
    std::cout << p2.name << " is " << p2.age << " years old." << std::endl;

    // 레퍼런스 및 포인터로 함수 호출
    updateAgeByRef(p1, 31);
    updateAgeByPtr(&p2, 26);

    std::cout << "\n--- After Update ---" << std::endl;
    std::cout << p1.name << " is now " << p1.age << " years old." << std::endl;
    std::cout << p2.name << " is now " << p2.age << " years old." << std::endl;

    // 레퍼런스 반환 값 사용
    std::cout << "\n--- Return-by-reference ---" << std::endl;
    std::cout << "Global person's age: " << getGlobalPerson().age << std::endl;

    // 반환된 레퍼런스를 통해 원본 수정
    getGlobalPerson().age = 100;
    std::cout << "Global person's new age: " << globalPerson.age << std::endl;

    return 0;
}

 

실행 결과

--- Initial State ---
Alice is 30 years old.
Bob is 25 years old.

--- After Update ---
Alice is now 31 years old.
Bob is now 26 years old.

--- Return-by-reference ---
Global person's age: 99
Global person's new age: 100

 

활용팁

  • 간결성과 안전성: 함수 인자로 객체를 전달할 때, null이 허용되지 않고 구문이 간결한 레퍼런스를 우선적으로 고려하는 것이 좋습니다.
  • 선택적 인자: 인자가 선택적이어서 nullptr를 전달해야 하는 경우 포인터를 사용합니다.
  • 효율성: 큰 객체를 함수에 전달할 때 값에 의한 전달(pass-by-value)은 비싼 복사를 유발하므로, const 레퍼런스를 사용하여 복사를 피하고 원본 데이터의 수정을 방지하는 것이 매우 효율적입니다. (예: void printPerson(const Person& p);)