POCU C++정주행 1회차 - 참조자

2023. 2. 10. 21:21C++ 복습

꽤 오랜만에 글을 쓰게 되었다... 원래 진작에 썼어야 했는데 이제서야 쓰게 된 이유는 준비할 것이 생겨서 그걸 준비하느라 그랬다. 정주행은 계속하고 있었고 지금은 정주행을 완전히 마친 상태다. 지금은 지금까지 정주행한 내용 중에서 코딩을 하는데 직접적으로 영향을 미칠만한 내용을 위주로 글을 쓰려고 한다. 이제 시작하겠다.

 

 

참조자

참조자가 무엇인지는 C++을 배운 적이 있다면 다들 알 것이다. 아마 아래와 같이 기억하고 있을 것이다.

 

"어떤 변수에 별칭을 붙이는 것"

 

맞는 말이다. 예제는 굳이 적지 않겠다. 어차피 복습이라는 단어가 들어간 카테고리에 있는 글은 전부 한 번쯤 배워본 적이 있다고 가정하고 쓴 글이기 때문이다. 여기서 중요하게 다뤄봐야 하는 부분은 따로 있다. 왜냐하면 내가 참조자를 처음 배울 때, 아래와 같은 생각을 했었기 때문이다.

 

"그래서 이거 어디다 씀? 어차피 포인터 쓰면 되지 않나? 이거 왜 만든거지?"

 

참조자라는 것이 아래와 같이 작성될 수 있고 두 Func는 main에 있는 a라는 변수에 접근하는데 아무런 문제가 없기 때문이다. ref는 별칭이니 그냥 ref = 20; 이라고 하면 a에 20이 들어갈 거고 ptr역시 *ptr = 20; 이러면 값 변경에 문제가 없다.

void Func(int& ref)
{
    // 아주 멋진 코드
}

void Func(int* ptr)
{
    // 아주 멋진 코드
}

int main(void)
{
    int a = 10;
    
    Func(a);
    Func(&a);
    
    return 0;
}

 

그럼 도대체 이걸 왜 쓰는 걸까? 처음 배울 때는 알지 못했지만 지금은 알 수 있다. 참조자와 포인터를 비교했을 때, 참조자는 그 장점이 무려 2개나 된다. 하나하나 알아보자.

 

참조자의 장점

1. nullptr 체크가 필요없다.

절대 무시할 수 없는 아주 큰 장점이다. 포인터를 쓸 때마다 널 체크를 하는 것에 대해서는 이전에 C언어 복습 카테고리의 글이 있으니 그걸 참고하면 될 것이다.

https://dafher-diary.tistory.com/15

 

POCU정주행 13회차 - 예외 처리, 나쁜 오류 처리, 오류 처리 전략

1. 예외 처리 C를 배운 사람은 아마 다음과 같은 사실을 알고 있을 것이다. "C는 예외 처리를 위한 기능이 없다." 다른 언어는 예외 처리가 있지만 C는 이런 것들을 지원하는 기능이 없기 때문에 C

dafher-diary.tistory.com

 

널 체크를 굳이 한다면 assert를 사용하는 정도일텐데 솔직히 하지 않아도 되는 방법이 있다면 그렇게 하고 싶을 것이라고 생각한다. 하지만 만약 참조자를 사용한다면 애초에 포인터가 아니기 때문에 주소를 전달받지 않기 때문에 널 체크를 하는 것 자체가 불가능하며 널 체크를 할 필요가 없다.

 

2. 불필요한 &, * 연산을 쓰지 않는다.

위의 예제에서 Func함수가 int* ptr; 을 전달받고 있다. 그런데 만약 ptr이라는 포인터의 주소 연산 자체가 필요없이 그냥 그 포인터가 가리키는 변수의 값만 읽거나 변경하면 된다면 어떨까? 포인터를 대신해서 참조자를 쓸 때, 아주 효율적일 것 같지 않은가? 함수를 호출할 때, &연산자를 굳이 타이핑을 칠 필요가 없으며 함수 내부에서 ' * ' 연산자 또한 타이핑을 전혀 칠 필요가 없다. 이는 코드를 타이핑을 많이 쳐야하는 사람의 입장에서 소소한 장점이 될 수 있고 결정적으로 연산량또한 아주아주 약간이지만 &와 * 연산자를 사용하는 것보다 참조자를 사용하는 것이 성능상 더 좋다.

 

 

참조자의 코딩 표준

이렇게 좋은 참조자를 이제 한 번 써보도록 하자.

struct Vector
{
    int X;
    int Y;
    int Z;
};

bool TryDivide(Vector& a, Vector& b, Vector& c);

혹시 위에 있는 함수의 결과가 어떻게 나올지 예측해볼 수 있겠는가? 3개의 Vector를 매개 변수로 전달하고 있는데 나눗셈에 성공하면 참, 실패하면 거짓을 반환할 것처럼 보이게 생겼다. 그리고 a, b, c 중 한 곳에 나눗셈의 결과가 저장될 것이다. 그렇다면 셋 중 어디에 저장될까? 당연히 코드를 직접 보기 전에는 알 수 있는 방법이 없다.

 

bool TryDivide(Vector& result, Vector& a, Vector& b);

int main(void)
{
    Vector a = { ... };
    Vector b = { ... };
    Vector result;
    
    TryDivide(a, b, result);
    return 0;
}

그렇다면 이런 방식으로 코드를 짜는 것은 어떨까? 명시적으로 result라고 네이밍을 시켜서 값이 나누기를 한 값이 저 안에 들어갈 것이라고 예상할 수 있다. 하지만 호출자는 여전히 함수를 호출할 때 실수를 하는 것이 가능하고 코드 리뷰를 하더라도 '아, 이건 그냥 result랑 나눗셈을 시킬 Vector 2개를 전달한 코드구나' 라고 생각하고 넘어갈 확률이 높다.

 

bool TryDivide(Vector* result, const Vector& a, const Vector& b);

int main(void)
{
    Vector a = { ... };
    Vector b = { ... };
    Vector result;
    
    TryDivide(&result, a, b);
    return 0;
}

강의에서는 위와 같은 기법을 제시했다. 우선 const 키워드를 사용해서 a, b가 결과를 반환하지 않는다는 것을 명시함과 동시에 결과값을 담는 매개변수만 포인터로 전달해서 호출자가 실수를 하고 싶어도 할 수 없게끔 만들어놓은 것이다. 즉, 값의 변경이 필요한 경우는 포인터로 전달하는 방법을 제시한 것이다.

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

POCU C++정주행 2회차 - 파일 입출력  (0) 2023.02.10
POCU C++ 정주행 - Intro  (0) 2023.01.16