책정리/혼자 연구하는 C,C++ 1

15장 포인터 고급

GONII 2015. 2. 19. 17:53

15.1 const

15.1.1 상수의 정의

const 키워드 : 상수를 정의

const type name = 초기값 ;

변수를 선언할 때 반드시 초기화 해야됨

  • 예제 const

#include <stdio.h>

   

const int hour = 24;

const int min = 60;

const int sec = 60;

   

void main(void)

{

printf("하루는 %d초\n", hour*min*sec ) ;        

}

const에 의해 만들어지는 상수는 컴파일시에 값이 결정되기 때문에 배열의 크기 지정에도 사용 가능

   

  • const의 장점
    • #define이 정의 하는 매크로 상수는 타입을 지정할 수 없지만 const는 타입을 명확하게 지정 가능함
    • 매크로 상수는 정의 된 후에는 언제든지 사용 가능하지만 const는 통용 범위 규칙의 적용을 받기 때문에 자신이 선언된 범위 내에서만 사용 가능함, 명칭 출돌 최소화
    • #define은 전처리기에 의해 치환되기 때문에 실제 소스에는 매크로가 치환된 상태로 실행되므로 디버깅 중에 매크로 상수의 값을 확인해 볼 수 없고 버그 확인이 어려움, 반면 const는 디버깅 중에도 값을 확인 가능
    • 매크로는 기계적으로 치환되기 때문에 부작용이 발생할 소지 많음

    15.1.2 포인터와 const

  • 예제 ConstPointer

#include <stdio.h>

void main(void)

{

int ar[5] = {1,2,3,4,5};

int *pi1 = &ar[0];

pi1++; // 포인터로다른대상체를가리킬수있음

*pi1 = 0; // 대상체를변경가능

   

const int *pi2 = &ar[2];

pi2++; // 포인터가다른대상체를다리킬수있음

*pi2 = 0; // 에러: 상수이기때문에변경불가

   

int *const pi3 = &ar[0];

pi3++; // 에러: 포인터가다른대상체를가리킬수없음

*pi3 = 0; // 변경가능

   

const int* const pi4 = &ar[0];

pi++; // 에러: 포인터가다른대상체가르킬수없음

*pi4 = 0; // 에러: 대상체상수

}

상수 지시자 포인터(Pointer to Constant) : const int *pi2 = &ar[2];

상수 포인터(Constant Pointer) : int *const pi3 = &ar[0];

pi4는 상수 지시 상수 포인터??라고 할 수 있다네요

   

  • 예제 ConstDblPointer

#include <stdio.h>

   

void main(void)

{

int i = 5;

int *pi = &i;

const int *pci;

int *const cpi = &i;

const int * const cpci = &i;

   

// 일반 이중 포인터 - 모두 가능

int **ppi1 = &pi;

ppi1++;

(*ppi1)++;

**ppi1 = 0 ;

   

// 상수 지시 포인터의 포인터

const int **ppi2 = &pci;

ppi2++;

(*ppi2)++;

//**ppi2 = 0 ;        // 에러 : 최종 대상체(정수) 변경 불가

   

int * const * ppi3 = &cpi;

//(*ppi3)++;        // 에러 : 중간 대상체(포인터)변경 불가

**ppi3=0;

   

// 비상수 지시 상수 포인터의 포인터

int **const ppi4 = &pi;

//ppi4++;        // 에러 : 포인터 자체 변경 불가

(*ppi4)++;

**ppi4 = 0 ;

   

// 상수 지시 상수 포인터를 지시하는 상수 포인터 - 전부 에러

const int * const * const ppi5 = &cpci;

//ppi5++;

//(*ppi5)++;

//**ppi5 = 0 ;

}

  • 예제 ConstAssign

#include <stdio.h>

   

void main(void)

{

int ar[5] = {1,2,3,4,5};

   

int *pi1 = &ar[0];

const int *pi2;

   

pi2 = pi1;                        // 가능

pi1 = pi2;                        // 불가능

pi1 = (int*)pi2;        // 가능하지만 바람직하지 않음

}

  • 예제 ConstTest

#include <stdio.h>

   

void main(void)

{

const int i = 2;

int *pi = (int*)&i;

   

*pi=3;

printf("i = %d\n", i );

printf("*pi = %d\n", *pi );

   

const double d = 1.0;

double *pd = (double*)&d;

*pd = 2.3;

printf("d = %f\n", d );

printf("*pd = %f\n", *pd ) ;

}

double 값은 바뀌지만 int 값은 바뀌지 않은 것처럼 보임

그러나 내부를 들여다보면 int도 실제로는 값이 바뀌지만 상수 참조문이 원래 정의된 상수값을 곧바로 사용하도록 컴파일 하는 것이다....

  • 예제 ConstTest2

#include <stdio.h>

   

void func( const int *ai )

{

// *ai = 3;

int *pi;

pi = (int*)ai;

*pi = 3;

}

   

void main(void)

{

int i = 2;

func( &i ) ;

printf("%d\n", i ) ;

}

func는 const int* 를 전달받아 *ai = 3 식으로 포인터가 가리키는 내용을 직접 변경하는 것은 안됨

15.1.3 const 인수

상수 포인터를 취하는 대표적인 함수들

char *strcpy( char *dest, const char*src ); // src의 내용을 dest로 복사

int strcmp( const char *s1, const char *s2 ); // s1과 s2를 비교-1,0,1을 리턴

char *strchr( const char *string, int c ); // 문자를 찾고 찾은 위치부터 끝까지 문자열 리턴

int atoi( const char *string ) ; // Ascii to Integer

15.1.4 volatile

cv 지정자(Qualifier:제한자) : 변수의 성질을 바꾸는 역할, const volatile

volatile 키워드를 사용하면 컴파일러는 이 변수에 대해서는 어떠한 최적화 처리도 하지 않는다

컴파일러가 보기에 코드가 비효율적이건 어쨌건 개발자가 작성한 코드 그대로 컴파일 된다.

15.2 함수 포인터

15.2.1 정의

함수 포인터(Pointer to Function) : 함수를 가리키는 포인터

포인터란 메모리상의 번지를 저장하는 변수인데 함수도 메모리에 존재하며 시작 번지가 있으므로 포인터 변수로 가리킬 수 있다

  • 함수 포인터 선언

    리턴타입 (*변수명)(인수의 목록);

    int func(int a); // func라는 이름의 함수가 있을 때 아래와 같이 함수 포인터를 선언하면 func를 가르킬 수 있는 함수 포인터가 만들어진다

  1. int pf(int a);        // 함수명을 변수명으로 바꾼다
  2. int *pf(int a);        // 변수명 앞에 *를 붙인다
  3. int (*pf)(int);        // 변수를 괄호로 싼다. 형식 인수의 이름 생략 가능

   

void func( int a, double b);

void (*pf)(int, double);

char *func(char *a, int b);

char*(*pf)(char*, int);

void func(void);

void (*pf)(void);

이렇게 선언한 함수 포인터는 자신과 원형이 같은 함수의 시작 번지를 가리킬 수 있는데 단순히 함수의 이름을 대입하면 된다

pf = func;

대입이 가능하나 이유는 괄호 없이 단독으로 사용된 함수명은 함수의 시작 번지를 나타내는 포인터 상수이기 때문

마치 배열명이 첨자 없이 사용되면 배열의 시작 번지를 나타내는 포인터 상수가 되는 것처럼 함수 포인터도 마찬가지다

  • 함수 포인터 호출

    (*pf)(2); // *pf는 func와 동일하므로 (*pf)(2)는 func(2)와 같다

    pf(2); // 함수 포인터를 마치 함수인 것처럼 쓰고 괄호 안에 인수를 넘긴다

  • 예제 FuncPointer

#include <stdio.h>

int func(int a)

{

return a*2;

}

void main(void)

{

int i ;

int (*pf)(int a) ; // 함수포인터선언

// 함수포인터대입

pf = func;

// 함수포인터호출

i = (*pf)(2);

printf("%d\n", i) ;

}

15.2.2 함수 포인터 타입

함수 포인터 타입도 일종의 고유한 타입이다. 따라서 원형이 다른 함수 포인터끼리는 곧바로 대입하 ㄹ수 없으며 함수의 인수로도 넘길 수 없다.

int (*pf)(char *);

void (*pf2)(double);

pf1=pf2; // 타입이 다르므로 에러

   

  • 함수 포인터 캐스팅

    int (*pf1)(char *);

    void (*pf2)(double);

    pf1=(int (*)(char*))pf2;

    캐스팅에 대한 부작용은 조심해야한다

       

  • 함수 포인터의 배열 선언

    int (*arpf[5])(int);

  • 함수 포인터의 포인터 선언

    int (**ppf)(int);

    ppf = int(*)(int)타입으로 선언된 함수 포인터 변수나 함수 포인터 배열을 가리킬 수 있는 이차 함수 포인터 변수이다. ppf = &pf, ppf = arpf

    함수 포인터를 자주 사용하거나 자신이 없다면 typedef으로 함수 포인터를 정의 후 사용 가능

    typedef int (*PFTYPE)(int);

    PFTYPE pf;

    배열이나 포인터 선언도 간편하다

    PFTYPE arpf[5];

    PFTYPE *ppt;

    15.2.3 포인터로 함수 호출하기

    언제나 한 단계를 더 거치면 여러 가지 기법들이 가능해진다.

    함수 포인터를 인수로 사용하면 함수를 다른 함수에게 전달하는 것도 가능해지며 함수 포인터 배열이나 구조체를 통해 여러 개의 함수군을 통째로 바꿔치기 할 수 있다.

  • 예제 PointerCall

#include <stdio.h>

   

int multi2(int a)

{

return a*2;

}

int multi3(int a)

{

return a*3;

}

   

void main()

{

char ch;

int i = 5;

int (*pf)(int a);

   

printf("5를 두배 하고 싶으면2, 세배 하고 싶으면 3입력\n");

scanf("%c", &ch);

   

if( ch=='2')

{

pf = multi2;

}

else

{

pf = multi3;

}

printf("결과 : %d\n",(*pf)(i));

}

  • 함수 포인터 사용 예
    • 선택해야 할 ㅅ함수가 두 개 이상인 경우, 예를 들어 수십개의 함수 중 하나를 호출해야 한다면 함수 포인터 배열을 선언하고 그 첨자를 선택하는 것이 더쉽다.
    • 함수를 선택하는 시점과 실제로 호출하는 시점이 완전히 분리되어 있는 경우도 함수 포인터를 쓰는 것이 유리하다. 호출할 함수에 대한 조건 점검은 필요할 때 한 번만 하고 선택된 함수는 별다른 조건 점검 없이 함수 포인터로 바로 호출 할 수 있다.
    • 호출할 함수가 DLL같은 외부 모듈에 있고 이 함수를 동적으로 연결할 경우는 컴파일할 때 함수의 존재가 알려지지 않으므로 반드시 함수 포인터를 사용해야 함. 함수 포인터를 사용하면 이름부터 원하는 함수의 번지를 찾아 호출할 수 있다.

    15.2.4 함수 포인터 인수

    함수 포인터는 함수를 가리키고 있지만 어쨋거나 변수이기 때문에 함수의 인수로 전달될 수 있다.

       

    qsort함수 원형

    void qsort(void *base, size_t num, size_t width, int(*compare)(const void*, const void*));

  • qsorttest

#include <stdio.h>

#include <algorithm>

   

int compare( const void *a, const void *b)

{

if( *(int*)a == *(int*)b ) return 0;

if( *(int*)a > *(int*)b ) return 1;

return -1;

}

   

void main(void)

{

int i;

int ar[] = { 34, 25, 27, 19, 4, 127, 9, 342, 3, 6, 230 };

   

qsort( ar, sizeof(ar)/sizeof(ar[0]), sizeof(int), compare );

   

for ( i = 0 ; i < sizeof(ar)/sizeof(ar[0]) ; i++ )

{

printf("%d번째 = %d\n", i, ar[i]);

}

}

윈도우즈 환경에서는 콜백함수라는 이름으로 함수 포인터를 빈번히 사용하는데 윈도우로 전달되는 메시지를 처리하는 WndProc이라는 함수가 바로 콜백함수이다.

  • 예제 DownCallBack

#include <stdio.h>

#include <conio.h>

#include <windows.h>

   

void FtpDown( const char *src, const char *dest, BOOL(*prog)(int, int))

{

int total, now;

BOOL UserBreak;

   

total = 600; // 실제 src의 크기를 조사해야 함

now = 0 ;

   

for ( now = 0 ; now < total ; now++ )

{

// 다운로드, 한번에 1M씩 받는다고 치고

// DownloadFile(src, dest);

Sleep(10);

// 과정 표시 함수를 불러줌

UserBreak = (*prog)(total, now);

if( UserBreak == TRUE )

{

printf("다운로드를 취소했습니다.\n");

break;

}

}

}

BOOL Progress(int total, int now)

{

// 다운로드 과정을 보여줌

printf("총 %d 중 %d만큼 받고 있습니다.\n", total, now );

   

// 만약 사용자가 중지하라고 했다면

if ( kbhit() && getch()==27 )

{

return TRUE;

}

else

{

return FALSE;

}

}

   

void main(void)

{

FtpDown("ftp::/asdf.com/asdf.mpg","c:/asdf.mpg", Progress);

}

함수 포인터는 다른 포인터와는 달리 ++, -- 등의 연산자를 사용할 수 없으며 정수와 가감 연산도 할 수 없다. 함수는 코드 덩어리이며 이 덩어리의 크기는 가변적이고 실행 중에 변경할 수도 없기 때문이다

15.2.5 함수 포인터 리턴

함수 포인터 리턴하는 함수의 원형

fp의 리턴타입 (*함수명)(인수목록))(fp의 인수목록)

  • 예제 FpReturn

#include <stdio.h>

   

int f1(int a, double b)

{

return 1;

}

int f2( int a, double b)

{

return 2;

}

   

int (*selectFunc(char ch))(int,double)

{

if( ch == 'a' )

{

return f1;

}

else

{

return f2;

}

}

   

void main()

{

int (*fp)(int, double);

   

fp = selectFunc('a');

printf("리턴된 값 = %d\n", fp(1, 2.3));

}

15.3 가변 인수

15.3.1 가변 인수 함수

가변 인수 : 인수의 개수와 타입이 미리 정해져 있지 않다는 뜻

대표적인 함수 : printf

printf 함수는 전달되는 인수의 개수와 타입이 모두 다르지만 정상적으로 컴파일 되고 실행된다

  • printf 함수 원형

    int printf( const char *format, ... ) ;

    format이라는 이름의 문자열 상수인데 흔히 서식 문자열이라고 부른다

    ... 생략 기호는 컴파일러에게 이후의 인수에 대해서는 개수와 타입을 점검하지 않도록 하는데 이 기호에 의해 가변 인수가 가능해진다

       

    가변 인수 함수의 개략적인 구조는 다음과 같다

void varFunc( int fix, ... )

{

va_list ap;

va_start( ap, fix );

while( 모든 인수를 읽을 때 까지 )

{

va_arg( ap, 인수타입 ) ;

}

va_end(ap);

}

함수의 이름이나 원형, 고정 인수의 개수 등은 필요에 따라 마음대로 작성할 수 있고, 마지막 인수 자리에 ...만 있으면 가변 인수 함수가 된다

   

:: va_list ap

함수로 전달되는 인수들은 스택(stack)이라는 기억 장소에 저장되며 함수는 스택에서 인수를 꺼내 쓴다. 스택에 있는 인수를 읽을 때 포인터 연산을 해야 하는데 현재 읽고 있는 번지를 기억하기 위해 va_list형의 포인터 변수 하나가 필요하다. va_list 타입은 char*형으로 정의되어 있다.

::va_start(ap, 마지막고정인수)

이 명령은 가변 인수를 읽기 위한 준비를 하는데 ap 포인터 변수가 첫 번째 가변 인수를 가리키도록 초기화한다. 첫 번째 가변 인수의 번지를 조사하기 위해서 마지막 고정 인수를 전달한다. va_start 내부에서는 ap가 마지막 고정 인수 다음 번지를 가리키도록 해 주므로 이후부터 ap 번지를 읽으면 순서대로 가변 인수를 읽을 수 있다.

::va_arg(ap, 인수타입)

가변 인수를 실제로 읽는 명령이다. va_start가 ap를 첫 번째 가변 인수 번지로 맞추어 주므로 ap 위치에 있는 값을 읽기만 하면 된다. 단, ap 번지에 있는 값이 어떤 타입인지를 지정해야 이 매크로가 값을 제대로 읽을 수 있으므로 두 번째 인수로 읽고자 하는 값의 타입을 지정한다. 예를 들어 ap 위치에 있는 정수값을 읽고자 한다면 va_arg(ap, int)를 호출하고 실수값을 읽고자 한다면 va_arg(ap, double)이라고 호출하면 된다.

그런데 int 나 double 같은 타입 이름이 어떻게 함수의 인수로 전달될 수 있는가 하는 점이다. 함수의 인수로는 값이 전달된느 것이 정상인데 타입명이 어떻게 함수의 인수가 될 수 있을까??? va_arg는 함수가 아니라 매크로 함수이기 때문이다. va_arg의 두번째 인수는 내부적으로 sizeof 연산자와 캐스트 연산자로 전달되기 때문에 타입명이 될 수 있다.

::va_end(ap)

이 명령은 가변 인수를 다 읽은 후 뒷정리를 하는데 별다른 동작은 하지 않으며 실제로 없어도 전혀 지장이 없다. 이 명령이 필요한 이유는 호환성 때문인데 플랫폼에 따라서는 가변 인수를 읽은 후에 뒷처리르 해야 하는 경우도 있기 때문이다. va_end가 중요한 역할을 할수도 있으므로 호환성을 위해서는 관례적으로 넣어 주는 것이 좋다.

   

  • 예제 GetSum

#include <stdio.h>

#include <stdarg.h>        // 가변인자 헤더

   

int getSum( int num, ... )

{

int sum = 0;

int i;

va_list ap;

int arg;

   

va_start(ap, num) ;

for ( i = 0 ; i < num ; i++ )

{

arg = va_arg(ap, int);

sum += arg;

}

va_end(ap);

return sum;

}

   

void main(void)

{

printf("1+2=%d\n", getSum(2,1,2));

printf("3+4+5+6=%d\n", getSum(4,3,4,5,6));

printf("10~15=%d\n", getSum(6,10,11,12,13,14,15));

}

첫 번째 인수num 은 전달될 정수 인수의 개수를 가지는 고정 인수

15.3.2 가변 인수 함수의 조건

  1. 가변 인수 함수는 반드시 하나 이상의 고정 인수를 가져야 한다. 왜냐하면 가변 인수를 읽기 위한 포인터 ap를 초기화하기 위해서 마지막 고정 인수의 번지를 알아야 하기 때문이다.
  2. 함수 내부에서 자신에게 전달된 가변 인수의 개수를 알 수 있도록 해야 한다. 전달될 수 있는 인수의 개수에는 제한이 없으며 컴파일러는 함수가 호출될 때 인수의 개수를 점검하지 않는다. 그래서 호출측에서 가변 인수가 몇 개나 전달되었는지를 알려주지 않으면 함수 내부에서 인수의 개수를 알 수 있는 방법이 없다. 함수 스스로 인수의 개수를 파악할 수 있도록 호출측이 정보를 제공해야 한다.
  3. 개수와 마찬가지로 함수 내부에서 각각의 가변 인수 타입을 알 수 있어야 한다. %d가 제일 처음 나왔으면 첫번째는 정수, %f가 나오면 실수라는 것을 알게 된다.

    가변 인수들의 타입을 알아야 하는 이유는 va_arg 매크로가 ap번지에서 가변 인수를 읽을 때 얼마만큼 읽어서 어떤 타입으로 해석해야 할지를 알아야 하기 때문이다. 가변 인수의 타입을 전달하는 방식도 여러 가지를 생각할 수 있는데 printf와 같이 하나의 고정 인수를 통해 모든 가변 인수의 타입을 판단할 수 있는 힌트를 제공하는 방식이 가장 좋다.

  • 예제 PrintSum

#include <stdio.h>

#include <stdarg.h>

   

void printSum( const char *msg, ... )

{

int sum = 0;

va_list ap;

int arg;

   

va_start(ap, msg);

for(;;)

{

arg = va_arg(ap, int);

if ( arg == 0 )

{

break;

}                

sum += arg;

}

va_end(ap);

printf(msg, sum);

}

   

void main(void)

{

printSum("1+2 = %d\n", 1,2,0);

printSum("3+4+5+6 = %d\n", 3,4,5,6,0);

printSum("10~15 = %d\n", 10,11,12,13,14,15,0);

}

printf는 첫 번째 고정 인수로 전달되는 서식 문자열에서 %d, %f, %s 같은 서식의 개수만큼 가변 인수를 읽음으로써 사실상 가변 인수의 개수를 전달받는다.

  • 예제 GetSum2

#include <stdio.h>

#include <stdarg.h>

   

double getSum2( const char *types, ... )

{

double sum = 0 ;

va_list ap;

const char *p;

   

va_start(ap, types);

   

for ( p = types ; *p ; p++ )

{

switch( *p )

{

case 'i':

sum += va_arg( ap, int );

break;

case 'd':

sum += va_arg( ap, double ) ;

break;

}

}

va_end(ap);

return sum;

}

   

void main(void)

{

printf("1+2=%f\n", getSum2("ii", 1,2));

printf("2.5+3.8+4=%f\n", getSum2("ddi", 2.5, 3.8, 4 ));

}

  • 예제 printfCall

#include <stdio.h>

   

void main(void)

{

printf("%d%d\n", 1, 2);

printf("%d%d%d\n",1, 2, 3, 4, 5);

printf("%d%d\n", 1, 3.14);

printf("%f%f\n", 1, 2);

printf("%s\n", 1); // 1을 포인터로 인식, 1번지에 접근

}

마지막 호출문은 %s로 되어 있어 가변 인수가 문자열인 것으로 전달되지만 실제 전달된 인수는 정수형이다. printf는 정수 1을 포인터로 해석하여 이 위치의 문자열을 읽으려고 시도하는데 절대 번지 2은 시스템 여역이기 때문에 다운되버린다.

15.3.3 매크로 분석

가변 인수에 대한 모든 지원은 표준 헤더파일 stdarg.h에 정의되어 있는 매크로에 의해 구현된다.

typedef char* va_list;

#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )

#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

#define va_end(ap) ( ap = (va_list)0 )

   

va_list는 단순히 char*형으로 정의되어 있다. 포인터 증감할 때 1바이트씩 증감하도록 하기 위해 char* 형으로 선언되어있다.

va_list 타입은 스택의 인수를 가리키는 포인터 타입이다.

_INTSIZEOF(n) 매크로는 인수로 전달된 타입 n의 길이를 계산하는데 n의 값에 따라 이 매크로의 계산 결과가 달라진다

printf("char = %d\n", _INTSIZEOF(char));                // char = 4

printf("short = %d\n", _INTSIZEOF(short));                // short = 4

printf("int = %d\n", _INTSIZEOF(int));                        // int = 4

printf("float = %d\n", _INTSIZEOF(float));                // float = 4

printf("double = %d\n", _INTSIZEOF(double));                // double = 8

이 매크로가 하는 일은 타입의 크기를 4의 배수로 올림한다고 할 수 있는데 좀 더 정확하게 표현하자면 정수형의 크기에 대한 배수로 올림한다.

정수형의 크기는 시스템마다 다른데 16비트 환경에서는 2바이트 32비트 환경에서는 4바이트이며 이 크기는 스택 하나의 크기이기도 하다

   

va_start 매크로는 가변 인수의 위치를 가리키는 포인터 ap를 초기화하는데 이 초기화를 위해 마지막 고정 인수 v를 전달해야 한다. ap는 마지막 고정 인수 v의 번지에 v의 크기를 더한 번지로 초기화된다. 스택에 인수가 들어갈 때는 전달된 역순으로 들어가므로 가변 인수들이 먼저 전달(높은 번지)되고 고정 인수가 가장 제일 끝에 전달(낮은 번지)된다.

이 상태에서 &v는 고정 인수의 번지를 가리키며 이 번지를 char*로 캐스팅한 후 고정 인수의 길이만큼 더하면 바로 아래에 있는 첫 번째 가변 인수의 번지를 구할 수 있다. va_start 매크로는 이 연산을 통해 ap를 가변 인수의 시작 번지로 초기화하여 가변 인수를 일기 위한 준비를 마친다. 이후 ap에 있는 값을 읽기만 하면 가변 인수의 값을 구할 수 있는데 이 동작을 하는 매크로가 바로 가변 인수 액세스의 핵심인 va_arg 매크로이다.

   

va_arg함수는 ap를 일단 가변 인수의 길이만큼 더해 다음 가변 인수 번지로 이동시킨다. 그리고 다시 길이를 빼서 원래 자리로 돌아온 후 이 번지를 t타입의 포인터로 캐스팅하여 *연산자로 그 값을 읽는다. 이 매크로는 ap의 값을 읽기만 하는 것이 아니라 다음 번 va_arg 호출을 위해 ap를 방금 읽은 가변 인수 다음의 번지로 옮겨 주는 동작까지 해야 하기 때문에 길이를 더했다가 다시 뺀 후 그 위치를 읽도록 되어 있다.

   

va_arg(ap,t) 호출문은 ap 번지에 있는 가변 인수를 t타입으로 읽고 그 길이만큼 ap를 증가시켜 다음 가변 인수를 읽을 수 있도록 준비한다. 그래서 va_arg를 계속 호출하면 가변 인수들을 연속적으로 액세스 할 수 있다. 단 va_arg가 인수를 정확하게 읽고 그 길이만큼 다음 위치로 이동하기 위해서는 가변 인수의 타입을 반드시 알려 주어야 한다. va_arg 매크로의 동작을 좀 더 잘게 분할해 본다면 다음과 같다.

ret = *(*t)ap;

ap += _INTSIZEOF(t);

return ret;

ap 포인터를 t*로 캐스팅한 후 이 자리에 있는 값을 읽어 ret에 대입해 놓고 ap는 t의 크기만큼 증가시켜 다음 위치로 이동한다. 그리고 전체 결과로 ret를 리턴한다

   

va_end 매크로는 가변 인수를 가리키던 ap 포인터를 NULL로 만들어 무효화시킨다

   

15.3.4 가변 인수 함수의 활용

가변 인수 함수는 한 번 호출로 여러 개의 정보를 다양한 방법으로 다룰 수 있다는 면에서 편리하다. 특히 printf 함수는 다양한 타입의 변수들을 한꺼번에 출력할 수 있어 변수값을 확인해 볼 때 아주 유용하다.

15.4 레퍼런스

15.4.1 변수의 별명

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

type &변수 = 초기값;

  • 예제 ref1

#include <stdio.h>

   

void main(void)

{

int i = 3;

int &ri = i;

   

printf("i = %d, ri = %d\n", i, ri ) ;

// i 값을 변경

i = 4;

printf("i = %d, ri = %d\n", i, ri ) ;

// ri 값을 변경

ri = 5;

printf("i = %d, ri = %d\n", i, ri ) ;

printf("i번지 : %x, ri번지 : %x\n", &i, &ri ) ;

}

  • 주의사항
  1. 레퍼런스와 대상체는 타입이 완전히 일치해야 한다.

    int i ;

    int &ri = i; // 가능

    double rd = i ; // 에러

  2. 레퍼런스는 생성 직후부터 별명으로 동작하기 때문에 선언할 때 초기식으로 반드시 대상체를 지정해야 한다.

    단 예외적으로 초기값이 없는 레퍼런스를 선언할 수 있다.

    1. 함수의 인수 목록에 사용되는 레퍼런스 형식 인수. 함수가 호출될 때 실인수에 대한 별명으로 초기화된다.
    2. 클래스의 멤버로 선언될 때, 이때는 클래스의 생성자에서 반드시 초기화해야 한다. 생성자에서 초기화하지 않으면 에러
    3. 변수를 extern 선언할 때. 이때는 레퍼런스의 초기식이 외부에 선언되어 있다는 뜻이므로 초기값을 주지 않아도 된다.

    이런 예외적인 경우라도 레퍼런스의 대상체 지정 함수 호출 시점이나 객체 생성 시점으로 연기될 뿐이지 대상체가 없는 레퍼런스를 허용하는 것은 아니다. 레퍼런스가 실제 메모리에 생성될 때 반드시 누구의 별명인지 지정되어 있어야 한다.

  3. 레퍼런스는 일단 선언되면 초기식에서 지정한 대상체의 별명으로 계속 사용된다. 선언된 후 중간에 참조 대상을 변경할 수 없으며 파괴될 때까지 같은 대상체만을 가리킨다.
  • 예제 ref2

#include <stdio.h>

   

void main(void)

{

int i = 3, j = 7;

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 );

}

ri는 i의 레퍼런스로 초기화됨. 이후부터 ri는 i의 별명으로 사용된다.

ri = j ㅐ대입문은 곧 i = j가 된다.

ri의 대상체를 j로 바꾸는 것이 아니라 ri가 가리키는 본래 변수 i에 j의 값을 대입하는 명령으로 해석됨

  1. 레퍼런스에 대한 모든 연산은 대상체에 대한 연산으로 해석된다.

    int i=3, j;

    int &ri = i;

    int *pi;

       

    ri++;

    ri*=5;

    j = ri >> 4;

    j = ri % 2;

    pi = &ri;

  2. 레퍼런스의 대상체는 실제 메모리 번지를 점유하고 있는 좌변값이어야 한다.

    int &ri = 123; /// 에러

    상수 지시 레퍼런스인 경우는 상수를 대상체에 취할 수는 있지만 실용성은 없다.

    const int &ri = 123; // 되긴하지만... const int i = 123; 으로 사용하는게

    15.4.2 레퍼런스 인수

    레퍼런스가 실용적인 위력을 발휘라 때는 함수의 인수로 전달될 때이다. 함수가 레퍼런스를 받아들이면 호출부의 실인수에 대한 별명을 전달받는 셈이므로 함수 내에서 실인수를 조작할 수 있게 된다. 레퍼런스의 값을 읽으면 실인수의 값을 읽을 수 있고 레퍼런스를 변경하면 실인수의 값도 같이 변경되므로 의미상으로 완전한 참조 호출이 되는 것이다.

  • 예제 CallRef2

#include <stdio.h>

   

void plusref2(int &a)

{

a = a + 1;

}

   

void main(void)

{

int i = 5;

   

plusref2(i);

printf("결과 = %d\n", i );

}

i를 레퍼런스로 전달했으므로 i는 6이 되어 돌아오며 출력 결과도 6이다.

포인터를 사용하는 방법에 비해 레퍼런스를 사용하는 방법은 다음과 같은 차이가 있다.

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

#include <stdio.h>

   

struct tag_Friend

{

char name[10];

int age;

double height;

};

   

void outFriend( tag_Friend f );

void outFriendPtr( tag_Friend *f );

void outFriendRef( tag_Friend &f ) ;

   

void main(void)

{

tag_Friend person = {"친구", 30, 170.2};

outFriend(person);

outFriendPtr(&person);

outFriendRef(person);

}

   

// 값 호출

void outFriend( tag_Friend f )

{

printf("이름 = %s, 나이 = %d, 키 = %.1f\n", f.name, f.age, f.height ) ;

}

// 포인터 참조 호출

void outFriendPtr( tag_Friend *f )

{

printf("이름 = %s, 나이 = %d, 키 = %.1f\n", f->name, f->age, f->height ) ;

}

// 레퍼런스 참조 호출

void outFriendRef( tag_Friend &f )

{

printf("이름 = %s, 나이 = %d, 키 = %.1f\n", f.name, f.age, f.height ) ;

}

값 호출을 사용하는 outFriend함수는 person 구조체의 사본 f를 전달받는데 이 과정에서 실인수가 형식인수로 복사된다. 구조체는 정수나 포인터에 비해 크기 때문에 복사 시간이 훨씬 더 오래 걸리며 따라서 함수 호출 속도가 느리다. 또한 구조체의 사본을 값으로 전달받기 때문에 outFriend 함수 내부에서 f의 멤버를 변경한다 하더라도 실인수 person의 값이 변경되지 않는다.

포인터나 레퍼런스는 값 자체가 복사되는 것이 아니라 단지 4바이트만 복사되므로 값 호출에 비해 속도가 빠르다. 구조체의 크기에 따라 수십배 이상 속도 차이가 날 수 있다. 또한 함수 내부에서 실인수를 직접 변경할 수 있다는 이점이 있다.

포인터를 쓰는 방법과 레퍼런스를 쓰는 방법은 효과는 거의 동일하며 형태상 몇가지 다른 점만 있다.

레퍼런스를 쓰는 방법은 포인터를 통한 참조 호출 방법에 비해 함수 내부가 훨씬 깔끔하고 직관적이다. *를 붙여 값을 변경하지 않고 직접 변경 가능하다.

그러나 레퍼런스를 통한 참조 호출 방법은 함수의 원형을 봐야만 레퍼런스를 전달받았는지 알 수 있다는 단점이 있다.

포인터는 잠재적으로 배열이므로 일단 넘기면 주변을 마음대로 건드릴 수 있지만 레퍼런스는 전달된 대상만 액세스 할 수 있다는 면에서 안전성이 높다.

15.4.3 레퍼런스의 대상체

포인터에 대한 레퍼런스도 선언할 수 있다.

  • 예제 PtrRef

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

   

void inputName( char *&name )

{

name = (char*)malloc(12);

strcpy(name, "cabin");

}

   

void main()

{

char *name;

   

inputName(name);

printf("이름은 %s입니다.\n",name);

free(name);

}

char *&name인수가 포인터의 레퍼런스이다. 2중 포인터를 쓰지 않아도 된다는 장점이 있다.

   

  • 예제 FuncRef

#include <stdio.h>

   

int ar[5] = { 100, 101, 102, 103, 104 } ;

   

void func(int a)

{

printf("%d\n", a);

}

   

void main(void)

{

void (*rf)(int) = func;        // 함수 레퍼런스

int (&rar)[5] = ar;        // 배열 레퍼런스

   

rf(rar[0]);

}

   

  • 레퍼런스가 선언되지 않는 경우
    • 레퍼런스에 대한 레퍼런스를 선언할 수 없다.
    • 레퍼런스에 대한 포인터를 선언할 수 없다.
    • 레퍼런스의 배열도 선언할 수 없다
    • 비트 필드는 주소를 가지지 않기 때문에 포인터의 대상체가 될 수 없으며 레퍼런스도 선언할 수 없다..

    15.4.4 레퍼언스 리턴값

  • 예제 RefReturn

#include <stdio.h>

   

int ar[] = {1, 2, 3, 4, 5};

   

int &getAr( int i )

{

return ar[i];

}

   

void main()

{

getAr(3) = 6;

printf("ar[3] = %d\n", ar[3]);

}

레퍼런스는 변수 그 자체이며 온전한 좌변값이기 때문에 함수가 리턴하는 레퍼런스가 대입 연산자의 좌변에 놓일 수 있다.

직관적이지 않고, 익숙하지 않기 때문에 꼭 필요하지 않으면 레퍼런스를 리턴하는 함수는 가급적 자재

레퍼런스가 꼭 필요할 때가 연산자를 오버로딩할 때

15.4.5 레퍼런스의 내부

레퍼런스의 내부를 들여다보면 포인터의 변형이다

컴파일러는 레퍼런스를 포인터로 바꾼 후 컴파일됨

레퍼런스를 참조하는 문장은 암시적으로 *연산자가 적용됨

int i = 3;

int &ri = i;

ri++;

int i = 3;
int *ri = &i;
(*ri)++;

 

반응형

'책정리 > 혼자 연구하는 C,C++ 1' 카테고리의 다른 글

17장 파일 입출력  (0) 2015.02.22
16장 함수 고급  (0) 2015.02.20
13장 구조체  (0) 2015.02.19
12장 문자열 함수  (0) 2015.02.19
11장 배열과 포인터  (0) 2015.02.19