본문 바로가기
Basic/C/C++

[C++ 언어] 제 2 강 : C++의 레퍼런스

by boxbop 2012. 2. 2.
반응형


 사실 이번장은 1강에 이어 설명하고자 하는 내용을 담았습니다. 레퍼런스에 대한 내용을 주로 설명할 것 입니다~

 C++에는 C언에서 존재하지 않았던 새로운 자료형이 등장합니다. bool형이 그것이죠. bool형의 변수는 true와 false둘 중 하나가 될 수 있습니다. 사실 이 값을 int형으로 변환해주면 true는 1을, false는 0을 나타내지만 그냥 bool형 데이터로서 인정을 해주는 편이 더 좋습니다.

 #include <iostream>
 using std::cin;
 using std::cout;
 using std::endl;

bool trueOrfalse(int i)
{
    if(i<0)
       return false;
    esle
       return ture;
 }

int main(void)
{
    int num;
    bool result;
    cin>>num;

    result = trueOrfalse(num);
    if(resutl == true)
    {
         cout<<"0보다 크거나 같은 수"<<enld;
     }
     else
     {
         cout<<"0보다 작은 수"<<enld;
      }
    return 0;
  }

 레퍼런스(reference)
: 레퍼런스는 이름을 지니고 있는 대상에게 지어주는 별명을 예로들면 쉽게 이해할 수 있습니다. 다음을 예로 들어보겠습니다.

  int &ref = value;

위의 문장은 vla이라는 int형 변수의 이름에 ref라는 별명을 붙인 것 입니다. &연산자는 C에서 주소 값을 얻기위해 사용했습니다만 사용하는 위치에 따라서 주소 값을 얻는데 사용될 수 있고, 레퍼런스를 선언하는데 사용될 수 있습니다.

 int *ptr = &value;  // 주소 값을 얻기 위한 &연산자
 int &ref = vla;       // 레퍼런스 선언을 위한 &연산자

 차이점 충분히 이해가 가셨나요? 그렇다면 ref와 value는 동일한 취급을 받습니다. 즉, 레퍼런스를 가지고 하는 연산은 레퍼런스가 참조하는 변수의 이름을 가지고 하는 연산과 같은 효과를 지니게 됩니다.

 메모리 관점에서 레퍼런스를 좀 더 살펴볼까요? 변수를 선언하게되면 메모리 공간 할당이 이루어지고 이름을 부여하게 됩니다. 사실 C언어에서는 하나의 메모리 공간에 하나의 이름만 부여할 수 있었지만 C++에서는 그렇지 않습니다. 하나의 메모리 공간에도 여러가지의 이름을 부여할 수 있는 것이죠~ 이름이 존재 하는 공간에 하나의 이름을 더 부여하는 행위가 레퍼런스 선언이 되는겁니다. 중요한것은 레퍼런스와 변수는 생성되는 방법에 있어서만 차이를 보일 뿐, 일단 만들어지고 나면 완전히 같은 것입니다.

 그러나 변수와 레퍼런스는 만들어지는 과정에서 차이점이 분명 존재합니다. 변수는 새로운 메모리 공간에 이름을 부여하지만 레퍼런스는 이미 이름을 지니고 있는 메모리 공간에 하나의 이름을 더 부여하는 것이기 때문이죠.

 int &ref;
 int &ref = 10;

 위와 같은 문장은 잘못된 문장입니다. 레퍼런스는 선언과 동시에 반드시 초기화 되어야하기 때문입니다.

 레퍼런스를 이용한 Call By Reference

void swap (int &a, int &b)
{
    int temp = a;
    a = b;
    b = temp;
 }

int main(void)
{
    int val1=10;
    int val2=20;

    swap(val1, val2);
    return 0;
 }

 swap이라는 함수에서 val1과 val2를 인자로 전달하는 과정을 유심히 보세요~ swap함수는 전달되는 인자를 레퍼런스로 받고 있습니다. 즉, val1은 a, val2는 b라는 별명이 주어지게 되고 각각의 변수는 동일하게 취급됩니다. 즉, main함수 내에서 선언한 변수 val1, val2라는 이름이 붙어 있는 메모리 공간에 a와 b라는 이름이 하나씩 더 붙게 된 것입니다. 때문에 swap함수 내에서는 a와 b라는 이름으로 main함수 내에 선언된 변수 val1, val2에 직접 접근이 가능하게 된 것입니다.

 레퍼런스의 다른 장점을 Call By Value에 적용시켜 예를 들어보겠습니다.

struct _Person
{
    int age;
    char name[20];
    char personalID[20];
 };
 typedef struct _Person Person;

 void ShowData(Person p)
 {
    cout<<"이 름 : <<p.name<<endl;
    cout<<"주민번호 : <<p.personalID<<enld;
    cout<<"나 이 : <<p.age<<endl;
 }

 int main(void)
{
    Person man;
   
    cin >> man.name;
    cin >> man.age;
    cin >> man.personalID;
 
    ShowData(man);
    return 0;
 }

 갈 수록 예제가 길어지네요...ㅠㅠㅠ헉헉헉.... 구조체 변수를 출력하는 함수에서 전달되는 인자를 주의 깊게 살펴 보길 바랍니다~ ShowData(man)에서 구조체 변수 man을 인자로 전달하고 있습니다. 전달하는 방식은 call by value입니다. 따라서 변수 man을 매개 변수 p에 복사하게됩니다. 이 과정에서 복사되는 바이트의 수는 int + char*20 + char*20 = 총 44바이트가 됩니다. 단지 출력하기 위해서 이만큼의 바이트를 복사합니다. 때문에 인자로 전달하는 변수의 크기가 클 수록 함수의 호출이 부담스러울 수밖에 없는거죠...

 그러나 레퍼런스를 이용해서 이러한 문제를 해결할 수 있습니다.

 void ShowData(Person &p)
 {
    cout<<"이 름 : <<p.name<<endl;
    cout<<"주민번호 : <<p.personalID<<enld;
    cout<<"나 이 : <<p.age<<endl;
 }

 인자가 &p로 바뀌었죠? 위의 함수는 전달되는 인자를 레퍼런스 형태로 받고 있습니다. 이름만 하나 더 추가하는 것이니까 44바이트나 되는 크기의 복사는 발생하지 않습니다. 때문에 성능은 향상이되죠.

 자 이제 우리는 좀 더 숙달된 프로그래머니까 프로그램의 안전성을 고려해봅시다. ShowData함수는 구조체 변수의 데이터를 출력만 하는 함수입니다. 때문에 레퍼런스를 이용한 값의 변형을 불가능 해야됩니다. 즉 참조만 하여 출력하기만 가능하고 값의 변경을 불가능해야 된다는 이야기입니다. const라는 키워드를 이용해봅시다.

 void ShowData(const Person &p)
 {
    cout<<"이 름 : <<p.name<<endl;
    cout<<"주민번호 : <<p.personalID<<enld;
    cout<<"나 이 : <<p.age<<endl;
 }

 레퍼런스의 선언 앞에 키워드 const를 붙여주었습니다. 이는 레퍼런스 p 자체를 상수화 하겠다는 의미입니다. 즉, 레퍼런스 p를 통한 데이터의 조작을 혀용하지 않겠다라고 선언해주는 것이지요~

 int& function(int &val)
{
    val++;
    return val;
 }

int main(void)
{
    int n = 10;
   
int &ref = increment(n);
   
    return 0;
 }

 int &ref = increment(n) 에서는 n을 인자로 전달하면서 함수를 호출하고 있습니다. 이 함수는 전달인자를 레퍼런스 val로 받고 있지요. function 함수의 구현 부분을 보시면 반환형이 int& 으로 레퍼런스를 반환형으로 받고 있습니다. return값도 val을 반환하기때문에 레퍼런스를 반환하구 있구요. 때문에 ref와 n은 동일하게 됩니다. 다만 function 함수의 매개 변수로 선언된 레퍼런스 val은 지역 변수와 마찬가지로 함수의 호출이 완료되면 사라져버립니다. 때문에 지역 변수를 레퍼런스로 리턴하는 일은 없어야 합니다~ 이정도만 하고 넘어가도록 하겠습니다.

 new 연산자와 delete 연산자

메모리를 동적으로 관리하기 위해서 malloc과 free함수를 사용해왔습니다. C++에서는 이를 대신하는 키워드 new, delete 가 등장합니다.

 int main(void)
{
    int size;
    cin >> size;

    int* arr = (int *)malloc(sizeof(int)*size); //배열을 동적할당합니다.

     ...... //생략

    free(arr); //할당된 메모리를 소멸시킵니다.
    
    return 0;
 }

 malloc 함수와 free함수를 이용하시려면 stdlib.h파일을 포함해주셔야되는거 잊지 마시구요~ malloc함수를 호출하여 인자로 전달된 크기만큼 단순히 메모리 공간을 할당만 하기 때문에 byte단위로 할당하고자 하는 메모리 공간의 크기를 전달해야 하고, void 포인터형으로 반환되는 포인터를 적절히 형 변환해서 사용해야 합니다. 조금은 복잡하죠~? 위 예제는 new, delete를 사용하면 간단해집니다.

 int *val = new int;

이 문장은 int형 데이터 1개를 저장하기 위해 메모리를 할당합니다.

 int *arr = new int[size];

 그렇다면 이 문장은 어떨까요? 길이가 size인 int형 배열을 위한 메모리를 할당해줍니다. malloc에 비하면 훨씬 직관적이고 쉽지않나요? malloc함수는 주소 값을 void 포인터형으로 반환하기 때문에 형 변환을 해야했지만 이제는 그럴필요가 없다는 겁니다. new연산자는 용도에 맞게 포인터를 반환해주기 때문이죠.

 위에서 할당된 메모리 공간을 반환하는 방법은 다음과 같습니다.

 delete val;
 delete []arr;

 주의할 것은 할당된 메모리 공간이 배열일 경우입니다. 모양이 조금 특이하죠?
malloc과 free를 사용한 예제를 new 와 delete를 사용하여 나타내보겠습니다.

 int main(void)
{
    int size;
    cin >> size;

    
int *arr = new int[size]; // 배열을 동적할당합니다.

     ...... //생략

    
delete []arr; //할당된 메모리를 소멸시킵니다.
    
    return 0;
 }

 즉, new 연산자를 사용하면 malloc과 달리, 할당하고자 하는 메모리 공간의 크기를 계산해야 할 필요도 없고, 적절한 형태로 포인터를 형 변환해 줄 필요도 없습니다~!

 여기서도 안전성을 고려해봅시다. new 연산자는 메모리를 동적으로 할당하지만 만약에 메모리의 공간이 부족하여 메모리 할당이 실패했다고 해봅시다. 그렇다면 new연산자는 NULL포인터를 리턴합니다. 그렇다면 위 예제를 다음과 같이 작성해볼 수 있겠네요

 int main(void)
{
    int size;
    cin >> size;

    int *arr = new int[size]; // 배열을 동적할당합니다.

    if(arr == NULL)
    {
         cout<<"메모리 할당 시패"<<endl;
         return -1; //프로그램 종료
     }

     ...... //생략

    delete []arr; //할당된 메모리를 소멸시킵니다.
    
    return 0;
 }

 
 그러나 일반적으로 프로그램을 작성할때는 위와 같은 코드를 삽입하지 않습니다. 운영체제의 메모리 관리 능력을 믿기 때문이죠. 때문에 메모리 검사 코드같은경우는 필요없다고 판단하고 오히려 조건검사 문장이 오기 때문에 성능만 저하시킨다고 생각합니다. 때문에 프로그램을 테스트 하는 과정에서만 위와 같은 오류 검사 코드를 넣고 최종 버전에서는 오류 검사 코드를 포함시키지 않는 방법을 선택하기도 한다더군요...ㅎㅎ 매크로를 이용하면 좀 더 편하게 구현할 수 있다는 겁니다. 걍 한번 보기만 하고넘어가세요~

#include <iostream>

#define DEBUG 1 //테스트 버전
//#define DEBUG 2 //최종 버젼

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

 int main(void)
{
    int size;
    cin >> size;

    int *arr = new int[size]; // 배열을 동적할당합니다.


#if DEBUG == 1

    if(arr == NULL)
    {
         cout<<"메모리 할당 시패"<<endl;
         return -1; //프로그램 종료
     }

#endif

     ...... //생략

    delete []arr; //할당된 메모리를 소멸시킵니다.
    
    return 0;
 }

 이번 장은 여기까지 하도록 하겠습니다. 레퍼런스와 메모리를 동적할당하는 새로운 연산자에 대해서 공부했구요~ 중요한 부분이니까 반드시 숙지하시구요~ 요즘 날씨가 장난아니게 추운대 조심들하세요~ 다음 장에서는 클래스에 관한 내용을 다루도록 하겠습니다~ (--)(__)(--)!!
   
반응형