3.3) dcl: C의 선언 분석 프로그램
이제 우리는 C의 선언이 어떠한지 이해했으므로, C의 선언을 분석하는 dcl 프로그램을 작성할 수 있다. 이 예제는The C Programming Language에 나온 것을 기반으로 작성하는 것이다.
이를 설명하기 전에 몇 가지 중요한 용어를 알려주고 진행하겠다.
- 예약어(keyword): 프로그래밍 언어에서 특정한 용도로 사용되기 때문에 사용자가 임의로 사용할 수 없는 단어를 말한다. int, char와 같은 자료형과 for, if와 같은 반복문, 조건문을 위한 예약어 등이 이에 속한다.
- 식별자(identifier): 개체를 식별하는 데 사용할 수 있는 이름을 의미한다. 변수 이름, 함수 이름, 사용자가 새롭게 정의한 자료형의 이름 등이 있다. C는 식별자라면 지켜야 할 규칙이 있는데, 바로 알파벳, 밑줄(_), 숫자만 가능하며, 첫 글자는 밑줄 또는 알파벳이어야 한다는 것이다.
- 태그(tag): 구조체, 공용체 및 열거 형식과 같은 사용자 정의 자료형을 지칭하는 이름이다. 식별자와 작성하는 규칙은 같지만 식별자와는 다르다. 예를 들어 다음의 문장이 적법한 이유는 태그는 식별자가 아니기 때문에 식별자를 중복으로 정의하는 것이 아니기 때문이다.
struct node node;
다만 이 경우 typedef 키워드를 이용해 node를 정의했다면 이 경우는 식별자로 인정된다.
typedef struct node node;
다음은 dcl 프로젝트를 위한 개념으로, 이 프로그램에서만 그렇다고 납득해야 하는 부분이다.
- 선언문(declaration-statement): 선언을 하는 문장이다. 형식은 다음과 같다.
declaration-statement: <형식(type)> <선언자(declarator)> ;
- 형식(type): 선언할 대상이 자료를 보관하는 방법을 말한다. int, char 등이 여기에 속한다.
선언문은 간단하게 형식과 선언자로 나눌 수 있다.
int var; // 형식: int / 선언자: var
int *ptr; // 형식: int / 선언자: *ptr
int arr[5]; // 형식: int / 선언자: arr[5]
int fnc(); // 형식: int / 선언자: fnc()
const int MAX; // 형식: const int / 선언자: MAX
- 직접 선언자(direct-declarator): 선언을 할 때 사용되는 이름 등 직접적으로 선언을 하는 데 사용되는 단어를 말한다.
- 선언자(declarator): 직접 선언자의 앞에 *가 붙어, 해당 직접 선언자가 포인터임을 나타낸다.
다음은 선언자와 직접 선언자 간의 관계를 나타낸 것이다.
declarator: * direct-declarator … (1)
direct-declarator: <이름> … (2)
(declarator) … (3)
direct-declarator() … (4)
direct-declarator[<크기>] … (5) (이때 크기는 생략 가능)
사실 이 내용만 가지고는 선언자와 직접 선언자를 이해하기 아주 어렵다. 예를 들어보자. 이 예제에서 선언자를dcl, 직접 선언자를 dirdcl이라고 간단하게 표기하겠다.
(*pfa[])()
pfa는 이름이므로 dirdcl이다. pfa가 dirdcl이므로 pfa[] 또한 dirdcl이다. (5)에 의해 dirdcl[] 또한 정의에 의해dirdcl이기 때문이다. *pfa[]는 pfa[]가 dirdcl이므로 (1)에 의해 dcl이다. (*pfa[])는 (3)에 의해, *pfa[]가 dcl이므로 dirdcl이 되고, (*pfa[])()는 dirdcl()의 꼴이므로 (4)에 의해 dirdcl이다.
여기서 완전하게 이해하지 못했다고 하더라도 일단은 진행할 수 있으니, 이제 dcl 프로그램을 만들어보자. 입력에 대해 다음과 같이 출력이 나오는 것이 목표다. 테스트의 편의를 위해 무한히 반복하다가, 입력으로 세미콜론이 처음 문자로 들어오면 종료하도록 하자.
입력 |
출력 |
int var; int arr[]; int *ptr; int arr2d[][]; int *ptrarr[]; int (*arrptr)[]; int fnc(); int arr_fnc()[]; int *ptrarr_fnc()[]; int (*arr_fncptr)()[]; int (*arrptr_fnc())[]; char (*(*x[])())[]; ; |
var: int arr: array of int ptr: pointer to int arr2d: array of array of int ptrarr: array of pointer to int arrptr: pointer to array of int fnc: function returning int arr_fnc: function returning array of int ptrarr_fnc: function returning array of pointer to int arr_fncptr: pointer to function return- ing array of int arrptr_fnc: function returning pointer to array of int x: array of pointer to function return- ing pointer to array of char |
다음은 필자의 dcl 구현이다. 먼저 main을 보자.
03_dcl_main.cpp |
// 식별자로 가능한 문자인지 확인합니다. bool is_namch(char ch) { // 식별자 문자라면 참입니다. return is_alnum(ch) || (ch == '_'); } bool is_fnamch(char ch) { // 첫 식별자 문자라면 참입니다. return is_alpha(ch) || (ch == '_'); } // 형식을 획득하여 문자열로 반환합니다. std::string get_type(StringBuffer &buffer_input); // 선언자를 분석하고 결과를 출력합니다. void dcl(StringBuffer &buffer_input); // 직접 선언자를 분석하고 결과를 출력합니다. void dirdcl(StringBuffer &buffer_input); int main(void) { try { const int MAX_INPUT_SIZ = 256; char input[MAX_INPUT_SIZ]; while (true) { // 입력을 받고 버퍼를 초기화한다 std::cin.getline(input, MAX_INPUT_SIZ); if (input[0] == ';') { break; } StringBuffer buffer(input); // 형식을 획득한다 std::string type = get_type(buffer); while (is_space(buffer.peekc())) { // 형식과 선언자 사이의 공백을 buffer.getc(); // 무시하고 포인터를 선언자 앞으로 옮긴다 } // 선언자를 분석한다 dcl(buffer); if (buffer.peekc() != ';') // 문장 종료 기호가 없으면 예외 throw Exception("문장 종료 기호가 없습니다."); std::cout << type.c_str() << std::endl; } return 0; } catch (Exception &ex) { std::cerr << ex.c_str() << std::endl; return 1; } } |
ptr에 대해 프로그램은 다음과 같이 진행된다.
코드 |
버퍼 상태 |
출력 |
StringBuffer(input) |
int *ptr; |
|
get_type(buffer) |
*ptr; |
|
while (is_space(...)) ... |
*ptr; |
|
dcl(buffer) |
; |
ptr: pointer to |
if (peekc() != ';') ... |
; |
ptr: pointer to |
cout<<type |
; |
ptr: pointer to int |
이 정도로 main 함수는 간단하게 이해할 수 있다. get_type과 공백을 제거하는 부분은 독자 스스로도 구현할 수 있을 정도로 어렵지 않다. 그러면 이제 정말 중요한 dcl 함수를 살펴보자.
03_dcl_main.cpp |
void dcl(StringBuffer &bin) { // 선언자를 분석하고 결과 출력 // declarator: * direct-declarator (1) *을 분석한다 int pointer_count = 0; char ch; while (bin.is_empty() == false) { // 버퍼에 문자가 남아있는 동안 ch = bin.getc(); // 문자를 획득하고 확인한다 if (ch == '*') { // *라면 그만큼 포인터를 출력하기 위해 ++pointer_count; // 카운터를 증가시킨다 } else { // *가 아니라면 포인터를 되돌리고 탈출한다 bin.ungetc(); break; } } // declarator: * direct-declarator (2) direct-declarator를 분석한다 dirdcl(bin); // *을 모두 획득했으므로 직접 선언자를 분석한다 while (pointer_count > 0) { // 선언자의 분석이 오른쪽에서 먼저 진행되므로 std::cout << "pointer to "; // 왼쪽에서 획득한 기호를 오른쪽의 분석이 --pointer_count; // 종료된 후에 출력해야 한다 } } |
선언자의 정의 그대로 코드로 옮긴 것이다 주석도 있으니 노력하면 이해할 수 있다.
dcl의 내부를 알았으니 예를 들어보자. ptr에 대해 이 함수는 다음과 같이 진행된다.
코드 |
버퍼 상태 |
출력 |
dcl(StringBuffer &) |
*ptr; |
|
while (c == '*') ... |
ptr; |
|
dirdcl(bin) |
; |
ptr: |
while (pc > 0) ... |
; |
ptr: pointer to |
arr에 대해서는 다음과 같이 진행된다.
코드 |
버퍼 상태 |
출력 |
dcl(StringBuffer &) |
arr[]; |
|
while (c == '*') ... |
arr[]; |
|
dirdcl(bin) |
; |
arr: array of |
while (pc > 0) ... |
; |
arr: array of |
이제 마지막으로 dirdcl의 내부를 보자.