C의 매크로 대 기능


101

나는 항상 매크로를 사용하는 것이 함수를 사용하는 것보다 나은 예와 사례를 보았습니다.

누군가 함수에 비해 매크로의 단점을 예로 설명해 줄 수 있습니까?


21
머리에 질문을 돌리십시오. 어떤 상황에서 매크로가 더 낫습니까? 매크로가 더 낫다는 것을 증명할 수 없다면 실제 함수를 사용하십시오.
David Heffernan

답변:


113

매크로는 텍스트 대체에 의존하고 유형 검사를 수행하지 않기 때문에 오류가 발생하기 쉽습니다. 예를 들어,이 매크로 :

#define square(a) a * a

정수와 함께 사용하면 잘 작동합니다.

square(5) --> 5 * 5 --> 25

그러나 표현식과 함께 사용하면 매우 이상한 일을합니다.

square(1 + 2) --> 1 + 2 * 1 + 2 --> 1 + 2 + 2 --> 5
square(x++) --> x++ * x++ --> increments x twice

인수를 괄호로 묶는 것은 도움이되지만 이러한 문제를 완전히 제거하지는 못합니다.

매크로에 여러 문이 포함 된 경우 제어 흐름 구조에 문제가 발생할 수 있습니다.

#define swap(x, y) t = x; x = y; y = t;

if (x < y) swap(x, y); -->
if (x < y) t = x; x = y; y = t; --> if (x < y) { t = x; } x = y; y = t;

이 문제를 해결하기위한 일반적인 전략은 "do {...} while (0)"루프 안에 문을 넣는 것입니다.

이름은 같지만 의미가 다른 필드를 포함하는 두 개의 구조가있는 경우 동일한 매크로가 두 가지 모두에서 작동하며 이상한 결과가 나타날 수 있습니다.

struct shirt 
{
    int numButtons;
};

struct webpage 
{
    int numButtons;
};

#define num_button_holes(shirt)  ((shirt).numButtons * 4)

struct webpage page;
page.numButtons = 2;
num_button_holes(page) -> 8

마지막으로, 매크로는 디버깅이 어려울 수 있으며, 이해하기 위해 확장해야하는 이상한 구문 오류 또는 런타임 오류 (예 : gcc -E 사용)가 발생할 수 있습니다. 디버거는 다음 예제와 같이 매크로를 단계별로 실행할 수 없기 때문입니다.

#define print(x, y)  printf(x y)  /* accidentally forgot comma */
print("foo %s", "bar") /* prints "foo %sbar" */

인라인 함수와 상수는 매크로와 관련된 이러한 문제를 방지하는 데 도움이되지만 항상 적용 가능한 것은 아닙니다. 매크로가 의도적으로 다형성 동작을 지정하는 데 사용되는 경우 의도하지 않은 다형성을 방지하기 어려울 수 있습니다. C ++에는 매크로를 사용하지 않고 형식이 안전한 방식으로 복잡한 다형성 구조를 만드는 데 도움이되는 템플릿과 같은 여러 기능이 있습니다. 자세한 내용은 Stroustrup의 The C ++ Programming Language 를 참조하십시오.


43
C ++ 광고는 무엇입니까?
Pacerier

4
동의합니다. 이것은 C 질문이며 편견을 추가 할 필요가 없습니다.
ideasman42

16
C ++는 C의 이러한 특정 제한을 해결하기위한 기능을 추가하는 C의 확장입니다. 저는 C ++의 팬이 아니지만 여기서 주제에 해당한다고 생각합니다.
D Coetzee 2014 년

1
매크로, 인라인 함수 및 템플릿은 성능 향상을 위해 자주 사용됩니다. 과도하게 사용되며 코드 팽창으로 인해 성능이 저하되는 경향이있어 CPU 명령 캐시의 효율성이 감소합니다. 이러한 기술을 사용하지 않고도 C에서 빠른 일반 데이터 구조를 만들 수 있습니다.
Sam Watkins

1
ISO / IEC 9899 : 1999 §6.5.1에 ​​따르면, "이전 시퀀스 포인트와 다음 시퀀스 포인트 사이에서 객체는 표현식 평가에 의해 최대 한 번 수정 된 저장된 값을 가져야합니다." (이전 및 후속 C 표준에도 유사한 표현이 있습니다.) 따라서 표현식 x++*x++x두 번 증가한다고 말할 수 없습니다 . 실제로 정의되지 않은 동작을 호출합니다. 즉, 컴파일러가 원하는 모든 작업을 자유롭게 수행 할 수 있습니다 x. 즉, 두 번 또는 한 번 증가 하거나 전혀 증가 하지 않을 수 있습니다. 오류로 인해 중단되거나 악마가 코에서 날아갈 수도 있습니다 .
Psychonaut

39

매크로 기능 :

  • 매크로가 전 처리됨
  • 유형 검사 없음
  • 코드 길이 증가
  • 매크로를 사용하면 부작용이 발생할 수 있습니다.
  • 실행 속도가 더 빠름
  • 컴파일 매크로 이름이 매크로 값으로 대체되기 전
  • 작은 코드가 여러 번 나타나는 경우 유용합니다.
  • 매크로가 컴파일 오류를 확인 하지 않음

기능 특징 :

  • 함수가 컴파일 됨
  • 유형 검사가 완료되었습니다.
  • 코드 길이는 동일하게 유지
  • 부작용 없음
  • 실행 속도가 느림
  • 함수 호출 중 제어 전송이 발생합니다.
  • 큰 코드가 여러 번 나타나는 경우 유용합니다.
  • 함수 검사 컴파일 오류

2
"실행 속도가 더 빠름"참조가 필요합니다. 지난 10 년 동안 어느 정도 유능한 컴파일러라도 성능상의 이점을 제공 할 것이라고 생각한다면 함수를 인라인 할 것입니다.
Voo

1
저수준 MCU (AVR, 즉 ATMega32) 컴퓨팅의 맥락에서 매크로는 함수 호출처럼 호출 스택이 증가하지 않기 때문에 더 나은 선택이 아닌가?
hardyVeles

1
@hardyVeles 그렇지 않습니다. AVR의 경우에도 컴파일러는 코드를 매우 지능적으로 인라인 할 수 있습니다. 예 : godbolt.org/z/Ic21iM
Edward

33

부작용은 큰 것입니다. 다음은 일반적인 경우입니다.

#define min(a, b) (a < b ? a : b)

min(x++, y)

다음으로 확장됩니다.

(x++ < y ? x++ : y)

x동일한 명령문에서 두 번 증가합니다. (및 정의되지 않은 동작)


여러 줄 매크로를 작성하는 것도 고통입니다.

#define foo(a,b,c)  \
    a += 10;        \
    b += 10;        \
    c += 10;

\각 줄 끝에는이 필요합니다 .


매크로는 단일 표현식으로 만들지 않는 한 아무것도 "반환"할 수 없습니다.

int foo(int *a, int *b){
    side_effect0();
    side_effect1();
    return a[0] + b[0];
}

GCC의 표현식 문을 사용하지 않는 한 매크로에서 그렇게 할 수 없습니다. (편집 : 쉼표 연산자를 사용할 수 있습니다 ... 간과 ...하지만 여전히 읽기 어렵습니다.)


운영 순서 : (@ouah 제공)

#define min(a,b) (a < b ? a : b)

min(x & 0xFF, 42)

다음으로 확장됩니다.

(x & 0xFF < 42 ? x & 0xFF : 42)

그러나 &보다 낮은 우선 순위를가집니다 <. 그래서 0xFF < 42먼저 평가됩니다.


5
매크로 정의에 매크로 인수와 함께 괄호를 넣지 않으면 우선 순위 문제가 발생할 수 있습니다. 예 :min(a & 0xFF, 42)
ouah

아 예. 게시물을 업데이트하는 동안 귀하의 댓글을 보지 못했습니다. 저도 언급 할 것 같아요.
Mysticial

14

예 1 :

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

int main() {
  int x = 2;
  int y = SQUARE(x++); // Undefined behavior even though it doesn't look 
                       // like it here
  return 0;
}

이므로:

int square(int x) {
  return x * x;
}

int main() {
  int x = 2;
  int y = square(x++); // fine
  return 0;
}

예 2 :

struct foo {
  int bar;
};

#define GET_BAR(f) ((f)->bar)

int main() {
  struct foo f;
  int a = GET_BAR(&f); // fine
  int b = GET_BAR(&a); // error, but the message won't make much sense unless you
                       // know what the macro does
  return 0;
}

비교 :

struct foo {
  int bar;
};

int get_bar(struct foo *f) {
  return f->bar;
}

int main() {
  struct foo f;
  int a = get_bar(&f); // fine
  int b = get_bar(&a); // error, but compiler complains about passing int* where 
                       // struct foo* should be given
  return 0;
}

13

확실하지 않은 경우 함수 (또는 인라인 함수)를 사용하십시오.

그러나 여기의 답변은 어리석은 사고가 가능하기 때문에 매크로가 악하다는 단순한 견해를 갖는 대신 대부분 매크로의 문제를 설명합니다.
함정을 인식하고이를 피하는 방법을 배울 수 있습니다. 그런 다음 적절한 이유가있을 때만 매크로를 사용하십시오.

특정있다 뛰어난 매크로를 사용하는 이점이있는 경우는 다음과 같습니다 :

  • 아래에 언급 된 일반 함수는 다양한 유형의 입력 인수에 사용할 수있는 매크로를 가질 수 있습니다.
  • 가변 개수의 인수는 C를 사용하는 대신 다른 함수에 매핑 할 수 있습니다 va_args.
    예 : https://stackoverflow.com/a/24837037/432509 .
  • 그들이 할 수있는 선택적 같은 디버그 문자열로 지역 정보를 포함 :
    ( __FILE__, __LINE__, __func__). 사전 / 사후 조건, assert실패 또는 정적 어설 션을 확인하여 부적절한 사용시 코드가 컴파일되지 않도록합니다 (대부분 디버그 빌드에 유용함).
  • 입력 인수를 검사합니다. 입력 인수에 대한 테스트를 수행 할 수 있습니다 (예 : 유형, sizeof, struct캐스팅 전에 멤버가 있는지 확인)
    (다형성 유형에 유용 할 수 있음) .
    또는 배열이 일부 길이 조건을 충족하는지 확인하십시오.
    참조 : https://stackoverflow.com/a/29926435/432509
  • 함수는 유형 검사를 수행하지만 C는 값도 강제합니다 (예 : 정수 / 수동). 드물게 문제가 될 수 있습니다. 입력 인수에 대한 함수보다 더 정확한 매크로를 작성할 수 있습니다. 참조 : https://stackoverflow.com/a/25988779/432509
  • 함수에 대한 래퍼로 사용합니다. 어떤 경우에는 반복을 피하고 싶을 수 있습니다. 예를 들어 ... func(FOO, "FOO");, 문자열을 확장하는 매크로를 정의 할 수 있습니다.func_wrapper(FOO);
  • 호출자 로컬 범위에서 변수를 조작하려는 경우 포인터를 포인터로 전달하는 것은 정상적으로 작동하지만 경우에 따라 매크로를 사용하는 것이 문제가 적습니다.
    (픽셀 당 작업의 경우 여러 변수에 대한 할당은 함수보다 매크로를 선호 할 수있는 예inline 입니다. 함수가 옵션 일 수 있으므로 여전히 컨텍스트에 많이 의존하지만 ) .

물론 이들 중 일부는 표준 C가 아닌 컴파일러 확장에 의존합니다. 즉, 이식성이 떨어지는 코드로 끝날 수 있거나 포함되어야 ifdef하므로 컴파일러가 지원하는 경우에만 활용됩니다.


다중 인수 인스턴스화 방지

매크로에서 오류의 가장 일반적인 원인 중 하나이기 때문에 ( x++예 : 매크로가 여러 번 증가 할 수있는 경우 전달 ) .

인수의 다중 인스턴스화로 부작용을 피하는 매크로를 작성할 수 있습니다.

C11 일반

square다양한 유형으로 작동하고 C11을 지원하는 매크로 를 갖고 싶다면 이렇게 할 수 있습니다.

inline float           _square_fl(float a) { return a * a; }
inline double          _square_dbl(float a) { return a * a; }
inline int             _square_i(int a) { return a * a; }
inline unsigned int    _square_ui(unsigned int a) { return a * a; }
inline short           _square_s(short a) { return a * a; }
inline unsigned short  _square_us(unsigned short a) { return a * a; }
/* ... long, char ... etc */

#define square(a)                        \
    _Generic((a),                        \
        float:          _square_fl(a),   \
        double:         _square_dbl(a),  \
        int:            _square_i(a),    \
        unsigned int:   _square_ui(a),   \
        short:          _square_s(a),    \
        unsigned short: _square_us(a))

문 표현

이것은 GCC, Clang, EKOPath 및 Intel C ++ (MSVC 아님)에서 지원하는 컴파일러 확장입니다 .

#define square(a_) __extension__ ({  \
    typeof(a_) a = (a_); \
    (a * a); })

따라서 매크로의 단점은 처음에는 이러한 매크로를 사용하는 방법을 알아야하며 널리 지원되지 않는다는 것입니다.

한 가지 이점은이 경우 square여러 유형에 대해 동일한 기능을 사용할 수 있다는 것 입니다.


1
"... 넓게 지원됨 .." 말씀하신 표현이 cl.exe에서 지원되지 않는 것 같습니다. (MS의 컴파일러)
gideon jul.

1
@gideon, 올바르게 편집 된 답변이지만 언급 된 각 기능에 대해 컴파일러 기능 지원 매트릭스가 필요한지 확실하지 않습니다.
ideasman42

12

매개 변수 및 코드의 유형 검사가 반복되지 않아 코드가 부풀어 질 수 있습니다. 매크로 구문은 세미콜론 또는 우선 순위가 방해가 될 수있는 이상한 경우로 이어질 수 있습니다. 다음은 일부 매크로 을 보여주는 링크입니다.


6

매크로의 한 가지 단점은 디버거가 확장 된 매크로가없는 소스 코드를 읽으므로 매크로에서 디버거를 실행하는 것이 반드시 유용하지는 않다는 것입니다. 말할 필요도없이, 함수에서 할 수있는 것처럼 매크로 내부에 중단 점을 설정할 수 없습니다.


여기서 중단 점은 매우 중요한 거래입니다. 지적 해 주셔서 감사합니다.
Hans

6

함수는 유형 검사를 수행합니다. 이것은 당신에게 추가적인 안전 층을 제공합니다.


6

이 답변에 추가 ..

매크로는 전처리기에 의해 프로그램에 직접 대체됩니다 (기본적으로 전 처리기 지시문이기 때문에). 따라서 필연적으로 각 기능보다 더 많은 메모리 공간을 사용합니다. 반면에 함수를 호출하고 결과를 반환하는 데 더 많은 시간이 필요하며 매크로를 사용하여 이러한 오버 헤드를 피할 수 있습니다.

또한 매크로에는 다른 플랫폼에서 프로그램 이식성을 도울 수있는 몇 가지 특수 도구가 있습니다.

매크로는 함수와 달리 인수에 대한 데이터 유형을 할당 할 필요가 없습니다.

전반적으로 프로그래밍에서 유용한 도구입니다. 그리고 상황에 따라 매크로 명령어와 함수를 모두 사용할 수 있습니다.


3

위의 답변에서 내가 매우 중요하다고 생각하는 매크로보다 함수의 장점 중 하나는 눈치 채지 못했습니다.

함수는 인수로 전달할 수 있지만 매크로는 전달할 수 없습니다.

구체적인 예 : 다른 문자열 내에서 검색 할 명시적인 문자 목록이 아닌 허용 할 표준 'strpbrk'함수의 대체 버전을 작성하려고합니다. 문자가 다음과 같을 때까지 0을 반환하는 (a에 대한 포인터) 함수 일부 테스트 (사용자 정의)를 통과 한 것으로 나타났습니다. 이렇게하려는 한 가지 이유는 다른 표준 라이브러리 함수를 이용할 수 있기 때문입니다. 구두점으로 가득 찬 명시적인 문자열을 제공하는 대신 ctype.h의 'ispunct'를 대신 전달할 수 있습니다. 'ispunct'가 다음과 같이 구현 된 경우 매크로라면 작동하지 않습니다.

다른 많은 예가 있습니다. 예를 들어, 함수가 아닌 매크로로 비교를 수행하는 경우 stdlib.h의 'qsort'에 전달할 수 없습니다.

파이썬에서 유사한 상황은 버전 2 대 버전 3에서 '인쇄'입니다 (비통과 성 문 대 통행 할 수있는 함수).


1
이 답변에 감사드립니다
Kyrol 2018-04-26

1

함수를 매크로에 인수로 전달하면 매번 평가됩니다. 예를 들어 가장 많이 사용되는 매크로 중 하나를 호출하는 경우 :

#define MIN(a,b) ((a)<(b) ? (a) : (b))

그렇게

int min = MIN(functionThatTakeLongTime(1),functionThatTakeLongTime(2));

functionThatTakeLongTime은 성능을 크게 떨어 뜨릴 수있는 5 번 평가됩니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.