관리 메뉴

Kim's Programming

C언어 - 포인터 고급(4/4) 본문

Programming/C

C언어 - 포인터 고급(4/4)

Programmer. 2015. 9. 7. 13:16

가변 인수 함수의 활용


가변 인수 함수는 한 번 호출로 여러 개의 정보를 다양한 방법으로 다룰 수 있다는 면에서 편리합니다. 특히 printf 함수는 다양한 타입의 변수들을 한꺼번에 출력할 수 있어 변수값을 확인해 볼때 아주 유용합니다. 이런 함수를 직접 만드려면 독자적으로 서식을 정의하고 서식 문자열과 대응되는 가변 인수를 직접 읽는 복잡한 루틴을 만들어야 하는데 다행히 이런 일을 대신해 주는 함수들을 준비되어 있습니다. 대표적으로 다음 두 함수만 보겠습니다.

1
2
int vprintf(const char * _Format, va_list  argptr);
int vsprintf(char *buffer, const char *format, va_list argptr); 
cs

이 외에 vscanf, vssanf등의 함수도 있는데 알파벳 v(Variable)로 시작한다고 해서 이런 함수들을 v 계열의 함수라고 합니다. 위 두 함수들은 printf, sprintf와 동일한 기능을 수행하는데 가변 인수를 직접 나열하는 대신 가변 인수가 시작되는 번지만 인수로 취한다는 점이 다릅니다. 즉 실제로 가변 인수를 취하지는 않으며 가변 인수를 취하는 다른 함수의 내부에서 printf의 서식을 해석하고 적용하는 일을 대신합니다. 이 두 함수를 사용하면 printf 처럼 동작하는 비슷한 함수를 직접 만들어 쓸 수 있습니다. 다음 함수는 C/C++언어의 가변 인수 기능을 활용하여 실행 중에 변수값을 디버거로 실시간 확인해 보는 기능을 제공합니다.

1
2
3
4
5
6
7
8
9
void CustomTrace(char *format, ...)
{
    char buf[1024];
    va_list marker;
 
    _crt_va_start(marker, format);
    vsprintf(buf, format, marker);
    OutputDebugString(buf);
}
cs

OutputDebugString라는 api 함수가 사용되었는데 이 함수는 주어진 문자열을 디버깅 창으로 출력합니다. Visual C++의 경우 Output 윈도우에 이 함수의 출력 내용이 나타나므로 실행 중에 변수값의 변화를 확인하거나 특정 함수의 호출 시점, 회수 등을 알고 싶을 때 중간 중간에 이 함수를 삽입해 놓으면 됩니다. 사용 예는 다음과 같습니다.

1
2
CustomTrace("변수 a=%d, 변수 f=%f\n",a,f);
CustomTrace("함수 function이 %d 번째 호출되었음",count++);
cs

CustomTrace 함수의 내부는 간단합니다. va_start로 첫 번째 가변 인수의 번지를 구한 후 그 번지를 서식 문자열과 함꼐 vsprintf 함수로 넘기기만 하면 됩니다. OutputDebugString 함수를 직접 사용할 수 있지만 이 함수는 단순 문자열만 출력할 수 있는데 비해 CustomTrace는 서식화된 문자열을 출력할 수 있어 훨씬 더 편리합니다.


레퍼런스


변수의 별명


레퍼런스(Reference)는 C++에서 새로 추가된 기능이며 변수의 별명(alias)을 정의합니다. 별명을 붙이게 되면 한 대상에 대해 두 개의 이름이 생기게 되고 본래 이름은 물론이고 별명으로도 변수를 사용할 수 있습니다. 레퍼런스를 선언하는 기본 형식은 다음과 같습니다.

1
type &변수 = 초기값;
cs

포인터 변수를 선언할 때 구두점 *을 사용하는데 비해 레퍼런스를 선언할 때는 구두점 &를 사용합니다. 포인터가 기본 타입에 대한 유도형이듯이 레퍼런스도 유도형이라는 점에서 동일하되 특정 대상체에 대한 별명이므로 선언할 때 어떤 대상체에 대한 별명인지를 반드시 밝혀야 한다는 점에서 다릅니다. 다음은 레퍼런스를 사용하는 소스입니다.

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
 
void main()
{
    int i = 2;
    int &ri = i;
    printf("i=%d, ri=%d\n", i, ri);
    ri++;
    printf("i=%d, ri=%d\n", i, ri);
    printf("i번지=%x, ri번지=%x\n"&i, &ri);
}
cs

정수형 변수 i를 3으로 초기화했으며 정수형 레퍼런스 ri를 i로 초기화 했습니다. int &ri=i선언에 의해 변수 i 에 대해 ri라는 별명을 만든 것입니다. 이후 ri는 i와 완전히 동일한 대상을 가리키며 둘 중 하나를 변경하면 나머지 하나도 바뀌게 됩니다. 위 소스의 실행 결과는 다음과 같습니다.


i와 ri의 값을 출력했는데 둘 다 똑같은 2를 가집니다 이 상태에서 ri를 ++로 증가시킨 후 값을 출력해보면 ri만 증가하는 것이 아니라 i와 ri가 같이 증가되어 둘 다 3이 됩니다. ri가 i의 별명이기 떄문에 ri에 대입되는 값은 i 에도 똑같이 대입되며 반대로 i의 값을 바꾸면 ri도 같이 변경됩니다. 두 변수가 가리키는 실제 번지를 출력해 보면 동일한 위치를 가리키고 있음을 확인 할 수 있습니다. 보다시피 레퍼런스는 대상체와 동일한 주소를 가지는 완전한 별명입니다. ri는 i와 이름만 다를 뿐이지 같은 변수인 것입니다. T형 변수 v의 별명 r을 만들고 싶다면 얼마든지 T &r=v;로 선언하면 됩니다. 별명이란 일상생활에서 사용하는 용어와 일치하므로 개념적으로 이해하기 쉽습니다. 다음은 레퍼런스를 선언하고 사용할 때의 일반적인 주의 사항입니다.


    1. 레퍼런스와 대상체는 타입이 완전히 일치해야 합니다. 레퍼런스가 대상 변수의 완전한 별명이 되려면 같은 타입을 가져야합니다. 다음 예시들을 보겠습니다.
      1
      2
      3
      4
      5
      int i;
      int &ri = i;        //가능
      double &rd        //에러
      short &rs=i;        //에러
      unsigned  &ru=i;    //에러
      cs

      정수형(int)변수 i의 레퍼런스는 반드시 정수형이어야 합니다. 실수형 레퍼런스로는 i의 별명을 만들 수 없으며 심지혀 int형과 호환되는 short, unsigned 형으로도 별명을 만들 수 없습니다.

    2. 레퍼런스는 생성 직후부터 별명으로 동작하기 때문에 선언할 때 초기식으로 반드시 대상체를 지정해야 합니다. 포인터의 경우는 일단 선언해놓고 나중에 가리킬 변수의 번지를 대입받을 수 있지만 레퍼런스는 그렇지 못합니다.

      1
      2
      3
      4
      int *pi;
      int &ri // 에러
      pi=&i;
      ri=i;
      cs

      아무도 가리키지 않는 NULL 레퍼런스를 인정하지 않기 떄문에 int &ri;라는 선언문이 에러로 처리됩니다. 선언할 때부터 누구의 별명인지에 대한 지정이 있어야 합니다. 단 다음의 경우는 예외적으로 초기값이 없는 레퍼런스 선언이 가능합니다. 물론 밑의 예외적인 경우더라도 레퍼런스의 대상체 지정이 함수 호출 시점이나 객체 생성 지점으로 연기되는 것뿐이지 대상체가 없는 레퍼런스를 허용하는 것은 아닙니다. 레퍼런스가 실제 메모리에 생성될 때는 반드시 누구의 별명인지 지정 되어 있어야 합니다.

      1. 함수의 인수 목록에 사용되는 레퍼런스 형식 인수, 함수가 호출될 때 실인수에 대한 별명으로 초기화됩니다.

      2. 클래스의 멤버로 선언될 때, 이때는 클래스의 생성자에서 반드시 초기화해야합니다. 만약 생성자에서 레퍼런스 멤버를 초기화하지 않으면 에러로 처리됩니다.

      3. 변수를 extern으로 선언할 때 이떄는 레퍼런스의 초기식이 외부에 선언되어 있다는 뜻이며 초기값을 주지 않아도 됩니다. extern int &ri; 선언문은 ri가 어떤 변수에 대한 별명으로 외부에 선언되어 있다는 뜻입니다.

    3. 레퍼런스는 일단 선언되면 초기시게서 지정한 대상체의 별명으로 계속 사용됩니다. 그래서 선언된 후 중간에 참조 대상을 변경할 수 없으며 파괴될 때까지 같은 대상체만 가리킬 수 있습니다. 다음 소스를 보겠습니다.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      #include<stdio.h>
       
      void main()
      {
          int i = 3;
          int j = 9;
          int &ri = i;
       
          printf("i=%d, ri=%d, j=%d\n", i, ri, j);
          ri = j;
          printf("i=%d, ri=%d, j=%d\n", i, ri, j);
      }
      cs

      ri는 i의 레퍼런스로 초기화 되었으므로 이후부터는 ri는 i의 별명으로 사용됩니다. 중간에 ri=j 대입문으로 ri의 대상체를 j로 변경해 봤는데 결과는 다음과 같습니다.


      최초 i와 ri는 3이고 j는 9었습니다. 이상태에서 ri = j; 대입문에 의해 ri가 j를 가리키도록 했으므로 i는 3이고 ri와 j 는 7이 될 것 같지만 그렇지는 않고 모든 변수들이 일제히 9로 바뀌어 버렸습니다. 왜냐하면 ri는 는 i의 별명으로 초기화 되었으므로 ri=j 대입문은 곧 i=j가 되기 떄문입니다. 이 대입문은 ri의 대상체를 j로 바꾸는 것이 아니라 i의 별명으로 초기화 되었으므로 ri가 가리키는 본래 변수 i 에 j의 값을 대입하는 명령으로 해석됩니다. 레퍼런스에 대한 대입 연산자(=)는 레퍼런스의 대상체를 바꾸는 것이 아니라 대상체의 값을 변경하는 것으로 정의되어 있습니다. 즉 실행중에 = 연산자로 레퍼런스의 가리키는 대상을 얼마든지 변경할 수 있으며 그래서 선언할 때 꼭 초기화하지 않아도 됩니다.

    4. 레퍼런스에 대한 모든 연산은 대상체에 대한 연산으로 해석됩니다. 그래서 다음 연산문들은 모두 문법적으로 합당합니다.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      int i = 0, j;
      int &ri = i;
      int *pi;
       
      ri++;
      ri *= 5;
      = ri >> 4;
      = ri % 2;
      pi = &ri;
      cs

      ri가 정수형 레퍼런스이므로 ri에 대한 모든 연산문은 정수형에 대한 연산입니다. 따라서 정수형 변수 i에 대해 사용할 수 있는 모든 연산자를 다 사용할 수 있으며 연산의 결과는 정수형에 대한 연산과 동일합니다. 이에 비해서 포인터의 경우는 곱셈, 나머지, 쉬프트, 등의 연산이 허용되지 않습니다.

    5. 레퍼런스의 대상체는 실제 메모리 번지는 점유하고 있는 좌변 값이어야 합니다. 다음 선언문은 에러로 처리됩니다.

      1
      int &ri = 123;
      cs

      아무리 타입이 일치하더라도 상수값은 좌변값이 아니기 때문에 레퍼런스의 대상체가 될 수 없습니다. 만약 이 선언문이 가능하다면 ri=456; 대입문으로 상수 123이 456으로 바뀔 수 있다는 이야기가 되어 버립니다. 단, 상수 지시 레퍼런스인 경우는 상수를 대상체로 취할 수 있습니다.

      1
      const int &ri = 123;
      cs

      이렇게 되면 ri는 123이라는 상수값을 가지며 이후 이 값은 변경할 수 없으므로 좌변값으로 사용되지 않습니다. 이 선언문은 일단 가능은 하지만 전혀 실용성이 었습니다. 왜냐하면 const int &ri = 123; 이라는 정수형 상수를 만드는 것과 아무런 차이가 없기 떄문입니다.

레퍼런스 인수


앞에서는 정수형 변수 i에 대한 별명으로 ri 레퍼런스를 선언하고 사용하는 예를 보였습니다. 하지만 실용적인 의미는 없습니다. 이미 존재 하는 변수를 같은 함수내에서 다른 이름으로 별명을 만들어 사용하는것은 의미가 있습니다. 차라리 ri가 i와 완전히 동일하므로 별명을 만들 필요없이 그냥 i를 바로 쓰는 것이 훨씬 더 간편합니다. 레퍼런스가 실용적인 위력을 발휘할 때는 함수의 인수로 전달될 떄입니다. 함수가 레퍼런스를 받아들이면 호출부의 실인수에 대한 별명을 전달받는 셈이므로 함수 내에서 실인수를 조작할 수 있게됩니다. 레퍼런스의 값을 읽으면 실인수의 값을 읽을 수 있고 레퍼런스를 변경하면 실인수의 값도 같이 변경되므로 의미상으로 완전한 참조호출이 되는 것입니다. 다음 소스를 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
void sumref(int &a);
 
void main()
{
    int i = 5;
    sumref(i);
    printf("결과 = %d\n", i);
}
 
void sumref(int &a)
{
    a = a + 1;
}
cs

실행 결과는 6이 나오게되면 인수로 전달된 정수를 1증가시켜서 돌려줍니다. 5의 값을 가지는 레퍼런스로 전달했기 떄문에 i는 6으로 돌아오며 출력 결과로 6이 나오게됩니다. 포인터를 사용하는 방법에 비해 레퍼런스를 사용하게 되면 다음과 같은 차이점이 있습니다.

    • 함수의 원형이 달라졌는데 int *a(포인터)가 아닌 int &a(레퍼런스)를 전달받습니다. 그래서 sumref 함수내에서 형식 인수 a는 실인수와 완전히 동일한 변수가 되며 형식 인수 a를 바꾸면 실인수값이 바뀝니다.
    • 함수 본체에서 형식 인수를 참조할 때 *연산자를 붙일 필요가 없습니다. sumref함수의 형식인수 a는 포인터가 아니라 레퍼런스이므로 *연산자를 붙이지 않아도 실인수를 액세스 할 수 있습니다. a=3으로 대입하면 실인수가 3이 되면 a++가 되면 실인수가 1 증가합니다. 만약 함수로 전달된 대상체가 구조체라면 ->연산자를 쓸 필요 없이 바로 .연산자를 사용하면 됩니다.
    • 함수 호출부도 달라졌습니다. 포인터를 전달하는 것이 아니므로 &i를 전달할 필요없이 i를 바로 전달하면 됩니다. 값 호출을 할 때도와 형식이 같습니다. sumref 함수는 실인수 i의 별명인 레퍼런스 a를 만들고 a를 통해 i를 조작합니다.

다음은 좀 더 실용적인 예인 구조체를 통해 값 호출과, 포인터를 통한 참조 호출 그리고 레퍼런스를 통한 참조호출을 비교해 보겠습니다. 다음 예제는 tag _Person 구조체를 출력하는 함수를 세 가지 방식으로 작성한 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<stdio.h>
 
struct tag_Person
{
    char Name[10];
    int Age;
    double Height;
};
void OutPerson(tag_Person F);
void OutPersonPtr(tag_Person *F);
void OutPersonRef(tag_Person &F);
 
void main()
{
    tag_Person Person = { "홍길동",23,134.23 };
    OutPerson(Person);
    OutPersonPtr(&Person);
    OutPersonRef(Person);
}
 
void OutPerson(tag_Person F)
{
    printf("이름 = %s, 나이 = %d, 키 = %.2f\n", F.Name, F.Age, F.Height);
}
void OutPersonPtr(tag_Person *F)
{
    printf("이름 = %s, 나이 = %d, 키 = %.2f\n", F->Name, F->Age, F->Height);
}
void OutPersonRef(tag_Person &F)
{
    printf("이름 = %s, 나이 = %d, 키 = %.2f\n", F.Name, F.Age, F.Height);
}
cs

Person 구조체를 하나 선언 및 초기화하고 이 구조체를 세 가지 방법을 출력 해 보았습니다. 세 방법 모두 구조체의 내용을 출력하기는 마찬가지이므로 결과는 동일하게 다음과 같이 나옵니다.


값 호출을 사용하는 OutPerson 함수는 Person 구조체의 사본 F를 전달받는데 이 과정에서 실인수가 형식인수로 복사됩니다. 구조체는 정수나 포인터에 비해 크기 떄문에 복사 시간이 훨씬 더 오래 걸리며 따라서 함수 호출 속도가 느립니다. 또한 구조체의 사본을 값으로 전달받았기 떄문에 OutPerson 함수 내부에서 F의 멤버를 변경한다 하더라도 실인수 Person의 값이 변경되는 것은 아닙니다.

포인터나 레퍼런스는 값 자체가 복사되는 것이 아니라 단지 4바이트만 복사되므로 값 호출에 비해 속도가 훨씬 더 빠릅니다. 정밀하게 측정해 보면 최소한 수 배 정도의 차이가 나며 구조체가 크면 수십배 이상 차이가 날 수도 있습니다. 또한 함수 내부에서 실인수를 직접 변경할 수 있다는 이점이 있습니다. OutPersonPtr, OutpersonRef 함수에서 F의 값을 변경하면 실인수 Person의 멤버가 변경됩니다. 포인터를 쓰는 방법과 레퍼런스를 쓰는 방법은 효과는 거의 동일하며 형태상 몇 가지 다른 점만 있습니다.


레퍼런스를 쓰는 방법은 포인터를 통한 참조 호출 방법에 비해 함수 내부가 훨씬 더 깔끔하고 직관적입니다. 포인터로 실인수를 조작할 때는 일일이 *를 붙여서 *a=*a+1과 같이 해야하지만 레퍼런스는 실인수와 완전히 같으므로 a=a+1로 훨씬 더 간단하게 코드를 작성할 수 있습니다. 코드의 길이가 길고 이 변수를 참조하는 회수가 많다면 포인터보다 레퍼런스가 더 읽기 쉽고 실수로 *를 빼먹는 사고를 방지 할 수 있습니다.


그러나 레퍼런스를 통한 참조 호출 방법은 호출부의 형식이 값 호출 방식과 동일해져서 오히려 더 혼란스러운 몇도 있습니다. Sumref(i) 형식으로 호출하므로 이 함수가 값을 전달받는지 레퍼런스를 전달받는지  함수의 원형을 봐야만 알 수 있다는 단점이 있습니다. 이는 코드를 읽는 사람으로 하여금 혼란을 느끼게 하며 문서화 하기도 무척 번거롭다는 단점이 있습니다. 그래서 꼭 필요치 않는 한 가급적 사용을 자제하고 불가피할 경우 레퍼런스를 받는 함수는 보통 함수명에 Ref나 ByRef같은 접미를 붙여 호출부에서 함수의 형식을 쉽게 파악할 수 있도록 해야합니다. 참조 호출이 꼭 필요할 경우는 레퍼런스보다는 가급적이면 포인터를 넘기는 것이 더 직관적인 것입니다. 그러나 레퍼런스를 넘기는 것은 또 다른 차이점이 있는데 포인터는 잠재적으로 배열이므로 일단 넘기면 주변을 마음대로 건드릴 수 있지만 레퍼런스는 전달된 대상만 액세스 할 있다는 면에서 오히려 더 안전성이 높습니다. 레퍼런스의 효용성에 대해서 다소 논란이 있는 편인데 필요할 때는 쓰는 것이 좋습니다. 상수는 레퍼런스의 대상체가 될 수 없습니다. 마찬가지로 레퍼런스 인수를 사용하는 함수로는 상수를 전달할 수 없습니다. 레퍼런스는 좌변값인 변수를 대상체로 취하며 상수의 별명이 될 수 없으므로 Sumref(5)와 같이 호출하는 것은 허용되지 않습니다. 상수 5에 대한 레퍼런스는 만들 수도 없고 함수 내에서 이 값을 변경할 수도 없기 떄문입니다. 뿐만아니라 Sumref(3+4)같은 수식도 전달할 수 없습니다.

만약 Sumref 같은 함수의 인수가 상수 지시 레퍼런스라면 즉 원형이 void Sumref(const int &a)형식이라면 상수나 수식을 전달할 수는 있습니다. 그러나 이 경우 함수 내부에서 전달된 인수값을 변경하는 것은 불가능합니다. 또한 형식 인수와 타입이 조금만 틀려도 안 되며 완전히 동일한 타입의 실인수를 사용해야합니다. 따라서 다음 호출문은 에러입니다.

1
2
short s = 5;
Sumref(s);
cs

Sumref 함수는 정수형 레퍼런스만 받아들일 수 있으므로 short형 레퍼런스를 넘길 수는 없습니다. 어떤 컴파일러는 레퍼런스 인수로 상수나 호환타입이 전달될 경우 임시 변수를 생성하기도 하지만 Visual C++은 임시 변수를 생성하지 않습니다. 이에 비해 값을 전달받는 함수는 변수나 상수 심지어 수식도 받아들일 수 있으며 타입이 정확하게 일치하지 않더라도 컴파일러가 내부적인 변환에 의해 타입을 맞춘 후 호출하므로 훨씬 더 활용도가 높습니다. 그래서 동일한 동작을 하는 함수라면 레퍼런스 보다는 값을 전달받는 함수를 만드는 것이 원칙이며 유리합니다.


레퍼런스의 대상체


지금까지 정수형과 구조체에 대해 레퍼런스를 선언하고 사용해보았는데 포인터에 대한 레퍼런스도 선언할 수 있습니다. 다음 소스를 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<malloc.h>
#include<string.h>
void InputName(char *&Name)
{
    Name = (char *)malloc(12);
    strcpy(Name, "Hong");
}
void main()
{
    char *Name;
 
    InputName(Name);
    printf("이름은 %s 입니다.\n", Name);
    free(Name);
}
cs

char *&Name인수가 포인터의 레퍼런스 입니다. T형의 레퍼런스는 T &이며 char* 자체가 하나의 타입이므로 이 타입에 대한 레퍼런스는 char *&가 됩니다. char &*가 아님을 주의해야합니다. 포인터 레퍼런스는 무척 실용적이며 종종 사용됩니다.  함수 레퍼런스도 선언이 가능합니다. 또한 배열에 대한 레퍼런스도 만들 수 있습니다. 다음 소스를 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
 
int ar[5= { 100101102103104 };
 
void function(int a)
{
    printf("%d\n", a);
}
void main()
{
    void(&rf)(int= function;//함수 레퍼런스
    int(&rar)[5= ar;          //배열 레퍼런스
 
    rf(rar[0]);
}
cs

rf는 정수형 인수 하나를 취하고 리턴값이 없는 함수에 대한 레퍼런스로 선언되었으며 같은 타입의 함수 function으로 초기화되었습니다. 그래서 rf() 호출문이 곧 function() 호출문과 같아집니다. rar과 ar은 같은 배열을 가리킵니다. 결국 위 예제에서 사용된 rf(rar[0]) 호출문은 function(ar[0])호출문과 같다고 할 수 있습니다. 별명을 사용했다 뿐이지 함수와 배열을 바로 사용한 것과 별반 차이가 없습니다. 이 둘은 문법적으로 가능은 하지만 실용성은 없습니다. T형이 있을 때 T형 포인터나 T형 배열은 항상 선언할 수 있습니다. 그러나 레퍼런스의 경우는 이 명제가 성립되지 않습니다. 임의의 T형에 대해 T & 레퍼런스 타입을 선언할 수 있지만 레퍼런스에 대해서 임의의 모든 타입을 다 만들 수 있는 것은 아닙니다. 안되는 경우들을 보겠습니다.

  1. 레퍼런스에 대한 레퍼런스를 선언할 수 없습니다. 레퍼런스가 별명인데 이 별명에 대한 다른 별명을 또 만드는 것은 실용적 가치가 없다고 할 수 있습니다. 포인터는 2중 3중도 가능하지만 레퍼런스는 2중 으로 선언 할 수 없습니다.

    1
    2
    3
    int i;
    int &ri=i;
    int &rri=rk
    cs

    ri는 i의 레퍼런스로 선언되었고 rri는 ri에 대한 레퍼런스로 선언되었습니다. 이때 rri가 2중 레퍼런스인 것처럼 보이지만 rri는 단순 레퍼런스에 불과합니다. ri가 i이므로 rri는 i의 또 다른 별명일 뿐이며 결국 rri와 ri는 같은 대상을 가리키는 별명일 뿐ㅇ비니다. int &&rri=i; 같은 선언문은 필요하지도 않으며 컴파일러에 의해 에러로 처리됩니다.

  2. 레퍼런스에 대한 포인터를 선언할 수 없습니다.

    1
    2
    3
    int i;
    int &ri=i;
    int &*pri=&ri;//에러
    cs

    레퍼런스에 대한 포인터 pri를 선언하고자 했는데 컴파일러는 이를 에러로 처리합니다. 개념적으로 레퍼런스에 대한 포인터는 레퍼런스의 대상체에 대한 포인터형이므로 곧 단순 포인터와 같으며 굳이 레퍼런스의 포인터형을 정의해야할 필요가 없습니다. int *pi=&ri라는 선언은 가능합니다.

  3. 레퍼런스의 배열도 선언할 수 없습니다. T형 배열이란 곧 T형 포인터인데 레퍼런스에 대한 포인터를 선언할 수 없으므로 배열도 선언할 수 없습니다.

    1
    2
    int i,j;
    int &ar[2]={i,j}; //에러
    cs

    i와 j에 대한 레퍼런스가 필요하면 각각 따로 레퍼런스를 선언해야합니다.

  4. 비트 필드에 대한 레퍼런스도 선언할 수 없습니다. 비트 필드는 주소를 가지지 않기 떄문에 포인터의 대상체가 될 수 없으며 마찬가지로 레퍼런스의 참조 대상이 될 수도 없습니다.


레퍼런스 리턴값

레퍼런스는 함수의 리턴값으로도 사용될 수 있습니다. 다음 소스의 GetAr 함수는 정수형 배열에서 i번째 요소 자체를 리턴합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
 
int ar[] = { 1234};
 
int &GetAr(int i)
{
    return ar[i];
}
 
void main()
{
    GetAr(3= 6;
    printf("ar[3]=%d\n", ar[3]);
}
cs

레퍼런스는 변수 그 자체이며 온전한 좌변값이기 때문에 함수가 리턴하는 레퍼런스가 대입 연산자의 좌변에 놓일 수 있습니다. 그래서 함수의 호출문이 좌변이 될 수 있으며 예제의 코드에서처럼 GetAr(3)=6;같은 문장이 가능해집니다. 위 소스의 실행 결과 ar[3]이 6으로 변경됩니다. GetAr함수는 인수로 전달된 i를 ar 배열의 첨자로 해석하여 ar[i]의 레퍼런스를 리턴하며 대입 연산문에 의해 ar[i]에 6을 대입했기 떄문입니다. 큰 배열에서 어떤 복잡한 조건으로 한 요소를 선택해서 그 값을 변경하고자 할 떄 C언어라면 다음과 같이 해야합니다. 편의상으로 배열은 arSome이라는 이름에 정수형 배열로 하겠습니다.

1
2
int FinMatch(char *name, int value, bool bCase);
arSome[FindMatch(...) ] = Data;
cs

FindMatch함수는 입력된 세가지 조건으로부터 적합한 배열 요소를 선택해서 그 첨자를 리턴하도록 되어있습니다. 호출원에서 이 함수가 리턴하는 첨자로부터 arSome 배열의 한 요소를 액세스합니다. 레퍼런스를 이용한다면 다음과 같이 작성할 수 있습니다.

1
2
int &FindMatchRef(char *name, int value, bool bCase);
FinMatchRef(...) = Data;
cs

FindMatchRef함수는 내부에서 조건에 맞는 요소를 선택하고 그 요소 자체를 리턴하며 호출원에서는 FindMatchRef함수가 리턴하는 배열 요소에 곧바로 값을 대입할 수 있습니다. 만약 이 함수가 리턴하는 대상이 구조체의 레퍼런스라면 FinMatchRef(...).Member = Data;형식으로 함수 호출부 다음에 멤버 연산자를 쓰는 것도 가능합니다. 함수가 레퍼런스를 리턴할 수 있음으로 해서 대입식의 좌변에 쓸 수 있다는 것은 무척 흥미롭고 재미있는 사실입니다. 그러나 function()=a;식의 문장은 직관적이지 못하고 익숙하지 않기 떄문에 혼란스러워 보입니다. 꼭 필요한 경우를 제외하고는 자제하는 것이 좋습니다. 리턴값으로 레퍼런스를 꼭 사용해야 할 경우는 연산자를 오버로딩할 때입니다.


레퍼런스의 내부


레퍼런스는 굉장히 특이하고 복잡해 보이지만 내부를 들여 다보면 포인터의 변형에 불과합니다. 컴파일러는 레퍼런스를 포인터로 바꾼 후 컴파일하는데 소스가 내부에서 어떻게 변경되는지 보도록 하겠습니다. 위쪽것은 실제 코드이고 오른쪽은 컴파일러 내부 변환 코드입니다. 실제와는 다를 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
 
void main()
{
    int i = 3;
    int &ri = i;
 
    printf("i = %d, ri = %d\n", i, ri);
    ri++;
    printf("i = %d, ri = %d\n", i, ri);
    printf("i번지 = %x, ri번지 = %x\n"&i, &ri);
}
cs


1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
 
void main()
{
    int i = 3;
    int *ri = &i;
 
    printf("i = %d, ri = %d\n", i, (*ri));
    (*ri)++;
    printf("i = %d, ri = %d\n", i, (*ri));
    printf("i번지 = %x, ri번지 = %x\n"&i, &(*ri));
}
cs


int & = i선언문에 대해 컴파일러는 ri를 정수형 포인터로 생성하고 초기값으로 i의 주소값을 대입합니다. 이후 ri를 참조하는 모든 문장에는 암시적으로 *연산자가 적용되어 (*ri)로 해석됩니다. (*ri)가 곧 i이므로 ri는 i의 별명으로 동작하는 것입니다. 오른쪽 변환 코드를 실행해보면 왼쪽 코드와 동일하게 동작한다는 것을 확인할 수 있습니다. 결국 레퍼런스 인수를 가지고 있는 함수가 받아들이는 인수의 실제 형태는 포인터이고 인수들에 대해 *연산자를 적용함으로써 실인수를 참조하도록 합니다.












'Programming > C' 카테고리의 다른 글

C언어 - 파일 입출력(1/3)  (2) 2015.09.12
C언어 - 포인터 고급(3/4)  (0) 2015.09.06
C언어 - 포인터 고급(2/4)  (0) 2015.09.05
C언어 - 포인터 고급(1/4)  (0) 2015.08.31
C언어 - 구조체(Structure)(2/2)  (23) 2015.08.21