C/C++ 어려운 선언문 해석하기..
CodeProject에서 최근에 아주 흥미로운 글을 읽었습니다. 글의 내용이 별로 길지도 않고 워낙 유용한 정보라 생각되서 날림으로 번역해봤습니다. 영어와 한글의 어순이 반대라서 매끄럽지 못한 부분이 많은데 이런 경우 원문도 같이 볼 수 있도록 같이 올렸습니다.
원문 : How to interpret complex C/C++ declarations (http://www.codeproject.com/cpp/complex_declarations.asp)
간혹 int * (* (*fp1) (int) ) [10]; 과 같은 선언문이나 혹은 이와 유사하게 난해한 선언문을 볼 기회가 있습니까? 이 글은 이런 C/C++
선언문을 직면했을 때 이를 어떻게 해석하는가를 알려주기 위한 글입니다. 매우 기본적이고 평범한 예제에서 시작해서 복작한 경우까지
다루겠습니다. 우리가 매일 흔히 볼 수 있는 선언문을 비롯해서 간간히 문제의 소지를 일으킬 수 있는 const 수정자와 typedef 및 함수
포인터를 다룬 후 마지막으로 "오른쪽에서 왼쪽으로" 규칙을 알아 봅니다.
이 글은 단순히 우리가 이러한 선언문을 맞닥뜨리게 되었을 때 어떻게 이 선언문을 이해할 수 있는가 방법을 알려주는 것입니다. 여기
예제에서 제시된 것과 같이 복잡한 선언문을 사용하여 이해하기 어려운 코드를 작성하는것은 결코 좋은 프로그래밍 습관이 아니겠죠.
[[기초]]
int a;
위의 문장은 '변수 n 을 int 형으로 선언한다'라고 해석할 수 있습니다.
포인터로 넘어가서 다음과 같은 선언문을 보게되면,
int *p;
'변수 p를 int *형으로 선언한다' 라고 해석할 수 있고 다시 말하면 '변수 p를 int형을 가리키는 포인터로 선언한다'라고 할 수 있습니
다. 여기서 잠깐 샛길로 빠져서 포인터 연산자(*) 또는 참조 연산자(&)는 변수 형 (int)에 붙이는 것 보다 변수(p)에 붙이는것이 항상
좋습니다. 왜냐하면 다음과 같은 선언문을 사용하면서 실수할만한 소지를 없애주기 때문입니다. (역자주: 참고로 C++ 창시자인 Bjarne
Stroustrup는 변수 형에 붙이는걸 더 선호한다고 했답니다.) (추가: 단순히 B. S.는 이걸 선호했다고 하더라는 무책임한 언급만 남기고
자세한 설명을 첨가하지 못해서 죄송합니다. 다음의 링크를 참조하시면 더 자세한 이유를 보실 수 있으실 겁니다. http://www.research.att.com/~bs/bs_faq2.html#whitespace )
int* p,q;
위의 선언문을 처음 보게 되면 변수 p와 변수 q가 마치 int를 가리키는 포인터형 (int *) 변수로 선언된 것 처럼 보입니다. 하지만 사실
변수 p만 int를 가리키는 포인터형으로 선언되었고 변수q는 int형으로 선언된 것입니다. (역자주: int *p; int *q; 처럼 각각 따로 선언
하는게 의미가 분명히 전달되고 속편하겠습니다.)
우리는 또한 포인터를 가리키는 포인터를 선언할 수 있습니다.
char **argv;
원리만 따지자면 이 관계는 무한히 반복될 수 있습니다. 따라서 우리는 float형을 가리키는 포인터를 가리키는 포인터를 가리키는 포인
터를 선언할 수도 있습니다. 몇 단계 더 반복해도 됩니다.
다음과 같은 선언문들을 봅시다. (역자주: 여기서 부터는 선언문 해석 원문을 괄호안에 넣습니다. 선언문을 한글로 표현하기가 이렇게
어렵군요.)
int RollNum[30][4];
int (*p)[4]=RollNum;
int *q[5];
변수 p는 int형을 요소로 하는 크기가 4인 배열을 가리키는 포인터(a pointer to an array of 4 ints)이며, 변수 q는 int형 포인터를 요
소로 하는 크기가 5인 배열(an array of 5 pointer to integers) 입니다.
[[ const modifier ]]
const 수정자는 변수가 변경되는 것을 금지 (변수 <-> 변경할 수 없다? 모순이군요 :P) 하기 위해서 사용하는 키워드입니다. const 변수
를 선언하는 경우 선언문에서 바로 초기화를 해줍니다. 변경이 불가능하기 때문에 선언문 외에서는 값을 초기화할 수 없겠죠.
const int n = 5;
int const m = 10;
위의 예제의 두 변수 n과 m은 똑같이 const 정수형으로 선언되었습니다. C++ 표준에서 두가지 선언이 모두 가능하다고 나와있습니만 개
인적으로는 const가 강조되어서 의미가 더 분명한 첫번째 선언문을 선호합니다.
const가 포인터와 결합되면 조금 복잡해집니다. 예를 들어서 다음과 같은 변수 p, q가 선언되었습니다.
const int *p;
int const *q;
그럼 여기서 퀴즈, const int형을 가리키는 포인터와 int형을 가리키는 const 포인터를 구별해보세요.
사실 두 변수 모두 const int를 가리키는 포인터입니다. int 형을 가리키는 const 포인터는 다음과 같이 선언됩니다.
int * const r = &n; // n 은 int형 변수로 선언 되었슴
위에서 p와 q는 const int형 변수이기 때문에 *p 나 *q의 값을 변경할 수 없습니다. r은 const 포인터이기 때문에 일단 위와 같이 선언
된 후에 다시 r = &m 과 같이 다른 주소값을 할당하는 것이 불가능합니다. (물론 m은 또 다른 int형 변수이겠죠) 물론 이 경우 *r의
값은 변경이 가능합니다.
위에서 나온 두 가지 선언문을 결합하여 const int 형을 가리키는 const 포인터를 선언하려고 하면 다음과 같습니다.
const int * const p = &n; // n은 const int형 변수로 선언되었슴
다음에 나열한 선언문들을 보시면 const를 어떻게 해석할 수 있는지 더 분명하게 아실 수 있을겁니다. 이 중에 몇몇 선언문은 선언하면
서 동시에 초기화해줘야만 컴파일 할 수 있는데 여기서는 쉽게 이해할 수 있도록 생략하였습니다. (컴파일 안된다고 딴지 사절)
char ** p1; // pointer to pointer to char
const char **p2; // pointer to pointer to const char
char * const * p3; // pointer to const pointer to char
const char * const * p4; // pointer to const pointer to const char
char ** const p5; // const pointer to pointer to char
const char ** const p6; // const pointer to pointer to const char
char * const * const p7; // const pointer to const pointer to char
const char * const * const p8; // const pointer to const pointer to const char
[[ typedef의 세밀한 구분 ]]
typedef는 typedef는 "* 또는 &가 형이 아닌 변수에 적용된다"라는 규칙을 극복하게 해줍니다. 다음과 같이 typedef 를 사용하게 되면
typedef char * PCHAR;
PCHAR p,q;
변수 p, q 모두 char를 가리키는 포인터 변수가 됩니다. typedef가 사용되지 않았다면 q는 char를 가리키는 포인터 변수가 아니라 char 형 변수였을 텐데 솔직히 이런 사실을 모르는 경우 실수하기 쉽상이죠.
아래에 typedef를 사용한 선언문들을 설명과 함께 나열해봅니다.
typedef char * a; // a is a pointer to a char
//
// a는
// char형을 가리키는 포인터
typedef a b(); // b is a function that returns
// a pointer to a char
//
// b는
// char형을 가리키는 포인터를 리턴하는
// 함수
typedef b *c; // c is a pointer to a function
// that returns a pointer to a char
//
// c는
// char형을 가리키는 포인터을 리턴하는
// 함수를 가리키는
// 포인터
typedef c d(); // d is a function returning
// a pointer to a function
// that returns a pointer to a char
//
// d는
// char형을 가리키는 포인터를 리턴하는
// 함수를 가리키는
// 포인터를 리턴하는
// 함수
typedef d *e; // e is a pointer to a function
// returning a pointer to a
// function that returns a
// pointer to a char
//
// e는
// char형을 가리키는 포인터를 리턴하는
// 함수를 가리키는
// 포인터를 리턴하는
// 함수를 가리키는
// 포인터
e var[10]; // var is an array of 10 pointers to
// functions returning pointers to
// functions returning pointers to chars.
//
// var는
// char형을 가리키는 포인터를 리턴하는
// 함수를 가리키는
// 포인터를 리턴하는
// 함수를 가리키는
// 포인터를 요소로하는 크기 10의 배열
typedef는 보통 아래에 나열한것과 같이 struct는 선언과 같이 사용됩니다. 다음과 같은 struct 선언문을 사용하면 C++ 뿐만 아니라 C에
서도 struct를 생략할 수 있습니다.
typedef struct tagPOINT
{
int x;
int y;
}POINT;
POINT p; /* C 에서도 유효 */
[[ 함수 포인터 ]]
아마도 선언문을 해석하는데 있어서 가장 혼돈을 초래하는 것은 함수 포인터일 것입니다. 함수 포인터는 DOS 시절에 백그라운드 상주 프
로그램(TSR)을 작성하는데 사용되기도 했고 Win32나 X-Windows서는 callback 함수를 작성하는데 주로 사용됩니다. 이 외에도 많은 곳에
서 함수 포인터가 사용됩니다. 예를 들자면 가상 함수 테이블, STL의 일부 템플릿 그리고 Win NT/2K/XP의 시스템 서비스에서 사용되는
것 을 볼 수 있습니다. 그럼 간단한 예제부터 시작해보겠습니다.
int (*p)(char);
위의 선언문은 변수 p를 char를 입력 인자로 하고 int를 리턴하는 함수를 가리키는 포인터(a pointer to a function that takes a char
argument and return an int)로 선언합니다.
두개의 float를 입력인자로 하고 char를 가리키는 포인터를 가리키는 포인터를 리턴하는 함수를 가리키는 포인터(a pointer to a
function that take two floats and returns a pointer to a pointer to a char)는 다음과 같이 선언합니다.
char ** (*p)(float, float);
그렇다면 char를 가리키는 const 포인터 두개를 입력 인자로 받고 void 포인터를 리턴하는 함수를 가리키는 포인터를 요소로하는 크기 5의 배열
(an array of 5 pointers to functions that receive two const pointers to chars and return void pointer)은 어떻게 선언하면 될까요
?
void * (*a[5])(char * const, char * const);
[[ 오른쪽에서 왼쪽으로 규칙 (중요함) ]]
이 규칙은 매우 간단하지만 어떠한 복잡한 선언문이라도 해석할 수 있게해줍니다.
선언문은 가장 안쪽의 괄호부터 읽기 시작한다. 괄호안에서 가장 오른쪽부터 시작해서 왼쪽으로 읽기 시작합니다. 이 때 괄호가 발견되
면 순서가 바뀌어야 합니다. (역자 주: "순서가 바뀐다"가 정확히 의미하는 바를 파악하기가 어렵습니다. 제 짐작에는 함수 입력인자를 포함하는 괄호안에서는 왼쪽에서 오른쪽으로 입력인자들을 읽는다라는 의미가 아닐까 싶습니다). 괄호안의 모든 내용이 해석되었으면 괄호 밖으로 해석을 확
대합니다. 모든 선언문이 해석될 때 까지 이 과정을 반복합니다.
"Start reading the declaration from the innermost parentheses, go right, and then go left. When you encounter parentheses, the
direction should be reversed. Once everything in the parentheses has been parsed, jump out of it. Continue till the whole
declaration has been parsed."
오른쪽에서 왼쪽으로 읽기 규칙에서 작은 변경 사항: 선언문을 처음으로 읽기 시작할 때에는 가장 안쪽의 괄호부터 읽기 시작하는게 아
니라 변수 이름 부터 읽기 시작합니다.
다음 선언문을 첫 번째 예제로 설명합니다.
int * (* (*fp1) (int) ) [10];
1. 변수 이름부터 시작합니다. --- fp1은
2. 오른쪽에 ) 말고는 아무것도 없으니까 왼쪽을 보면 *가 있네요 --- 포인터입니다. 이 포인터는 3.을 가리킵니다.
3. 괄호를 벗어나서 오른쪽을 보면 (int)가 있습니다. --- 함수인데 int를 입력 인자로 받는군요.
4. 왼쪽으로 가면 *가 있습니다. - 이 함수는 포인터를 리턴하는데 이 포인터는 4.를 가리킵니다.
5. 괄호를 벗어나서 오른쪽을 보면 [10] 이 있습니다. --- 크기가 10인 배열입니다.
6. 왼쪽을 보면 *가 있습니다. --- 배열의 요소는 포인터인데 이 포인터는 7.를 가리킵니다.
7. 더 왼쪽을 보면 int가 있습니다. --- 포인터가 int형을 가리킵니다.
어순이 달라서 영어로 표현하는 것보다 한글로 표현하기 무지 힘듭니다. 위의 내용을 종합하면 다음과 같습니다.
"fp1은 int형을 가리키는 포인터를 요소로 하는 크기가 10인 배열을 가리키는 포인터를 리턴하면서 int를 입력 인자로 받는 함수를 가리
키는 포인터"입니다.
1. Start from the variable name -------------------------- fp1
2. Nothing to right but ) so go left to find * -------------- is a pointer
3. Jump out of parentheses and encounter (int) --------- to a function that takes an int as argument
4. Go left, find * ---------------------------------------- and returns a pointer
5. Jump put of parentheses, go right and hit [10] -------- to an array of 10
6. Go left find * ----------------------------------------- pointers to
7. Go left again, find int -------------------------------- ints.
다음은 두 번째 예제입니다.
int *( *( *arr[5])())();
1. 변수 이름부터 시작합니다. --- arr은
2. 오른쪽을 보면 [5]가 있습니다. --- 크기가 5인 배열입니다.
3. 왼쪽을 보면 *가 있습니다. --- 배열의 요소는 포인터인데 4.를 가리킵니다.
4. 괄호를 벗어나서 오른쪽을 보면 ()가 있습니다. --- 함수인데 입력인자를 아무것도 받지 않습니다.
5. 왼쪽을 보면 *가 있습니다. --- 함수는 포인터를 리턴하는데 포인터는 6.을 가리킵니다.
6. 괄호를 벗어나서 오른쪽을 보면 ()가 있습니다. --- 함수인데 입력인자를 아무것도 받지 않습니다.
7. 왼쪽을 보면 *가 있습니다. --- 함수는 포인터를 리턴하는데 포인터는 8.을 가리킵니다.
8. 더 왼쪽을 보면 int가 있습니다. --- 포인터가 int형을 가리킵니다.
"arr은 int형을 가리키는 포인터를 리턴하면서 입력인자로 아무것도 받지 않는 함수를 가리키는 포인터를 리턴하면서 입력인자로 아무것
도 받지 않는 함수를 가리키는 포인터를 요소로 하는 크기가 5인 배열"입니다.
1. Start from the variable name --------------------- arr
2. Go right, find array subscript --------------------- is an array of 5
3. Go left, find * ----------------------------------- pointers
4. Jump out of parentheses, go right to find () ------ to functions
5. Go left, encounter * ----------------------------- that return pointers
6. Jump out, go right, find () ----------------------- to functions
7. Go left, find * ----------------------------------- that return pointers
8. Continue left, find int ----------------------------- to ints.
[[ 추가 예제 ]]
(역자 주: 여기서 직역은 제가 오른쪽-왼쪽 규칙을 따라가보면서 직접 쓴것이고 의역은 직역에 근거해서 나름대로 해석을 한글 어순에
맞게 다시 쓴것입니다.)
float ( * ( *b()) [] )(); // b is a function that returns a
// pointer to an array of pointers
// to functions returning floats.
//
// (직역) b는 함수인데 포인터를 리턴합니다.
// 이 포인터는 배열을 가리키는데 배열의 요소는 포인터입니다.
// 이 포인터는 함수를 가리키는데 이 함수는 float를 리턴합니다.
//
// (의역) b는 float를 리턴하는 함수를
// 가리키는 포인터를 요소로하는 배열을 가리키는 포인터를
// 리턴하는 함수입니다.
void * ( *c) ( char, int (*)()); // c is a pointer to a function that takes
// two parameters:
// a char and a pointer to a
// function that takes no
// parameters and returns
// an int
// and returns a pointer to void.
//
// (직역) c는 포인터인데 이 포인터는 함수를 가리킵니다.
// 이 함수는 char형과
// int형을 리턴하는 함수를 가리키는 포인터를 입력인자로 합니다.
// 이 함수는 포인터를 리턴는데 이 포인터는 void를 가리킵니다.
//
// (의역) c는 void를 가리키는 포인터를 리턴하면서
// char형과 int를 리턴하는 함수를 가리키는 포인터를 입력인자로 가지는
// 함수를 가리키는 포인터입니다.
void ** (*d) (int &,
char **(*)(char *, char **)); // d is a pointer to a function that takes
// two parameters:
// a reference to an int and a pointer
// to a function that takes two parameters:
// a pointer to a char and a pointer
// to a pointer to a char
// and returns a pointer to a pointer
// to a char
// and returns a pointer to a pointer to void
//
// (직역) d는 포인터인데 이 포인터는 함수를 가리킵니다.
// 이 함수는 int형의 참조와 함수를 가리키는 포인터를 인자로 가집니다.
// 이 포인터가 가리키는 함수는
// char를 가리키는 포인터와
// char를 가리키는 포인터를 가리키는 포인터를 인자로 가지면서
// char를 가리키는 포인터를 가리키는 포인터를 리턴합니다.
// 이 함수는 void를 가리키는 포인터를 가리키는 포인터를 리턴합니다.
//
// (의역) d는 void를 가리키는 포인터를 가리키는 포인터를 리턴하면서
// int형의 참조와
// char를 가리키는 포인터와
// char를 가리키는 포인터를 가리키는 포인터를 인자로 가지면서
// char를 가리키는 포인터를 가리키는 포인터를 리턴하는
// 함수를 인자로 하는 함수를
// 가리키는 포인터입니다.
// 악~~~
float ( * ( * e[10])
(int &) ) [5]; // e is an array of 10 pointers to
// functions that take a single
// reference to an int as an argument
// and return pointers to
// an array of 5 floats.
//
// (직역) e는 크기가 10인 배열인데 그 요소는 포인터입니다.
// 이 포인터는 함수를 가리키는데 함수는 int형 참조를 입력으로 받으면서
// 포인터를 리턴합니다.
// 이 포인터는 크기가 5인 배열을 가리키는데 배열의 요소는 float입니다.
//
// (의역) e는 float을 요소로 하는 크기가 5인 배열을 가리키는
// 포인터를 리턴하면서
// int형 참조를 입력인자로 하는 함수를 가리키는 포인터를 요소로하는
// 크기가 10인 배열입니다.
[[ 클래스 멤버 함수 포인터 ]]
원문에서는 설명이 없었지만 클래스 멤버 함수 포인터의 경우에도 똑같은 규칙이 적용될 수 있다는걸 알 수 있습니다. 원문에 달린 답글
중에서 예제를 복사해왔습니다.
int (CFoo::*p)(); // p is a pointer to a method in class CFoo
// that takes no argument and return an int
//
// (직역) p는 포인터인데 이 포인터는 CFoo 클래스의 멤버함수를 가리킵니다.
// 이 멤버함수는 아무런 입력인자도 받지 않고 int를 리턴해줍니다.
//
// (의역) p는 아무런 입력인자도 받지 않고 int를 리턴해주는
// CFoo 클래스의 멤버 함수를 가리키는 포인터입니다.
[[ 호출 규약 (calling convetion) ]]
같은 맥락으로 호출규약이 들어간 경우에도 똑같은 규칙을 적용합니다.
int (__stdcall* q)(); // q is a pointer to a __stdcall function
// that takes no argument and return int
//
// (직역) q는 포인터인데 이 포인터는 __stdcall 함수를 가리킵니다.
// 이 함수는 아무런 입력인자도 받지않고 int를 리턴해줍니다.
//
// (의역) q는 아무런 입력인자도 받지않고 int를 리턴해주는
// __stdcall 함수를 가리키는 포인터입니다.
데브피아에서 가져왔습니다.
퇴근합시다^^
좋은 글 감사합니다..
좋은 글 감사합니다..
무엇을 위해 사는가..
데브피아에서 보고...출력해서....읽어봤는데...역시..--;;
데브피아에서 보고...출력해서....읽어봤는데...
역시..--;; 읽어봐도...
이걸로 한단계 더 발전할수 있을꺼 같습니다 (--)(__) 꾸벅!
이걸로 한단계 더 발전할수 있을꺼 같습니다 (--)(__) 꾸벅!
종종 자신을 돌아보아요!~
하루 1% 릴리즈~~
TCPL에 비슷한 내용이 있지 않았던가요? 선언문 넣으면 말로 바꿔서 출
TCPL에 비슷한 내용이 있지 않았던가요? 선언문 넣으면 말로 바꿔서 출력해주는... 아무튼 좋은글 감사합니다. ^^
--
Life is short. damn short...
Good..! 8)
Good..! 8)
> 여기서 잠깐 샛길로 빠져서 포인터 연산자(*) 또는 참조 연산자
> 여기서 잠깐 샛길로 빠져서 포인터 연산자(*) 또는 참조 연산자(&)는 변수 형 (int)에 붙이는 것 보다 변수(p)에 붙이는것이 항상 좋습니다.
리눅스 커널 소스같은 경우는 타협안(?)을 사용하더군요.
int * p;
[quote="jj"]TCPL에 비슷한 내용이 있지 않았던가요? 선언문
예, dcl이라는 예제 프로그램이 있었죠. 그런데 말로 풀어쓴 게 더 복잡하더군요. ^^;
댓글 달기