1. const
1.1. 상수 정의
- 키워드 const는 값을 변경할 수 없는 상수를 정의한다.
const int HourPerDay=24
- const 키워드는 타입 다음에 붙일 수도 있으며, 타입이 생략될 경우 int형으로 간주된다.
- const에 의해 만들어진 상수는 컴파일시에 값이 결정되기 때문에 배열의 크기 지정에도 사용할 수 있다.
- const 는
#define에 비해 다음과 같은 장점을 가지고 있다.
#define이 정의하는 매크로 상수는 타입을 지정할 수 있지만, const는 타입을 명확하게 지정할 수 있다.
- 매크로 상수는 일단 정의된 후에는 언제든지 어느 곳에서나 사용할 수 있지만, const는 통용 범위 규칙의 적용을 받기 때문에 자신이 선언된 범위 내에서만 사용할 수 있다. 따라서 명칭간의 충돌을 최소화할 수 있다.
#define은 컴파일러가 아닌 전처리기에 의해 치환되기 때문에 실제 소스에는 매크로가 치환된 상태로 실행된다.
그래서 디버깅 중에 매크로 상수의 값을 확인해볼 수 없다. 반면 const 상수는 컴파일러가 처리하기 때문에 디버깅 중에도 값을 확인해 볼 수 있어 복잡한 단계를 통해 정의된 상수의 값도 쉽게 살펴볼 수 있다.
- 매크로는 기계적으로 치환되기 때문에 부작용이 발생할 소지가 많다. 괄호를 싸지 않으면 연산 순위에 의해 예상하지 못한 값이 될 위험이 있다. 그러나 cosnt 상수는 컴파일러가 문맥에 맞게 처리하기 때문에 이런 부작용이 거의 없다.
#define A 1+2의 A는 3이 될 가능성이 있을 뿐 주변 연산문에 따라 3이 아닐 수도 있지만 const int A=1+2;는 어떤 경우라도 3이다.
#define이 c언어에서 사용하던 방법이라면 const는 C++에서 새로 도입된 좀더 진보된 방법이다.(물론 gcc에서 컴파일이 된다)
- 상수를 정의할 때 가급적이면
#define보다 const를 사용할 것을 더 권장하는 편이다.
1.2. 포인터와 const
- const의 위치에 따라 상수가 되는 대상이 조금씩 달라진다.
const int *pi1 = &ar[0];
- 이 포인터는 const int를 가리키는 포인터 변수로 선언되었다.
- 이 포인터가 가리키는 대상체는 정수형 상수이며 '포인터' 자체는 상수가 아니다.
- 따라서 이 포인터 변수에 증감 연산자를 이용하여 다른 대상체의 번지를 직접 대입하여 다른 대상체를 가리키도록 할 수 있다.
- 하지만
*포인터변수를 변경하는 것은 허락되지 않는다.
- 이런 포인터를 상수 지시 포인터라고 한다.
int* const pi1 = &ar[0];
- 이 구문은 포인터 변수를 상수로 만든다.
- 이런 포인터를 상수 포인터 라고 한다.
- 변수 자체가 상수이므로 증감연산자를 이용하여 다른 대상체를 가리킬 수 없다.
- 하지만 이 포인터가 가리키는 생상체는 상수가 아니므로 대상체의 값을 변경하는 것은 가능하다.
const int* const pi1 = &ar[0];
- 이 구문은 포인터 변수 자체도 상수이고 대상체도 상수 취급된다.
- 정리하면 첫 번째 예시는 대상체를 상수만들고, 두 번째 예시는 포인터를 상수로 만든다.
- 이중 포인터의 경우 세 군데에 const 키워드를 사용할 수 있다.
const int* const * const ppi;
- 첫 번째 const는 ppi가 가리키는 포인터가 가리키는 정수가 상수라는 뜻이다.
- 두 번째 const는 ppi가 가리키는 포인터가 상수라는 뜻이다.
- 세 번재 const는 ppi 자체가 포인터 상수라는 뜻이다.
- 컴파일러 차원에서 이런 상수 포인터를 지원하는 이유는 고의든 실수든 바뀌지 말아야 할 중요한 값을 보호하기 위해서다.
int ar[5]={1,2,3,4,5};
int *pi1=&ar[0];
const int *pi2;
pi2=pi1;
pi1=pi2;
pi1=(int *)pi2;
pi1=(int *)&ar[0];
1.3. const 인수
- const인수가 꼭 필요한 경우는 포인터를 통한 참조 호출일 때뿐이며 값 호출일 때는 큰 의미가 없다.
- 표준 함수들은 자신이 전달 받은 포인터의 대상체를 변경하지 않을 때 항상 const 인수를 전달받도록 되어 있다.
- const는 우발적인 코드로부터 중요한 값을 보호하는 문법적 장치이므로 안정성을 위해 적극적으로 활용하는 것이 좋다.
1.4. volatile
- volatile 키워드는 const와 함께 변수의 성질을 바꾸는 역할을 하는데 이 둘을 묶어 cv지정자(제한자)라고 한다.
- const에 비해 상대적으로 사용빈도가 지극히 낮으며 이 키워가 꼭 필요한 경우는 무척 드물다.
- 예를 들면 다음과 같은 코드가 있다고 하자.
double j;
for (int i=0;<100;i++){
j=sqrt(2.8)+log(3.5)+56;
}
- 이 예제에서는 j의 값은 루프의 i를 참조하지 않기 때문에 j의 값은 상수나 마찬가지이며 절대로 변경되지 않는다.
- 따라서 j를 계산하고 대입하는 코드를 컴파일러가 루프 밖에 위치시키는 최적화를 수행한다.
- 이 값을 매 루프마다 다시 계산하는 것은 시간 낭비이기 때문.
- 하지만 최적화 전의 코드와 최적화된 코드의 작동이 동일하게 작동될 것이라고 보장할 수는 없다.
- 아주 특수한 경우 최적화된 코드가 원래 코드와 다른 동작을 할 경우가 있다.
- 프로그램이 아닌 외부에서 j의 값을 변경할 때이다.
- 도스 환경에서는 인터럽트, 유닉스 환경에서는 데몬, 윈도우즈 환경에서는 서비스 등의 백그라운드 프로세스가 항상 실행된다.
- 이런 백그라운드 프로세스가 메모리의 어떤 상황이나 전역변수를 변경할 수 있으며,
같은 프로세스 내에서도 스레드가 여러 개라면 다른 스레드가 j의 값을 언제든지 변경할 가능성이 있다.
또한 하드웨어에 의해 전역 환경이 바뀔 수도 있다.
- 두 번째 경우에 따라 위 코드의 결과가 달라질 수 있다.
- 이러한 경우에 쓰는 것이 volatile이다.
- 이 키워드를 변수 선언문 앞에 붙이면 컴파일러는 이 변수에 대해서는 어떠한 최적화 처리도 하지 않는다.
- 어떤 변수를 다른 프로세스나 스레드가 바꿀 수도 있다는 것을 컴파일러는 알 수 없기 대문에 전역 환경을 참조하는 변수에 대해서는 개발자가 volatile 선언을 해야 한다.
* volatile double j;
2. 함수 포인터
2.1. 정의
- 함수 포인터의 난해함은 모든 포인터 내용중에 가장 난해하다.
- 다행히 함수 포인터는 실적에서 사용빈도가 그리 높지 않다.
- 함수 포인터란 함수를 가리키는 포인터다.
- 함수도 메모리에 존재하며 시작 번지가 있으므로 포인터 변수로 가리킬 수 있다.
- 함수 포인터를 선언하는 형식은 함수의 원형 선언 형식과 유사하다.
- 함수의 원형을 써 놓고 함수명을 변수명으로 바꾸고 앞에
*를 붙인 후 *변수명을 괄호로 싸면 된다.
char *func(char *a, int b); -> char *(*pf)(char *, int);
- 이렇게 선언한 함수 포인터는 자신과 원형이 같은 함수의 시작 번지를 가리킬 수 있는데 단순히 함수의 이름을 대입하면 된다.
pf=func;
- 이렇게 대입이 가능한 이유는 괄호없이 단독으로 사용된 함수명은 함수의 시작 번지를 나타내는 포인터 상수이기 때문이다.
- pf 자체는 변수이므로 원형만 일치한다면 다른 함수를 가리킬 수도 잇다.
- 함수 포인터에 함수의 시작 번지를 저장했으면 이제 함수 대신 포인터로 함수를 호출할 수 있다.
- 변수의 번지를 가리키는 데이터 포인터(여태 배웠던 일반적으로 변수를 가리키는 포인터)로 변수 값을 읽을 수 있듯이,
함수 포인터로는 이 포인터가 가리키는 번지의 함수를 호출할 수 있다.
- 함수 포인터로 함수를 호출하는 형식은 다음 두 가지가 있다. (func함수를 가리키는 pf로 func 함수를 호출하는 예다)
(*pf)("ABC", 2); 또는 pf("ABC", 2);
- 이때 첫 번째 형식에서는 괄호를 생략할 수 없는데
*pf("ABC", 2);와 같이 실행한다면 *연산자 보다 ( )연산자가 우선순위가 더 높아 pf("ABC", 2) 호출문이 리턴하는 포인터로 부터 대상체를 읽는 문장이 되어 버린다.
- 원칙적으로 첫 번째 형식처럼 쓰는 것이 맞는 방법이지만 괄호와
*연산자를 쓰는 것이 번거롭기 때문에 컴파일러 차원에서 두 번째 형식처럼 간편하게 사용할 수 있다.
- 즉, 함수 포인터를 함수와 동일한 방법으로 사용하는 것을 허용한다.
2.2. 함수 포인터 타입
- 함수 포인터 타입도 일졸의 고유한 타입이다.
- 따라서 원형이 다른 함수 포인터끼리는 곧바로 대입할 수 없으며 함수의 인수로도 넘길 수 없다.
- 그러나 타입이 다른 함수 포인터끼리라도 강제로 대입할 수는 있는데 이것이 일단은 가능해야 한다.
- void 포인터에 저장된 함수의 번지를 대입 받는다거나 자료 구조 설계시에 미리 알 수 없는 함수에 대한 포인터를 다루고자 할 때 이다.
- 데이터 포인터에서와 마찬가지로 함수 포인터에도 캐스트 연산자를 쓸 수 있는데 문제는 함수 포인터의 캐스트 연산자와 모양이 생소해서 조금 어렵다.
int (*pf1)(char *);
void (*pf2)(double);
pf1=(int (*)(char *))pf2;
- 이 예에서
(int (*)(char *))가 캐스트 연산자이다.
- 함수 포인터형의 캐스트 연산자를 만드는 방법은 함수 포인터 선언식에서변수명을 빼고 전체를 괄호로 한 번 더 감싸주면 된다.
- pf3라는 변수에 func함수의 번지를 강제로 대입할 때도 마찬가지로 캐스트 연산자를 사용할 수 있다.
- 물론 이렇게 강제로 대입했을 때의 부작용에 대해서는 스스로 책임져야 한다.
- 함수 포인터의 배열 선언하는 방법
- func타입의 함수를 가리킬 수 있는 함수 포인터를 요소로 가지는 크기 5의 arpf 배열은 다음과 같이 선언한다.
int (*arpf[5])(int);
- 이 선언에 의해
int (*)(int)형의 함수의 번지를 가리킬 수 있는 함수 포인터 arpf[0]~arpf[4] 까지 5개의 변수가 생성되며, 각 변수(요소)는 int func(int)와 같은 원형의 함수를 가리키는 번지를 가질 수 있다.
- 함수 포인터의 포인터 선언하는 방법
* 구두점만 하나 더 적으면 된다.
- `
int (**ppf)(int);
- 이렇게 선언된 ppf는 `int (*)(int) 타입으로 선언된 함수 포인터 변수나 함수 포인터 배열을 가리킬 수 있는 이차 함수 포인터 변수다.
ppf=&pf 또는 ppf=arpf식으로 함수 포인터 변수의 번지를 대입받을 수 있으로 ppf로 부터 함수를 호출할 때는 (**ppf)(2) 형식을 사용한다.
- 함수 포인터의 타입은 함수가 취하는 인수들의 타입과 리턴값까지 정확하게 밝혀야 하기 때문에 타입의 형식이 너무 길어서 쓰기에 번거롭다.
- 또한 함수 포인터로부터 파생된 타입을 만드는 것도 헷갈리고 생소한 면이 있다.
- 그래서 함수 포인터 타입을 자주 사용하거나 자신이 없다면 직접 타입을 기술하는 것보다 typedef로 함수 포인터 타입을 따로 정의한 후 사용하는 것이 편리하다.
typedef int (*PFTYPE)(int);
PFTYPE pf;
- 함수 포인터를 선언하는 문장에서 변수명을 원하는 타입 이름으로 바꾸고 앞에 typedef만 붙이면 된다.
- 이후 컴파일러는 PFTYPE이라는 명칭을
int(*)(int) 타입으로 인식하므로 PFTYPE으로 함수 포인터를 쉽게 선언할 수 있으며 캐스트 연산자로도 사용할 수 있다.
- 또한 함수 포인터로부터 배열이나 포인터 같은 파생 변수를 선언하는 것도 훨씬 더 간편하다.
PFTYPE arpf[5], PFTYPE *PPF
- 마치 int형으로 부터 배열이나 포인터를 선언하듯이 PFTYPE을 사용할 수 있으므로 직관적이고 읽기에도 좋다.
2.3. 포인터로 함수 호출하기
- 다음과 같은 경우 함수 포인터를 사용하는 것이 더 좋다.
- 선택해야할 함수가 두 개 이상인 경우, 함수 포인터 배열을 선언하고 그 첨자를 선택한느 것이 더 쉽다.
- 함수를 선택하는 시점과 실제로 호출하는 시점이 완전히 분리되어 있는 경우도 함수 포인터를 쓰는 것이 유리하다.
호출할 함수에 대한 조건 점검은 필요할 때 한 번만 하고 선택된 함수는 별다른 조건 점검없이 함수 포인터로 바로 호출할 수 있다.
- 호출할 함수가 DLL 같은 외부 모듈에 있고, 이 함수를 동적으로 연결할 경우는 컴파일할 때 함수의 존재가 알려지지 않으므로 반드시 함수 포인터를 사용해야 한다. 함수 포인터를 사용하면 이름으로부터 원하는 함수의 번지를 찾아 호출할 수 있다.
2.4 함수 포인터 인수
- 함수 포인터는 함수를 가리키고 있지만 어쨌거나 변수이기 때문에 함수의 인수로 전달될 수 있다.
- 이렇게 하면 함수 내부에서 어떤 함수를 호출할 것인지를 호출측에서 지정할 수 있다.
- 함수 포인터는 다른 포인터와 달리 증감 연산자를 사용할 수 없으며 정수와 가감 연산도 할 수 없다.
- 함수는 코드 덩어리이며 이 덩어리의 크기는 가변적이고 실행 중에 변경할 수도 없기 때문이다.
2.5. 함수 포인터 리턴
- 함수 포인터를 리턴하는 함수도 만들 수 있다
- 이런 함수는 실용성이 지극히 떨어진다.
- 함수 자체의 인수와 리턴할 함수 포인터의 인수 목록을 같이 적여야 하기 때문에 무척 복잡할 수 밖에 없다.
- 함수는 모든 타입을 리턴할 수 있어야 하므로 이런 요구 사항을 충족하고 함수 포인터를 인수로 전달할 수 있다는 사실과 대칭을 이루기 위해 문법이 지원하고 있을 뿐 실용적인 가치는 전혀 없다고 할 수 있다.
3. 가변 인수
3.1. 가변인수 함수
- 가변 인수 함수가 어떻게 동작하는지 설명할 수 있다면 포인터를 정복했다고 생각해도 좋다.
- 가변 인수를 읽어내는 포인터 연산식을 해석할 수 있다는 것은 포인터를 충분히 이해했다는 증거이다.
- 가변 인수를 이용하는 함수의 원형
int printf(const char *format, ... );
- 두 번째 이후의 인수에는 타입과 인수 이름이 명시되어 있지 않으며 대신 생략 기호(ellipsis)인
...이 적혀 있다.
- 생략 기호는 컴파일러에게 이후의 인수에 대해서는 개수와 타입을 점검하지 않도록 한다.
- 컴파일러는
... 이후의 인수에 대해서는 개수가 몇 개든지 어떤 타입이든지 상관하지 않고 있는 그대로 함수에게 넘기므로 임의의 타입의 인수들을 개수에 상관없이 전달할 수 있다.
- 대신 전달된 인수의 정확한 타입을 판별하여 꺼내쓰는 것은 함수가 알아서 해야 한다.
- 컴파일러는 인수를 마음대로 취할 수 있도록 허락은 해 주지만 뒷일에 대해서는 절대로 책임지지 않는다.
- 다음과 같이 가변 인수들을 사용할 수 있다.
- 관건은 자신에게 전달된 임의 타입의 인수들을 순서대로 꺼내서 정확한 값을 읽는 것이다.
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;
}
va_list ap
- 함수로 전달되는 인수들은 스택(Stack)이라는 기억 장소에 저장되며 함수는 스택에서 인수를 꺼내쓴다.
- 스택에 있는 인수를 읽을 때 포인터 연산을 해야 하는데 현재 읽고 있는 번지를 기억하기 위해 va_list형의 포인터 변수 하나가 필요하다.
- va_list 타입은
char *형으로 정의되어 있다.
- 가변 인수를 읽기 위한 포인터 변수를 선언했다고 생각하면 된다.
va_start(ap, 마지막고정인수)
- 이 명령은 가변 인수를 읽기 위한 준비를 한다.
- 첫 번째 가변 인수의 번지를 조사히기 위해서 마지막 고정 인수를 전달한다.
- va_start 내부에서는 ap가 마지막 고정 인수 다음 번지를 가리키도록 해주므로 이후부터 ap 번지를 읽으면 순서대로 가변 인수를 읽을 수 있다.
va_arg(ap, 인수타입)
- 가변 인수를 실제로 읽는 명령이다.
- va_start가 ap를 첫 번째 가변 인수 번지를 맞추어 주므로 ap위치에 있는 값을 읽기만 하면 된다.
- 단, ap번지에 있는 값이 어떤 타입인지를 지정해야 이 매크로가 값을 제대로 읽을 수 있으므로 두 번째 인수로 읽고자 하는 값의 타입을 지정한다.
- 리턴되는 값은 인수타입에 맞는 변수로 대입받아야 한다.
- 이 명령은 ap에서 타입에 맞는 값을 읽어 리턴하며 또한 ap를 다음 가변 인수 위치에 옮겨준다.
- 그래서 va_arg를 반복적으로 호출하면 전달된 가변 인수를 순서대로 읽을 수 있다.
- 함수의 인수로 타입 이름을 전달할 수 있는 이유는 va_arg가 진짜 함수가 아니라 매크로 함수이기 때문이다.
- va_arg의 두 번째 인수는 내부적으로 sizeof 연산자와 캐스트 연산자로 전달되기 때문이다.
va_end(ap)
- 이 명령은 가변 인수를 다 읽은 후 뒷정리를 하는데 별다른 동작은 하지 않으며, 실제로 없어도 전혀 지장이 없다.
- 이 명령이 필요한 이유는 호환성 때문이다.
플랫폼에 따라서는 가변 인수를 읽은 후에 뒷처리를 해야 하는 경우도 있기 때문이다.
적어도 인텔 계열의 CPU에서는 va_end가 아무 일도 하지 않는다.
* 그러나 다른 플랫폼이나 미래의 환경에서는 va_end가 중요한 역할을 할 수도 있으므로 호환성을 위해서는 관례적으로 넣어 주는 것이 좋다.
3.2. 가변 인수 함수의 조건
- 가변 인수 함수에서 지켜야할 규칙들
- 가변 인수 함수는 반드시 하나 이상의 고정 인수를 가져야 한다.
- 즉, 첫 번재 인수부터 가변 인수일 수는 없다.
- 함수 내부에서 자신에게 전달된 가변 인수의 개수를 알 수 있도록 해야 한다.
- 개수와 마찬가지로 함수 내부에서 각각의 가변 인수 타입을 알 수 있어야 한다.
- 가변 인수들의 타입을 알아야 하는 이유는 va_arg 매크로가 ap 번지에서 가변 인수를 읽을 때 얼마만큼 읽어서 어떤 타입으로 해석해야 할지를 알아야 하기 때문이다.
- 가변 인수 함수를 잘못 쓰면 상당히 위험해질 수 있다.
printf("%f%f\n",1,2) : 주어진 가변 인수를 8바이트씩 실수형으로 읽으려고 시도할 것이다.
printf("%s\n", 1); : 가변 인수가 문자열인 것으로 전달된다.
- 정수 1을 포인터로 해석하여 이 위치의 문자열을 읽으려고 시도하는데 전대 번지 1은 시스템 영역이기 때문에 프로그램이 즉시 다운 된다.
- 위 코드들은 (가변 인수를 쓰기 때문에 당연하지만) 컴파일러가 경고나 에러를 출력하지 않는다.
3.3.매크로 분석
- 가변 인수에 대한 모든 지원은 오로지 표준 헤더 파일 stdarg.h에 정의되어 있는 매크로에 의해 구현된다.
- gcc에서는 stdarg.h 내에 각 매크로 함수의 구체적인 구현 코드를 확인할 수 없었다.
- 다음 비주얼 C++ 6.0의 stdarg.h 헤더 파일에 기록된 내용이다.
typedef char * va_list;
#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1) & ~(sizeof(int)-1))
#define va_start (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 *형으로 정의되어 있다.
- char에 대한 포인터라는 것은 별다른 의미는 없고 증감할 때 1바이트씩 증감하도록 하기 위해 char형 포인터로 선언된 것이다.
- 실제로 어떤 컴파일러는 va_list를
void *로 정의해 놓고 증감할 때 캐스팅해서 사용하기도 한다.
- 중요한 것은 va_list 타입이 스택의 인수들을 가리키는 포인터 타입이라는 것이다.
_INTSIZEOF(n) 매크로는 인수로 전달된 타임 n의 길이를 계산한다.
- char, short, int, float을 이 매크로의 인수로 전달하면 4, double을 인수로 전달하면 8로 출력된다.
- 정확하게 이 매크로의 기능을 설명하자면 주어진 타입의 크기를 '정수형의 크기에 대한 배수로 올림'한다.
- 정수형의 크기는 시스템마다 다른데 16비트 환경에서는 2바이트이고 32비트 환경에서는 4바이트며 이 크기는 또한 스택 하나의 크기이기도 하다.
- 결국 이 매크로는 각 타입의 변수가 스택을 통해 함수로 전달될 때 몇 바이트를 차지하는가를 계산한다.
- va_start 매크로는 가변 인수의 위치를 가리키는 포인터 ap를 초기화하는데, ap는 마지막 고정 인수 v의 번지에 v의 크기를 더한 번지로 초기화 된다.
- 스택에 인수가 들어갈 때는 전달된 역순으로 들어갈 때는 전달된 역순으로 들어가므로 가변 인수들이 먼저 전달(높은 번지)되고 고정 인수가 제일 끝에 전달(낮은 번지)된다.
(스택은 거꾸로 자란다 라는 말이 있다. 즉, 높은 주소에서 낮은 주소 방향으로 스택이 자라난다)
- va_arg 함수는 일단 ap를 일단 가변 인수의 길이만큼 더해 다음 가변 인수의 길이만큼 더해 다음 가변 인수 번지로 이동시킨다.
- 다시 길이를 빼서 원래 자리로 돌아온 후 이 번지를 t타입의 포인터로 캐스팅하여
*연산자로 그 값을 읽는다.
(쉽게 말해서 첫 가변인수를 읽고, 다음 가변인수의 시작 번지를 가리키는 두 줄의 코드를 한 줄로 줄인 것이다)
- 마지막으로 va_end 매크로는 가변 인수를 가리키던 ap 포인터를 NULL로 만들어 무효화시키는 사실 이 동작은 굳이 필요없다.
- 어차피 ap는 지역 변수로 선언되었고 함수가 종료되면 사라지므로 어떤 값을 가지더라도 아무 문제가 없으며 실제로 va_end 호출을 빼도 별 문제없이 잘 동작한다.
- va_end 매크로는 미래의 플랫폼에서 가변 인수를 읽는 방법이 달라질 경우 뒷정리를 할 수 있는 위치를 확보하는 역할 이외에는 아무 의미가 없다.
3.4. 가변 인수 함수의 활용
- printf와 같은 함수를 직접 만들려면 독자적으로 서식을 정의하고 서식 문자열과 대응되는 가변 인수를 직접 읽는 복잡한 루틴을 만들어야 하는데 다행히 이런 일을 대신 해수는 함수들이 준비되어 있다.
int vprintf(const char *format, va_list argptr);
int vsprintf(char *buffer, const char *format, va_list argptr);
- 이 외에 vscanf, vsscanf 등의 함수도 있는데 v(Variable)로 시작한다고 해서 이런 함수들은 v계열의 함수라고 한다.
- 위 두 함수들은 printf와 sprintf와 동일한 기능을 수행하는데 가변 인수를 직접 나열하는 대신 가변 인수가 시작되는 번지만을 인수로 취한다는 점이 다르다.
- 즉 실제로 가변 인수를 취하지 않으며 가변 인수를 취하는 다른 함수의 내부에서 printf의 서식을 해석하고 적용하는 일을 대신한다.
void CustomTrace(char *format, ...){
char buf[1024];
va_list marker;
va_start(maker, format);
vsprintfbut, format, maker);
OUtputDebufString(buf)
}
CustomTrace("변수 a=%d, 변수 f=%f\n", a, f);
CustomTrace("함수 func가 %d번째 호출되었음", count++);
- 위 예시는 로그를 OUtputDebufString 함수를 통해 화면에 출력하지만, 파일과 같은 곳에 기록할 수도 있다.
- 멀티 스레드 환경이나 실시간으로 동작하는 프로그램을 디버깅할 때는 디버거를 쓰기 쉽지 않기 때문에 모든 디버깅 정보를 파일에 일단 기록한 후 파일에 남겨진 로그(log) 정보를 분석하는 것이 더 효율적이다.
- 릴리즈 모드에서만 증상이 나타날 때라든가 디버거를 쓸 수 없는 서비스류의 프로그램을 디버깅할 때 최후의 디버깅 방법으로 사용할 수 있다.
4. 레퍼런스(자세한 설명 생략)
- C++에서만 쓸 수 있는 기능임.
- 변수의 별명(alias)을 정의한다.
- 별명을 붙이게 되면 한 대상의 대해 두 개의 이름이 생기게 되고 본래 이름은 물론이고 별명으로도 변수를 사용할 수 있다.
- 레퍼런스가 실용적인 위력을 발휘할 때는 함수의 인수로 전달될 때이다.
- 함수가 레퍼런스를 받아들이면 호출부의 실인수에 대한 별명을 전달받는 셈이므로 함수 내에서 실인수를 조작할 수 있게 된다.
void plusref2(int &a)
{
a=a+1;
}
void main()
{
int i;
i=5;
plusref2(i);
printf("결과=%d\n",i);
}
- 레퍼런스를 통한 참조 호출 방법은 호출부의 형식이 값 호출 방식과 동일해져서 오히려 더 혼란스러운 면도 있다.
- 그래서 꼭 필요치 않는 한 가급적 사용을 자재하고 불가피할 경우 레퍼런스를 받는 함수는 보통 함수명에 Ref나 ByRef 같은 접미를 붙여 호출부에서 함수의 형식을 쉽게 파악할 수 있도록 권유한다.
- 참조 호출이 꼭 필요한 경우는 레퍼런스보다 가급적이면 포인터를 넘기는 것이 더 직관적이다.
- 그러나 포인터는 잠재적을 배열이므로 일단 넘기면 주변을 마음대로 건드릴 수 있지만,
- 레퍼런스는 전달된 대상만 액세스할 수 있다는 면에서 안정성이 높다.
- 레퍼런스의 효용성에 대해서 다소 논란이 있는 편인데 필요할 때는 쓰는 것이 좋다고 판단됨.
void InputName(char *&Name){
Name=(char *)malloc(12);
strcpy(Name,"Cabin");
}
void main(){
char *Name;
InputName(Name);
printf("%s\n",Name);
free(Name);
}
- 이 예제는 2중 포인터를 쓰지 않아 함수 내부가 깔끔하며 직관적이라는 장점이 있다.
- 포인터에 대한 레퍼런스는 무척 실용적이며 종종 사용된다.
- 딱히 그럴 일은 없지만, 함수 레퍼런스라는 것도 선언할 수 있으며, 배열에 대한 레퍼런스도 만들 수 있다.
- 레퍼런스는 함수의 리턴값으로도 사용될 수 있다.
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]);
}
- 레퍼런스는 변수 그 자체이며 온전한 좌변값이기 때문에 함수가 리턴하는 레퍼런스가 대입 연산자의 좌변에 놓일 수 있다.
- 꼭 필요하지 않다면 레퍼런스를 리턴하는 함수는 가급적 자재하는 것이 좋다.
- 리턴값으로 레퍼런스를 꼭 사용해야 하는 경우는 바로 연산자를 오버로딩할 때다.
- 또한 함수의 지역변수의 레퍼런스를 리턴하는 것은 위험하다.
출처 : 혼자 연구하는 C/C++ 1 / 김상형 저 / 와우북스