C 전처리기
1. 개요
C 전처리기는 1973년경 C 언어에 도입된 기능으로, 파일 포함, 매크로 확장, 조건부 컴파일 등을 수행한다. C 표준의 번역 단계 중 처음 4단계를 담당하며, 트라이그래프 대체, 라인 결합, 토큰화, 매크로 확장 및 지시어 처리를 거친다. `#include`를 사용하여 다른 파일의 내용을 현재 위치에 포함시키고, `#if`, `#ifdef` 등의 지시어를 통해 조건부로 코드를 컴파일하거나 제외할 수 있다. `#define`을 사용하여 매크로를 정의하고 확장하며, 특수한 매크로인 `__FILE__`, `__LINE__` 등을 통해 디버깅 정보를 제공한다. C23 표준에서는 바이너리 리소스 포함을 위한 `#embed` 지시어가 도입될 예정이다. C 전처리기는 C, C++, Objective-C 등에서 사용되며, 컴파일러별로 다양한 확장 기능을 제공한다. 또한 다른 언어에서도 사용될 수 있지만, 입력 언어가 C와 유사해야 한다.
| 종류 | 컴퓨터 프로그램, 컴파일러 |
|---|---|
| 개발자 | 여러 개발자 |
| 발표일 | 1972년 |
| 최신 버전 | GNU cpp 13.2 (2023년 8월 2일) |
| 상태 | 활발 |
| 운영체제 | 크로스 플랫폼 |
|---|---|
| 언어 | C, C++, Objective-C |
| 종류 | 전처리기 |
| 라이선스 | GNU GPL (일부) |
-
매크로 프로그래밍 언어 -
TeX
TeX는 도널드 커누스가 개발한 수학, 과학, 공학 분야의 고품질 문서 출력을 위한 조판 시스템이며, 수학 수식 조판에 특화된 매크로 시스템과 높은 확장성을 제공한다. -
매크로 프로그래밍 언어 -
M4 (프로그래밍 언어)
M4는 1977년 데니스 리치와 브라이언 커니건이 개발한 매크로 프로세싱 프로그래밍 언어이며, 텍스트 재사용, 파일 포함, 문자열 조작 등 다양한 기능을 제공하고 튜링 완전성을 갖춘 언어이다. -
C 프로그래밍 언어 -
C (프로그래밍 언어)
C는 하드웨어 제어와 이식성이 뛰어난 고급 절차적 프로그래밍 언어로서, 다양한 분야에서 사용되며 후속 언어에 영향을 주었고, 성능과 효율성이 높지만 안전성 문제 개선이 필요한 언어이다. -
C 프로그래밍 언어 -
헤더 파일
헤더 파일은 프로그래밍 언어에서 코드 재사용성, 모듈화, 컴파일 시간 단축에 기여하며 함수 프로토타입, 변수 선언 등을 포함하고 `#include` 지시어로 소스 코드에 포함되어 사용되는 파일이다.
2. 역사
C 전처리기는 1973년경 앨런 스나이더(Alan Snyder)의 제안과 BCPL 및 PL/I에서 사용 가능한 파일 포함 메커니즘의 유용성을 인식하여 C에 도입되었다. 초기 버전은 파일 포함과 단순 문자열 교체만 제공했으며, 매개변수가 없는 매크로를 위해 `#include`와 `#define`을 사용했다. 이후 마이크 레스크(Mike Lesk)와 존 라이저에 의해 매개변수가 있는 매크로 및 조건부 컴파일 기능을 포함하도록 확장되었다.
C 전처리기는 1959년 더글러스 이스트우드와 더글러스 맥길로이(Douglas McIlroy)에 의해 시작된 벨 연구소(Bell Labs)의 긴 매크로 언어 전통의 일부였다.
3. 번역 단계
전처리(preprocessor)는 C 표준에 명시된 여덟 단계의 "번역 단계" 중 처음 네 단계로 정의된다.
1. 트라이그래프 대체: 전처리기는 트라이그래프 시퀀스를 해당 문자로 대체한다. 이 단계는 C23에서 C++17의 단계를 따라 제거될 예정이다.
2. 라인 결합: 이스케이프 문자로 이스케이프된 개행 문자 시퀀스로 계속되는 물리적 소스 라인은 논리적 라인을 형성하기 위해 '결합'된다.
3. 토큰화: 전처리기는 결과를 '전처리 토큰'과 공백 문자로 나눈다. 주석은 공백으로 대체한다.
4. 매크로 확장 및 지시어 처리: 파일 포함 및 조건부 컴파일을 포함한 전처리 지시어 라인이 실행된다. 전처리기는 매크로를 동시에 확장하고, 1999년 버전의 C 표준부터는 `_Pragma` 연산자를 처리한다.
4. 파일 포함
`#include` 지시어는 다른 소스 파일을 현재 파일에 포함시켜 하나의 파일처럼 컴파일되도록 하는 기능이다. 보통 IDE 개발 환경에서는 프로젝트 단위로 여러 파일을 관리하고 컴파일하여 실행 파일을 생성하는데, 이때 `#include`를 사용하여 서로 다른 파일 간에 함수, 변수 선언 등을 연결한다.
예시:
```cpp
#include
int main(void)
{
printf("Hello, world!\n");
return 0;
}
```
위 코드에서 `#include
`#include` 지시어는 `<>` (꺾쇠 괄호) 또는 `""` (큰따옴표)를 사용하여 파일 이름을 감쌀 수 있다.
* `<>`: 표준 컴파일러 include 경로에서 파일을 찾는다.
* `""`: 현재 소스 파일 디렉터리를 포함하여 확장된 경로에서 파일을 찾는다.
C 컴파일러 및 프로그래밍 환경은 include 파일을 찾을 위치를 지정하는 기능을 제공하며, 이는 makefile을 통해 매개변수화된 명령줄 플래그를 사용하여 설정할 수 있다. 이를 통해 운영 체제에 따라 다른 include 파일 집합을 사용할 수 있다.
관례상 include 파일은 `.h` 또는 `.hpp` 확장자를 사용하지만, 반드시 지켜야 하는 것은 아니다. `.def` 확장자는 여러 번 포함되도록 설계된 파일을 나타내며, `#include "icon.xbm"`과 같이 XBM 이미지 파일(C 소스 파일이기도 함)을 참조할 수도 있다.
`#include`를 사용할 때는 이중 포함을 방지하기 위해 `#include` 가드 또는 `#pragma once`를 사용하는 것이 일반적이다.
5. 조건부 컴파일
`#if`, `#ifdef`, `#ifndef`, `#else`, `#elif`, `#endif`는 특정 조건에 따라 코드 블록을 컴파일하거나 제외하는 조건부 컴파일 기능을 제공한다. 조건이 맞지 않으면 해당 코드는 컴파일되지 않아 마치 소스 코드 자체가 없는 것과 같은 효과를 낸다.
예시:
```cpp
#if VERBOSE >= 2
print("trace message");
#endif
```
위 코드에서 `VERBOSE` 값이 2보다 크거나 같으면 `print("trace message");` 코드가 컴파일되고, 그렇지 않으면 컴파일되지 않는다. `VERBOSE` 값은 미리 정의되어 있어야 한다.
조건부 컴파일을 활용하면 다양한 환경에 맞게 코드를 컴파일할 수 있다.
```cpp
#ifdef __unix__ /* __unix__는 일반적으로 유닉스 시스템을 대상으로 하는 컴파일러에 의해 정의됩니다 */
# include
#elif defined _WIN32 /* _WIN32는 일반적으로 32 또는 64비트 윈도우 시스템을 대상으로 하는 컴파일러에 의해 정의됩니다 */
# include
#endif
```
위 코드는 운영체제가 UNIX 환경(`__unix__`가 정의된 경우)이면 `
`#if`와 `defined`를 함께 사용하면 더 복잡한 조건부 컴파일도 가능하다.
```cpp
#if !(defined __LP64__ || defined __LLP64__) || defined _WIN32 && !defined _WIN64
// 우리는 32비트 시스템을 위해 컴파일하고 있습니다.
#else
// 우리는 64비트 시스템을 위해 컴파일하고 있습니다.
#endif
```
`#error` 지시어를 사용하면 컴파일을 강제로 실패시킬 수 있다.
```cpp
#if RUBY_VERSION == 190
#error 1.9.0은 지원되지 않습니다.
#endif
```
위 코드는 `RUBY_VERSION`이 190이면 컴파일 오류를 발생시킨다.
윈도우를 대상으로 하는 대부분의 컴파일러는 `_WIN32` 매크로를 암묵적으로 정의한다. 이를 이용하면 윈도우 시스템을 대상으로 할 때만 코드가 컴파일되도록 할 수 있다. `_WIN32` 매크로를 정의하지 않는 컴파일러의 경우 `-D_WIN32` 옵션을 사용하여 컴파일러 명령줄에서 지정할 수 있다.
6. 매크로 정의 및 확장
`#define` 지시어는 특정 식별자를 다른 토큰으로 대체하는 매크로를 정의하는 데 사용된다. 이러한 매크로는 객체 유사 매크로와 함수 유사 매크로 두 가지 유형이 있다. 객체 유사 매크로는 주로 상수에 이름을 붙여 코드의 가독성을 높이는 데 사용된다. 예를 들어, `#define PI 3.14159`와 같이 정의하면 코드 내에서 `PI`라는 식별자를 사용할 때마다 `3.14159`로 대체된다. 이렇게 하면 숫자 자체를 사용하는 것보다 의미를 명확하게 전달할 수 있다.
함수 유사 매크로는 함수처럼 인수를 받아 더 복잡한 변환을 수행할 수 있다. 예를 들어, `#define RADTODEG(x) ((x) * 57.29578)`와 같이 정의하면 `RADTODEG(34)`와 같은 코드는 `((34) * 57.29578)`로 확장된다. 이렇게 하면 코드를 간결하게 유지하고 반복을 줄일 수 있다.
`#undef` 지시어를 사용하면 정의된 매크로를 제거할 수 있다. `#undef PI`와 같이 사용하면 `PI`에 대한 매크로 정의가 제거된다.
C++에서는 `const` 한정자나 `constexpr` 키워드를 사용하여 객체 유사 매크로와 유사한 기능을 수행할 수 있다. `constexpr`는 컴파일 시간에 값을 계산하도록 지정하여 매크로처럼 사용할 수 있게 해준다.
C 전처리기는 1973년경 앨런 스나이더(Alan Snyder)의 제안으로 C에 도입되었으며, 초기에는 파일 포함과 단순 문자열 교체 기능만 제공했다. 이후 마이크 레스크(Mike Lesk)와 존 라이저에 의해 기능이 확장되었다. C 전처리기는 더글러스 맥길로이(Douglas McIlroy) 등이 참여한 벨 연구소의 매크로 언어 전통을 계승한 것이다.
매크로를 정의할 때는 연산자 우선순위로 인한 오류를 방지하기 위해 주의해야 한다. 매크로 인자와 전체 표현식을 괄호로 묶는 것이 일반적인 방법이다. 예를 들어 `RADTODEG(r + 1)`은 `((r + 1) * 57.29578)`로 확장되어 올바른 계산 결과를 얻을 수 있다.
6.1. 확장 순서
함수형 매크로 확장은 다음 단계로 수행된다.
1. 문자열화 연산자는 인수의 대체 목록의 텍스트 표현으로 대체된다. (확장 수행하지 않음).
2. 매개변수는 대체 목록으로 대체된다.(확장 수행하지 않음).
3. 연결 연산자는 두 피연산자의 연결된 결과로 대체된다. (결과 토큰을 확장하지 않음).
4. 매개변수에서 파생된 토큰이 확장된다.
5. 결과 토큰은 정상적으로 확장된다.
이는 놀라운 결과를 초래할 수 있다.
```cpp
#define HE HI
#define LLO _THERE
#define HELLO "HI THERE"
#define CAT(a,b) a##b
#define XCAT(a,b) CAT(a,b)
#define CALL(fn) fn(HE,LLO)
CAT(HE, LLO) // "HI THERE", 연결은 일반 확장 전에 수행된다.
XCAT(HE, LLO) // HI_THERE, 매개변수("HE" 및 "LLO")에서 파생된 토큰이 먼저 확장된다.
CALL(CAT) // "HI THERE", 이는 CAT(a,b)로 평가되기 때문이다.
7. 특수 매크로 및 지시어
C 전처리기는 프로그램 실행에 필요한 여러 특수 매크로와 지시어를 제공한다.
* `__FILE__`과 `__LINE__`: 이 매크로들은 각각 현재 소스 파일의 이름과 줄 번호를 나타낸다. 주로 디버깅 목적으로 사용되며, 오류 메시지를 출력할 때 파일 이름과 줄 번호를 함께 표시하여 문제 발생 위치를 쉽게 찾도록 도와준다.
```cpp
#define DEBUGPRINT(_fmt, ...) fprintf(stderr, "[file %s, line %d]: " _fmt, __FILE__, __LINE__, __VA_ARGS__)
DEBUGPRINT("x = %d\n", x);
```
* `#line`: 이 지시어는 `__FILE__`과 `__LINE__`의 값을 변경한다. 예를 들어 `#line 314 "pi.c"`는 이후 코드의 줄 번호를 314로, 파일 이름을 "pi.c"로 설정한다. 이는 다른 언어로 작성된 코드를 C 코드로 변환할 때 유용하다.
```cpp
#line 314 "pi.c"
printf("line=%d file=%s\n", __LINE__, __FILE__); // 출력: line=314 file=pi.c
```
* `__STDC__`, `__STDC_VERSION__`, `__cplusplus`: 이 매크로들은 각각 컴파일러가 표준 C를 따르는지 (`__STDC__`), 지원하는 표준 버전은 무엇인지 (`__STDC_VERSION__`), C++ 컴파일러인지 (`__cplusplus__`)를 나타낸다. 이를 통해 특정 표준에 맞는 코드를 작성하거나, C와 C++ 간 호환성을 확보할 수 있다.
* `__DATE__`와 `__TIME__`: 현재 날짜와 시간을 나타낸다.
* `__func__`: (C99 표준) 현재 함수의 이름을 나타낸다.
* 가변 매크로 (C99 표준): 가변 매크로는 `printf` 함수처럼 인수의 개수가 정해지지 않은 매크로를 정의할 수 있게 해준다. 로깅 함수를 만들 때 유용하다.
* X-Macros: 이 패턴은 헤더 파일에 매크로 호출 목록을 정의하고, 이 파일을 여러 번 포함시켜 코드 중복을 줄이는 기법이다. 주로 확장자가 `.def`인 파일에 정의된다.
7.1. 토큰 문자열화
`#` 연산자 (문자열화 연산자 또는 스트링화 연산자라고도 함)는 토큰을 C 문자열 리터럴로 변환하며, 따옴표나 백슬래시를 적절하게 이스케이프 처리한다.
예시:
```cpp
#define str(s) #s
str(p = "foo\n";) // 출력: "p = \"foo\\n\";"
str(\n) // 출력: "\n"
```
매크로 인수의 확장을 문자열화하려는 경우, 두 단계의 매크로를 사용해야 한다.
```cpp
#define xstr(s) str(s)
#define str(s) #s
#define foo 4
str (foo) // 출력: "foo"
xstr (foo) // 출력: "4"
```
매크로 인수는 추가 텍스트와 결합된 다음 문자열화될 수 없다. 그러나 일련의 인접한 문자열 상수와 문자열화된 인수는 작성할 수 있으며, C 컴파일러는 모든 인접한 문자열 상수를 하나의 긴 문자열로 결합한다.
7.2. 토큰 연결
`##` 연산자 ("토큰 접합 연산자"라고도 함)는 두 개의 토큰을 하나의 토큰으로 연결한다.
```cpp
#define DECLARE_STRUCT_TYPE(name) typedef struct name##_s name##_t
DECLARE_STRUCT_TYPE(g_object); // 출력 결과: typedef struct g_object_s g_object_t
8. 사용자 정의 컴파일 오류
`#error` 지시어는 컴파일 오류를 발생시키고 지정된 메시지를 출력한다.
```c
#error "오류 메시지"
```
이 코드는 컴파일 시 "오류 메시지"라는 오류 메시지를 출력하고 컴파일을 중단시킨다. 주로 특정 조건이 충족되지 않았을 때 컴파일을 중단시키기 위해 사용된다.
9. 바이너리 리소스 포함
C23은 바이너리 리소스 포함을 위한 `#embed` 지시자를 도입할 예정이다. 이를 통해 바이너리 파일(예: 이미지)을 프로그램에 포함할 수 있다. `#embed` 지시자는 지정된 리소스의 데이터에 해당하는 쉼표로 구분된 정수 목록으로 대체된다. 이는 `unsigned char` 유형의 배열이 `#embed` 지시자를 사용하여 초기화된 경우, fread를 사용하여 리소스를 배열에 쓴 것과 동일한 결과가 나타나는 것과 같다.
포함할 파일은 `#include`와 동일한 방식으로 지정할 수 있으며, 산형 괄호 또는 따옴표로 묶을 수 있다. 이 지시자는 파일 이름 뒤에 오는 동작을 사용자 정의하기 위해 특정 매개변수를 전달할 수 있도록 한다. C 표준은 다음과 같은 매개변수를 정의한다.
* `limit`: 포함된 데이터의 너비를 제한한다. 주로 urandom과 같은 "무한" 파일에 사용하기 위한 것이다.
* `prefix` 및 `suffix`: 포함된 데이터에 대한 접두사 및 접미사를 지정할 수 있으며, 이는 포함된 리소스가 비어 있지 않은 경우에만 사용된다.
* `if_empty`: 리소스가 비어 있는 경우(파일이 비어 있거나 0의 제한이 지정된 경우 발생) 전체 지시자를 대체한다.
모든 표준 매개변수는 C23의 표준 속성과 마찬가지로 이중 밑줄로 묶을 수도 있다. 예를 들어, `__prefix__`는 `prefix`와 상호 교환이 가능하다.
10. 구현체
C, C++, Objective-C 구현은 모두 전처리기를 제공하며, 이는 해당 언어들의 필수적인 단계이다. 전처리기의 동작은 ISO C 표준과 같은 언어의 공식 표준에 의해 설명된다.
구현은 자체적인 확장 및 변형을 제공할 수 있으며, 표준 준수 정도는 다를 수 있다. 정확한 동작은 호출 시 제공되는 명령줄 플래그에 따라 달라질 수 있다. 예를 들어, GNU C 전처리기는 특정 플래그를 통해 표준 준수를 높일 수 있다.
10.1. 컴파일러별 전처리기 기능
`#pragma` 지시어는 컴파일러 관련 지시어로, 컴파일러 제작자가 자체적인 목적으로 사용할 수 있다. 예를 들어, `#pragma`는 특정 오류 메시지 표시를 억제하고, 힙 및 스택 디버깅 등을 관리하는 데 자주 사용된다. OpenMP 병렬화 라이브러리를 지원하는 컴파일러는 `#pragma omp parallel for`를 사용하여 `for` 루프를 자동으로 병렬화할 수 있다.
C99는 부동 소수점 구현을 제어하는 데 사용되는 `#pragma STDC ...` 형식의 몇 가지 표준 `#pragma` 지시어를 도입했다. 매크로와 유사한 형태인 `_Pragma(...)`도 추가되었다.
* 많은 구현에서 삼중 문자열을 지원하지 않거나 기본적으로 대체하지 않는다.
* 많은 구현(예: GNU, Intel, Microsoft 및 IBM의 C 컴파일러)은 출력에 경고 메시지를 출력하지만 컴파일 프로세스를 중지하지 않는 비표준 지시어를 제공한다(C23 및 C++23은 이 목적으로 표준에 `#warning`을 추가한다). 일반적인 사용법은 호환성을 위해 포함된 더 이상 사용되지 않는 오래된 코드의 사용에 대해 경고하는 것이다. 예를 들면 다음과 같다.
GNU, Intel 및 IBM
```
#warning "ABC는 더 이상 사용되지 않으므로 사용하지 마십시오. 대신 XYZ를 사용하십시오."
```
Microsoft
```
#pragma message("ABC는 더 이상 사용되지 않으므로 사용하지 마십시오. 대신 XYZ를 사용하십시오.")
```
* 일부 유닉스 전처리기에서는 프로그래밍에서 사용되는 어설션과는 거의 유사성이 없는 "어설션"을 전통적으로 제공했다.
* GCC는 동일한 이름의 헤더를 연결하기 위해 `#include_next`를 제공한다.
10.2. 언어별 전처리기 기능
Objective-C 전처리기는 `#include`와 유사하지만 파일을 한 번만 포함하는 `#import`를 가지고 있다. C에서 유사한 기능을 가진 일반적인 공급업체 프래그마는 `#pragma once`이다.
C++는 C++20부터 모듈을 위한 `import` 및 `module` 지시어를 가지고 있다. 이 지시어들은 `#` 문자로 시작하지 않는 유일한 지시어이다. 대신, 선택적으로 `export`가 앞에 올 수 있으며, 각각 `import`와 `module`로 시작한다.
11. 기타 용도
C 전처리기는 컴파일러와 별도로 호출할 수 있어 다른 언어에서도 사용할 수 있다. 주목할 만한 예로는 현재는 사용되지 않는 imake 시스템과 포트란 전처리에 사용된 경우가 있다. GNU 포트란 컴파일러는 특정 파일 확장자를 사용하면 포트란 코드를 컴파일하기 전에 자동으로 "전통 모드"(ISO C 이전의 C 전처리기처럼 작동) cpp를 호출한다. 인텔은 ifort 컴파일러와 함께 사용할 수 있는 포트란 전처리기인 fpp를 제공하며, 이는 비슷한 기능을 제공한다.
CPP는 대부분의 어셈블리 언어 및 알골 계열 언어와도 문제없이 작동한다. 이를 위해서는 언어 구문이 CPP 구문과 충돌하지 않아야 하는데, 이는 `#`으로 시작하는 줄이 없어야 하고, cpp가 문자열 리터럴로 해석하여 무시하는 큰따옴표가 그 외의 구문적 의미를 가지지 않아야 한다. "전통 모드"(ISO C 이전의 C 전처리기처럼 작동)는 일반적으로 더 관대하며 이러한 사용에 더 적합하다.
C 전처리기는 튜링 완전성을 만족하지 않지만, 거의 근접한다. 재귀적인 계산을 지정할 수 있지만, 수행되는 재귀의 양에 고정된 상한이 있다. 그러나 C 전처리기는 일반적인 목적의 프로그래밍 언어처럼 설계되지 않았으며, 성능도 좋지 않다. C 전처리기는 재귀 매크로, 인용에 따른 선택적 확장, 조건부 문자열 평가와 같은 다른 전처리기의 기능을 가지고 있지 않으므로, m4와 같은 보다 일반적인 매크로 프로세서에 비해 매우 제한적이다.