[C++] 참조자 (Reference)

👑 참조자(Reference)란?

C 언어에서는 어떠한 변수를 참조하기 위해 포인터라는 개념을 사용하였다. 물론 C++에서도

포인터를 사용할 수 있지만, C와는 다르게 참조자 (reference)를 사용하여 변수를 가리킬 수 있다.

C 언어에서의 두 수의 값을 교환하는 것을 예로 들면, 아래와 같이 포인터를 사용하여 swap 함수의

인자로 x, y의 주소값을 전달하여 참조하고, 값을 바꿀 수 있었다. C++에서는 이와 다른 방법을 제공하는데

그것이 바로 참조자이다.

#include <stdio.h>

void swap(int *a, int *b) {
    int temp = *b;
    *b = *a;
    *a = temp;
}

int main() {
    int x = 2;
    int y = 4;
    
    printf("x: %d, y: %d\n", x, y); // x: 2, y: 4
    
    swap(&x, &y);
    
    printf("x: %d, y: %d", x, y);   // x: 4, y: 2

    return 0;
}


참조자는 쉽게 말하면 변수의 별명이라고 할 수 있다.

아래처럼 어떠한 변수의 참조자를 & 연산자를 통해 정의할 수 있다.

int x = 66;
int& ref = x; // x의 참조자 ref를 정의

// ❗ 다음과 같은 상수 참조는 X
int& ref2 = 10; // 오류

이제 참조자 refx의 다른 이름으로써 x를 사용하는 것과 동일한 효과를 같는다. 즉, ref

어떠한 연산을 수행해도 x에 수행하는 것과 마찬가지의 결과를 낳는다.

ref = 7;
std::cout << "x: " << x << " ref: " << ref; // x: 7 ref: 7


또한, 참조자는 메모리에서 독립적인 주소를 가지지 않으며, 참조자가 가리키는 변수의 메모리 주소를

공유한다. 컴파일러는 참조자에 대한 메모리 주소를 할당하지 않고, 참조자가 가리키는 변수의 주소를

사용한다. 이는 성능상의 오버헤드를 줄이는 효과를 가져다 줄 수 있다.


💡 포인터 vs 참조자

참조자와 포인터 모두 어떤 다른 변수의 주소값을 통해 해당 변수의 값에 접근하여 연산을 수행할 수 있다.

둘은 비슷한 역할을 수행하며 상당히 유사하게 동작한다. 하지만, 둘 사이에는 몇 가지 중요한 차이점이

존재하며, 이는 참조자와 포인터를 적절히 사용하는 데 중요한 역할을 한다.

  • 초기화

    • 참조자의 경우 선언 시 반드시 초기화되어야 하며, 이후에 다른 객체를 참조할 수 없다.

    • 포인터의 경우 선언 시 반드시 초기화할 필요는 없으며, 나중에 다른 주소를 가리킬 수 있다.

    • 즉, 참조자는 포인터에 비해 더 안전하다고 볼 수 있다.

int x = 5;
int& ref = x; // 가능
int& ref2;    // 오류: 정의 시에 반드시 초기화 되어야 함.

int y = 3;
int* ptr;
ptr = &y;     // 가능: 선언 시 반드시 초기화할 필요 없음.
  • NULL 값

    • 참조자의 경우 NULL 값을 가리킬 수 없으며, 항상 유효한 객체를 참조해야 한다.

    • 포인터는 NULL 값을 가질 수 있다.

int& ref = nullptr;  // 오류: 참조자는 NULL을 가리킬 수 없음

int* ptr = nullptr;  // 가능
  • 재할당

    • 한 번 초기화된 참조자는 다른 객체를 참조하도록 변경할 수 없다.

    • 포인터의 경우 다른 주소를 가리키도록 변경할 수 있다.

int x = 5;
int y = 10;
int& ref = x;
ref = y;  // 'ref'는 여전히 'x'를 참조하며, 'x'의 값이 'y'의 값으로 변경됨

int x = 5;
int y = 10;
int* ptr = &x;
ptr = &y;  // 이제 'ptr'은 'b'를 가리킴
  • 이외에도 참조자의 경우 * 연산자의 사용 없이 간단하게 사용할 수 있다는 장점을 가지고 있다.


💡 참조자의 사용

참조자는 포인터에 비해 쉽고 간결하게 사용할 수 있음을 알게 되었다. 위의 swap 함수를 참조자를

사용하여 아래와 같이 구현할 수 있다.

#include <iostream>

void swap(int& a, int& b) {
    int temp = b;
    b = a;
    a = temp;
}

int main() {
    int x = 2;
    int y = 4;

    std::cout << "x: " << x << " y: " << y << '\n';   // x: 2 y: 4

    swap(x, y);
    
    std::cout << "x: " << x << " y: " << y << '\n';   // x: 4 y: 2
}


위에서 아래와 같이 상수를 참조하는 것은 불가능하다고 했다.

int& ref = 10;

하지만, const를 사용하여 리터럴을 참조할 수 있으며, 다음과 같이 사용할 수 있다.

const int &ref = 66;
int x = ref;   // x = 66; 과 동일함.


또한, 함수 인수로 참조자를 사용하면 값을 복사하지 않고도 큰 객체를 다룰 수 있어 오버헤드를 줄일

수 있는 효과를 준다. 아래의 경우를 예로 들면, 참조자를 사용하지 않고 함수의 인자로 str을 전달하면

원본 문자열의 복사본을 생성하게 된다. (PrintStr의 경우 = Call by Value)

하지만, 참조자를 사용한 인자의 전달은 원본 문자열을 복사하는 것이 아니라 참조만 하여 추가적인 메모리를

사용하지 않게 된다. (PrintStrRef의 경우 = Call by Reference) 만약, 아래와 같은 단순한 문자열이

아니라 큰 객체를 전달할 경우 둘의 성능 차이는 더욱 클 것이다. 따라서, 불필요한 복사를 피하기 위해

참조자를 이용하는 방식이 바람직하다.

#include <iostream>
#include <string>

void PrintStr(const std::string str) { // Call by Value
    std::cout << str << '\n';
}

void PrintStrRef(const std::string& str) { // Call by Reference
    std::cout << str << '\n';
}

int main() {
    std::string str = "Hello World!";
    
    PrintStr(str);
    PrintStrRef(str);
    
    return 0;
}

Categories:

Updated:

Leave a comment