본문 바로가기

컴퓨터공학/C, C++

C,C++> 포인터 고급

 

Const

정의

타입 뒤에 const 키워드를 써도 되지만 앞에 쓰는게 가독성이 있어서 앞에 쓴다. 

컴파일시 값이 결정되므로 배열 크기 지정에도 사용할 수 있다.

변수는 실행 중에 바뀔 수 있으므로 배열 크기를 지정에 사용할 수 없다. 

const 예약어는 매크로 상수를 정의하는 #define 전처리문과 유사하다. 

상수에 이름을 부여하는 것과 자주 사용하는 상수를 한곳에서 정의하여 일괄 수정이 쉽게 된다는 점에서 공통점이라고 할 수 있다.

const는 #define에 비해 다음 장점을 가지고 있다. 

1. 매크로 상수는 타입지정이 안되지만 const는 타입지정이 가능하다.

2. 매크로 상수는 어느 곳에서나 사용할 수 있지만 const는 선언 범위 내에서만 사용 가능하다. 

3. #define은 컴파일러가 아니라 전처리기로 치환되기 때문에 실제 소스에서 매크로가 치환된 상태로 실행된다. 

그래서 디버깅 중 매크로 상수 값을 확인할 수 없으며 버그 잡기가 힘들다. const 상수는 컴파일러가 처리하므로 디버깅 중 값을 확인할 수 있다. 

4. 매크로는 기계적 치환이므로 부작용이 생길 가능성이 있다.

괄호를 사용하지 않으면 연산 순위에 오류가 생길 수 있다.

const는 컴파일러가 문맥에 맞게 처리하므로 부작용이 없다. 

 

포인터와 const

포인터와 const를 같이 쓰면 효과가 달라진다.

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

int *pi1 = &ar[0];
pi1++;
*pi1 = 0;

const int *pi2 = &ar[0];
pi2++;
*pi2 = 0;

int * const pi3 = &ar[0];
pi3++;
*pi3 = 0;

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

pi1은 다른 대상체를 가리킬 수 있고 변경도 가능하다.

pi2는 포인터가 ++나 --를 사용하여 다른 대상체를 가리킬 수 있지만 상수를 가리키고 있으므로 대상체 값을 변경할 수 없다. 상수 지시 포인터(Pointer to Constant)라고 한다.

pi3는 포인터 변수를 상수로 만든다. 상수 포인터(Constant Pointer)라고 하는데 변수가 상수이므로 다른 대상체로 변경할 수 없다. 하지만 대상체 값 변경은 가능하다. 

pi4 최초 선언된 대상체 외 다른 대상체를 가리킬 수 없으며 값도 변경할 수 없다. 

 

다음은 이중 포인터와 const 관계이다.

int **ppi1 = π
ppi1++;
(*ppi1)++;
**ppi1=0;

const int **ppi2 = &pci;
ppi2++;
(*ppi2)++;
**ppi2 = 0; //에러

int * const *ppi3 = &cpi;
ppi3++;
(*ppi3)++; //에러
**ppi3 = 0;

int ** const ppi4 = π
ppi4++; //에러
(*ppi4)++;
**ppi4 = 0;

const int * const * const ppi5 = &cpci;
ppi5++; //에러
(*ppi5)++; //에러
**ppi5 = 0; //에러

포인터를 안전하게 사용하기 위해서 const를 사용한다. 

 

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

int *pi1 = &ar[0];
const int *pi2;

pi2 = pi1; //가능
pi1 = pi2; //불가능
pi1 = (int *)pi2; //가능

pi2가 pi1의 주소값을 받는 건 가능하지만 반대는 안되는데,

pi2가 가리키는 상수 대상체를 pi1을 통해서 변경할 수 있기 때문이다.

(const int *)을 강제로 (int *)으로 캐스팅해서 대입하면 가능하지만 비추천. 

 

volatile

volatile은 const와 함께 cv 지정자(Qualifier/제한자)라고 부른다. 

for(int i = 0; i<100; i++){
	j = sqrt(2.8) + log(3.5) + 56;
    //do something
}

j = sqrt(2.8) + log(3.5) + 56;
for(int i = 0; i<100; i++){
	//do something
}

컴파일러는 최적화 기능이 있어서 위처럼 작성하면 아래 같이 작동한다.

어차피 결과는 같으니까 상관 없겠지만 최적화된 코드가 원래 코드랑 다르게 작동할 수 있다. 

예를 들어 프로세스가 두 개 스레드를 갖고 있고 다른 스레드에서 조건에 의해 전역 변수값 j를 바꾼다고 하면 루프 내부에서 매번 j값을 계산하는 것과 루프 들어가기 전 미리 계산해 놓은 것은 다른 결과물을 가져올 수 있다.

50번째 계산 중 다른 스레드가 j를 바꿀 수 있다는 것이다. 

그래서 이 경우 volatile을 사용한다. 이 키워드가 있으면 컴파일러는 변수에 관해서 최적화 처리를 하지 않는다. 

volatile이 없으면 컴파일러가 위 for문에서 j를 바깥으로 꺼내겠지만 volatile을 변수 앞에 붙으면 작성한 그대로 컴파일한다. 

 

 

함수 포인터

정의

Pointer to Function은 함수를 가리키는 포인터이다. 함수도 메모리에 있고 시작 주소값이 있으니 가리킬 수 있다. 

함수 포인터와 구별하기 위해서 일반 포인터를 데이터 포인터라고 부른다.

데이터 포인터는 대상체 타입만 밝히면 되어서 선언 형식이 간단하다. 

함수 포인터는 대상체가 되는 함수 반환 타입과 인수 목록까지 같이 밝혀야 한다. 

 

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

 

리턴값과 인수 목록은 유지하되 형식 인수 이름은 생략해도 된다. 

함수 원형 선언에서 형식 인수 이름은 의미가 없듯이 함수 포인터 선언도 인수 타입만 의미있다.

예를 들어 다음 함수를 가리키는 함수 포인터를 선언한다고 가정하자.

 

int func(int a);

 

func이라는 함수 원형인데 이 함수를 가리키는 함수 포인터 pf를 선언하는 절차는 다음과 같다.

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

 

int func(int a) 함수를 가리키는 함수 포인터 pf에 func 주소값을 대입하는 법은

pf=func; 이렇게 가능하다.

함수명은 함수 시작 번지를 나타내는 포인터 상수이기 때문에 가능하다. 

pf는 변수이므로 원형만 일치하면 다른 함수도 모두 가리킬 수 있다.

 

함수 포인터에 함수 시작 주소값을 저장했으면 함수 대신 포인터로 함수 호출이 가능하다. 

함수 포인터로 함수 호출 형식은 두 가지가 있다.

(*pf)(2);

pf(2);

여기서 *pf를 감싸는 괄호는 생략할 수 없다. 생략하면 *pf(2)가 되고  * 연산자보다 () 연산자가 더 우선순위가 높기 때문에 pf(2) 호출문이 리턴하는 포인터로 대상체를 읽는 문장이 된다. * 연산자를 먼저 실행해서 포인터가 가리키는 함수를 찾은 다음 이 함수로 인수를 넘겨야 한다. 

원칙상 (*pf)(2)이지만 괄호와 *연산자를 쓰기에는 번거롭기 때문에 간략한 호출 방법을 지원한다.

바로 pf(2) 호출 형식이다. 함수 포인터를 마치 함수처럼 사용한다. 문법으로 따지면 틀린 문장이지만 컴파일러가 예외를 인정한다. pf가 함수 포인터라는 걸 컴파일러가 알고 있어서 *와 ()연산자를 쓰지 않아도 된다.

 

함수 포인터 타입 

원형이 다른 함수 포인터끼리 곧바로 대입할 수 없으며 인수로 넘길 수 없다. 

함수 포인터에도 캐스트 연산자를 사용할 수 있다. 예시는 다음과 같다. 

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

 

함수 포인터 배열이나 포인터를 선언하는 형식을 알아보자. 

func 타입 함수를 가리킬 수 있는 함수 포인터 요소로 가지는 크기 5 배열은 다음과 같다.

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

변수명 다음에 첨자 크기를 밝혀 주면 된다. 

 

함수 포인터의 포인터는 * 구두점만 하나 더 적으면 된다.

int (**ppf)(int);

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

ppf = &pf 또는 ppf = arpf 식으로 함수 포인터 변수 주소값을 대입받을 수 있다. 

ppf으로 함수 호출은 (**ppf)(2) 형식을 사용한다. 

 

 

함수 포인터 타입은 인수타입과 리턴값까지 밝혀야해서 귀찮다. 

그래서 typedef로 함수 포인터 타입을 따로 정의하고 사용하는게 편리하다. 

typedef int (*PFTYPE)(int);
PFTYPE pf;

이런 식으로 사용하면 된다. 

 

포인터로 함수 호출

함수 포인터를 인수로 사용하면 함수를 다른 함수에게 전달할 수 있고,

함수 포인터 배열이나 구조체를 통해 여러 개 함수를 통째로 바꿀 수 있다. 

다음 경우 함수 포인터를 사용하는 게 좋다. 

1. 선택해야 할 함수가 두 개 이상인 경우

2. 함수 선택 시점과 실제 호출 시점이 분리되어 있는 경우.

3. 호출 함수가 dll 같은 외부 모듈에 있고 이 함수를 동적으로 연결할 경우는 컴파일할 때 함수 존재가 알려지지 않았으니 함수 포인터를 사용해야 한다. 

 

함수 포인터 인수 

함수 포인터를 인수로 전달

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

 

함수 포인터 리턴 

함수 포인터를 리턴하는 함수 원형은 다음과 같다.

fp가 리턴되는 대상 함수이다. 

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

예시를 보면서 이해하자

char (*func(char **buf, char *(*strf[9])(void), int *pi))(unsigned short, unsigned (**)(const char *));

 

함수 포인터 이름은 func 어떤 함수를 가리키는 포인터냐면

문자형 이중 포인터 buf를 첫 번째 인수로 받고,

인수가 없으며 문자형 포인터를 반환하는 크기 9 함수 포인터 배열을 두 번째 인수로 받고,

정수형 포인터를 세 번째 인수를 받는 함수를 가리키는 포인터인데 

반환하는 함수 특징은 

부호없는 정수를 첫 번째 인수로 받고,

문자형 포인터를 인수를 받고 부호없는 정수를 반환하는 함수 이중 포인터를 두 번째 인수를 받고,

char 문자형을 반환하는 함수를 가리키는 포인터를 반환하는 함수이다. 

 

가변 인수 

가변 인수 함수

가변 인수란 인수 개수와 타입이 미리 정해져 있지 않다는 뜻이며 그런 인수를 사용하는 함수를 가변 인수 함수라고 한다. 

가장 좋은 예는 printf 함수이다. 다음은 printf 함수 원형이다. 

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

첫 번째 인수는 format이라는 문자열 상수이다.  서식 문자열이라고 부른다. 

두 번째 이후 인수는 타입과 인수 이름이 명시되어 있지 않으며 대신 생략 기호(ellipsis)인 ...가 있다. 

생략 기호는 컴파일러에게 이후 인수에 관해서 개수와 타입을 점검하지 않도록 한다. 

이 기호로 가변 인수가 가능하다. 

컴파일러는 ... 이후 인수에 관해서 개수와 타입을 상관하지 않고 있는 그대로 함수에게 넘긴다. 그래서 임의 타입 인수를 개수에 상관 없이 전달할 수 있다. 대신 전달된 인수 타입을 판별해서 쓰는 건 함수가 알아서 해야 한다. 

생략 기호 이전에 전달되는 인수를 고정 인수라고 한다. 

여기서 format이 고정 인수이다. 고정 인수는 타입과 개수가 명싣죄어 있으니 정확하게 전달해야 한다. 

printf(1, 2)나 printf(3.14) 같은 호출은 안된다. 

printf("이름 = %s, 나이 = %d, 키 = %f", "ㅇㄴㄹ", 23, 234.1);

쉼표 앞으로 고정인수 뒤는 가변 인수이다. 

사용하는 원리는 고정 인수인 서식 문자열을 먼저 전달하고 서식 개수와 타입에 맞는 인수를 순서대로 전달하면 된다. 

관건은 순서대로 꺼내서 정확한 값을 읽는 것이다. 

구조는 다음과 같다. 

 

void VarFunc(int Fix, ...){
	va_list ap;
    va_start(ap, Fix);
    while(인수를 다 읽을 때까지){
    	va_arg(ap, 인수타입);
    }
    va_end(ap);
}

va_list ap

함수로 전달되는 인수들은 스택에 저장된다. 함수는 스택에서 인수를 꺼내 쓴다. 

스택에 있는 인수를 읽을 때 포인터 연산을 해야 한다.

현재 읽고 있는 번지를 기억하기 위해 va_list형 포인터 변수 하나가 필요하다. 

변수 이름은 ap인데 Argument Pointer로 추측.

va_list 타입은 char * 형으로 되어 있다. 

가변 인수를 읽기 위해 포인터 변수를 선언했다고 생각하자. 

 

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

가변 인수를 읽기 위한 준비를 한다. ap 포인터 변수가 첫 번째 가변 인수를 가리키도록 초기화한다. 

첫 번째 가변 인수 주소값을 조사하기 위해 마지막 고정 인수를 전달한다. 

ap가 마지막 고정 인수 다음 주소값을 가리키게 하여 이후로는 ap 번지를 읽으면 순서대로 가변 인수를 읽을 수 있다. 

 

va_arg(ap, 인수타입)

가변 인수를 실제로 읽는 명령이다. 

ap 번지에 있는 값이 어떤 타입인지 지정해야 매크로가 값을 제대로 읽을 수 있으니

두 번째 인수로 읽고자 하는 값 타입을 지정 한다.

예를 들어 ap 위치에 있는 정수값을 읽고 싶으면 va_arg(ap, int)를 호출하고,

실수값을 읽고 싶으면 va_arg(ap, double)이라고 호출하면 된다. 

리턴 값은 인수 타입에 맞는 변수로 대입받아야 한다. 

이 명령은 ap 위치에서 타입에 맞는 값을 읽어 리턴하며 또한 ap를 다음 가변 인수 위치로 옮겨준다. 

이런 식으로 va_arg를 반복 호출하면 전달된 가변 인수를 순서대로 읽을 수 있다. 

int와 double 타입이 어떻게 함수로 전달할 수 있는 걸까? 

타입명은 함수 인수가 될 수 없는데 인수를 받을 수 있는 이유가 va_arg이 함수가 아니라 매크로 함수이기 때문이다.

va_arg의 두 번째 인수는 내부적으로 sizeof와 캐스트 연산자로 전달되기 때문에 타입명이 될 수 있다. 

 

va_end(ap)

가변 인수를 다 읽고 뒷정리하는 함수. 별다른 동작은 안해서 없어도 지장이 없다. 

그래도 이게 있는 이유가 호환성 때문이다. 플랫폼에 따라서 가변 인수를 읽고 뒷처리를 해야 하는 경우가 있기 때문이다. 

인텔 계열 CPU는 va_end가 아무 일도 하지 않는다. 

 

실제 사용 예시

 

 

 

가변 인수 함수 조건

1. 가변 인수 함수는 반드시 하나 이상 고정 인수를 가져야 한다. 

2  함수 내부에서 자신에게 전달된 가변 인수의 개수를 알아야 한다. 

컴파일러는 함수가 호출될 때 인수 개수를 점검하지 않는다. 

그래서 호출 측에서 가변 인수가 몇 개 전달 되었는지 알려주지 않으면 함수 내부에서 인수 개수를 알 수 있는 방법이 없다. 

고정 인수로 개수 전달하는 것이 귀찮으면 기변 인수 목록 끝에 특이값을 전달하는 방법을 쓸 수 있다. 예를 들어 0을 만나면 이 값을 가변 인수 끝으로 인식하도록 약속하는 것이다. 어떤 방법이든 함수 내부에서 가변 인수 개수를 알 수 있게 해주면 된다. 

그렇다면 표준 함수 printf는 인수 개수를 어떻게 알까? 

서식 문자열에 포함된 서식 개수가 가변 인수 개수와 일치하는 걸 알 수 있다.

그래서 printf는 고정 인수로 전달되는 서식 문자열에서 %d, %f, %s 같은 서식 개수만큼 가변 인수를 읽는 방식으로 인수 개수를 파악한다. 

3. 함수 내부에서 각각 가변 인수 타입을 알 수 있어야 한다. 

 

가변 함수를 잘못 작성해도 컴파일러는 에러를 만들지 않기 때문에 주의해서 써야 한다. 

 

매크로 분석

va_ 매크로 함수 동작 원리를 알아보자.

 

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를 void *으로 정의하고 증감할 때 캐스팅하여 사용한다. 

중요한 내용은 va_list 타입이 스택 인수를 가리키는 포인터라는 것이다.

 

_INTSIZEOF(n) 매크로는 인수로 전달된 타입 n의 길이를 계산한다. 

이 매크로가 하는 일은 4의 배수로 올림한다. 

정수형 크기는 시스템마다 다르다.

16비트 환경은 2바이트이고 32비트 환경에서는 4바이트이다.

정수형 크기는 또한 스택 하나의 크기와 같다.

결국 이 매크로는 각 타입 변수가 스택을 통해 함수로 전달될 때 몇 바이트를 차지하는지 계산한다. 

그래서 char가 1바이트지만 함수 인수로 전달될 때는 int형으로 확장되어 스택에 4바이트로 들어간다. 

즉, _INTSIZEOF는 인수가 스택에 들어가 있을 때 크기를 계산하는 것이다. 

 

va_start 매크로는 가변 인수 위치를 가리키는 포인터 ap를 초기화하는데

초기화를 위해 마지막 고정 인수 v를 전달해야 한다.

ap는 마지막 고정 인수 v 주소값에 v 크기를 더한 주소값으로 초기화한다. 

스택에 인수가 들어갈 때 전달된 역순으로 들어간다. 

그래서 가변 인수들이 먼저 전달되어 높은 번지가 되고 

고정 인수가 제일 끝에 전달되어 낮은 번지가 된다. 

참고로 위가 낮은 번지이고 아래가 높은 번지이다. 

 

va_arg 함수는 ap를 일단 가변 인수 길이만큼 더해 다음 가변 인수 번지로 이동시킨다. 

그리고 주소값을 다시 길이만큼 빼서 원래 자리로 돌아와 t 타입 포인터로 캐스팅하여 * 연산자로 값을 읽는다.

오히려 수식을 보면 이해하기 쉽다.

 

( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

1. ap += _INTSIZEOF(t)) : 다음 가변 인수 위치를 ap 변수에 저장한다.

2. - _INTSIZEOF(t) : 지금 주소값은 ap값을 이용하는게 아니라 다음 주소값 ap에서 크기만큼 뺀 값을 사용하는 방식이다.

ap는 언제나 다음 주소값이 들어이는 셈이다.

3.  (t *) : 다음 주소값에서 크기만큼 뺀 값이 지금 값인데 캐스팅한다.

4. * : 캐스팅해준 주소값을 읽는다. 

 

va_arg를 계속 호출하면서 가변 인수들을 연속적으로 접근할 수 있다. 

그래서 가변 인수 타입을 반드시 알려 줘야 한다. 

 

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

이건 굳이 필요없는데 ap는 지역 변수로 선언되어서 함수가 종료되면 사라지기 때문이다. 

 

 

 

 

레퍼런스 

변수의 별명

한 대상에  두 개 이름이 생기고 별명으로도 변수를 사용할 수 있다.

type &변수 = 초기값;

예시는 다음과 같다. 

int i = 3;
int &ri = i;

 

기본 형식은 위와 같다. 포인터 변수는 구두점 *를 사용하지만 레퍼런스는 구두점 &을 사용한다. 

포인터가 기본 타입에 대한 유도형이듯 레퍼런스도 유도형이라는 점에서 동일하지만
특정 대상체 별명이므로 어떤 대상체에 대한 별명인지 반드시 밝혀야 한다는 점에서 다르다. 

1. 레퍼런스와 대상체 타입은 완전 일치해야 한다.

2. 레퍼런스는 선언할 때 초기식으로 반드시 대상체를 지정해야 한다. 

초기값 없는 레퍼런스는 세 가지이다. 

함수 인수 목록에 사용되는 레퍼런스 형식 인수. 호출될 때 실인수에 관한 별명으로 초기화된다.

클래스 멤머로 선언될 때 클래스 생성자에서 반드시 초기화해야 한다. 

변수를 extern으로 선언할 때 레퍼런스 초기식이 외부에 있다는 뜻으로 초기값을 주지 않아도 된다. 

3. 선언 후 중간에 참조 대상을 변경할 수 없다. 

4. 레퍼런스 연산은 대상체에 관한 연산으로 해석된다. 

5. 레퍼런스 대상체는 실제 메모리에 점유하고 있는 좌변값이어야 한다. 

상수값은 좌변값이 아니라서 안된다. 

 

레퍼런스 인수 

앞 예제에서 변수 i에 관한 별명으로 ri 레퍼런스를 선언하고 사용했다. 

ri가 i와 동일해서 별명을 만들 필요 없이 그냥 사용 i를 쓰면 간편한데 레퍼런스를 왜 사용할까?

레퍼런스가 위력을 발휘할 때는 함수 인수로 전달할 때이다. 

함수가 레퍼런스로 받아들이면 함수 내에서 실인수를 조작할 수 있게 된다.

레퍼런스 값을 읽으면 실인수 값을 읽을 수 있고, 수정하면 실인수 값도  같이 변경된다.

완전한 참조 호출이다. 

예시를 보고 이해하자.

void plusref2(int &a);

void main(){

	int i;
    i=5;
    plusref2(i);
    printf("결과=%d\n", i);
}

void plusref2(int &a){
	a=a+1;
}

 

구조체를 선언, 초기화, 호출 시 세 가지 방법 차이점

값 호출 : 함수에서 구조체 사본을 전달받는데 실인수가 형식인수로 복사된다. 구조체는 복사 시간이 오래 걸려서 함수 호출 속도가 느리다. 사본값이 전달받아서 실인수 값이 변경되는 건 아니다. 

포인터 참조 호출 : 값 자체 복사가 아니라 4바이트만 복사되므로 빠르다. 실인수를 직접 변경 가능

레퍼런스 참조 호출 : 포인터는 실인수 조작할 때 일일이  * 연산자를 써야  하지만 레퍼런스는 직관적으로 코드를 작성할 수 있다.

그러나 호출부 형식이 값 호출 방식과 동일하여 헷갈릴 수 있다. 함수 원형을 봐야 값인지 레퍼런스인지 확인할 수 있다.

따라서 레퍼런스를 받는 함수는 함수명에 Ref나 ByRef 같은 접미를 붙여서 쉽게 파악할 수 있도록 작성하자.

포인터는 배열을 받으면 주변을 마음대로 건드릴 수 있지만 레퍼런스는 전달 대상만 접근할 수 있다. 

상수는 레퍼런스 대상체가 될 수 없어어서 레퍼런스 인수를 사용하는 함수는 상수를 전달할 수 없다. 

함수 내에서 이 값을 변경할 수 없다. plusref2(i+j) 같은 수식도 전달할 수 없다.

원형이 void plusref2(const int &a) 형식이라면 상수나 수식을 전달할 수 있다.

물론 함수 내에서 인수값을 변경하는 건 불가능하다. 

 

값을 전달받는 함수는 변수나 상수 심지어 수식도 받을 수 있다. 

타입이 정확하지 않아도 컴파일러가 내부 변환으로 타입을 맞추고 호출한다. 

그래서 동일 동작을 하는 함수면 레퍼런스보다 값을 전달받는 함수가 유리하다. 

 

레퍼런스 대상체 

포인터 레퍼런스를 알아보자. 

char *&Name 인수가 포인터 레퍼런스이다.

T형의 레퍼런스는 T &이며 char * 자체가 하나의 타입이므로 레퍼런스는 char *&d 이다.

char &*이 아니다. 

 

int ar[5] = {100, 200, 300, 400};

void func(int a){
	printf("%d\n", a);
}

void main(){
	void (&rf)(int) = func; //함수 레퍼런스
    int (&rar)[5] = ar; // 배열 레퍼런스
    
    rf(rar[0]);
}

 

1. 레퍼런스의 레퍼런스는 불가능

2. 레퍼런스 포인터는 불가능

레퍼런스 포인터는 곧 대상체에 관한 포인터형이다. 즉 단순 포인터이다. 레퍼런스 포인터라고 정의할 수 없다.

3. 레퍼런스 배열 불가능

T형 배열은 T형 포인터이므로 레퍼런스에 대한 포인터는 선언할 수 없으므로 배열도 선언할 수 없다.

4. 비트 필드 레퍼런스 불가능

비트 필드는 주소를 가지지 않으므로 포인터 대상체가 될 수 없으며 레퍼런스 참조 대상이 될 수 없다. 

 

레퍼런스 리턴값

레퍼런스는 변수 그 자체이다. 온전한 좌변값이다.

따라서 함수가 반환하는 레퍼런스는 대입 연산자 좌변에 둘 수 있다.

 

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

int &GetAr(int I){
	return ar[i];
}

void main(){
	GetAr(3) = 6;
    printf("ar[3]=%d\n", ar[3])'
}

&GetAr(i) = ar[i] 이다. 

 

값을 리턴하는 C 함수는 이것이 불가능하다. 

레퍼런스를 리턴하는 C++에서는 함수 호출문에 어떤 값을 대입하는 것이 가능하다. 

int FindMatch(char *name, int value, BOOL bCase);
arSome[FindMatch(...)] = Data; // C

int &FindMatchRef(char *name, int value, BOOL bCase);
FindMatchRef(...) = Data; // C++

함수가 레퍼런스를 리턴하는 건 직관적이지 않으므로 가급적 자재하는 것이 좋다.

리턴값으로 레퍼런스를 사용하는 경우는 연산자 오버로딩할 때이다. 

레퍼런스를 리턴할 때 주의사항을 알아보자.

 

함수 지역변수 레퍼런스를 반환하는데 문법적으로 문제가 없더라도 안전하지 않다.

함수가 끝나면 사라질 변수에 대한 레퍼런스를 반환하므로 호출부에서는 이 레퍼런스를 참조하면 엉뚱한 메모리 위치를 접근하게 될 것이다. 

포인터도 마찬가지로 지역변수 주소값을 리턴하는 건 옳지 않다. 

 

동적 할당한 변수 레퍼런스를 리턴하는 것도 가능하지만 비추천한다. 

왜냐하면 내부에서 할당한 변수를 외부에서 해제하기 위해서 결국 포인터가 필요한데,

리턴된 레퍼런스를 바로 대입받아 버리면 포인터를 잃어버리기 때문이다. 

따라서 동적으로 정수형 변수 하나를 할당하여 레퍼런스를 리턴하는데 호출부에서는 리턴값의 번지를 포인터 변수로 받아야 한다. 

 

int &func(){
	int *pt;
    pt = (int *)malloc(sizeof(int));
    *pt = 3;
    return *pt;
}

void main(){
	int *pi;
    pi = &func();
    printf("%d\n", *pi);
    free(pi);
}

 

래퍼런스 내부 

레퍼런스를 쓰는 것과 포인터를 쓰는 것은 별반 다를게 없다.

레퍼런스는 대상체 주소를 사용할 때 * 연산자를 붙여야 하는 번거로움이 없어서 사용한다. 

 

레퍼런스는 한 번 누군가 별명이 되면 파괴될 때까지 계속 참조 대상이다. 

int & const ri = i;

에서 const는 있으나 마나이고 ri 대상자를 상수로 처리하고 싶으면 

const int &ri = i;

이렇게 하면 된다. 

 

 

 

'컴퓨터공학 > C, C++' 카테고리의 다른 글

C,C++> 파일 입출력  (0) 2022.07.28
C,C++> 함수 고급  (0) 2022.07.27
C,C++> 구조체  (0) 2022.07.21
C,C++> 배열과 포인터  (0) 2022.07.19
C,C++> 포인터  (0) 2022.07.05