XiaoboTalk

C 语音宏定义

宏作为 C 语言强大的预处理特性,在 C 系列开发中有着举足轻重的作用,一个显而易见的作用是底层 API 的跨平台能力。例如 iOS 开发平台的 CGFloat 定义:
#if defined(__LP64__) && __LP64__ #define CGFLOAT_TYPE double #else #define CGFLOAT_TYPE float #endif typedef CGFLOAT_TYPE CGFloat;
上述宏定义,让 CGFloat 在 64 位操作系统中变为 double 类型,在 32 位操作系统中变为 float 类型。
一直以来我对 C 的宏定义理解,都停留在表面;最近花了点时间,搞清楚了宏的大部分写法和使用场景,我主要参考的的是 C 语言进阶这本书
notion image
c 语言进阶
和 Xcode 以及一些开源代码(WebViewJavascriptBridge),下面是我的笔记:
正确的使用宏的关键其实只有一点:宏定义只做替换,不做任何运算和表达式求解,带参宏的参数也遵循这个规则

利用 Xcode 预处理功能展开宏


有时候,我们写完一个宏,想知道展开后到底是什么样的,可以利用 Xcode 的 Preprocess (预处理)功能,下图是展开系统自带宏 MIN(A, B) 示例:
notion image
preprogress

一、简单宏替换


简单宏替换的定义如下:
#define AINT 10
上边定义的宏 AINT ,就是一个简单的宏定义,有的教材也称简单宏为对象宏。简单宏可以用来提升代码可读性,例如:
#define PI 3.1415
也能增强代码的可移植性:
#define INT_SIZE sizeof(int)
不同的编译环境,sizeof(int) 结构可能不相同,用宏来表示,能提高代码的可移植性。

二、带参数宏


带参数宏,因其使用起来像函数调用,有时也被称为函数宏,但和函数调用完全不一样,宏只做替换。带参数定义格式如下:
#define 宏名(参数表)
定义时需要注意以下几点:
1: 宏名和参数表之间不能有括号 2: 宏只做替换,不做计算和表达式求解。
示例:
#define ADD(_A, _B) (_A+_B) int add = ADD(10, 20); // 宏展开:int add = (10+20);
上边的宏用来计算两个数的和,定义时需要注意:
  1. 宏名和参数名最好都大写,必要的情况下命名以 _ 开头。
  1. 定义体 (_A+_B) 中的括号不能少。
第一点,关于命名,要格外注意,因为宏展开时会匹配相同字符,匹配到后进行替换,所以尽量大写字母配合下划线命名,来避免宏替换过程的误伤。这一点在 OC 语言中容易发生:
#define ADDNUM(num1, num2) [self addNum1:num1 num2:num2] - (int)addNum1:(int)num1 num2:(int)num2 { return num1 + num2; } ADDNUM(1, 2); // 此时宏展开会变为:[self addNum1:1 2:2]
上边代码,编译报错,因为宏展开时,会找到所有的 num2 统一替换为 2。很显然 [self addNum1:num1 num2:num2] 中匹配到了两个 num2,展开后变为:[self addNum1:1 2:2]
第二点,宏定义中如果有表达式,一定要加括号,下边示例演示不加括号会发生什么:
#define ADD(_A, _B) _A+_B int c = ADD(1, 2) * 3; // c 为 7
上边的代码,预期 c 是 3*3 = 9;但实际并不是,宏展开会变为:int c = 1+2 * 3; 结果会先做乘法运算 2*3,然后 + 1 得到: 7。因为宏只做替换,不进行表达式求解。

三、宏定义特殊字符:‘#’


‘#’ 在宏定义中,用来将其后出现的宏,变为字符串,举例来说:
#define NAME(x) #x char *name = NAME(Bob); // 展开为:char *name = "Bob";
上边的宏中,‘#’ 将 Bob 转为为一个字符串 “Bob” 。‘#’ 宏用来在 C 代码中插入其他代码:
#define _JSCODEINJECT_(x) #x NSString *jsCode = @_JSCODEINJECT_( ;(function() { if (window.InjectObject) { return; } var globalText = "call from c code"; window.InjectObject = globalText; })(); );
上边的代码,由于使用 ‘#’ ,使代码的可读性和美观性大大增加。同时我们使用了 ‘@’ 语法糖。
# 定义宏的时候注意点:
一般定义一个 # 宏,都会用如下格式:
#define _STRINGYFY(x) #x #define STRINGYFY(x) _STRINGYFY(x)
使用的时候,使用不带下划线的 STRINGYFY(x),这样做,并非多此一举。而是为了产生宏嵌套的时候,也能按照预期展开宏。
我用下边的例子,来说明问题:
#define NAME Bob #define _STRINGYFY(x) #x #define STRINGYFY(x) _STRINGYFY(x) char *myName1 = _STRINGYFY(NAME); char *myName2 = STRINGYFY(NAME); // myName1: "NAME" // myName2: "Bob" ,为预期结果
上边的代码,先定义了对象宏 NAME,随后定义了 _STRINGYFYSTRINGYFY,用来转换字符串; 1: myName1 我们用 _STRINGYFY 宏来定义,结果为 “NAME”,原因是宏展开的时候,从左向右依次展开,所以先展开 _STRINGYFY#NAME ,此时编译器遇到了 # ,所以直接把 # 后边的宏 NAME 替换为字符串 “NAME” 。 2: myName2 我们用 STRINGYFY 宏来定义,展开时,先展开STRINGYFY为:_STRINGYFY(NAME),然后编译器会展开 NAMEBob,最后再次展开_STRINGYFY(Bob),得到 “Bob”。

四、宏定义特殊字符:‘##’


‘##’ 在宏定义中用来连接前后两项参数,例如:
#define CONCAT(A, B) A##B int main(int argc, const char * argv[]) { BOOL CONCAT(is, Correct) = NO; // 展开 BOOL isCorrect = NO; return 0; }
上边的例子,CONCAT 宏将 is Correct 合并为 isCrorrect,但是注意,## 不能直接用来连接字符串,想要连接字符串,需要配合前边说的 # 宏:
#define _STRINGIFY(x) #x #define STRINGIFY(x) _STRINGIFY(x) #define _CONCAT(A, B) A##B #define CONCAT(A, B) _CONCAT(A, B) #define CONCATCOUNTER(A) CONCAT(A, __COUNTER__) int main(int argc, const char * argv[]) { char *param0 = STRINGIFY(CONCATCOUNTER(param)); char *param1 = STRINGIFY(CONCATCOUNTER(param)); char *param2 = STRINGIFY(CONCATCOUNTER(param)); /** 上边宏展开为: char *param0 = "param0"; char *param1 = "param1"; char *param2 = "param2"; */ return 0; }
类似#的定义,为了在嵌套宏中也能让 ## 正常工作,一般也需要先定义_CONCAT(A, B),再次定义CONCAT(A, B)。上边的例子,很好理解,需要说明的是 __COUNTER__,这个宏是预编译宏,初始值为 0,预编译展开一次,值自动自增一次,所以上边的例子会出现 param0 param1 param2,0、1、2 就是__COUNTER__的功劳。
## 的另一个使用场景就是自定义 log 的时候:
#define MYLog(format, ...) NSLog(format, ##__VA_ARGS__)
__VA_ARGS__ 宏表示的是宏定义中的...中的所有剩余参数,这里用 ## 来连接可变参数,当可变参数的个数为零时,## 会自动吃掉 format 后边的 , 号,保证编译正常。

五、换行宏符号 \


\ 宏使用很简单,就是让宏内容能后换行:
#define _STRINGYFY(x) #x #define STRINGYFY(x) \ _STRINGYFY(x)

六、文章开头的 MIN(A, B) 宏


我们将系统的宏 MIN(a, b) 展开如下:
({ __typeof__(a) __a0 = (a); __typeof__(b) __b0 = (b); (__a0 < __b0) ? __a0 : __b0; });
这也正是 GNU C 中 MIN 的标准写法,MIN 宏先用一对 ({...}) 来限制一个作用域,同时这种语句会把最后一行的结果作为返回值,然后在作用域中定义零时变量 __a0, __b0,它们的值就是宏参数 a, b 的值。然后比较这两个零时变量的值,然后进行返回。这样的写法,基本可以兜底各种使用方式,比如:
float a = 1.0f; float b = MIN(a++, 1.5f);
读者可以试着自己写写 MIN(A, B) 宏,然后去体会 GNU C 中 MIN 的标准写。
(全文完)