본문 바로가기

Programming/C++

[C++] 2. const / 메모리 공간 / Reference 참조자 / 함수에서의 참조(Reference in function)

이 포스팅은 윤성우님의 열혈 C++ 프로그래밍 책을 기반으로 작성되었습니다.

먼저 시작 전에 헷갈리거나 중요한 개념을 몇개 잡고 시작해보고자 한다.

Const는 어떤 의미를 가지는가?

const는 말 그대로 상수화이다.
이렇게 보면 굉장히 쉬운 개념 같지만 쓰임새가 다양하면서 사람들에게 혼란을 주기도 하는데, 다음과 같이 깔끔하게 정리할 수 있다.

const int num=10; 
//num이라는 변수는 10을 값으로 가지며 이를 변경할 수 없다.

const int *ptr = #
//ptr포인터는 변수 num을 가리키며 이 ptr을 이용해서 가리키는 변수의 값을 변경할 수 없다. 

int * const ptr = #
//ptr포인터는 변수 num을 가리키며 이 ptr이 가리키는 변수를 변경할 수 없다.
//ex)ptr은 계속 num을 가리켜야하며 이 num의 값은 포인터를 통해 바뀔 수도 있음.(num1을 가리킬 수 없음)

const int * const ptr = #
//ptr포인터는 변수 num을 가리키며 이 ptr이 가리키는 변수를 변경할 수 없고 포인터를 이용해 그 값도 변경할 수 없다.

const가 사용하면서 까다롭다고 여겨지는 이유는 이러한 복잡성도 있지만 한번 const 선언이 들어가면 그 변수와 관련된 변수들은 줄줄이 const 선언이 들어가야하기 때문이다. 이는 다른 변수를 조작함으로써 기존의 변수가 조작될수도 있기 때문이다. 흔히들 이런 에러가 발생하면 조용히 const를 지우고는 하는데, const를 빈번히 계속 사용하는 것은 코드가 지저분해지는 것이 아닌 안전성을 높이는 좋은 코드라는 뜻이다. 
ex) const int num =20 으로 설정했다면 int &ref = num은 오류가 난다. const int &ref = num으로 해줘야 한다.
마지막으로 뒷부분에 참조자는 상수를 참조할 수 없다고 하지만 const 참조자는 상수를 참조할 수 있다. ex) const int &ref=1; 이 가능.
이는 void AAA (const int &num){}과 같은 함수에 매개변수로 변수 뿐만 아니라 상수또한 넘기는 것을 가능하게 해준다.

 

메모리공간

우리가 프로그램을 돌릴 때, 운영체제로부터 크게 데이터, 스택, 힙 세가지 영역의 메모리를 할당받게 된다.
데이터는 전역변수가 저장되는 공간.
스택은 지역변수 및 매개변수가 저장되는 공간.
힙은 동적으로 할당이 이루어지는 영역으로 c에서는 malloc과 free  c++에서는 new와 delete개념으로 볼 수 있다.
동적할당에 대한 것은 다음 포스팅에서 자세히 다뤄보도록 하겠다.

포인터

포인터에 대해서 잠깐만 언급을 하고 넘어가자면, 포인터는 어떠한 데이터의 주소를 가리키는 값이다.
int *ptr = # 과 같이 정의할 수 있으며 이렇게 정의와 동시에 포인팅할 값을 할당할 경우는 포인팅할 주소값을 함께 적어준다.
int *ptr;로만 정의 후 이후에 포인팅할 값을 할당할 수도 있다.
이 경우는 int *ptr;이후에 ptr = &num 이렇게 해준다.

이유는 간단하다.
int *ptr = &num과 같이 정의할 때 만약 *이 없다면 이건 그냥 일반 변수를 정의하는 것과 전혀 구별할 수 없을 것이다.
그렇다면 int *ptr; 선언 이후에는 왜 안 붙이는 것일까?
선언 이후에 사용되는 *은 해당 포인터가 가리키는 값을 의미하며 *을 안 붙였을 경우 가리키고 있는 주소를 의미한다.
따라서 처음에 포인터로 어디를 포인팅할 지 할당할 경우에는 ptr=&num과 같이 주소를 할당해주는 것이 맞고
그 num값을 변경하고자 할 때는 *ptr=10과 같이 num의 값을 10으로 변경할 수 있다.
마지막으로 pointer를 초기화하거나 아무것도 안 가리키게 만들고자 할 때는 간단하게 ptr=NULL로 설정해주면 된다.

참조자 Reference

참조자는 흔히들 주소값을 저장한다는 개념으로 많이 아는데, 쉽게 생각해서 기존에 선언된 변수에 이름을 하나 더 붙인다고 생각할 수 있다.
예를 들어,  int &num2 = num1; 라고 하면 기존 num1에 저장된 값이 num2라는 이름도 가진 느낌이다. 사실상 거의 맞다.
이는 제한이 없기에 int &num3 = num2; int &num4 = num3; 등등을 통해 하나의 data가 4개의 이름을 가지게 될 수도 있다.
하지만 이러한 참조자는 오직 변수만 대상으로 선언할 수 있기에 대상을 선언하지 않거나, 상수나 NULL 값을 선언할 수 없다.

또한 포인터 역시 변수라는 것을 잊어서는 안된다!
따라서 포인터 역시 참조할 수 있으며 이럴 경우에는 다음과 같이 선언한다.

int main(void)
{
    int num = 10;
    int *ptr = #
    int **dptr = &ptr; //ptr의 주소 가리키는 이중포인터 dptr
    
    int &ref = num;
    int *(&pref) = ptr;
    int **(&dpref) = dptr;
    
    cout<<ref<<" "<<*pref<<" "<<**dpref<<endl;
    return 0;
}

>>> 10 10 10

 

함수에서의 참조자 (Call by reference)

함수 호출 시 매개변수의 값을 전달하는 것을 call by value, 주소값을 전달하는 것을 call by reference라고 한다. 
따라서 call by value에선 함수 외부에서 선언된 변수를 조작할 수 없지만 call by ref에선 주소값으로 접근하기에 조작이 가능하다.
Call by reference는 다음과 같이 포인터나 주소값을 이용한다.

// 포인터를 이용하는 경우
int callbyref(int *ptr)
{
    *ptr=1;
    return ptr;
}

// 주소값을 이용하는 경우
void callbyref2(int &ref)
{
    ref=1;
}

// 주소값을 이용하지만 ref값을 변경시키지 않을 거에요!
int callbyref3(const int &ref)
{
    int num = 10;
    num = num + ref;
    return num;
}


하지만 또 참조자를 이용해 call by reference하더라도 외부의 값을 조작하지 않고자 할 수 있다.
이 때는 void callbyref3(const int &ref)와 같이 작성해주면 된다.
이러한 const를 활용한 습관은 실수도 방지해줄 뿐만 아니라 이 함수에서는 데이터 변경이 일어나지 않는다는 정보도 주기 때문에 더욱 좋다.

또한 참조를 매개변수로 하는 데이터가 참조를 반환하는 경우를 생각해보자.
해당 함수를 RefRet1(num)이라 하고 이가 넘겨받은 매개변수를 리턴한다고 하면
num2 = RefRet1(num)은 num의 값을 num2에 저장하는 형태가 될 것이며,
&num2 = RefRet1(num)은 num의 주소를 넘겨주기에 num2가 num의 데이터의 또 다른 이름이 될 것이다.
반면 위의 함수가 그냥 값을 리턴한다면 후자의 코드는 에러가 날 것이다.
뿐만 아니라, 변수를 리턴한다고 할지라도 매개변수가 아닌 함수 내에서 정의된 변수를 리턴한다면 함수가 종료되며 해당 주소는 사라지기에 에러가 날 것이다.