LISTORY

[C++] 레퍼런스 함수 정리 본문

IT/C++

[C++] 레퍼런스 함수 정리

LiStoryTeller 2018. 11. 22. 15:13



이제까지 C++ 언어를 사용하고는 있었지만, 막상 기초부터 차근차근히 공부한 적이 없는 것 같아 다시 C++을 기초부터 공부하려고 한다.


인터넷 강좌를 보면서 배우는 것이 가장 좋을 것 같다는 생각이 들어 YouTube에서 C++ 강좌를 찾았고, 그 중에서 열혈 C++ 책의 저자이신  윤성우 님의 강좌를 찾아서 듣게 되었다.


열혈 C++ YouTube 강좌 :  https://www.youtube.com/playlist?list=PLJRimEWvctNAfE5JrkwswQv6Yy4abqDl2


강좌를 처음부터 끝까지 전부 듣고 싶지만, 뭔가 마음이 급한 관계로 원하는 부분만 일단 들을 생각이고, 필요한 부분을 정리 및 공부하여 블로그에 남길 생각이다.


첫번째로 남길 강의는 레퍼런스 함수에 관련된 부분이다.


책에서는 2장에 해당한다.



레퍼런스(Reference)의 이해


레퍼런스와 변수(변수의 이름)

- 레퍼런스와 변수는 생성되는 방법에 있어서만 차이를 보인다.

- 만들어지고 나면 완전히 같은 것!


int function(void)

{

int val;

val = 20;

int &ref = val;

return val;

}


int function(void)

{

int val;

val = 20;

int &ref = val;

return ref;

}


여기 두개의 예제 sample이 있다. 


이 두개의 예제의 차이점은, 위의 예제의 경우 int형의 변수를, 밑의 예제는 int &형의 변수를 return 하고 있다는 것이다. 


두 예제의 함수 return 형은 int이다. 그렇다면 밑에 예제는 사용할 수 있는 것일까?


int의 레퍼런스 타입으로 리턴해야 하는 경우도 있다. 하지만 여기서는 문제가 없다.


레퍼런스와 변수 이름은 생성되는 방식에 있어서만 차이가 있지, 만들어지고 나면 완전히 같은 것이기 때문이다.


그러므로 이 두 예제의 차이점은 전혀 없다. 


다음 예제를 보겠다.


void function(void)

{

int val;

val = 20;

int &ref1 = val;

int &ref2 = ref1;

}


void function(void)

{

int val;

val = 20;

int &ref1 = val;

int &ref2 = val;

}


val이라는 메모리 공간을 만들고 20으로 값을 초기화했다. 


위에 예제를 보면 그 메모리 공간은 val이라는 이름을 지니고 ref1이라는 이름도 지니게 된다.


또한 ref1의 공간에 ref2의 이름도 부여하기를 원하고 있다.


밑에 예제는 val에 ref1, ref2의 이름을 모두 부여하기를 원하고 있다.


모두 문제가 없는 코드이다. 다 결과적으로 ref2라는 이름도 val에 붙게 된다.


즉, C++에서는 특정 메모리 공간에 여러 개의 이름을 부여할 수 있다.


이게 바로 레퍼런스 이다.



레퍼런스의 제약

- 이름이 존재하지 않는 대상을 레퍼런스 할 수 없다.

  선언과 동시에 반드시 초기화되어야 한다.


int main(void)

{

int &ref1; // 초기화 되지않았으므로 error!

int &ref2 = 10; // 상수가 올 수 없으므로 error!

...

}


그렇다면 이 레퍼런스라는 것은 왜 등장하게 된걸까?



레퍼런스 함수


레퍼런스가 주는 장점과 단점을 다뤄보겠다.


기본적으로 call-by-value, call-by-reference 가 무엇을 의미하는지 알고 있어야 한다.


포인터를 이용한 Call-By-Reference

- 함수 외부에 선언된 변수의 접근이 가능

- 포인터 연산에 의해서 가능한 것이다

- 따라서 포인터 연산의 위험성 존재

- swap1.cpp


void swap(int *a, int *b)

{

int temp = *a;

*a = *b;

*b = temp;

}


위에 swap 코드가 있다. 코드를 보면, 함수 내부에서 함수 외부에 있는 변수에 직접 access 하여 조작하고 있다. 이러한 것을 Call-By-Reference라고 한다.


- swap2.cpp


int main(void)

{

int val1 = 10;

int val2 = 20;

...

swap(val1, val2);

...

return 0;

}


- swap function area


void swap(int &a, int &b)

{

int temp = a;

a = b;

b = temp;

}


위에 코드를 보면 두개의 메모리 공간을 할당하여 val1, val2라는 이름을 붙여주고 각각의 값(10, 20)으로 초기화해주었다.


그리고 나서 swap 함수를 써서 각각을 인자로 전달하는데, 문제는 전달되는 인자를 레퍼런스로 받고 있다.


즉, val1을 전달했는데 그것을 a라는 이름의 레퍼런스로 받고 있는 것이다.


그러므로 이는


int &a = val1; 

int &v = val2;


위의 문장과 같은 뜻이다.


물론 차이점은 지닌다. a와 b는 swap 함수 내에서 사용할 수 있는 변수이고 , val1, val2는 main 함수에서 사용되는 변수이다.


중요한 사실은 a나 b도 메모리 공간에 붙어진 이름이기에 이러한 연산을 할 경우에 둘이 지니고 있는 값은 완전히 바뀌게 된다는 것이다.


temp라는 값을 선언하여 a가 지닌 값을 temp에 넣고 b가 지닌 값은 a에 둔다.


마지막으로 temp에 값을 b에 넣어주니까 b는 10이 되고, a는 20이 된다.


그러면 이는 call by value일까 reference 일까? 이 또한 reference 이다.


swap 함수 내부에서 외부에 선언된 변수를 직접 access 하여 조작하기 때문이다.


즉, swap1.cpp와 비슷하다. 기능이 같다.


다만 swap1.cpp 예제는 포인터를이용한 것이고, swap2.cpp는 레퍼런스를 이용한 call-by-reference 이다.



swap2.cpp의 장점은 포인터를 사용하지 않는 다는 것이다.


포인터는 메모리를 직접 접근할 수 있는 자유를 갖고 있지만 프로그래머가 상당히 주의해야 한다는 제한사항이 존재한다.


C++에서는 가급적으로 포인터를 사용하지 않는 것이 좋다.


그래서 swap2.cpp의 코드가 더 안정적이다.


다시 swap1.cpp의 코드를 보겠다.


void swap(int *a, int *b)

{

int temp = *a;

*a = *b;

*b = temp;

}


기본적으로 a는 현재 어떤 메모리 공간을 가리키고 있다.


이 a라는 포인터를 가지고 포인터 연산을 하고 있다. 여기서 위험한 점은,


만일 val이라는 변수를 선언한 다음에 그 주소값을 a를 통해 전달되었다하면, a를 1만큼 증가했을 경우 실제로 4바이트가 증가한다. 


즉, 그럼 a가 가리키는 곳에서 4바이트를 건너뛴 곳을 가리키는 것이다.


이건 처음에 의도한 주소가 아닌, 전혀 다른 위치이기 때문에 아주 치명적인 오류가 될 수 있다.



Call-By-Value 관점에서 봤을 때 제공하는 장점


- reffunc.cpp 


struct _Person{

int age;

char name[20];

char personalID[20];

}


typedef struct _Person Person;


void ShowData(Person p)

{

cout<>"개인정보 출력"<<endl;

...

}


int main(void)

{

Person man;


cout<<"이름 : ";

cin >> man.name;


cout<<"나이 : ";

cin >> man.age;


cout<<"주민번호 : ";

cin >> man.personalID;


ShowData(man); // Call-By-Value

return 0;

}


위의 코드를 보면 ShwData()함수 호출 시, 44바이트의 메모리 공간 복사가 진행된다.


이를 만약 reference로 받았다고 가정해보자. 


void ShowData(Person& p)

{

cout<>"개인정보 출력"<<endl;

...

}


int main(void)

{

Person man;


cout<<"이름 : ";

cin >> man.name;


cout<<"나이 : ";

cin >> man.age;


cout<<"주민번호 : ";

cin >> man.personalID;


ShowData(man); // Call-By-Value

return 0;

}


이렇게 인자를 레퍼런스 형식으로 고치면 44바이트의 메모리 공간 복사가 발생하지 않는다. 


훨씬 더 메모리 공간을 효율적으로 사용하는 것이다.



레퍼런스를 쓰는 것에 단점이 있다면 레퍼런스로 받으면 p라는 레퍼런스의 값을 바꾸면 원본 데이터가 변경된다는 것이다.


이는 레퍼런스의 경우, 같은 메모리 공간을 가리키고 있기에 당연한 것이다.


그렇다면 원본 데이터를 애초에 조작하지 못하게 하려면 어떻게 해야할까?


void ShowData(const Person& p)

{

cout<>"개인정보 출력"<<endl;

...

}


이렇게 하면 아예 변경을 하지 못한다.



레퍼런스를 리턴하는 함수의 정의


/* ref_return.cpp */


int& increment(int &val)

{

val++;

return val;

}


int main(void)

{

int n = 10;

int &ref = increment(n);


cout<<"n : "<<n<<endl;

cout<<"ref "<<ref<<endl;

return 0; 

}


위의 예제는 레퍼런스를 리턴하는 함수이다. main 함수 내에서 n이라는 이름으로 변수 선언하고, 10으로 초기화 하였다.


그걸 increment 함수를 호출하면서 val이라는 이름을 하나 더 얻게된다.


마지막엔 val을 return하는데, 이를 레퍼런스타입으로 받아주고 있다.


val이라는 메모리 공간에 ref라는 이름을 하나 더 붙여주겠다는 뜻이 된다.


즉, n, val, ref 모두 같은 메모리 공간을 의미한다.


하지만 val이라는 이름은 함수 내에서 지역적으로 붙은 것이다.


즉, val이라는 레퍼런스는 함수 내에서만 유효하고, 함수가 끝나면 사라져 버린다.



int increment(int &val)

{

val++;

return val;

}


int main(void)

{

int n = 10;

int &ref = increment(n);


cout<<"n : "<<n<<endl;

cout<<"ref "<<ref<<endl;

return 0; 

}


만일 return 형이 int&이 아니라 int이면 어떨까?


컴파일하면 바로 에러가 발생한다.

val을 리턴하는데 int로 리턴한다는 것은 값을 복사했다는 뜻이므로, ref에는 상수가 리턴이 된다.

이건 문제가된다. 레퍼런스는 상수에 이름을 부여하는 용도로는 절대 사용할 수 없기 때문이다.

이 코드가 원활히 돌아가려면 하단의 int& reference 또한 int 형으로 바꾸어 주어야 한다.

즉, 메모리 공간이 독립적으로 할당이 된다.


/* ref_error.cpp */


int& function(void)

{

int val = 10;

return val;

}


int main(void)

{

int ref = function();


cout<<ref<<endl;

return 0; 

}


그렇다면 이 모드는 어떨까?


컴파일에서 오류가 발생하지 않고 런타임에도 그럭저럭 잘 돌아간다.


하지만 이 코드는 짜선 안되는 코드이다.


val이라는 변수는 지역변수이다. 함수 호출이 끝나면 사라진다.


그럼 main에서 ref가 가리키는 대상은? 보상받을 수 없다.


그러므로 지역변수는 reference 타입으로 할당하지 말아야 한다.



new와 delete 연산자의 기본적 기능


/* new_delete.cpp */


int main(void)

{

int *val = new int;

int *arr = new int[size];

...

delete val;

delete []arr;

...

return 0;

}


/* malloc_free.cpp */


int main(void)

{

int *val = (int*)malloc(sizeof(int));

int *arr = (int*)malloc(sizeof(int)*size);

...

free(val);

free(arr);

...

return 0;

}


하단의 코드를 보면 int 형 변수를 heap에 할당하고 포인터가 리턴된다.


malloc 함수는 단순히 인자로 전달되는 int형 데이터의 크기만큼 임의적으로 메모리 할당한다.


또한 malloc 함수는 해당 변수가 어떠한 용도(여기서는 int로 사용되는...)로 사용할지 모르므로 void 포인터 타입으로 리턴한다.


그러므로 우리가 할당하고자 원하는 메모리의 특성에 따라 크기를 인자로 계산하여 전달해야 한다.


또한 리턴되는 포인터도 용도에 맞게 적절히 형변환을 해줘야 한다.


근데 c++은 다 자동으로 처리해준다.


new를 하면 할당하고자 하는 데이터의 특성을 피연산자로 갖는다


직접 계산하여 힙에 할당 하고 알아서 int형으로 리턴해주기 때문에 malloc과는 달리 명시적 형변환이 필요하지 않다.


* new & delete 특성 하나 더! 


NULL 포인터 리턴하는 new 연산자

- 메모리 할당 실패 시 NULL 포인터 리턴

- 새로운 표준에 관한 내용은 예외 처리를 통해서 다시 언급





'IT > C++' 카테고리의 다른 글

[C++] 템플릿(Template)  (1) 2018.11.26
[C++] 복사 생성자  (0) 2018.11.22
Comments