IT · 프로그래밍 기초

#define이란? C·C++ 상수와 매크로 기초

최종 업데이트: 2026년 8월 27일

#define은 C와 C++에서 컴파일 전에 텍스트를 바꾸는 전처리기 지시문입니다. 짧고 편리하지만, 함수나 상수와 다르게 동작하므로 괄호와 이름 규칙을 지키는 습관이 중요합니다.

#define과 전처리기 흐름을 설명하는 정보 카드
매크로는 컴파일 이전에 소스 코드에 적용됩니다.

#define은 무엇을 하나요?

소스 파일을 컴파일하기 전에 전처리기가 먼저 실행됩니다. 이 단계에서 #include, #if, #define 같은 지시문을 처리합니다. #define으로 정한 이름을 만나면 전처리기는 그 이름을 지정한 텍스트로 바꿉니다. 즉 일반 변수처럼 값을 저장하는 기능이 아니라, 코드의 표현을 치환하는 기능에 가깝습니다.

#define MAX_USERS 100
#define APP_NAME "고해"

int count = MAX_USERS;

위 코드에서 컴파일러가 보는 count 초기화 부분은 사실상 int count = 100;과 같습니다. 이름을 한 곳에서 관리할 수 있다는 점이 장점이지만, 자료형과 범위 검사 같은 언어 기능을 제공하지 않는다는 차이도 있습니다.

상수 매크로와 const, constexpr의 차이

단순한 숫자나 문자열을 이름으로 관리할 때는 매크로를 쓸 수 있습니다. 다만 C++에서는 대체로 const 또는 constexpr가 더 안전합니다. 이들은 자료형을 가지며, 디버거와 컴파일러가 더 많은 오류를 찾아줄 수 있기 때문입니다.

// C 스타일 상수 매크로
#define TAX_RATE 0.1

// C++에서 권장되는 방식
constexpr double taxRate = 0.1;

C 코드나 오래된 라이브러리 헤더에서는 대문자 매크로가 흔합니다. 하지만 새 C++ 코드에서는 컴파일 시점 상수가 필요할 때 constexpr를 우선 검토하는 편이 읽기 쉽고 안전합니다. 프로젝트의 기존 규칙이 있다면 그 규칙을 먼저 따르는 것이 좋습니다.

함수형 매크로가 위험해지는 이유

인자를 받는 매크로는 함수처럼 보이지만 함수 호출이 아닙니다. 전달한 식이 그대로 복사되므로, 괄호가 빠지거나 부작용이 있는 식을 넘기면 예상과 다른 결과가 나올 수 있습니다.

#define SQUARE(x) x * x

int a = SQUARE(1 + 2);  // 1 + 2 * 1 + 2 → 5

의도한 결과는 9이지만, 치환 뒤 연산자 우선순위 때문에 5가 됩니다. 매크로를 꼭 써야 한다면 인자 전체와 결과 전체를 괄호로 감싸야 합니다.

#define SQUARE(x) ((x) * (x))

int b = SQUARE(1 + 2);  // ((1 + 2) * (1 + 2)) → 9
함수형 매크로가 코드로 치환되는 과정을 보여주는 안내 그림
매크로는 인자를 평가하는 것이 아니라 소스 텍스트를 먼저 펼칩니다.

증가 연산자를 인자로 넘기지 마세요

괄호를 잘 넣어도 모든 문제가 해결되지는 않습니다. 예를 들어 SQUARE(i++)i++가 두 번 들어간 코드로 치환됩니다. 한 번만 증가할 것으로 생각했다면 버그가 됩니다. 함수형 매크로에 ++, --, 함수 호출처럼 실행 결과가 변할 수 있는 식을 넘기지 않는 이유입니다.

C++에서는 템플릿이나 인라인 함수가 훨씬 나은 대안입니다. 인라인 함수는 인자를 한 번만 평가하면서도 컴파일러가 최적화를 적용할 수 있습니다.

template<typename T>
constexpr T square(T value) {
  return value * value;
}

여러 문장을 담는 매크로 작성법

디버그 로그처럼 여러 문장을 하나의 매크로로 묶어야 할 때도 있습니다. 이때는 do { ... } while (0) 형태를 자주 사용합니다. 호출하는 쪽에서 일반 함수처럼 세미콜론을 붙여도 if 문과 안전하게 결합되도록 하기 위한 관례입니다.

#define LOG_VALUE(value) do { \
  printf("value: %d\n", (value)); \
} while (0)

다만 로그, 플랫폼 분기, 컴파일 옵션처럼 전처리기가 꼭 필요한 상황인지 먼저 판단하세요. 단순 로직을 매크로로 만들수록 검색, 리팩터링, 디버깅이 어려워집니다.

이름과 범위에서 주의할 점

매크로는 네임스페이스나 클래스 범위를 따르지 않고, 정의된 뒤부터 #undef되기 전까지 넓게 영향을 줍니다. 흔한 이름인 MIN, MAX, ERROR는 다른 라이브러리와 충돌하기 쉽습니다. 프로젝트 접두어를 붙인 GOHAE_MAX_ITEMS처럼 구체적인 이름이 낫습니다.

헤더 파일에서 매크로를 제공한다면 중복 포함 방지도 함께 고려해야 합니다. 현대 C++에서는 #pragma once를 쓰기도 하고, 이식성이 중요하면 전통적인 include guard를 사용합니다.

#ifndef GOHAE_CONFIG_H
#define GOHAE_CONFIG_H

#define GOHAE_MAX_ITEMS 64

#endif

작성 전 체크리스트

  • 단순 상수라면 C++의 constexpr 또는 const를 먼저 검토합니다.
  • 함수형 매크로의 인자와 전체 식을 괄호로 감쌉니다.
  • i++처럼 부작용이 있는 식을 인자로 넘기지 않습니다.
  • 이름 충돌을 피하도록 프로젝트 접두어와 대문자 규칙을 사용합니다.
  • 여러 문장 매크로는 do { ... } while (0) 관례를 적용합니다.
  • 전처리기 기능이 꼭 필요한지, 함수나 템플릿으로 바꿀 수 있는지 다시 확인합니다.
#define 매크로를 작성하기 전 점검할 항목 안내 그림
작은 매크로라도 치환 결과를 한 번 펼쳐 보면 오류를 줄일 수 있습니다.

정리

#define은 컴파일 전 코드를 바꾸는 강력한 도구입니다. 헤더 구성, 조건부 컴파일, 플랫폼별 설정처럼 전처리기만 할 수 있는 작업에는 여전히 유용합니다. 반면 상수와 일반 로직에는 자료형과 범위를 제공하는 언어 기능이 더 적합한 경우가 많습니다.

매크로를 사용할 때는 “치환된 뒤의 코드가 무엇인가”를 기준으로 읽어 보세요. 괄호, 부작용, 이름 충돌 세 가지만 꾸준히 점검해도 초보 단계에서 자주 겪는 문제를 크게 줄일 수 있습니다.