포인터는 잘못 사용할 확률이 높고, 참조자의 활용일 상대적으로 포인터의 활용보다 쉽기 때문에, 참조자 기반의 함수 정의가 더 좋은 선택이라고 생각할 수 있습니다. 그러나 참조자 기반의 함수 정의에 좋은 점만 있는 것은 아닙니다. 


int num = 24;

HappyFunc(num);

cout << num << endl;


C언어의 관점에서는 100% 24가 출력됩니다. 그런 C++에서는 얼마가 출력될지 알 수 없습니다. 함수가 다음과 같이 정의 되어 있다면 24가 출력되겠지만,


void HappyFunc(int prm) {...}


다음과 같이 정의되어 있다면, 참조자를 이용해서 num에 저장된 값을 변경할 수도 있는 일입니다.


void HappyFunc(int &ref) {...}


이는 분명히 참조자의 단점이 됩니다. 예를 들어서 코드를 분석하는 과정에 있다면, 함수의 호출문장만 보고도 함수의 특성을 어느 정도 판단할 수 있어야 합니다. 그러나 참조자를 사용하는 경우, 함수의 원형을 확인해야 하고, 확인결과 참조자가 매개변수의 선언에 와있다면, 함수의 몸체까지 문장 단위로 확인을 해서 참조자를 통한 값의 변경이 일어나는지를 확인해야 합니다.


그렇다면, 이러한 단점을 어떻게 해결하면 좋을까요? 사실 완벽한 해결은 불가능합니다. C++에서는 최소한 함수의 원형은 확인해야 합니다. 따라서 완벽한 해결을 원한다면, 참조자 기반의 함수정의를 하지 말아야 합니다. 그러나 const 키워드를 이용하면, 이러한 단점을 어느 정도는 극복할 수 있습니다.


void HappyFunc(const int &ref) {...}


참조자 ref에 const 선언이 추가되었습니다. 이는 "함수 HappyFunc 내에서 참조자 ref를 이용한 값의 변경은 하지 않겠다!"라는 의미를 지닙니다.


여기서의 const 선언으로 인해서, 참조자 ref에 값을 저장하는 경우 컴파일 에러가 발생합니다. 따라서 함수 내에서 값의 변경이 이뤄지지 않음을 확신할 수 있습니다.  따라서 다음의 원칙을 정하고 가급적 이 원칙을 지켜주는 것이 좋습니다.


"함수 내에서, 참조자를 통한 값의 변경을 진행하지 않을 경우, 참조자를 const로 선언해서, 함수의 원형만 봐도 값의 변경이 이뤄지지 않음을 알 수 있게 한다"



const 참조자의 또 다른 특징


const int num = 20;

int &ref = num;

ref+=10;

cout << num << endl;


const 선언을 통해 변수 num을 상수화했는데, 참조자 ref를 통해서 값을 변경한다? 이것을 허용한다면 사실상 변수 num의 상수화는 의미가 없습니다. 상수화되었다면 어떠한 경로를 통하더라도 값의 변경을 허용하면 안됩니다. 다행히도 C++에서는 이를 허용하지 않습니다. 위의 코드 중 다음 문장에서 컴파일 에러를 일으키기 때문입니다.


int &ref = num;


따라서 변수 num과 같이 상수화된 변수에 대한 참조자 선언은 다음과 같이 해야 합니다.


const int num = 20;

const int &ref = num;


이렇게 선언이 되면 ref를 통한 값의 변경이 불가능하기 때문에 상수화에 대한 논리적인 문제점은 발생하지 않습니다. 그리고 const 참조자는 다음과 같이 상수도 참조가 가능합니다.


const int &ref = 50;


50 같은 프로그램상에서 표현되는 숫자를 가리켜 '리터럴(literal)' 또는 '리터럴 상수(literal constant)'라 합니다. 그리고 이들은 다음의 특징을 지닙니다.


"임시적으로 존재하는 값이다. 다음 행으로 넘어가면 존재하지 않는 상수다."


위의 코드는 숫자 50이 메모리 공간에 계속 남아있을 때에나 성립이 가능한 문장입니다. 그래서 C++에서는 위의 문장이 성립할 수 있도록, const 참조자를 이용해서 상수를 참조할 때 '임시변수'라는 것을 만듭니다. 그리고 이 장소에 상수 50을 저장하고선 참조자가 이를 참조하게끔 합니다.


임시로 생성한 변수를 상수화하여 이를 참조자가 참조하게끔 하는 구조이니, 결과적으로는 상수화된 변수를 참조하는 형태가 됩니다.


int Adder(const int &num1, const int &num2)

{

return num1 + num2;

}


위와 같이 정의된 함수에 인자의 전달을 목적으로 변수를 선언하는 것은 매우 번거로운 일이 아닐 수 없습니다.


int num1 = 3;

int num2 = 4;


이런식으로 말입니다. 그러나 임시변수의 생성을 통한 const 참조자의 상수 참조를 허용함으로써, 위의 함수는 다음과 같이 매우 간단한 호출이 가능해졌습니다.


cout << Adder(3, 4) << endl;



출처

  • 열혈 C++ 프로그래밍


C++에서는 함수 외부에 선언된 변수의 접근방법으로 두 가지가 존재합니다. 하나는 '주소 값'을 이용하는 방식이고, 다른 하나는 '참조자'를 이용하는 방식입니다. 

  • 주소 값을 이용한 Call-by-reference
  • 참조자를 이용한 Call-by-reference

Call-by-reference의 가장 큰 핵심은 함수 내에서 함수 외부에 선언된 변수에 접근할 수 있다는 것입니다. 참조자를 이용해서 함수를 정의해도 이러한 일이 가능합니다.


void SwapByRef2(int & &ref1, int &ref2)

{

int temp = ref1;

ref1 = ref2;

ref2 =temp;

// Call-by-reference

}


다음과 같이 질문할 수도 있겠습니다. " 참조자는 선언과 동시에 변수로 초기화되어야 한다면서요!"


맞습니다. 하지만 매개변수는 함수가 호출되어야 초기화가 진행되는 변수들입니다. 즉, 위의 매개변수 선언은 초기화가 이뤄지지 않은 것이 아니라, 함수호출 시 전달되는 인자로 초기화를 하겠다는 의미의 선언입니다.


int main(void)

{

int val1 = 10;

int val2 = 20;

SwapByRef2(val1, val2);

cout << "val1: "<<val1 << endl;

cout << "val2: "<<val2 << endl;

return 0;

}


매개변수로 선언된 참조자 ref1과 ref2는 main 함수에서 선언된 val1과 val2의 또 다른 이름이 됩니다. 그리고 SwapByRef2 함수 내에서는 이 두 참조자를 통해서 값의 교환 과정을 거치기 때문에, 그 결과는 실제로 val1과 val2의 값의 교환으로 이어집니다.



출처

  • 열혈 C++ 프로그래밍

참조자의 이해


무엇을 가리켜 변수라 할까요? 다음은 여러분이 잘 알고 있는 변수의 정의입니다.


변수는 할당된 메모리 공간에 붙여진 이름입니다. 그리고 그 이름을 통해서 해당 메모리 공간에 접근이 가능합니다.



int &num2 = num1;


이 문장은 다소 혼란스러울 수 있습니다. 왜냐하면 & 연산자는 변수의 주소 값을 반환하는 연산자이기 때문입니다. 하지만 위의 문장에서 보이듯이 & 연산자는 전혀 다른 의미로도 사용됩니다. 이미 선언된 변수의 앞에 이 연산자가 오면 주소 값의 반환을 명령하는 뜻이 되지만, 새로 선언되는 변수의 이름 앞에 등장하면, 이는 참조자의 선언을 뜻하는게 됩니다.


int *ptr = &num1;    // 변수 num1의 주소 값을 반환해서 포인터 ptr에 저장해라!

int &num2 = num1    // 변수 num1에 대한 참조자 num2를 선언해라!


따라서 변수 num1의 선언 이후에 다음 문장이 실행되면,


int &num2 = num1;


num2는 num1의 '참조자'가 되며, 이는 num1이라 이름 붙어있는 메모리 공간에 num2라는 이름이 하나 더 붙은 꼴입니다.


참조자는 자신이 팜조하는 변수를 대신할 수 있는 또 하나의 이름인 것입니다.


전통적으로 C++에서는 참조자를 다음과 같이 설명합니다.


변수에 별명(별칭)을 하나 붙여주는 것 입니다.




참조자의 수에는 제한이 없으며, 참조자를 대상으로도 참조자를 선언할 수 있습니다. 


하지만, 필요 이상으로 참조자를 선언하는 것은 바람직하지 않으며, 참조자를 대상으로 또 다른 참조를 만드는 일이 흔히 필요하지는 않습니다. 



참조자의 선언 가능 범위


참조자는 변수에 대해서만 선언이 가능하고, 선언됨과 동시에 누군가를 참조해야만 합니다.


int &ref = 20; (x)


참조자는 본디, 변수에 또 다른 이름을 붙이는 것이기 때문에 상수를 대상으로 참조자를 선언할 수는 없습니다. 또한 미리 참조자를 선언했다가, 후에 누군가를 참조하는 것은 불가능하며, 참조의 대상을 바꾸는 것도 불가능합니다.


int &ref; (x)


참조자를 선언하면서 NULL로 초기화하는 것도 불가능합니다. 


int &ref = NULL; (x)


참조자는 무조건 선언과 동시에 변수를 참조하도록 해야 합니다.


int arr[3] = {1, 3, 5};

int &ref1 = arr[0];

int &ref2 = arr[1];

int &ref3 = arr[2];


배열요소는(배열이 아니라, 배열의 요소는) 변수로 간주되어 참조자의 선언이 가능합니다. 그리고 포인터 변수도 변수이기 때문에 참조자의 선언이 가능합니다.


int num = 12;

int *ptr = &num;

int **dptr=&ptr;


int &ref = num;

int *(&pref) = ptr;

int **(&dpref) = dptr;



출처

  • 열혈 C++ 프로그래밍


실행중인 프로그램은 운영체제로부터 메모리 공간을 할당 받는데, 이는 크게 데이터, 스택, 힙 영역으로 나뉩니다.


  • 데이터 : 전역변수가 저장되는 영역


  • 스택 : 지역변수 및 매개변수가 저장되는 영역


  • 힙 : malloc 함수 호출에 의해 프로그램이 실행되는 과정에서 동적으로 할당이 이뤄지는 영역


  • malloc & free : malloc 함수 호출에 의해 할당된 메모리 공간은 free 함수 호출을 통해서 소멸하지 않으면 해제되지 않습니다.


출처

  • 열혈 C++ 프로그래밍
  • const int num = 10;

-> 변수 num을 상수화!


  • const int * ptr1 = &val1;

-> 포인터 ptr1을 이용해서 val1의 값을 변경할 수 없음


  • int * const ptr2 = &val2;

-> 포인터 ptr2가 상수화 됨


  • const int * const ptr3 = &val3;

-> 포인터 ptr3이 상수화 되었으며, ptr3을 이용해서 val3의 값을 변경할 수 없음


출처

  • 열혈 C++ 프로그래밍
#include <iostream>

inline int SQUARE(int x)
{
    return x*x;
}

int main(void)
{
    std::cout << SQUARE(5) << std::endl;
    std::cout << SQUARE(12) << std::endl;
    return 0;
}

매크로를 이용한 함수의 인라인화는 전처리기에 의해서 처리되지만, 키워드 inline을 이용한 함수의 인라인화는 컴파일러에 의해서 처리가 됩니다. 따라서 컴파일러는 함수의 인라인화가 오히려 성능에 해가 된다고 판단할 경우, 이 키워드를 무시해버리기도 합니다. 또한 컴파일러는 필요한 경우 일부 함수를 임의로 인라인 처리하기도 합니다.


다음과 같이 정의된 인라인 함수는

inline int SQUARE(int x) { return x*x; }


int형 기반으로 정의된 함수이기 때문에 다음의 함수 호출 문장에서 데이터 손실이 발생합니다.

std::cout << SQUARE(3.15);


함수의 오버로딩을 통해서 이 문제를 해결할 수는 있으나, 그렇게 되면 여러 개의 함수를 추가로 정의하는 꼴이 되니, 한번만 정의하면 되는 매크로 함수의 장점과는 거리가 멀어지게 됩니다. 그러나 c++의 템플릿이라는 것을 이용하면 매크로 함수와 마찬가지로 자료형에 의존적이지 않은 함수가 완성됩니다.


#include <iostream>

template 
inline T SQUARE(T x)
{
    return x*x;
}

int main(void)
{
    std::cout << SQUARE(5.5) <<< std::endl;
    std::cout << SQUARE(12) <<< std::endl;
    return 0;
}


위 코드를 실행해보면, 데이터의 손실이 발생하지 않음을 알 수 있습니다.



출처

    • 열혈 C++ 프로그래밍



int main(void)
{
    MyFunc(20);
    MyFunc(30, 40);
    return 0;
}

함수호출 시 전달되는 인자를 통해서 호출하고자 하는 함수의 구분이 가능하기 때문에 매개변수의 선언형태가 다르다면, 동일한 이름의 함수정의를 허용할 수 있습니다. 이러한 형태의 함수정의를 가리켜 '함수 오버로딩(Function Overloading)'이라 합니다.


c++은 호출할 함수를 찾을 때, '함수의 이름', '매개변수의 선언', 이 두 가지 정보를 동시에 활용합니다.


즉, 다음의 함수 호출문을 보면,


My Function(30, 40);


다음과 같이 판단하고, 함수를 찾습니다.


"두 개의 int형 정수를 인자로 전달받을 수 있는 MyFunc라는 이름의 함수를 찾아야겠군!"


반면 c언어는 함수의 이름만 이용해서 호출대상을 찾는다. c언어에서는 함수의 오버로딩이 불가능하며, 이를 문법적으로 허용하지도 않습니다.


함수의 오버로딩이 가능하려면 다음 조건을 만족해야 합니다.


"매개변수의 자료형 또는 개수가 다르다."



출처

    • 열혈 C++ 프로그래밍


+ Recent posts