Skip to content

67. 매크로(Macro)


1. 키워드

  • 매크로(Macro)
  • C 전처리기(C Preprocessor)


2. 매크로 사용하기

  • C에서는 컴파일러에 특정 작업을 지시하는 전처리기를 제공한다.
  • 보통 전처리기는 반복되는 값이나 작업을 미리 정의할 때 사용하며 컴파일 옵션 설정이나 조건부 컴파일도 가능하다.
  • 이번에는 전처리기에서 #define으로 매크로를 정의하는 방법을 알아보자.


3. 매크로 정의하기

  • 배열이나 동적 메모리를 할당한 포인터는 반복문과 함께 사용하는 경우가 많다.
  • 보통 배열, 메모리 크기만큼 반복하면서 값을 출력하거나 저장한다.


  • 간단하게 배열을 선언한 뒤 반복문으로 문자를 하나씩 저장하고 출력해 보자.


#include <stdio.h>

int main()
{
    char s1[10]; // 문자 10개 크기의 배열 선언

    for (int i = 0; i < 10; i++) // 배열 크기만큼 반복
    {
        s1[i] = 97 + i;
    }

    for (int i = 0; i < 10; i++) // 배열 크기만큼 반복
    {
        printf("%c ", s1[i]);
    }

    return 0;
}

// a b c d e f g h i j


  • 이 코드의 문제점은 103번 반복된다는 점이다.
  • 만약 배열의 크기를 바꾸면 반복문에 있는 10도 전부 바꿔야 한다.
  • 특히, 10은 배열 s1의 크기를 뜻하지만, 코드에 내용이 더 추가되어서 배열의 크기와는 별개로 10을 사용할 수도 있다.
  • 즉, 의미가 서로 다른 10이 섞여서 사용되면 각각의 의미가 헷갈리기 쉽다.


  • 이런 문제를 해결할 때는 매크로를 정의하면 된다.
  • #define 지시자는 값을 다른 이름으로 정의하며 이름을 짓는 방법은 변수와 같다.
  • 보통 매크로는 대문자를 사용한다.


#define 매크로이름 값
#include <stdio.h>

#define ARRAY_SIZE 10 // 10을 ARRAY_SIZE로 정의

int main()
{
    char s1[ARRAY_SIZE]; // 10 대신 ARRAY_SIZE 매크로 사용

    for (int i = 0; i < ARRAY_SIZE; i++) // 10 대신 ARRAY_SIZE 매크로 사용
    {
        s1[i] = 97 + i;
    }

    for (int i = 0; i < ARRAY_SIZE; i++) // 10 대신 ARRAY_SIZE 매크로 사용
    {
        printf("%c ", s1[i]);
    }

    return 0;
}

// a b c d e f g h i j


  • 먼저 #define을 사용하여 값 10ARRAY_SIZE로 정의했다.


#define ARRAY_SIZE 10 // 10을 ARRAY_SIZE로 정의


  • 이제 10을 써야 할 자리에 앞에서 정의한 ARRAY_SIZE 매크로를 사용했다.


char s1[ARRAY_SIZE]; // 10 대신 ARRAY_SIZE 매크로 사용

for (int i = 0; i < ARRAY_SIZE; i++) // 10 대신 ARRAY_SIZE 매크로 사용
{
    s1[i] = 97 + i;
}

for (int i = 0; i < ARRAY_SIZE; i++) // 10 대신 ARRAY_SIZE 매크로 사용
{
    printf("%c ", s1[i]);
}


  • #define은 컴파일 직전에 처리되므로 전처리기 과정을 거치면 ARRAY_SIZE10으로 바뀌게 된다.
  • 바뀐 코드는 전처리기 과정 때 임시로 생성될 뿐 원본 코드에 영향을 주지 않는다.


001


  • 이처럼 연관되는 값이 계속 사용될 때는 값을 일일이 입력하기 보다는 매크로를 정의하여 사용하는 것이 좋다.
  • 만약 배열 s1 크기를 변경하고 싶다면 #define 부분의 10을 수정하면 ARRAY_SIZE 매크로가 사용된 부분에 일괄 적용된다.


매크로를 매크로로 정의하기

  • #define으로 정의한 매크로는 다른 이름으로 다시 정의할 수 있다.


#define ARRAY_SIZE 10                 // 10을 ARRAY_SIZE로 정의
#define DEFAULT_ARRAY_SIZE ARRAY_SIZE // ARRAY_SIZE를 DEFAULT_ARRAY_SIZE로 정의


  • 이렇게 정의하면 결과적으로 DEFAULT_ARRAY_SIZE10이다.


  • #define으로 정의한 매크로를 해제하고 싶을 때는 #undef를 사용한다.


#undef 매크로이름
#include <stdio.h>

#define COUNT 10 // 10을 COUNT로 정의

int main()
{
    printf("%d\n", COUNT);

#undef COUNT     // 앞에서 정의한 COUNT 해제
#define COUNT 20 // 20을 COUNT로 정의

    printf("%d\n", COUNT); // #undef로 COUNT를 해제한 뒤 20을 COUNT로 정의했으므로 20이 출력됨

    return 0;
}

// 10
// 20


  • #define으로 정의한 매크로를 해제(삭제)하고 싶다면 #undef에 매크로 이름을 지정해 주면 된다.
  • 만약 #undef로 해제된 매크로를 계속 사용하면 컴파일 에러가 발생한다.
  • 여기서는 #undef를 사용하여 COUNT를 해제한 뒤 다시 #define을 사용하여 20COUNT로 정의했다.


4. 함수 모양의 매크로 정의하기

  • #define은 함수 모양의 매크로도 정의할 수 있다.


#define 매크로이름(x) 함수(x)
#define 매크로이름(x) 코드조합
#include <stdio.h>

#define PRINT_NUM(x) printf("%d\n", x) // printf("%d\n", x)를 PRINT_NUM(x)로 정의

int main()
{
    PRINT_NUM(10); // printf("%d\n", 10)

    PRINT_NUM(20); // printf("%d\n", 20)

    return 0;
}

// 10
// 20


  • #define으로 함수 모양의 매크로를 정의할 때는 ()(괄호)안에 자료형은 생략하고 인수의 이름만 지정한다.
  • 그리고 호출할 함수를 작성한 뒤 매크로에 지정했던 인수를 그대로 함수 안에 넣으면 된다.


002


  • 전처리기 과정을 거치면 PRINT_NUM(x)는 다음과 같이 printf("%d\n", 10), printf("%d\n", 20)으로 바뀌게 된다.


003


함수를 사용하지 못 하도록 만들려면?

  • #define을 사용하면 함수가 아무 동작도 하지 않도록 만들 수 있다.
  • 다음과 같이 함수 이름으로 된 빈 매크로를 지정하면 이후에 해당 이름으로 함수를 호출해도 사용할 수 없게 된다.


#define printf // printf를 빈 매크로로 정의

printf("Hello, world!"); // 아무것도 출력되지 않음


5. 여러 줄을 묶어서 매크로로 정의하기

  • 지금까지 숫자 한 개나 코드 한 줄을 매크로로 정의해서 사용했다.
  • #define은 줄바꿈이 일어날 때 \를 사용하여 여러 줄을 매크로로 만들 수 있다.
  • 단, 맨 마지막 줄은 \를 사용하지 않아도 된다.


#define 매크로이름 코드1 \
                코드2 \
                코드3
#include <stdio.h>

// printf 세 줄을 PRINT_NUM3으로 정의
#define PRINT_NUM3(x)      \
    printf("%d\n", x);     \
    printf("%d\n", x + 1); \
    printf("%d\n", x + 2);

int main()
{
    PRINT_NUM3(10);

    PRINT_NUM3(20);

    return 0;
}

// 10
// 11
// 12
// 20
// 21
// 22


  • printf 세 줄을 묶어서 PRINT_NUM3으로 정의했다.
  • 여기서는 두 번째 printf에는 1을 더했고, 세 번째 printf에는 2를 더했다.


// printf 세 줄을 PRINT_NUM3으로 정의
#define PRINT_NUM3(x)      \
    printf("%d\n", x);     \
    printf("%d\n", x + 1); \
    printf("%d\n", x + 2);


  • 전처리기 과정을 거치면 PRINT_NUM3(x)는 다음과 같이 printf 세 줄로 바뀌게 된다.


004


  • 여러 줄로 된 매크로를 사용할 때는 조건문, 반복문 사용에 주의해야 한다.


#include <stdio.h>

#define PRINT_NUM3(x)      \
    printf("%d\n", x);     \
    printf("%d\n", x + 1); \
    printf("%d\n", x + 2);

int main()
{
    int num1 = 1;

    if (num1 == 2)
        PRINT_NUM3(10);

    return 0;
}

// 11
// 12


  • 소스 코드를 실행했을 때 num11이므로 다음 if 조건문은 실행되지 않아야 하지만 11, 12가 출력되었다.


  • 이 코드가 전처리기 과정을 거치면 다음과 같이 if 아래 줄에 printf 세 줄이 생긴다.
  • 자세히 보면 첫 번째 줄 printfif의 영향을 받고 나머지 두 줄은 영향을 받지 않는다는 것을 알 수 있다.


int num1 = 1;

if (num1 == 2)
    printf("%d\n", 10);     // ← 첫 째줄만 if의 영향을 받음
    printf("%d\n", 10 + 1); // ← 두 번째 세 번째 줄은 if의 조건과는 상관없이 무조건 실행됨
    printf("%d\n", 10 + 2); // ← 두 번째 세 번째 줄은 if의 조건과는 상관없이 무조건 실행됨


  • 따라서 if 조건문에 여러 줄로 된 매크로를 사용할 때는 반드시 {}(중괄호)로 묶어줘야 한다.


005


  • if 조건문뿐만 아니라 for, while 반복문도 여러 줄로 된 매크로를 사용할 때는 반드시 {}로 묶어줘야 한다.


for (int i = 0; i < 10; i++)
{ // 중괄호로 묶어줌
    PRINT_NUM3(10); // 여러 줄로 된 매크로
}


6. 스왑 매크로 정의하기

  • 이번에는 두 변수의 값을 바꾸는 매크로를 정의하는 방법이다.
  • 매크로를 정의하기 전에 먼저 함수로 만들어보자.


void swap(int *a, int *b)
{
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
}


  • 이 함수의 단점은 바꿀 수 있는 변수의 자료형이 int 타입으로 고정되어 있다는 점이다.
  • 매개변수를 int 타입 포인터 대신 void 타입 포인터로 지정하고, 자료형의 종류를 열거형 등으로 받아도 되지만 코드가 길어진다.


  • #define으로 매크로를 만들 때 do while을 사용하면 모든 자료형을 만족하면서 두 변수의 값을 바꿀 수 있다.


#include <stdio.h>

// 매크로 안에서 변수를 선언할 수 있도록 do while 사용
// a와 b의 값을 서로 바꿈
#define SWAP(a, b, type) \
    do                   \
    {                    \
        type temp;       \
        temp = a;        \
        a = b;           \
        b = temp;        \
    } while (0)

int main()
{
    int num1 = 10;
    int num2 = 20;

    SWAP(num1, num2, int);         // 값을 바꿀 자료형으로 int 타입을 지정
    printf("%d %d\n", num1, num2); // 두 변수의 값이 바뀜

    float num3 = 1.5f;
    float num4 = 3.8f;

    SWAP(num3, num4, float);       // 값을 바꿀 자료형으로 float 타입을 지정
    printf("%f %f\n", num3, num4); // 두 변수의 값이 바뀜

    return 0;
}

// 20 10
// 3.800000 1.500000


  • #define으로 SWAP을 정의할 때 바꿀 변수가 들어갈 ab 그리고 자료형이 들어갈 type을 지정해 준다.
  • 함수에서는 자료형을 매개변수에 그대로 전달할 수 없지만, 매크로는 전달하는 것이 아니라 지정한 코드 자체가 바뀌게 되므로 자료형도 지정할 수 있다.


// 매크로 안에서 변수를 선언할 수 있도록 do while 사용
// a와 b의 값을 서로 바꿈
#define SWAP(a, b, type) \
    do                   \
    {                    \
        type temp;       \
        temp = a;        \
        a = b;           \
        b = temp;        \
    } while (0)


  • 다음은 SWAP(num1, num2, int);와 같이 int 타입 변수 num1, num2와 자료형 int 타입을 지정한 뒤 전처리기를 거쳤을 때 바뀐 코드를 나타낸다.
  • 즉, type 부분이 자료형 int 타입으로 바뀌면서 temp 변수를 선언하게 된다.
  • ab도 지정된 변수로 바뀌므로 자료형은 따지지 않아도 된다.


do {
//   ↓ type 부분이 자료형으로 바뀌므로 지정한 자료형으로 변수를 선언할 수 있음
    int temp;
    temp = num1;
    num1 = num2;
    num2 = temp;
} while (0);


  • 여기서 눈 여겨볼 부분은 do while (0)이다.
  • 별 의미없는 코드 같지만 실제로는 중요한 역할을 담당한다.
  • do while (0){}로 묶여있기 때문에 안에서 변수를 마음대로 선언할 수 있고, 선언된 변수는 do while (0)을 벗어나면 변수가 사라진다.
  • 즉, SWAP 매크로를 계속 사용했을 때 같은 이름으로 된 변수가 여러 개 생기지만 컴파일 에러를 방지할 수 있다.


  • 다음은 SWAP(num1, num2, int);SWAP(num3, num4, float);이 전처리기를 거쳤을 때 바뀐 코드를 나타낸다.


do {
//   ↓ 이름이 같은 변수가 두 개 선언되지만 do while (0) 안이므로 컴파일 에러가 발생하지 않음
    int temp;
    temp = num1;
    num1 = num2;
    num2 = temp;
} while (0);

do {
//    ↓ 이름이 같은 변수가 두 개 선언되지만 do while (0) 안이므로 컴파일 에러가 발생하지 않음
    float temp;
    temp = num3;
    num3 = num4;
    num4 = temp;
} while (0);


  • 즉, 변수 temp가 두 개 선언되지만 do while (0) 안이므로 컴파일 에러가 발생하지 않는다.
  • 만약 SWAP 정의에서 do while (0){}를 삭제하고 컴파일하면 temp가 재정의되었다는 컴파일 에러가 발생하게 된다.


  • 특히 매크로에서 do while (0)을 사용하면 if 조건문과 for, while 반복문에서도 문제없이 사용할 수 있다.


if (num1 == 10)
    SWAP(num1, num2, int);
else
    printf("10이 아님\n");


  • 이 코드가 전처리기를 거치면 다음과 같이 된다.


if (num1 == 10)
    do {
        int temp;
        temp = num1;
        num1 = num2;
        num2 = temp;
    } while(0);
else
    printf("10이 아님\n");


  • 만약 SWAP 매크로를 do while (0) 대신 {}로만 정의하면 전처리기를 거쳤을 때 잘못된 코드가 나온다.


// 중괄호로만 매크로 정의
#define SWAP(a, b, type) { \
    type temp; \
    temp = a;  \
    a = b;     \
    b = temp;  \
}

if (num1 == 10)
    SWAP(num1, num2, int);
else
    printf("10이 아님\n");


  • 이 코드가 전처리기를 거치면 다음과 같이 }(닫는 중괄호) 뒤에 ;(세미콜론)이 붙어서 컴파일 에러가 발생한다.


if (num1 == 10)
    {
        int temp;
        temp = num1;
        num1 = num2;
        num2 = temp;
    };    // 세미콜론 때문에 컴파일 에러가 발생
else
    printf("10이 아님\n");


7. 매크로와 연산자 우선순위 알아보기

  • 매크로를 정의할 때는 연산자 우선순위를 주의해야 한다.
  • 먼저 두 수를 곱하는 매크로와 더하는 매크로를 정의해 보자.


#include <stdio.h>

// a와 b를 곱함
#define MUL(a, b) a * b

// a와 b를 더함
#define ADD(a, b) a + b

int main()
{
    printf("%d\n", MUL(10, 20));       // 10 * 20
    printf("%d\n", MUL(1 + 2, 3 + 4)); // 1 + 6 + 4, 2 * 3이 먼저 계산됨

    printf("%d\n", ADD(1, 2));     // 1 + 2
    printf("%d\n", ADD(1, 2) * 3); // 1 + 6, 2 * 3이 먼저 계산됨

    return 0;
}

// 200
// 11
// 3
// 7


  • MULab를 곱하고, ADDab를 더하도록 정의되어 있다.


// a와 b를 곱함
#define MUL(a, b) a * b

// a와 b를 더함
#define ADD(a, b) a + b


  • 이제 매크로를 사용해 보면 MUL(10, 20)200이 잘 나오지만 MUL(1 + 2, 3 + 4)3 * 7의 결과인 21이 나오지 않고 11이 나왔다.
  • 마찬가지로 ADD(1, 2)는 결과가 잘 나오지만 ADD(1, 2) * 39가 나와야 하는데 7이 나왔다.


printf("%d\n", MUL(10, 20));       // 10 * 20
printf("%d\n", MUL(1 + 2, 3 + 4)); // 1 + 6 + 4, 2 * 3이 먼저 계산됨

printf("%d\n", ADD(1, 2));     // 1 + 2
printf("%d\n", ADD(1, 2) * 3); // 1 + 6, 2 * 3이 먼저 계산됨


  • 왜냐하면 MUL이 전처리기를 거쳤을 때 다음과 같은 모양이 되므로 연산자 우선순위에 따라 2 * 3이 먼저 계산된다.
  • 즉, 1 + 6 + 4라서 11이 나오게 된다.


/*
  a       b
  ↓       ↓    */
1 + 2 * 3 + 4
//    ↑ 먼저 계산됨


  • 마찬가지로 ADD도 전처리기를 거치면 숫자와 연산자가 그냥 나열된다.
  • 여기서도 연산자 우선순위에 따라 덧셈보다 곱셈이 먼저 계산되므로 2 * 3이 먼저 계산된다.
  • 1 + 6이라 7이 된다.


/*
a   b
↓   ↓     */
1 + 2 * 3
//    ↑ 먼저 계산됨


  • 이렇게 연산자 우선순위에 따라서 의도치 않게 계산 결과가 바뀌는 문제를 해결하려면 매크로의 인수와 결과를 모두 ()로 묶어주면 된다.


#include <stdio.h>

// a와 b, 결과를 모두 괄호로 묶어줌
#define MUL(a, b) ((a) * (b))

// a와 b, 결과를 모두 괄호로 묶어줌
#define ADD(a, b) ((a) + (b))

int main()
{
    printf("%d\n", MUL(10, 20));       // 10 * 20
    printf("%d\n", MUL(1 + 2, 3 + 4)); // 3 * 7

    printf("%d\n", ADD(1, 2));     // 1 + 2
    printf("%d\n", ADD(1, 2) * 3); // 3 * 3

    return 0;
}

// 200
// 21
// 3
// 9


  • MULADD를 정의할 때 ab, 결과를 모두 ()로 묶어줬다.


// a와 b, 결과를 모두 괄호로 묶어줌
#define MUL(a, b) ((a) * (b))

// a와 b, 결과를 모두 괄호로 묶어줌
#define ADD(a, b) ((a) + (b))


  • 이제 MUL(1 + 2, 3 + 4)는 전처리기를 거쳐도 ((1 + 2) * (3 + 4))와 같은 모양이 되므로 연산자 우선순위에 영향을 받지 않고 의도대로 계산이 된다.
  • 마찬가지로 ADD(1, 2) * 3((1) + (2)) * 3이 되어 덧셈이 먼저 계산된다.


8. 매크로 연결 사용하기

  • #define에서 ##을 사용하면 여러 코드(값)을 붙일 수 있다.


#define 매크로이름(a, b) a##b
#include <stdio.h>

// a와 b를 붙이는 CONCAT 매크로 정의
#define CONCAT(a, b) a##b

int main()
{
    printf("%d\n", CONCAT(1, 2));

    return 0;
}

// 12


  • 먼저 다음과 같이 ##을 사용하여 ab를 붙이는 CONCAT 매크로를 정의한다.


// a와 b를 붙이는 CONCAT 매크로 정의
#define CONCAT(a, b) a##b


  • printfCONCAT(1, 2)를 출력해 보면 12가 나온다.
  • 즉, 12를 더해서 3이 나오는 것이 아니라 12를 그대로 붙여서 12가 된다.


006


  • ##을 좀 더 응용하면 다음과 같이 매크로로 함수를 호출할 수도 있다.


#include <stdio.h>

// hello와 x를 붙여서 호출하는 EXECUTOR 매크로 정의
#define EXECUTOR(x) hello##x()

void hello1()
{
    printf("hello 1\n");
}

void hello2()
{
    printf("hello 2\n");
}

int main()
{
    EXECUTOR(1); // hello1 함수 호출

    EXECUTOR(2); // hello2 함수 호출

    return 0;
}

// hello 1
// hello 2


  • 먼저 다음과 같이 hellox를 붙이는 EXECUTOR 매크로를 정의했다.
  • 이때 x 뒤에 ()를 붙여 함수 호출이라는 것을 나타내준다.


// hello와 x를 붙여서 호출하는 EXECUTOR 매크로 정의
#define EXECUTOR(x) hello##x()


  • EXECUTOR1을 넣으면 hello1이 붙게 되므로 hello1 함수를 호출하고, 2를 넣으면 hello2 함수를 호출한다.


EXECUTOR(1); // hello1 함수 호출

EXECUTOR(2); // hello2 함수 호출


007


References