NEVERTHELESS

C++에서 ref를 사용하는 이유

by Ungbae


 

 

C++에서 ref를 사용하는 이유는? pointer를 사용하면 되지 않나?

 

 

참조자가 더 안전하고 편리하다.

포인터로 할 수 있는 것들을 참조자로도 할 수 있지만, 참조자가 더 간단하고 실수를 줄여준다.

 

 

일단 문법이 훨씬 간단하다.

void swap(int *x, int *y) {
    int temp = *x;  // * 붙여야 함
    *x = *y;        // * 붙여야 함
    *y = temp;      // * 붙여야 함
}

int main() {
    int a = 5, b = 10;
    swap(&a, &b);   // & 붙여서 주소 전달해야 함
}

 

 

하지만 참조자의 경우 코드를 보자

void swap(int &x, int &y) {
    int temp = x;   // 그냥 쓰면 됨
    x = y;          // 그냥 쓰면 됨
    y = temp;       // 그냥 쓰면 됨
}

int main() {
    int a = 5, b = 10;
    swap(a, b);     // 그냥 전달
}

 

훨씬 직관적이고 실수할 가능성이 적다.

 

 

 

두 번째로 NULL 포인터 문제가 없다.

void print(int *ptr) {
    cout << *ptr << endl;  // ptr이 nullptr이면? 충돌 발생
}

int main() {
    int *p = nullptr;
    print(p);  // 위험. 프로그램 종료
}

 

그래서 항상 nullptr 체크를 해야 한다.

void print(int *ptr) {
    if (ptr != nullptr) {  // 매번 체크 필요
        cout << *ptr << endl;
    }
}

 

 

반면에 참조자는 안전하다.

void print(int &ref) {
    cout << ref << endl;  // 항상 안전. nullptr 불가능
}

int main() {
    int x = 10;
    print(x);  // 안전
    // print(nullptr);  // 컴파일 에러. 애초에 불가능
}

참조자는 반드시 유효한 객체를 가리켜야 하기 때문에 더 안전하다.

 

 

세 번째로 읽기 쉬운 코드를 작성할 수 있다.

void calculate(int *a, int *b, int *result) {
    *result = (*a) + (*b);  // * 많아서 읽기 어려움
}

int main() {
    int x = 5, y = 10, res;
    calculate(&x, &y, &res);  // & 붙이는 것도 번거로움
}

 

하지만 참조자는

void calculate(int a, int b, int &result) {
    result = a + b;  // 간단명료
}

int main() {
    int x = 5, y = 10, res;
    calculate(x, y, res);  // 자연스러움
}

 

 

그리고 연산자 오버로딩에 필수이다.

class Vector {
    int x, y;
public:
    // [] 연산자 오버로딩 - 참조 반환 필수!
    int& operator[](int index) {
        if (index == 0) return x;
        return y;
    }
};

int main() {
    Vector v;
    v[0] = 10;  // 이렇게 쓰려면 참조 반환이 필수!
    v[1] = 20;
}

참조자 없이는 이렇게 불가능한 것들이 있다.

포인터로는 이렇게 자연스러운 문법을 만들 수 없다.

 

 

const의 정확성도 한 몫한다(Const Correctness).

 

포인터의 경우

void print(const int *ptr) {  // 값은 못 바꿈
    *ptr = 10;  // 에러
    ptr = nullptr;  // 포인터 자체는 바꿀 수 있음
}

 

참조자의 경우

void print(const int &ref) {  // 값은 못 바꿈
    ref = 10;  // 에러
    // ref는 다른 걸 가리킬 수 없음 (더 안전)
}

 

 

 

그리고 참조자를 사용하면 큰 객체 전달을 하게 될 때 효율적이다.

 

큰 객체를 값 복사를 하게되면 매우 느리다.

void process(string str) {  // 문자열 전체 복사
    cout << str << endl;
}

 

포인터를 사용하면 복잡하다.

void process(const string *str) {  // 복사 안함
    if (str != nullptr) {  // 체크 필요
        cout << *str << endl;  // * 붙여야 함
    }
}

 

참조자가 GOAT

void process(const string &str) {  // 복사 안함, 간단함
    cout << str << endl;
}

 

 

 


 

그럼 포인터는 언제 쓰라는거냐

포인터가 필요한 경우에 쓰면 된다. (당연한 얘기)

 

nullptr가 의미있는 경우가 이에 해당한다.

int* findValue(int arr[], int size, int target) {
    for (int i = 0; i < size; i++) {
        if (arr[i] == target) {
            return &arr[i];  // 찾으면 주소 반환
        }
    }
    return nullptr;  // 못 찾으면 nullptr
}

 

포인터를 재할당하는 경우에도 포인터를 쓴다.

int a = 10, b = 20;
int *ptr = &a;
ptr = &b;  // OK 다른 곳을 가리킬 수 있음

int &ref = a;
ref = b;   // ref가 b를 가리키는 게 아니라 a의 값이 바뀜

 

동적 메모리를 할당할 때에도

int *arr = new int[100];  // 포인터 필요
delete[] arr;

 

배열이나 C 스타일의 문자열에서도

char *str = "Hello";
int *arr = new int[10];

 

 

 


클로드의 조언

  • 일반적으로는 참조자를 먼저 고려하라
  • 함수 매개 변수로 큰 객체를 전달할 때에는 const & 를 사용할 것
  • 함수에서 값 수정할 때에는 &를 사용할 것
  • nullptr가 의미있는 경우만 포인터를 사용할 것

블로그의 정보

그럼에도 불구하고

Ungbae

활동하기