Menu

C 매크로: #define, 함수형 매크로, 그리고 괄호 규칙

#define으로 만드는 객체형·함수형 매크로 - 모든 인자와 본문 전체에 괄호가 필요한 이유, 다중 평가의 함정, do-while(0)을 쓰는 여러 줄 매크로, 그리고 함수나 const가 더 나은 도구인 경우.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

매크로는 컴파일러가 실행되기 전에 전처리기가 어떤 텍스트로 바꿔주는 이름입니다. 메커니즘은 이게 전부이며, 이 페이지의 모든 규칙은 여기서 파생됩니다. 매크로에는 타입이 없고, 범위를 존중하지 않으며, 인자를 평가하지 않고, 함수 호출이 무엇인지도 모릅니다. 텍스트를 복사할 뿐입니다.

조심해서 쓰면 없어서는 안 될 도구입니다. 이식성 스위치, 줄 번호를 잡아내는 단언 매크로, 배열 크기에 쓸 수 있는 컴파일 타임 상수 같은 것들이죠. 부주의하게 쓰면 파일을 전개해 보기 전까지는 불가능해 보이는 버그를 만들어냅니다.

객체형 매크로

가장 단순한 형태는 어떤 값을 대신하는 이름을 정의합니다.

관례상 이런 이름은 UPPER_SNAKE_CASE로 씁니다. 그래야 읽는 사람이 그 심벌이 변수가 아니라 매크로임을 한눈에 알 수 있습니다. 여기서는 이 관례가 평소보다 더 중요한데, 매크로가 범위를 무시하기 때문입니다. 함수 안의 #define도 파일의 나머지 전체에 적용되며, 다른 함수에 있는 같은 이름의 변수까지 서슴없이 바꿔 씁니다.

#undef NAME은 정의를 제거하므로, 재정의 경고 없이 나중에 같은 이름을 다시 정의할 수 있습니다.

함수형 매크로

이름 바로 뒤에, 여는 괄호 앞에 공백 없이 매개변수 목록을 붙이면 매크로가 인자를 받습니다.

공백 규칙은 진짜입니다. #define SQUARE (x) ((x)*(x))는 치환 텍스트가 (x)로 시작하는 SQUARE라는 이름의 객체형 매크로를 정의합니다. 뒤따르는 컴파일러 오류는 공백에 대해 한마디도 하지 않을 것입니다.

괄호 규칙

두 가지가 있으며, 둘 다 매크로를 작성할 때는 볼 수 없는 표현식 안으로 본문이 붙여 넣어지기 때문에 존재합니다.

규칙 1: 모든 매개변수를 괄호로 감싸라. 그러지 않으면 그 자체가 표현식인 인자가 연산자 우선순위에 의해 찢어집니다.

#define SQUARE_BAD(x) x * x

SQUARE_BAD(2 + 3)   /* 2 + 3 * 2 + 3 이 되어  ==  25가 아니라 11 */

규칙 2: 본문 전체를 괄호로 감싸라. 그러지 않으면 주변 표현식이 결과를 찢어놓습니다.

#define DOUBLE_BAD(x) (x) + (x)

10 / DOUBLE_BAD(5)  /* 10 / (5) + (5) 가 되어  ==  1이 아니라 7 */

두 실패 사례를 고친 버전과 나란히 실행해 봅니다.

잘못된 두 버전 모두 경고 하나 없이 컴파일되고 틀린 숫자를 내놓습니다. 몸에 익혀야 할 습관은 기계적입니다. 각 매개변수를 감싸고, 그다음 결과를 감싸라. ((a) > (b) ? (a) : (b))는 지저분해 보이는데, 그 지저분함이 바로 핵심입니다.

다중 평가의 함정

괄호는 두 번째 위험으로부터는 구해주지 못합니다. 매크로는 매개변수가 나타나는 모든 자리에 인자 텍스트를 붙여 넣으므로, 부수 효과가 있는 인자는 여러 번 실행됩니다.

이 매크로는 ((i++) > (j) ? (i++) : (j))로 전개됩니다. 비교가 i를 6으로 올리면서 5와 3을 비교하고, 참 분기가 i를 다시 7로 올리면서 6을 내놓습니다. 그래서 m은 6, i는 7이 되는데, 둘 다 "ij 중 큰 값"이 줘야 할 결과가 아닙니다. 함수 max(i++, j)였다면 올바르게 동작합니다. 인자가 호출 전에 한 번만 평가되기 때문입니다.

같은 함정이 부수 효과가 있는 모든 것에 걸립니다. SQUARE(rand())rand()를 두 번 호출해서 서로 다른 두 수를 곱합니다. CHECK(read_byte())는 두 바이트를 소비합니다.

선호 순서대로 방어책은 두 가지입니다.

  1. 진짜 함수를 쓰세요. 여러 타입에 필요하다면 타입마다 하나씩 작성하거나, 헤더에 static inline 함수를 두세요.
  2. 반드시 매크로여야 한다면, 인자가 여러 번 평가된다는 사실을 크게 문서화하고 호출하는 자리에 부수 효과를 두지 마세요.

여러 줄 매크로와 do-while(0)

마지막 줄을 제외한 각 줄이 역슬래시로 끝나면 매크로 본문이 여러 줄에 걸칠 수 있습니다. 순진한 버전은 괜찮아 보입니다.

#define LOG_TWICE(msg) \
    printf("%s\n", msg); \
    printf("%s\n", msg)

그러다가 정작 중요한 한 곳에서 조용히 깨집니다.

if (verbose)
    LOG_TWICE("hello");
else
    printf("quiet\n");

치환 후에 if는 첫 번째 printf만 소유하고, 두 번째는 조건과 무관하게 실행되며, else는 짝이 되는 if가 없어집니다. 그 결과는 메시지가 아무 도움도 되지 않는 컴파일 오류입니다. 본문을 그냥 중괄호로 감싸도 나아지지 않습니다. LOG_TWICE("hello") 뒤의 ;if를 끝내는 빈 문장이 되어 else가 또 깨집니다.

제대로 동작하는 관용구는 do { ... } while (0)입니다.

do { ... } while (0)은 하나의 문장이고, 본문을 정확히 한 번 실행하며, 뒤에 세미콜론을 요구합니다. 그래서 호출하는 자리가 평범한 함수 호출처럼 읽히고 모든 제어 흐름 문맥에서 그렇게 동작합니다. 역슬래시를 잘 보세요. 역슬래시 뒤에 공백이 하나라도 있으면 줄 연결이 끊기면서 도무지 알 수 없는 오류가 납니다.

미리 정의된 매크로와 단언 패턴

전처리기는 스스로 몇 가지 매크로를 정의하며, 어떤 것들이 함수로는 도저히 만들 수 없는 이유가 바로 여기에 있습니다.

__FILE____LINE__쓰인 자리에서 전개되므로, 매크로 안에 넣으면 호출한 쪽의 위치를 잡아냅니다. 함수로는 이렇게 할 수 없습니다. 함수 안에서라면 언제나 로깅 함수 자신의 파일과 줄을 보고할 테니까요. 표준 assert 매크로가 실패한 표현식의 위치를 알려주는 방식이 정확히 이것입니다.

그 밖에 쓸모 있는 미리 정의된 이름으로는 __DATE__, __TIME__, 그리고 __STDC_VERSION__(예: C17이면 201710L)이 있습니다.

알아둘 만한 두 연산자

매크로 본문 안에서 #는 매개변수를 문자열 리터럴로 바꾸고("문자열화"), ##는 토큰들을 이어 붙입니다("토큰 붙이기").

SHOW(width * height)printf("width * height" " = %d\n", (width * height));가 됩니다. 인접한 문자열 리터럴은 컴파일러가 이어 붙이므로, 한 번의 호출로 표현식 텍스트와 그 값이 함께 출력됩니다. 기억해 둘 만한 디버깅 요령입니다.

##는 더 드물고 주로 코드 생성 매크로에 나타납니다. #define MAKE_VAR(n) int var_##nMAKE_VAR(3)int var_3으로 바꿉니다. 아껴 쓰세요. 전처리기가 조립한 식별자는 이름으로 검색할 수 없어서 코드를 돌아다니기 어렵게 만듭니다.

매크로 대 함수 대 const

일을 해내는 도구 중 가장 약한 것을 쓰세요.

  • const 변수 - const double PI = 3.14159;는 타입이 있고, 범위를 따르며, 디버거에 나타나고, 무관한 헤더가 재정의할 수 없습니다. 런타임에 쓰는 값이라면 이쪽을 우선하세요.
  • enum - enum { MAX_USERS = 100 };은 C가 컴파일 타임 상수를 요구하는 자리에서 동작하는, 타입과 범위를 갖춘 이름 있는 정수 상수를 줍니다. 정수 한계값에 좋습니다.
  • static inline 함수 - 타입 검사를 받고, 각 인자를 정확히 한 번 평가하며, 요즘 컴파일러는 매크로와 똑같이 인라인해 줍니다. 거의 모든 함수형 매크로의 올바른 대체재입니다.
  • 매크로 - __LINE__이 필요할 때, 타입이 존재하기 전에 동작해야 할 때(예전 코드의 배열 크기), 조건부 컴파일로 코드를 갈아 끼울 때, 또는 반복적인 코드를 생성할 때 씁니다.

매크로가 여전히 경쟁자 없이 쓰이는 한 곳은 배열 크기 계산입니다. 결과가 반드시 컴파일 타임 상수여야 하기 때문입니다.

이 매크로에는 그 자체로 널리 알려진 주의사항이 있습니다. 진짜 배열에 대해서만 올바르다는 것입니다. 포인터를 넘기면(배열을 함수에 건네면 바로 그렇게 됩니다) sizeof가 포인터의 크기를 재서, 틀렸으면서도 당당하게 아무 말 없는 답을 내놓습니다.

매크로 디버깅하기

매크로가 이상하게 동작한다면 노려보지 말고 전개해 보세요.

gcc -E program.c | tail -30

치환된 텍스트를 보면 문제가 빠진 괄호인지, 이중 평가인지, 다른 것과의 이름 충돌인지 즉시 알 수 있습니다. -Wall -Wextra로도 컴파일하세요. GCC와 clang은 "in expansion of macro"라는 메모를 덧붙여, 보고된 오류 줄을 정의가 있는 곳으로 이어 줍니다.

다음은 헤더 파일입니다. 거기서는 #define#include가 결합해, 같은 선언을 두 번 붙여 넣지 않고도 하나의 프로젝트가 여러 소스 파일에 걸칠 수 있게 해 줍니다.

자주 묻는 질문

C에서 매크로란 무엇인가요?

컴파일 전에 전처리기가 소스에 치환해 넣는, 이름이 붙은 텍스트 조각입니다. #define MAX 100은 이후의 모든 MAX100으로 바꾸고, #define SQUARE(x) ((x) * (x))는 인자를 받아 정해진 패턴에 끼워 넣습니다. 매크로에는 타입이 없고 범위 규칙도 따르지 않습니다. 그저 텍스트 치환일 뿐입니다.

C 매크로에는 왜 그렇게 괄호가 많이 필요한가요?

본문이 주변 코드에 붙여 넣어진 뒤 전체가 한꺼번에 파싱되기 때문입니다. #define SQUARE(x) x * xSQUARE(2 + 3)2 + 3 * 2 + 3으로 바꿔서 25가 아닌 11이 됩니다. 모든 인자와 본문 전체를 감싸면(((x) * (x))) 주변에 무엇이 있든 치환된 텍스트가 의도대로 묶입니다.

C에서 매크로와 함수의 차이는 무엇인가요?

함수는 한 번 컴파일되고 타입 검사를 받으며 각 인자를 정확히 한 번 평가합니다. 매크로는 쓰이는 곳마다 붙여 넣어지고 아무것도 검사하지 않으며 인자를 여러 번 평가할 수 있어서, MAX(i++, j)i를 두 번 증가시킬 수 있습니다. 함수(그리고 상수에는 const/enum)를 우선하고, __LINE__을 잡아내거나 코드를 생성하는 등 함수로 할 수 없는 일에만 매크로를 쓰세요.

여러 줄 매크로는 왜 do { ... } while (0)으로 감싸나요?

하나의 문장처럼 동작하게 하기 위해서입니다. 그냥 { ... } 본문은 if/else 안에서 뒤에 세미콜론이 붙으면 깨지고(세미콜론이 if를 일찍 끝냅니다), 중괄호 없는 문장 나열은 중괄호 없는 if 안에서 깨집니다. do { ... } while (0)은 뒤에 세미콜론을 받아들이면서 본문을 정확히 한 번 실행하는 단일 문장입니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기