3.10.선행처리기 명령어
모든 선행처리기 명령어는 줄의 첫 칸부터 적어야 하며 첫번째 글자는 #이어야 합니다.
선행처리기는 전처리기라고 합니다. 이미 앞서 선행처리기가 무엇을 하는지에 대해서는 설명을 드렸으므로 여기서는 선행처리기 명령어들만 간단하게 요약 설명하겠습니다. 많은 선행처리기 명령어가 있지만 실제로 자주 사용하는 선행처리기 명령어는 #include와 #define의 두 가지 명령어입니다. 그러므로 이 두 가지 명령어만 배워두고 나머지는 필요할 때 들추어보는 볼 생각으로 가볍게 읽고 지나치시기 바랍니다.
먼저 선행처리기 명령과 관련되어 몇 가지 규칙이나 관습을 말씀드리겠습니다. 모든 선행처리기 명령어는 줄의 첫번째 칸부터 써야 합니다. 그리고 항상 그 줄의 첫번째 글자는 #이어야 합니다. 그러니까 #으로 시작하는 줄은 무조건 선행처리기 명령어를 사용하는 명령문으로 봐도 무방합니다.
3.10.1. #include
#include문은 헤더파일을 포함시킬 때 사용합니다.
이 명령은 C++언어에서 가장 많이 사용하는 명령어이지 거의 필수적인 명령어입니다. include라는 말은 '포함하다'라는 뜻을 가진 낱말입니다. 그러니까 무엇인가를 포함시키라는 명령어인데, 바로 헤더파일을 포함시키라는 명령어입니다. 즉 컴파일하기 전에 헤더파일을 포함시켜서 먼저 헤더파일을 읽게 합니다. 그래서 컴파일러가 헤더파일로부터 각종 정보를 얻게 합니다. 그리고 헤더파일에서 얻은 정보를 바탕으로 볼랜드C++은 단 한 번에 컴파일을 진행시키는 것입니다.
사용형식은 다음과 같습니다.
사용형식
#include 파일이름
보기
#include
#include "kim.h"
<>는 컴파일러에서 지정한 디렉토리, ""로 묶으면 현재 디렉토리에서 헤더파일을 찾습니다.
보기를 보면 윗 줄의 파일은 <>로 포함시켰고, 아랫줄의 파일은 ""로 포함시켰습니다. 이 둘은 어떤 차이가 있을까요? <>라고 해주면 볼랜드C++의 옵션항목을 통해서 설정한 디렉토리에서 찾으라는 뜻이 됩니다. 그래서 볼랜드C++의 표준 헤더파일을 포함시킬 때는 대부분 <>기호를 이용합니다.
그리고 "" 기호를 사용하면 현재의 디렉토리에서 헤더파일을 찾아서 포함시키라는 뜻입니다. 그래서 ""기호는 주로 사용자가 만들어놓은 헤더파일을 포함시킬 때 사용합니다.
헤더파일에는 경로명을 함께 적을 수 있습니다.
만약 볼랜드C++에서 설정한 디렉토리에도 없고, 현재 디렉토리에도 헤더파일이 없다면 어떻게 할까요? 그 헤더파일이 프로그램의 제작에 꼭 필요한 헤더파일이라는 컴파일에러가 날 겁니다. 그렇다고 해서 다른 디렉토리에 있는 헤더파일을 포함시키기 위해서 컴파일러 옵션을 바꾸기도 귀찮고. 이럴 때는 디렉토리 이름까지 적어주면 됩니다.
보기
#include
#include "C:\KIM\KIM.H"
3.10.2. #define, #undef
#define문은 자료를 새롭게 정의할 때 사용하며, #undef는 취소할 때 사용합니다.
#include 명령어와 함께 가장 많이 사용하는 명령어입니다. #define 명령어의 유용함에 대해서는 세금을 변수로 처리할 때의 예로 설명드렸습니다. 사용형식을 살펴보겠습니다. #define 명령어는 어떤 자료를 새롭게 정의하고자 할 때 사용하는 것이고 #undef는 반대로 #define로 정의된 것을 취소하고자 할 때 사용합니다. 그러니까 파일의 어느 순간까지는 #define 명령을 이용해서 rate를 30으로 바꾸고, 어느 줄 다음부터는 rate를 40으로 처리하고 싶을 때가 있을 겁니다. 그럴 때는 rate를 30으로 처리해야 하는 구간이 끝나자 마자 #undef명령을 이용해서 30이라는 #define을 취소시키면 됩니다.
보기
#define RATE 30
#define TAX 40
보기와 같이 #define문을 사용했을 경우에는 컴파일러가 컴파일을 하다가 RATE라는 낱말을 만날 경우 모두 30이라는 숫자로 바꾸어서 컴파일합니다. 그러니까 소스파일에서는 RATE라고 적혀 있지만 실제로 컴파일이 될 때는 30이라는 숫자로 바뀌어 컴파일이 되는 것입니다.
예를 들어서 printf("rate= %d", RATE);라는 명령문의 경우 컴파일할 때 printf("rate= %d", 30);이라는 문장으로 바뀌어 컴파일이 되는 겁니다. 따라서 출력도 'rate=30'으로 출력됩니다.
#define 명령어는 숫자만이 아니고 모든 변수나 수식, 함수 등이 처리 가능합니다.
보기
#define NAME "Kim JoongTae"
위의 보기와 같이 문자열도 처리할 수 있습니다. 이 경우 printf(NAME);이라는 명령어는 컴파일할 때 선행처리기에서 먼저 printf("Kim JoongTae");라고 바꾸어서 컴파일이 됩니다.
보기
#define JEGOB(n) (n*n)
또 위의 보기와 같이 수식도 처리할 수 있습니다. 이 경우 printf("%d", JEGOB(3));라는 명령어는 printf("%d", (3*3));이라는 명령문으로 바뀌어 컴파일됩니다.
#define으로 정의한 내용을 취소시킬 때 #undef문을 사용합니다.
그런데 이렇게 사용하다가 어느 순간부터는 RATE를 쓰지 않았으면 합니다. 또는 RATE를 30으로 바꾸면 안됩니다. 이럴 때는 #undef문을 사용합니다.
보기
#undef RATE
#undef TAX
#define NAME
#define JEGOB(n)
물론 다시 사용하고 싶다면 #undef문을 사용한 뒤에 다시 #define을 사용하면 됩니다.
보기
#undef RATE
#define RATE 40
#define문을 사용할 때는 한 줄 안에 정의를 끝내야 합니다.
#define문은 사용할 때 몇 가지 주의할 점이 있습니다. 먼저 #define문을 두 줄에 걸쳐서 정의하지 말아야 한다는 것입니다. 이는 모든 선행처리기 명령어에 해당하는 이야기라고 보셔도 됩니다.
그리고 보통 매크로상수 이름으로는 대문자를 사용하고, 매크로함수에는 소문자를 사용합니다. 이는 일종의 습관이자 관례입니다.
문자열 안에서는 선행처리기 명령어가 실행되지 않음에 주의합니다.
주의해야 할 점은 선행처리기 명령어의 명령이 안먹는 부분이 있다는 점입니다. 즉 매크로가 안되는 부분이 있는데 바로 문자열 안에서입니다. 즉 printf("RATE is 30. \n")이라는 함수에서는 RATE가 30으로 바뀌지 않습니다. 이 역시 다른 선행처리기 명령어에도 해당되는 이야기입니다. 그리고 #define문은 #define 명령어 다음에 나오는 첫 단어를 매크로 이름으로 기억하고, 그 뒤의 공백부터는 매크로명령어로 받아들이므로 #define문으로 정의하는 매크로 이름에는 공백이 들어가지 않도록 해야 합니다.
**요약: #define문은 자료를 새롭게 정의할 때 사용하며, #undef는 취소할 때 사용합니다. 단 문자열 안에서는 선행처리기 명령어가 실행되지 않음에 주의해야 합니다.
3.10.3. #if, #ifdef, #ifndef, #else, #elif, #endif
#if문은 선행처리기 명령의 참거짓 판별에 사용합니다.
명령문의 이름을 보면 대충 뭐에 쓰는 명령문인지 감이 잡힐 것입니다. 이것은 '~만약'이라는 뜻을 가지고 있으므로 어떤 조건을 검사하고 처리하는 명령문입니다. 만약 조건이 거짓이라면 0을 돌려줄테고, 참이라면 0이 아닌 값을 돌려줄 겁니다.
#if 명령문은 '만약 ~이라면'의 뜻을 가진 명령문입니다. #ifdef 는 '만약 ~이 정의되어 있다면'의 뜻을 가진 명령문입니다. 그러니까 #ifdef는 if명령문과 define명령문을 합쳐놓은 명령문입니다. #else는 #if나 #ifdef 명령어의 조건을 만족하지 못할 경우에 사용하는 '~아니라면'의 뜻을 가지고 있습니다. #elif는 else와 if문의 복합입니다. 즉 '~이 아니고 만약 ~라면'의 뜻입니다. #endif는 #if나 #ifdef 등으로 시작한 블럭이 끝났을 때 사용합니다.
그러니까 이 명령어는 실질적으로는 #define문에 사용한다는 점만 제외하면 일반적인 if문의 사용법과 같습니다. 다만 일반 if문이 변수나 함수, 수식을 검사할 때 주로 사용한다면, #if문은 #define와 같은 선행처리기 명령어의 참 거짓을 검사할 때 사용된다는 점이 다릅니다. 즉 조건에 따라서 #define문에서 정할 값이 다를 때 사용합니다.
보기
#define VAT 1 // 컴파일할 때 VAT를 1로 교체할 것임
#if VAT // 만약 VAT가 참이라면
#define PRICE 110 // PRICE를 110으로 선언하고
#else // 아니고 VAT가 거짓이라면
#define PRICE 100 // PRICE를 100으로 선언하라
#endif // #if문을 끝낸다는 뜻임
보기의 예제는 부가가치세를 받는다면 부가가치세 10%를 곱해서 110원을 받고, 부가치세를 안받는다면 100원을 가격으로 정하겠다는 뜻입니다. 그런데 VAT가 1이므로 부가가치세를 받는다는 이야기가 됩니다. 그러므로 PRICE는 110으로 정의되는 겁니다. #if문은 이렇게 사용하는 겁니다.
보기
#define VAT 1 // VAT를 정의했습니다.
#ifdef RATE // 만약 RATE가 정의되어 있다면
#define PRICE 30 // PRICE는 30으로 정의하고
#elif VAT // 아니고 VAT가 정의되어 있다면
#define PRICE 110 // PRICE는 110으로 정의한다
#endif // 그리고 끝냄
3.10.4. typedef
typedef문은 변수의 형명을 바꿀 때 사용합니다.
이 명령문은 변수의 형명을 바꿀 수 있는 명령문입니다. 예제를 보시기 바랍니다.
보기
typedef unsigned char moonja; // unsigned char형을 moonja 형으로 정의
typedef unsigned char pmoonja; // unsigned char형을 pmoonja 형이라는
이름으로 새롭게 정의했음
typedef char *string; // string 형을 정의
typedef int jeongsoo; // int형을 jeongsoo형으로 정의
보기와 같이 해놓으면 원래는 char a, b, c 또는 int n, n2라고 써야할 문장을 아래와 같이 바꿀 수 있습니다.
보기
moonja a, b, c;
pmoonja d, e;
string s1, s2, s3;
jeong i, j, k;
형명을 바꾸면 이해하기 쉽고, 경제적이며, 이식성이 높아집니다.
형명을 바꾸면 기억하기도 쉽고, 이해하기도 쉽습니다. 그러나 보통은 C++언어의 초기값을 사용하는 경우가 많기 때문에 일반적으로 그렇게 많이 사용하는 명령문은 아닙니다.
다만 몇 가지 자료형은 바꾸어서 사용하는 경우가 많습니다. 자료형 이름을 바꾸는 것이 프로그램 짤 때 한결 편하기 때문입니다. 주로 바꾸는 이름은 다음과 같습니다.
표: 프로그래머가 새롭게 만들어 쓰는 자료형 이름
typedef enum {false, true} boolean;
typedef unsigned char byte;
typedef unsigned word;
typedef unsigned long longword;
그러니까 enum형을 불린형으로 바꾸어 쓰고, unsigned char형은 바이트형으로 많이 바꾸어 씁니다. 특히 이 두 가지 형명을 애용하는 프로그래머도 있습니다. unsigned char보다는 byte가 쉽게 이해되기도 하지만, 눈에도 쉽게 들어오고, 무엇보다도 unsigned char보다 글자 수가 적다는 점이 매력입니다. 즉 복잡한 자료형 이름을 byte나 word와 같이 알아보기 쉽게 사용할 수 있다는 점이 매력입니다.
그 이외에도 몇 가지 장점이 있습니다. 대표적인 장점으로 이식성을 높일 수 있다는 점입니다. 예를 들어서 IBM-PC와는 달리 UNIX 등을 운영체제로 하는 대형컴퓨터에서는 int형이 1바이트가 아니라 2바이트고 short를 1바이트로 사용할 수 있습니다. 이럴 경우 int a,b;와 같은 명령문을 모두 short a, b;와 같은 문장으로 바꾸어야 할 겁니다. 이렇게 바꾸기란 쉽지 않습니다. 이럴 경우에는 typedef어로 int의 형이름을 다른 형명, 즉 twobyte로 바꾸고, short의 자료형 이름을 int형으로 바꾸면 됩니다. 즉 short가 int형으로 바뀌었으므로 프로그램을 그대로 두어도 int a, b;는 실질적으로 short a, b;로 바뀐 셈입니다.
typedef문은 컴파일러에 의해 처리됩니다.
이와 같이 typedef문으로 정의한 새로운 형 이름은 형 이름을 쓸 수 있는 곳에는 어느 곳이라도 사용할 수 있습니다. 물론 캐스트 연산자 안에서도 사용할 수 있습니다. 즉 m=(int)n;이라는 문장은 m = (jeongsoo)n;이라는 문장으로 써도 되는 겁니다.
한 가지 알아둘 점은 typedef문은 선행처리기에 의해 처리되는 것이 아니라 컴파일러에 의해 처리되는 독특한 명령어라는 점입니다.
3.10.5. #line, #error#, pragma
#line 명령어는 소스코드의 줄번호를 지정하기 위해서 사용합니다. 예를 들어서 #line 30이라고 하면 이 명령어 다음의 줄은 줄번호 31을 부여받습니다. 그렇게 쭉 줄번호를 부여받으므로 프로그램을 짜면서 문제가 되는 줄이 몇 번째 줄인지 파악할 수 있습니다. 그러나 요즘은 컴파일러에서 에러가 난 줄로 이동을 해주기 때문에 #line명령어는 거의 사용하지 않습니다.
#error 명령어는 프로그램의 소스라인에 직접 에러메시지를 넣고 싶을 때 사용합니다. 그러나 요즘의 컴파일러에서는 에러메시지를 정확하게 알려주므로 역시 요즘은 거의 사용하지 않습니다.
#pragma 명령어는 컴파일러의 여러가지 옵션을 프로그램 안에서 직접 설정하는 것으로 역시 요즘 일반인은 거의 사용할 일이 없습니다.
3.10.6. ##
조금 특이한 명령어입니다. 이 명령어는 두 개의 인수(매개변수) 사이를 없애고 둘을 붙여주는 명령어입니다. 말로는 잘 이해가 안되실 것이므로, 예제를 보기 바랍니다.
// s09.cpp
#include
#define HAB(n, m) (n##m)
void main(void)
{
int a = 10, b = 20, ab = 50;
printf("a = %d\n", a);
printf("b = %d\n", b);
printf("ab = %d\n", HAB(a, b));
}
**그림: s09.cpp의 소스파일 내용
보기의 프로그램을 실행하면 다음과 같은 결과가 나옵니다.
결과
a = 10
b = 20
ab = 50
**그림: s09.exe를 실행시킨 화면
즉 HAB(a,b)는 a와 b라는 두 인수를 붙여서 ab라는 낱말로 바꾼겁니다. 따라서 printf("ab = %d\n", HAB(a, b));라는 문장은 printf("ab = %d\n", ab);라는 문장과 같습니다. 그래서 ab=50이라는 내용이 출력된 것입니다.
3.10.7. 선행처리기 명령어(매크로 명령어) 사용시 주의할 점
선행처리기 명령어를 매크로 명령이라고 합니다.
선행처리기 명령어를 흔히 매크로 명령이라고 합니다. 만약 특별한 약속 없이 C++에서 매크로라고 하면 선행처리기 명령어를 뜻하는 것입니다. 선행처리기 명령어는 앞서 배운대로 매우 유용합니다. 그래서 프로그램을 짜는 실력이 늘수록 선행처리기 명령어를 많이 사용합니다. 선행처리기 명령어를 이용하면 간단한 함수도 만들 수 있습니다.
그러나 선행처리기 명령어를 쓸 때는 주의할 점이 있습니다. 선행처리기가 어떻게 처리할 것인지 꼼꼼하게 생각하지 않고 쓰다보면 에러가 발생하기 때문입니다. 다음과 같은 프로그램을 봅시다.
#define JEGOB(n) n*n
이 명령어는 제곱을 구해주는 간단한 함수로 사용할 수 있습니다. 그런데 아래의 두 문장으로 이 명령어를 실행하면 결과가 서로 다르게 나옵니다.
보기
printf("3 * 3 = %d\n", JEGOB(3));
printf("3 * 3 = %d\n", JEGOB(2+1));
먼저 나온 문장은 제대로 결과가 나옵니다. 즉 9가 나옵니다. 그러나 다음 문장은 그렇지 않습니다. 왜 그럴까요? 2+1에 문제가 있는 겁니다. 선행처리기는 #define로 정한 말을 그대로 옮겨줍니다. 그러므로 보기의 두 문장은 다음과 같이 옮겨줍니다.
보기
printf("3 * 3 = %d\n", 3*3);
printf("3 * 3 = %d\n", 2+1*2+1);
JEGOB(n)은 n*n입니다. 그래서 JEGOB(3)을 3*3으로 옮깁니다. 즉 괄호안의 내용끼리 그대로 곱합니다. 2+1을 계산한 후에 변환하는 것이 아니라는 점입니다. 2+1 전체를 n이라는 것으로 보는 것이며, 2+1을 계산한 뒤 3을 n으로 보지 않는다는 점이 두번째 문장과 같은 실수를 만드는 겁니다. 그러므로 '#define JEGOB(n) n*n'과 같은 명령문은 틀린 것은 아니지만 결코 잘 만드는 명령문이 아닙니다. 항상 함수를 만들 때는 결과가 제대로 나올 것인가보다는 혹시 잘못된 결과는 안나올까를 먼저 따져야 합니다. 그러므로 이 명령문은 다음과 같이 정의하는 것이 좋습니다.
#define JEGOB(n) (n)*(n)
이렇게 하면 'printf("3 * 3 = %d\n", JEGOB(2+1));'를 아래와 같이 바꾸게 됩니다.
보기
printf("3 * 3 = %d\n", (2+1)*(2+1));
괄호를 많이 쳐주면 연산순서를 혼동하지 않아서 좋습니다.
따라서 이제는 올바른 결과를 얻을 수 있습니다. n*n과 (n)*(n)의 차이는 이처럼 큰 차이로 나타날 수 있습니다. 그러므로 선행처리기 명령어를 쓸 때는 괄호를 어떻게 쳐줄 것인가를 잘 생각해야 합니다. 가능하면 괄호를 많이 쳐주는 것이 좋습니다. 이는 모든 함수를 만들 때 마찬가지입니다. 괄호를 많이 쳐주면 연산순서를 혼동하지 않기 때문에 좋습니다. 초보자라면 연산순서를 잘못 알 수 있으니 먼저 계산해야 할 것이 있으면 괄호를 쳐서 순서를 구별하는 것이 좋습니다.
**요약: 선행처리기 명령어는 단순히 컴파일 하기 전에 글자를 교체해주는 기능만 하므로, 선행처리기를 이용해서 수식을 표현할 때 연산순서를 정확하게 파악하고 사용해야 합니다.