宏分为两大类:类似对象的宏和类似函数的宏。宏在编译过程的早期被视为令牌替换。这意味着可以将大型(或重复)代码段抽象为预处理器宏。

// This is an object-like macro
#define    PI         3.14159265358979

// This is a function-like macro.
// Note that we can use previously defined macros
// in other macro definitions (object-like or function-like)
// But watch out, its quite useful if you know what you're doing, but the
// Compiler doesnt know which type to handle, so using inline functions instead
// is quite recommended (But e.g. for Minimum/Maximum functions it is quite useful)
#define    AREA(r)    (PI*(r)*(r))

// They can be used like this:
double pi_macro   = PI;
double area_macro = AREA(4.6);

Qt 库利用这种技术创建一个元对象系统,方法是让用户在扩展 QObject 的用户定义类的头部声明 Q_OBJECT 宏。

宏名称通常以全部大写字母书写,以便更容易区分普通代码。这不是一个要求,但仅仅被许多程序员认为是好的风格。

当遇到类似对象的宏时,它会作为简单的复制粘贴操作进行扩展,宏的名称将被其定义替换。遇到类似函数的宏时,会扩展其名称及其参数。

double pi_squared = PI * PI;
// Compiler sees:
double pi_squared = 3.14159265358979 * 3.14159265358979;

double area = AREA(5);
// Compiler sees:
double area = (3.14159265358979*(5)*(5))

因此,类似函数的宏参数通常包含在括号内,如上面的 AREA() 所示。这是为了防止在宏扩展期间可能发生的任何错误,特别是由单个宏参数由多个实际值组成的错误。

#define BAD_AREA(r) PI * r * r

double bad_area = BAD_AREA(5 + 1.6);
// Compiler sees:
double bad_area = 3.14159265358979 * 5 + 1.6 * 5 + 1.6;

double good_area = AREA(5 + 1.6);
// Compiler sees:
double good_area = (3.14159265358979*(5 + 1.6)*(5 + 1.6));

另请注意,由于这种简单的扩展,必须注意传递给宏的参数,以防止意外的副作用。如果在评估期间修改了参数,则每次在扩展宏中使用它时都会对其进行修改,这通常不是我们想要的。即使宏将参数括在括号中以防止扩展破坏任何内容,也是如此。

int oops = 5;
double incremental_damage = AREA(oops++);
// Compiler sees:
double incremental_damage = (3.14159265358979*(oops++)*(oops++));

此外,宏不提供类型安全性,导致难以理解的类型不匹配错误。

由于程序员通常使用分号终止行,因此用作独立行的宏通常被设计为吞下分号; 这可以防止任何意外的错误由额外的分号引起。

#define IF_BREAKER(Func) Func();

if (some_condition)
    // Oops.
    IF_BREAKER(some_func);
else
    std::cout << "I am accidentally an orphan." << std::endl;

在这个例子中,无意的双分号打破了 if...else 块,阻止编译器将 elseif 匹配。为了防止这种情况,宏定义中省略了分号,这将导致它在使用分号后立即吞下分号。

#define IF_FIXER(Func) Func()

if (some_condition)
    IF_FIXER(some_func);
else
    std::cout << "Hooray!  I work again!" << std::endl;

退出尾随分号还允许使用宏而不结束当前语句,这可能是有益的。

#define DO_SOMETHING(Func, Param) Func(Param, 2)

// ...

some_function(DO_SOMETHING(some_func, 3), DO_SOMETHING(some_func, 42));

通常,宏定义在行尾结束。但是,如果宏需要覆盖多行,则可以在行尾使用反斜杠来表示这一点。此反斜杠必须是行中的最后一个字符,它向预处理器指示应将以下行连接到当前行,将它们视为单行。这可以连续多次使用。

#define TEXT "I \
am \
many \
lines."

// ...

std::cout << TEXT << std::endl; // Output:   I am many lines.

这在复杂的类函数宏中尤其有用,它可能需要覆盖多行。

#define CREATE_OUTPUT_AND_DELETE(Str) \
    std::string* tmp = new std::string(Str); \
    std::cout << *tmp << std::endl; \
    delete tmp;

// ...

CREATE_OUTPUT_AND_DELETE("There's no real need for this to use 'new'.")

在更复杂的类函数宏的情况下,给它们自己的范围以防止可能的名称冲突或导致在宏的末尾销毁对象(类似于实际函数)是有用的。一个常见的习惯用语是 0 ,其中宏被包含在 do-while 块中。该块通常带分号,允许它吞下分号。

#define DO_STUFF(Type, Param, ReturnVar) do { \
    Type temp(some_setup_values); \
    ReturnVar = temp.process(Param); \
} while (0)

int x;
DO_STUFF(MyClass, 41153.7, x);

// Compiler sees:

int x;
do {
    MyClass temp(some_setup_values);
    x = temp.process(41153.7);
} while (0);

还有可变宏; 类似于可变参数函数,它们采用可变数量的参数,然后将它们全部展开以代替特殊的 Varargs 参数 __VA_ARGS__

#define VARIADIC(Param, ...) Param(__VA_ARGS__)

VARIADIC(printf, "%d", 8);
// Compiler sees:
printf("%d", 8);

请注意,在扩展期间,__VA_ARGS__ 可以放在定义中的任何位置,并且可以正确扩展。

#define VARIADIC2(POne, PTwo, PThree, ...) POne(PThree, __VA_ARGS__, PTwo)

VARIADIC2(some_func, 3, 8, 6, 9);
// Compiler sees:
some_func(8, 6, 9, 3);

在零参数可变参数的情况下,不同的编译器将以不同方式处理尾随逗号。某些编译器(如 Visual Studio)将在没有任何特殊语法的情况下静默地吞下逗号。其他编译器,如 GCC,要求你在 __VA_ARGS__ 之前立即放置 ##。因此,在关注可移植性时有条件地定义可变参数宏是明智的。

// In this example, COMPILER is a user-defined macro specifying the compiler being used.

#if       COMPILER == "VS"
    #define VARIADIC3(Name, Param, ...) Name(Param, __VA_ARGS__)
#elif     COMPILER == "GCC"
    #define VARIADIC3(Name, Param, ...) Name(Param, ##__VA_ARGS__)
#endif /* COMPILER */