전처리기는 프로그램을 컴파이하기 전 소스 파일을 처리하는 컴파일러의 한 부분이다. 전처리기는 몇 가지의 전처리기 지시자들을 처리하며 이 지시자들은 # 기호로 시작해서 줄 바꿈으로 끝난다.
전처리기는 컴파일러 실행 직전에 단순히 텍스트를 치환하는 역할을 하기도 하고, 디버깅에 도움을 주며 헤더 파일의 중복을 방지해주는 기능을 한다.
#include <iostream>
// 선행처리 후 명령문은 소멸
#define PI 3.14
int main(void)
{
// 선행처리 후 단순 치환
double num = PI * 3; // int num = 3.14 * 3; 으로 치환 시켜준다.
return 0;
}
#define 지시자를 이용하면 숫자 상수에 이름을 부여할 수 있으며, 이를 단순 매크로 또는 매크로 상수라고 한다.
#define 의 경우 전처리기 문법이기 때문에 메모리를 사용하지 않는다.
#define 지시자는 전처리기에게 다음과 같은 내용을 지시한다.
#define MACRO MACROBODY
// 이어서 등장하는 MACRO를 MACROBODY로 치환하라.
#include <iostream>
// 단순 매크로 정의 (기호 상수 PI를 3.14로 정의)
#define PI 3.14 // 원주율
// #define : 지시자
// PI : 매크로
// 3.14 : 매크로 몸체 (대체 리스트)
int main()
{
// 원의 반지름
int radius = 2;
// 원의 반지름, 둘레 출력
std::cout << "원의 반지름 = " << radius << std::endl;
std::cout << "원의 둘레 = " << 2 * PI * radius << std::endl;
std::cout << "PI = " << PI << std::endl;
return 0;
}
원의 반지름 = 2
원의 둘레 = 12.56
PI = 3.14
전처리기는 위의 소스 파일에서 PI 를 3.14 로 전부 치환한다.
이렇게 기호 상수를 이용하면 프로그램의 가독성이 높아지고, 값의 변경에 용이해진다. 만약 기호 상수가 여러 곳에서 사용되고 있을 때 값을 변경하고 싶다면 #define 문장만 변경하면 된다.
매크로의 이름은 대문자로 정의하는 것이 일반적이며 #define 은 숫자 상수뿐만 아니라 어떠한 텍스트도 다른 텍스트로 바꿀 수 있다.
#define PI 3.14 // 원주율
#define EOF (-1) // 파일의 끝표시
#define EPS 1.0e-9 // 실수의 계산 한계
#define DIGITS "0123456789" // 문자 상수 정의
#define BRACKET "(){}[]" // 문자 상수 정의
함수 매크로란 매크로가 함수처럼 매개 변수를 가지는 것이다. 함수 매크로를 사용하면 복잡한 계산을 숨기고 보다 간단하게 나타낼 수 있다. 또한 함수 매크로에서는 매개 변수의 자료형을 써주지 않아 어떠한 자료형에 대해서도 적용이 가능하다.
#include <iostream>
// 함수 매크로 정의
#define SQUARE(x) ((x) * (x)) // 제곱을 구하는 함수 매크로
// SQUARE(x)과 동일한 패턴을 만나면 ((x) * (x))로 치환해 버린다.
#define SUM(x, y) ((x) + (y)) // 두 수의 합을 구하는 함수 매크로
// SUM(x, y)과 동일한 패턴을 만나면 ((x) + (y))로 치환해 버린다.
int main()
{
for (int i = 1; i < 6; i++)
{
std::cout << i << "의 제곱 : " << SQUARE(i) << std::endl; // 아래 코드로 치환됨
// cout << i << "의 제곱 : " << ((i) * (i))<< "\\t";
}
std::cout << std::endl;
for (int i = 0; i < 6; i++)
{
std::cout << i << " + " << i << " = " << SUM(i, i) << std::endl; // 아래 코드로 치환됨
// cout << i << " + " << i << " = " << ((i) + (i)) << endl;
}
return 0;
}
1의 제곱 : 1
2의 제곱 : 4
3의 제곱 : 9
4의 제곱 : 16
5의 제곱 : 25
0 + 0 = 0
1 + 1 = 2
2 + 2 = 4
3 + 3 = 6
4 + 4 = 8
5 + 5 = 10
전처리기가 소스 코드에서 함수 매크로를 발견하면 정의된 문자열로 변환하고 매개 변수를 매크로 호출 시 주어지는 인수로 치환한다.
함수 매크로에서는 매개 변수가 기계적으로 대치되기 때문에 반드시 매크로의 매개 변수들을 괄호로 묶어 주어야 한다. 그렇지 않으면 원래 의도와 다르게 동작할 수 있다.
#include <iostream>
// 함수 매크로 정의 (제곱근을 구하는 함수 매크로)
#define SQUARE1(x) ((x) * (x))
#define SQUARE2(x) x * x
int main()
{
// ((1 + 2) * (1 + 2)) 로 계산됨
std::cout << SQUARE1(1 + 2) << std::endl;
// 1 + 2 * 1 + 2 로 계산됨
std::cout << SQUARE2(1 + 2) << std::endl;
return 0;
}
9
5
매크로 이름과 괄호 사이에 공백이 있으면 안된다.
#define SUM (x, y) ((x) + (y)) // 오류!
줄의 맨 끝에 \\ 를 적어주어 한 줄 이상의 매크로를 만들 수 있다.
#include <iostream>
#define MAX(x, y) if(x > y) \\
std::cout << x << std::endl;\\
else\\
std::cout << y << std::endl;
int main()
{
MAX(1, 10);
return 0;
}
10
함수 매크로의 장점은 다음과 같다.
일반 함수가 호출되면 다음과 같은 사항들이 동반된다.
return 문에 의한 값의 반환따라서 일반 함수의 빈번한 호출은 실행 속도의 저하로 이어진다.
반면 함수 매크로는 전처리기에 의해 몸체 부분이 호출 문장을 대신하기 때문에 위의 사항들을 동반하지 않아 실행 속도상 이점이 있다.
또한 함수 매크로는 위의 코드에서 보았듯이 전달인자의 자료형에 상관없이 치환되기 때문에 자료형에 따라서 별도로 함수를 정의하지 않아도 된다.
함수 매크로의 단점은 다음과 같다.
기본적으로 매크로는 한 줄에 정의하는 것이 원칙이며 만약 두 줄이상에 걸쳐 정의해야 할 경우 \\ 문자를 활용해 줄이 바뀌었음을 명시해야 하기 때문에 함수가 길어지면 정의하기 까다롭다.
또한 함수 매크로를 잘못 정의했을 경우, 에러 메시지는 전처리 이전의 소스 파일 기준이 아닌, 전처리 이후의 소스 파일을 기준으로 출력 되기 때문에 일반적인 에러 메시지보다 이해하기 힘들다.
위의 장단점을 종합해 보면, 다음의 특성을 지니는 함수들을 매크로의 형태로 정의하는 것이 좋아 보인다.
내장 매크로란 컴파일러가 프로그래머들이 유용하게 사용하도록 제공하는 미리 정의되어 있는 매크로이다. 주로 사용하는 것은 다음 4가지이다.
| 내장 매크로 | 설명 |
|---|---|
__DATE__ |
소스가 컴파일된 날짜(월 일 년)로 치환 |
__TIME__ |
소스가 컴파일된 시간(시:분:초)으로 치환 |
__LINE__ |
소스 파일에서의 현재의 라인 번호로 치환 |
__FILE__ |
소스 파일 이름으로 치환 |
#include <iostream>
int main()
{
std::cout << "날짜 : " << __DATE__ << std::endl;
std::cout << "시간 : " << __TIME__ << std::endl;
std::cout << "라인 : " << __LINE__ << std::endl;
std::cout << "파일 : " << __FILE__ << std::endl;
return 0;
}
날짜 : Oct 22 2023
시간 : 01:35:49
라인 : 8
파일 : D:\\Project1\\Study.cpp
오류가 발생했을 경우 __LINE__ 과 __FILE__ 을 같이 출력해주면 어떤 소스 파일의 몇 번째 라인에서 발생한 오류인지를 알 수 있다.
#if, #endif#if 는 조건부 컴파일을 지시하는 전처리 지시자이다. 조건부 컴파일이란 어떠한 조건이 만족되는 경우에만 지정된 소스 코드 블록을 컴파일 한다.
#if 는 뒤의 매크로를 검사하여 매크로가 true 면 #if 와 #endif 사이의 모든 문장들을 컴파일하고, false 면 컴파일하지 않는다.
#include <iostream>
#define DEBUG1 true // 매크로 DEBUG1 정의
#define DEBUG2 false // 매크로 DEBUG2 정의
int main()
{
std::cout << "Hello World!" << std::endl;
#if DEBUG1
std::cout << "매크로 DEBUG1 이 참이면 컴파일 됩니다." << std::endl;
#endif
#if DEBUG2
std::cout << "매크로 DEBUG2 가 참이면 컴파일 됩니다." << std::endl;
#endif
return 0;
}
Hello World!
매크로 DEBUG1 이 참이면 컴파일 됩니다.
#ifend, #endif#ifdef 는 뒤의 매크로를 검사하여 매크로가 정의되어 있으면 #ifdef 와 #endif 사이의 모든 문장들을 컴파일하고, 매크로가 정의되어 있지 않으면 컴파일 하지 않는다.
#include <iostream>
#define DEBUG1 // 매크로 DEBUG1 정의
int main()
{
std::cout << "Hello World!" << std::endl;
#ifdef DEBUG1
std::cout << "매크로 DEBUG1 이 정의되어 있으면 컴파일 됩니다." << std::endl;
#endif
#ifdef DEBUG2
std::cout << "매크로 DEBUG2 가 정의되어 있으면 컴파일 됩니다." << std::endl;
#endif
return 0;
}
Hello World!
매크로 DEBUG1 이 정의되어 있으면 컴파일 됩니다.
#ifndef, #endif#ifndef 는 뒤의 매크로가 정의되어 있지 않으면 #ifndef 와 #endif 사이의 문장이 컴파일에 포함된다.
#include <iostream>
#define DEBUG1 // 매크로 DEBUG1 정의
int main()
{
std::cout << "Hello World!" << std::endl;
#ifndef DEBUG1
std::cout << "매크로 DEBUG1 이 정의되어 있지 않으면 컴파일 됩니다." << std::endl;
#endif
#ifndef DEBUG2
std::cout << "매크로 DEBUG2 이 정의되어 있지 않으면 컴파일 됩니다." << std::endl;
#endif
return 0;
}
Hello World!
매크로 DEBUG2 이 정의되어 있지 않으면 컴파일 됩니다.
#else#if, #ifdef, #ifndef 문에도 #else 문을 추가할 수 있다.
#include <iostream>
#define DEBUG false // 매크로 DEBUG 정의
int main()
{
std::cout << "Hello World!" << std::endl;
#if DEBUG
std::cout << "매크로 DEBUG 가 참이면 컴파일 됩니다." << std::endl;
#else
std::cout << "매크로 DEBUG 가 참이 아니면 컴파일 됩니다." << std::endl;
#endif
return 0;
}
Hello World!
매크로 DEBUG 가 참이 아니면 컴파일 됩니다.