나만의 작은 도서관

[C++][Build] 전처리 단계 - 전처리기 지시문(preprocessor directive) 본문

C++/문법 및 메소드(STL)

[C++][Build] 전처리 단계 - 전처리기 지시문(preprocessor directive)

pledge24 2025. 4. 14. 16:35

썸네일: https://dotnettutorials.net/lesson/preprocessor-directives-in-cpp/

전처리기 지시문(preprocessor directive)이란?

  • 전처리기 지시문은 전처리 단계에서 전처리기(preprocessor)에 의해 처리되는 명령어로, 명령어들은 모두 ‘#’ 기호로 시작한다.
  • 전처리기 지시문을 활용하면 파일 포함, 조건부 컴파일, 매크로 정의 등 다양한 작업을 수행할 수 있다.

대표적인 전처리기 명령어

  • #include
  • #define
  • #ifdef, #ifndef

#include

#include "path-spec" // 사용자 정의 파일 추가 방식
#include <path-spec> // 표준 라이브러리 추가 방식
  • #include는 전처리기 지시문 중에서도 가장 자주 사용되는 명령어로, 지정한 파일을 현재 소스 파일에 추가한다.

 

 

#include가 파일을 추가하는 방식

  • #include가 실행되면 지정한 파일의 모든 내용이 #include 위치에 그대로 "복사"된다. 복사된 내용은 기존 파일과 독립적으로 존재하기 때문에, 복사된 내용에 정의된 변수의 메모리 또한 독립적인 공간을 가지게 된다.
  • 만약 아래와 같은 두 헤더 파일이 존재한다면,
// a.h
void funcA();
int varA;

// b.h
void sayHello();
int varB;
  • 두 헤더 파일을 #include 한 main.cpp의 변화는 아래와 같다.
// -----#include 실행 전 main.cpp-----
#include "a.h"
#include "b.h"

int main() {
    sayHello();
    return 0;
}

// -----#include 실행 후 main.cpp-----
void funcA();
int varA;

void sayHello();
int varB;

int main() {
    sayHello(); // 호출 시 이동할 위치를 연결하는건 링킹 단계에서 진행됨.
    return 0;
}

 

 

꺾쇠(<>) 방식과 따옴표(””) 방식의 차이

  • #include를 통해 지정된 파일을 가져올 때 꺾쇠(<>) 방식따옴표(””) 방식으로 가져올 수 있다. 두 방식의 차이점은 경로가 불완전하게 지정되었을 때 전처리기가 검색하는 경로의 순서가 다르다는 것이다.
  • 두 방식의 차이에 대해 간단히 요약하자면 아래 표와 같다.
구분 검색 순서 용도
꺾쇠 괄호 < > 형식 (#include <파일명>) 시스템 또는 설정된 경로들부터 먼저 찾는다. 시스템이나 표준 라이브러리 헤더 파일을 포함할 때 사용한다.
ex. #include <iostream>
따옴표 " " 형식
(#include "파일명")
현재 파일과 관련된 디렉터리부터 먼저 검색하고, 없으면 시스템 디렉토리에서 찾는다. 사용자 정의 헤더 파일을 포함할 때 사용한다.
ex. #include "myheader.h"

 


#define

#define 식별자(identifieropt, ... , identifieropt) token-string opt
  • #define 지시문은 컴파일러가 원본 파일에서 식별자가 발생할 때마다 토큰 문자열(token-string)을 대체한다.
    • 즉, #define으로 정의한 식별자를 지정한 “문자열”로 치환한다고 보면 된다.
  • #define을 정의할 때 여러 줄을 사용하고 싶다면, 백슬래시(’\’)를 문장 끝마다 붙여 확장한다.
  • 식별자 뒤에 ()를 붙이면 매크로 함수로도 사용할 수 있다.

 

#define 예시

// #define 기본 형태
#define MAXN 1'000'000

// #define 매크로 함수
#define SAYMESSAGE(str) std::cout << "message: " << str << std::endl;

// #define 여러 줄로 확장(백슬래시('\\') 활용)
#define CRASH(cause)                      \\
{                                         \\
    uint32* crash = nullptr;              \\
    __analysis_assume(crash != nullptr);  \\
    *crash = 0xDEADBEEF;                  \\
}

 

 

토큰 붙여 넣기 연산자 ##

  • 토큰 붙여 넣기 연산자인 ##을 활용하면 이어지지 않은 두 토큰 문자열을 하나로 합친다.
    • 예를 들어, A##B처럼 되어있다면, 이는 전처리 단계 이후 AB가 된다.
  • 주로 매크로 안에서 변수 이름을 동적으로 만들 때 사용된다.
#define USING_SHARED_PTR(name)	using name##Ref = std::shared_ptr<class name>;

USING_SHARED_PTR(IocpCore); 
// => using IocpCoreRef = std::shared_ptr<class IocpCore>;

 

 

#define보다 enum을 사용하는 것이 좋은 이유

  • #define을 사용하면 식별자가 문자열로 치환된 상태에서 컴파일되기 때문에 디버깅 시 해당 내용에 대한 추적이 어려워진다.
  • 따라서 디버깅 시 이름과 값 추적에 용이한 enum이나 enum class를 사용하는 것이 권장된다.
#define MODE_PLAY 0
#define MODE_STOP 1

enum class Mode
{
    Play = 0,
    Stop = 1
};

int main(void)
{
    MODE_PLAY;  // 전처리 단계 이후 0으로 치환됨
    Mode::Play; // Mode::Play라는 코드가 유지됨
    return 0;
}

 

 

 


#ifdef, #ifndef

#ifdef 식별자 // => "#if defined 식별자"와 동일
#ifndef 식별자 // => "#if !defined 식별자"와 동일
#endif
  • #ifdef 지시문은 조건부 컴파일 지시문 중 하나로, 특정 식별자가 정의되어 있는지 확인해서 코드 일부를 포함하거나 제외하기 위해 사용한다.
  • #ifdef 이후의 코드는 식별자가 #define으로 정의되었을 때 활성화되며, 반대로 #ifndef 이후의 코드는 식별자가 #define으로 정의되어있지 않을 때 활성화된다.
    • 비슷한 계열인 #if - #elif - #else 지시문들과 혼용해서 사용하기도 한다.
  • 활성화되는 끝 범위는 #endif를 통해 표시한다.

 

#ifdef 예시

#define DEBUG // DEBUG를 #define으로 정의(빈 문자열로 치환됨)

#ifdef DEBUG
    std::cout << "DEBUG 모드입니다." << std::endl;
#elif defined(REALESE)
    std::cout << "REALEASE 모드입니다." << std::endl;
#endif
		
// 실행 결과 => "DEBUG 모드입니다."

 

 

#ifndef를 활용한 헤더 가드(Header Guard)

// "a.h" 해더 파일
#ifndef A_H // A_H는 a.h 헤더 파일을 의미
#define A_H

// 헤더에 포함된 코드...
class A {};

#endif
  • 하나의 소스 파일에 같은 헤더 파일이 중복 포함될 경우, 1) 같은 내용을 여러 번 파싱하여 빌드 시간이 길어지거나, 2) 헤더에 포함된 변수, 함수, 클래스 등이 여러 번 정의되어 중복 정의 오류를 발생한다.
  • 이러한 문제를 방지하기 위해 헤더 파일의 중복 포함을 막아주는 "헤더 가드"가 필요하며, 헤더 가드는 #ifndef를 활용해 구현할 수 있다.
    • 참고로 헤더 가드를 추가했을 때 1) 문제를 해결하여 얻은 이점을 “다중 포함 최적화”, 2) 문제를 해결하여 얻은 이점을 “ODR(One Definition Rule) 위반 방지”라고 한다.
  • 헤더 가드 역할로썬 #pragma once가 더 간편하기 때문에, 최근에는 #ifndef보단 #pragma once를 더 자주 사용한다.

 

#ifndef를 이용한 헤더 가드 패턴 예제

  • #ifndef는 a.h 헤더 파일이 정의되지 않았을 때만 a.h를 #include 하므로,  아래 코드에서 두 번째 #ifndef의 #include 구문은 실행되지 않는다.
// -----#include 실행 전 main.cpp-----
#include "a.h"
#include "a.h"

int main() {}

// -----#include 실행 후 main.cpp-----
#ifndef A_H
#define A_H // => 실행 O

// 헤더에 포함된 코드...
class A {};
#endif
#ifndef A_H
#define A_H // => 실행 X

// 헤더에 포함된 코드...
class A {};
#endif
int main() {}

 

 


이외에도 많은 전처리기 지시문들이 있다.

  • 다른 전처리기 지시문에 대한 자세한 내용은 아래 링크를 확인하면 된다.

https://learn.microsoft.com/ko-kr/cpp/preprocessor/preprocessor-directives?view=msvc-170

 

전처리기 지시문

자세한 정보: 전처리기 지시문

learn.microsoft.com

 

 


참고 자료

https://learn.microsoft.com/ko-kr/cpp/preprocessor/preprocessor-directives?view=msvc-170

https://modoocode.com/103

https://nodiscard.tistory.com/385