C/C++에서 복잡한 선언문 이해하기

뱅뱅 돌려 읽기 기법 - 2018년 11월 4일

카테고리: C++

태그: C++

안녕하세요, static입니다. 굉장히 오랜만에 글을 작성하네요. 이 글은 이 사이트를 번역한 글입니다. 의역이 많이 포함되어 있습니다.


뱅뱅 돌려 읽기 기법1으로 알려진 C/C++에서 복잡한 선언문을 이해하는 방법이 있습니다. 다음의 간단한 3가지 규칙을 따르면 됩니다.

  1. 의미를 알고자 하는 식별자로부터 시작하고, 다음에 열거된 토큰을 만나면 다음 한국어 문장으로 만듭니다.
    • [N] 또는 []: N개의 ~를 가진 또는 크기가 정해지지 않은 배열
    • (typeA, typeB, ...): typeAtypeB를 받아 ~를 반환하는 함수
    • *: ~의 포인터
  2. 모든 토큰을 만날 때까지 계속 진행합니다.
  3. 항상 괄호 안에 있는 토큰부터 풉니다.

예제 1: 간단한 선언문

    +-------+
    | +--+  |
    | ^  |  |
char* str[10];
 ^  ^    |  |
 |  +----+  |
 +----------+

str은 무엇일까요?

  1. str에서 시작합니다. 가장 먼저 만나는 토큰은 [ 입니다. 이는 배열임을 의미합니다.
    str은 10개의 ~를 가진 배열
  2. 계속 시계 방향으로 진행합니다. 그러면 우리가 만나는 토큰은 * 입니다. 이는 포인터를 가짐을 의미합니다.
    str은 10개의 ~의 포인터를 가진 배열
  3. 계속 시계 방향으로 진행하면 ;를 만나므로 무시하고, 계속 진행하면 char를 만납니다.
    str은 10개의 char의 포인터를 가진 배열”
  4. 모든 토큰을 만났으므로 종료합니다.

예제 2: 함수 포인터

    +------------------+
    | +---+            |
    | |+-+|            |
    | |^ ||            |
char*(*fp)(int, float*);
 ^  ^ ^  ||            |
 |  | +--+|            |
 |  +-----+            |
 +---------------------+

fp는 무엇일까요?

  1. 시계 방향으로 진행하면 가장 먼저 만나는 토큰은 ) 입니다. fp는 괄호 안에 있으므로 계속 진행합니다. 그럼 우리가 만나는 토큰은 * 입니다. 이는 fp가 포인터임을 의미합니다.
    fp는 ~의 포인터
  2. 괄호를 벗어나 시계 방향으로 계속 진행하면 (를 만납니다. 이는 함수를 가짐을 의미합니다.
    fpintfloat*를 받아 ~를 반환하는 함수의 포인터
  3. 시계 방향으로 계속 진행하면 *를 만납니다.
    fpintfloat*를 받아 ~의 포인터를 반환하는 함수의 포인터
  4. 시계 방향으로 계속 진행하면 ;를 만나지만, 모든 토큰을 만나지 않았으므로 계속 진행합니다. 그러면 char를 만납니다.
    fpintfloat*를 받아 char의 포인터를 반환하는 함수의 포인터”

예제 3: 최종 보스

     +----------------------------+
     |                 +---+      |
     |+-----+          |+-+|      |
     |^     |          |^ ||      |
void(*signal(int, void(*fp)(int)))(int);
 ^   ^      |      ^   ^  ||      |
 |   +------+      |   +--+|      |
 |                 +-------+      |
 +--------------------------------+

signal은 무엇일까요? signal은 괄호 안에 있으므로 주의해야 합니다.

  1. 시계 방향으로 진행합니다. 우리는 (를 만납니다.
    signalint와 ~를 받아 ~를 반환하는 함수
  2. 우리는 fp에 같은 방법을 적용할 수 있습니다. fp도 괄호 안에 있으므로, 계속 진행하면 *를 만납니다.
    fp는 ~의 포인터
  3. 시계 방향으로 계속 진행하면 (를 만납니다.
    fpint를 받아 ~를 반환하는 함수의 포인터
  4. 이제 괄호 밖에서 계속 진행합니다. 그럼 우리는 void를 만납니다.
    fpint를 받아 아무 것도 반환하지 않는(void) 함수의 포인터”
  5. fp의 의미를 이해했으므로 다시 signal을 해석합니다.
    signalintint를 받아 아무 것도 반환하지 않는 함수의 포인터를 받아 ~를 반환하는 함수
  6. 우리는 여전히 괄호 안에 있으므로 *를 만납니다.
    signalintint를 받아 아무 것도 반환하지 않는 함수의 포인터를 받아 ~의 포인터를 반환하는 함수
  7. 괄호를 해결했으므로 시계 방향으로 계속 진행하면 또 새로운 (를 만납니다.
    signalintint를 받아 아무 것도 반환하지 않는 함수의 포인터를 받아 int를 받아 ~를 반환하는 함수의 포인터를 반환하는 함수
  8. 마지막으로 계속 진행하면 우리는 void를 만나게 됩니다. 따라서 signal의 의미는
    signalintint를 받아 아무 것도 반환하지 않는 함수의 포인터를 받아 int를 받아 아무 것도 반환하지 않는 함수의 포인터를 반환하는 함수”

이 방법을 constvolatile에도 적용할 수 있습니다. 예를 들어:

const char* chptr;
  • chptr는 무엇일까요?
    chptr는 변하지 않는(const) char의 포인터”
char* const chptr;
  • chptr는 무엇일까요?
    chptrchar의 변하지 않는 포인터”
volatile char* const chptr;
  • chptr는 무엇일까요?
    chptr는 최적화 되지 않는(volatile) char의 변하지 않는 포인터”

굉장히 쉽게 C/C++의 복잡한 선언문을 이해할 수 있는 규칙입니다. C/C++ 프로그래머라면 이 방법 정도는 알아두는 것이 좋겠네요. 사실 이런 사이트도 있습니다. 영어 사이트이긴한데, 선언문을 입력하면 영어 문장으로 만들어 줍니다. 사실 저는 개인적으로 한국어로 이해하는 것보다는 영어로 이해하는 것이 더 이해하기 쉬운 것 같아요. 영어는 무엇이 무엇을 수식하는지 알기 쉬운데, 한국어는 영어에 비해 상대적으로 알기 어려운 것 같은 느낌이 듭니다.

예를 들어, 예제 3의 경우 답(?)이 “signalintint를 받아 아무 것도 반환하지 않는 함수의 포인터를 받아 int를 받아 아무 것도 반환하지 않는 함수의 포인터를 반환하는 함수”였는데, “intint를 받아 아무 것도 반환하지 않는 함수의 포인터”가 매개 변수가 int, int인지 int, void(*)(int)인지 애매한데, 영어로 하면 “passing an int and a pointer to a function passing an int returning nothing (void)”로, 한국어에 비해 구분이 잘 됩니다. “int and a pointer to …” 라서 int와 어떤 것에 대한 포인터라는 것을 직관적으로 알 수 있습니다.

뭐 허튼, 이번 글은 여기까지로 하겠습니다. 오역/오타 등의 피드백 모두 받습니다. 감사합니다.

  1. (역자 주) 직역하면 시계 방향/나선형 규칙입니다.