참조 변수 기초 C++ 자습서 40

참조 변수 기초 C++

참조 변수는 C에는 없는 C++만의 특징입니다. 연산자 &를 C와 조금 다르게 사용하기 때문에 C언어의 숙련자라면 혼동 스러울 수 있습니다. C++의 참조 변수란 무엇인가? 결론적으로 말하면 하나의 변수에 대해 이름을 하나 더 부여하는 것 입니다. 리눅스 Bash의 alias(별명)을 생각하면 와닿을 것 입니다. 변수에 꼭 하나의 이름만 부여하라는 법은 없습니다. 변수는 메모리 주소에 연결되어 있을 뿐이기 때문입니다. 한 사람의 이름은 하나지만 여러개의 별명을 붙일 수 있는 것 처럼 C++에서는 참조 변수로 하나의 변수를 여러가지 방법으로 부를 수 있습니다.

참조 변수 예제

백문이 불여 일타로 예제를 보겠습니다.

아래 예제는 참조 변수의 특징을 보여줍니다. var 변수에 varRef1 라는 별명을 붙이고 또 varRef2라고 붙였습니다. 참조 변수의 개수는 제한이 없습니다. 여기서 &를 보고 주소 연산자가 아닌가? 주소 연산자와 관계가 있는가를 생각한다면 C언어와 혼동되는 것 입니다. &var 는 var 변수의 주소를 가리키는게 맞습니다. 하지만 int &varRef1=var 에서의 &varRef1 은 참조변수 선언을 의미합니다. C++ 문법의 어려움은 이렇게 연산자가 사용되는 위치에 따라서 다른 의미를 갖는 일이 있기 때문입니다. 다른 아스키 기호도 있는데 헷갈리게 하는 부분이 있습니다. 포인터 선언할 때도 그런 부분이 있었지요. int *ptr=&var 로 포인터를 선언할 때 *ptr은 역참조(변수의 값을 가져오는 것)가 아니라 var 변수의 포인터로 선언한다는 뜻 입니다. 참조변수도 &varRef1가 선언의 뜻으로 사용되었습니다.

아래 예제를 보면 참조 변수는 여러 개지만 메모리 주소는 하나입니다. 처음의 변수 원본 여기서는 var 가 선언되고 이를 사용할 수 있는 이름으로 지정되었을 뿐 추가적으로 메모리를 할당하지 않습니다. 또 참조는 선언과 동시에 초기화 하지 않으면 허용되지 않습니다. 특성적으로 const 포인터와 비슷한 부분이 있습니다.

참조 변수는 하나의 메모리 주소를 여러개의 이름으로 접근하는 것 입니다. 변수를 하나만 쓰면 될텐데 그렇게 귀찮은 일을 하는 이유는 함수의 매개변수로 전달하기 위해서 입니다.

#include<iostream>

using std::cout;
using std::cin;
using std::endl;


int main()
{
    int var=77;
    int &varRef1=var;
    int &varRef2=var;


    cout <<"-> var     : "<<var<< ", address: "<<&var<<endl;
    cout <<"-> var ref1: "<<varRef1<<", address: "<<&varRef1<<endl;

    var++;
   
    cout <<"-> var ref2: "<<varRef2<<", address: "<<&varRef2<<endl;

    int num=15;
    // 참조를 바꾸는 것이 아니다. var=num 과 같다
    varRef2=num;

    cout <<"-> var ref1: "<<varRef1<<", address: "<<&varRef1<<endl;
    cout <<"-> var     : "<<var<< ", address: "<<&var<<endl;



    // 참조는 선언과 동시가 초기화하지 않으면 허용되지 않는다
    // int &varRef3;
    // varRef3=var;

    return 0;
}

[실행결과]
-> var     : 77, address: 0x7ffe4f734770
-> var ref1: 77, address: 0x7ffe4f734770
-> var ref2: 78, address: 0x7ffe4f734770
-> var ref1: 15, address: 0x7ffe4f734770
-> var     : 15, address: 0x7ffe4f734770

참조 변수 매개변수에 사용

참조 변수는 함수의 매개변수로 사용할 수 있습니다. 함수를 호출하며 값을 전달하면 호출된 함수 내부에서는 그 값의 원본에 대하여 접근할 수 없습니다. 참조 변수는 변수의 별명이라고 했는데 매개변수로 받으면 직접 원본에 접근할 수 있게 됩니다. 포인터를 사용하는 방법과 유사하므로 아래 swap 예제에 포인터와 참조 변수로 원본 값을 교환해 보겠습니다.

#include<iostream>

void swapPtr(int *, int *);
void swapRef(int &, int &);

int main()
{

    using std::cout;
    using std::cin;
    using std::endl;

    int a=15, b=7;
    cout <<"<first values>"<< endl;
    cout << "a: " <<a<< ", b: "<<b<< endl;

    cout <<"swap using potinter"<<endl;
    swapPtr(&a, &b);
    cout <<"<after swap>"<< endl;
    cout << "a: " <<a<< ", b: "<<b<< endl;

    cout <<"swap using reference C++"<<endl;
    cout <<"<after swap>"<< endl;
    swapRef(a, b);
    cout << "a: " <<a<< ", b: "<<b<< endl;

    return 0;

}

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

void swapRef(int &a, int &b)
{
    int temp;
    temp=a;
    a=b;
    b=temp;
}
[실행결과]
<first values>
a: 15, b: 7
swap using potinter
<after swap>
a: 7, b: 15
swap using reference C++
<after swap>
a: 15, b: 7

실행결과는 같습니다. 원본 변수를 바꾼다는 것은 포인터와 참조 변수가 같습니다. 문법적으로 보면 포인터 보다 참조 변수가 더 읽기가 좋은 것 같습니다. (사람에 따라 포인터를 더 선호할 수도 있겠지만) 한 가지 목적을 달성하기 위해서 방법이 많은 것이 C++의 장점이자 단점인데요. 프로그래머는 최대한의 자유를 누리지만 한편으로 그 자유에 대한 책임을 져야 한다, 즉 성능이나 유지 보수를 위한 가독성 등을 평가 받습니다. 100% 정답이란 것은 없지만 어떤 방법을 사용할 지는 C++ 사용자에게 달려있습니다.

매개 변수에서 차이 (참조 변수, 값에 의한 전달)

참조 변수는 함수의 매개변수에서 사용하기 좋습니다. 원래 함수에서 값의 전달은 값에 의한 전달(pass by value)이 기본인 것은 초기 프로그래밍 시대의 문제와 관련이 있습니다. youtube의 computerphile 채널에서 들은 것 으로 기억하는데 이제 우리는 함수의 지역변수 개념이 있지만 그것이 없던 시절에 (1950~1970년대) 변수를 그냥 모두 전역으로 놓고 사용하니까 많은 오류가 발생했다고 합니다. 스파게티 코드라는게 goto 문과 연관이 있다고 알려졌지만 단순한 무질서의 문제라기 보다 지역변수와 전역변수를 구분하지 않고 코드를 쓰다보니 결국 사람이 실수하게 되고 또 그 문제도 전역적으로 걸리니까 시스템이 Crash 하기도 쉬웠습니다.

그 문제의 해결책으로 함수의 지역변수 개념이 도입되고 값을 직접 주고 받는게 아니라 복사본으로 전달했습니다. 그러니까 통계적으로 또 경험적으로 프로그램의 오류가 엄청나게 줄었다고 합니다. 수십년 전 IT업계의 이야기니까 이제 정확한 집계는 힘들겠지만, 경험적으로도 변수는 최대한 사용 범위를 축소하는게 오류를 줄일 수 있다는 것을 알게 되었습니다.

또 당시 하드웨어 성능은 지금에 비해 낮았기에 스택 구조를 사용해서 작은 메모리로도 큰 프로그램을 실행할 수 있게 되었습니다. 예를 들어 전체 프로그램 크기가 10메가 이지만 스택 메모리의 관리를 통해서 1메가의 메모리로도 실행할 수 있습니다. 마치 피자 한판을 한입에 다 넣을 수 없으니 10 조각으로 잘라서 먹는 것과도 비슷합니다.

C언어에는 참조 변수가 없고 C++ 에 있어서 약간 이상할 수 있는데 후에 도입된 것을 보면 구조체와 클래스 등 복잡한 구조가 등장해서 필요성이 있었습니다. 참조 변수(reference variable)이라는 용어는 그 동안 수많은 논쟁을 불러 일으켰는데요. 어떻게 보면 C++에서 이름을 그렇게 붙인 것인데 단어 자체 혼동의 소지가 좀 있습니다. 결국은 포인터를 사용하는 하나의 방식인데 ‘참조’ 라는 단어에만 포커스를 맞추면 뭔가 전혀 다른 변수 같이 들리기도 합니다. 이를 극복하기 위해서는 의미가 무엇인가 내부적인 동작의 이해가 필요합니다.

아래의 예제는 함수 매개변수에서 값에 의한 전달과 참조 변수의 차이를 보여줍니다. 값에 의한 전달은 값을 복사하는 것이기 때문에 매개변수에 표현식 사용이 가능하지만, 참조 변수는 표현식 사용이 불가능합니다.

매개변수가 double형인 cube 함수는 cube(x+1), cube(2*3) 처럼 표현식 사용이 가능합니다. 내부적으로는 double a=x+1; 처럼 변환되겠지요. 그러나 참조 변수는 표현식이 될 수 없는게 double &a=x+1; 를 하려고 하면 cannot bind non-const lvalue reference of type ‘double&’ to an rvalue of type ‘double’ 의 오류 메시지가 나옵니다. lvalue double&에 맞지 않는 rvalue 타입 double 이기 때문입니다.

& 때문인지 문법적으로 헷갈리는 것은 사실입니다. 연산자 오버로딩 이란 것도 있으니까 &의 용도가 한 가지가 아니라 유연하게 볼 필요가 있습니다.

#include<iostream>

double cubeRef(double &);
double cube(double);

int main()
{

    using std::cout;
    using std::endl;

    double x=3.0;

    cubeRef(x);
    // 매개변수를 참조 변수로 넘기면 표현식 사용이 불가능
    // cubeRef(x+1);

    cout <<x<< endl;
    // x 값은 수정되었음

    // 값에 의한 전달(pass by value)은 표현식 전달이 가능
    cout <<cube(x)<<endl;
    cout <<cube(x+3)<<endl;

    return 0;

}

double cubeRef(double & a)
{
    a = a*a*a;
    return a;
}
double cube(double a)
{
    a= a*a*a;
    return a;
};

[실행결과]
27
19683
27000

참조 변수를 구조체에 사용

참조 변수는 보통의 변수 보다 복잡한 구조체나 클래스에 유용하게 사용됩니다. 아래 예제는 참조변수를 매개변수로 받는 함수와 구조체 요소에 참조변수를 사용하는 것을 보여줍니다. 또 리턴값을 구조체 참조 변수로 받는 경우도 있습니다. 이런 경우 동적 메모리 할당을 해야 하는데 함수 바깥에서 메모리를 해제해야 하므로(delete) 일반적으로 좋은 방법은 아닙니다.

#include<iostream>
#include<string>

using std::cout;
using std::string;
using std::endl;

struct myData
{
    int id;
    string name;
};

// 참조변수를 매개변수로 받는 함수
void showMyData(const myData &);
myData & addMyData(int, string);

int main()
{
    myData d1={1, "Mika"};
    myData d2={2, "Kony"};
    myData d3={3, "Olodo"};

    showMyData(d1);
    showMyData(d2);
    showMyData(d3);

    cout <<"--------------"<< endl;

    // 구조체 요소들에 대한 참조변수

    int & d1Id=d1.id;
    int & d2Id=d2.id;
    int & d3Id=d3.id;

    string & d1NameRef=d1.name;
    string & d2NameRef=d2.name;
    string & d3NameRef=d3.name;

    cout <<d1Id<< " : " <<d1NameRef<<endl;
    cout <<d2Id<< " : " <<d2NameRef<<endl;
    cout <<d3Id<< " : " <<d3NameRef<<endl;

    // 리턴값으로 사용. 동적할당

    myData & dRef=addMyData(4, "Loki");
    showMyData(dRef);

    // 할당 해제한다.
    delete &dRef;

    return 0;

}

void showMyData(const myData &dRef)
{
    cout <<dRef.id<< " : " <<dRef.name<<endl;
    return;
}
myData & addMyData(int id, string name)
{
    myData *tempData=new myData;
    
    tempData->id=id;
    tempData->name=name;
    
    return *tempData;
}
[실행결과]
1 : Mika
2 : Kony
3 : Olodo
--------------
1 : Mika
2 : Kony
3 : Olodo
4 : Loki

요약

참조 변수를 사용하는 주요 이유는 포인터를 사용하지 않기 위해서 입니다. 또 간단한 변수도 사용하지만 복잡한 구조체나 클래스에서 유용하게 사용됩니다. 참조 변수의 사용에 대해서 찬반이 갈리기도 하는데요. 어쨋든 많이 사용하기 때문에 사용법을 알아둘 필요가 있습니다. 문법보다는 내부 구조를 생각하면서 코드를 보는 습관이 도움이 됩니다.

외부참고

참조 (C++) | Microsoft Docs

Leave a Comment