在 C 和 C 中#define 和 typedef 都可以用来给现有的类型起一个别名但其实他们两个有着本质上的区别本文将带你彻底区分 #define 和 typedef 。1. 核心本质差异我们先讲最干货的让你知道他们在底层到底有什么不同。#define#define本质上只是简单的文本替换。在预处理阶段也就是说编译器真正开始编译代码之前预处理器就会把代码中所有出现宏名称的地方按照宏定义替换成相应的内容。typedeftypedef本质上是为一个已有的数据类型创建一个新的别名。它不仅仅是简单的文本替换它实际上产生了一个新的类型名称。在编译阶段编译器会将用typedef定义的类型别名视为全部类型的一种进行语法和语义分析。2. 实际使用场景分析在了解#define和typedef的核心本质差异之后本章我们从实际的使用场景入手结合代码分析二者的差异。2.1 连续声明指针这个应该是大家最熟悉的一点也是#define和typedef在处理指针别名时的最大区别。请看下面 C 代码#definePTR_INTint*typedefint*Ptrint;PTR_INT a,b;Ptrint x,y;我们先来分析#define第一章中说过#define只是简单的文本替换因此PTR_INT a, b在预处理阶段进行替换之后会变成int* a, b我们都知道*只与a结合这就导致a是整型指针而b却是一个整型变量。再看typedefPtrint被编译器视为一个确确实实存在的类型而不是文本替换。因此Ptrint x, y声明了两个变量x和y它们的类型都是Ptrint因此x和y都是整型指针。我们进行实际测试将上面的 C 代码文件命名为 demo.c然后用下面命令将 C 代码分别进行预处理和编译gcc-Edemo.c-odemo.i gcc-Sdemo.i-odemo.s产生的 demo.i 代表预处理之后的文件demo.s 代表编译之后的文件。我们先来看 demo.i这是预处理之后的文件可以看到PTR_INT确实被替换成int *了。并且如我们第一章所说#define的处理发生在预处理阶段而typedef的处理发生在编译阶段因此Ptrint并没有发生变化这也是符合预期的。再来看看编译后的文件 demo.s这里只截取了我们需要注意的部分并没有截取全部内容。还要先说明一下在我用的 64 位 Ubuntu 虚拟机中指针大小为 8 字节int的大小为 4 字节。我们可以看到截图中.size a, 8 .size b, 4 .size x, 8 .size y, 8这就说明除了b只分配了 4 个字节之外别的axy都分配了 8 个字节刚好证明b的类型是int而其他三个都是int *。2.2 与const结合使用当与const结合使用时二者的表现也完全不同。请看下面代码#definePTR_INTint*typedefint*PtrInt;constPTR_INT p1;constPtrInt p2;对于#define预处理之后是const int* p1这里const修饰的是int表示p1是一个指向常量的指针也就是说指针的指向可以变但不能通过指针修改值。对于typedefPtrInt是一个指向int的指针。当用const修饰它时const修饰的是指针本身所以p2会解析为int* const p2即 p2 是一个常量指针也就是说指针的指向不能变但是可以通过指针修改其指向的值。我们用下面代码进行编译测试#definePTR_INTint*typedefint*PtrInt;intmain(){intnum[2]{2,5};constPTR_INT p1num[0];constPtrInt p2num[1];p1num[1];//改变p1指向*p110;//改变p1指向位置的值p2num[0];*p220;return0;}编译结果如下可以看到指出了存在错误的地方也正如我们上述所说p1可以改变指向但不能通过解引用修改值p2相反。2.3 作用域的差异#define一旦在一个文件中定义从定义的那一点开始直到文件结束所有的宏都会被替换并且它可以无视大括号{}构成的块。typedef他遵循块作用域的规则如果定义在一个{}内部那么别的{}内是不可以使用的。比如下面代码voidfunc1(){#defineMY_INTinttypedeffloatMY_FLOAT;MY_FLOAT f1.0;}voidfunc2(){MY_INT i10;MY_FLOAT f;//这里会报错}我们编译一下可以看到它提示func2中有个不认识的类型名称MY_FLOAT。2.4 类型检查相关#define 预处理阶段就完成了替换不进行任何类型检查。如果宏定义有语法错误预处理器不会报错错误会被带入编译阶段但编译器的报错会指向替换后的代码而不是宏定义本身一旦出错极难调试。typedef 在编译阶段处理有严格的类型检查。如果类型不匹配或有语法错误编译器会明确指出typedef存在的问题有利于调试。2.4.1 #define错误用法示例如下代码加入我们把unsigned误写成unsign#defineUINTunsignintintmain(){UINT a10;UINT b20;UINT c30;return0;}尝试编译一下可以看到报错的内容比代码本身都长的多看起来就让人头大正如上面所说预处理器只是做简单的文本替换并不检查错误这就导致编译器会产生许多无法准确定位问题根源的报错导致难以调试。2.4.2 一个稍复杂的语法错误当你想当然的写下下面的代码问题就产生了#defineARRAY_5int[5]intmain(){ARRAY_5 arr;arr[0]100;return0;}大家可以看看编译后的报错信息报错信息极其难懂“在[之前期望一个标识符或(”。拿着这种报错信息你怎么排查错误为什么报错是这样呢归根结底还是预处理器的无脑替换替换之后第 5 行变成了int[5] arr;而在 C 语言中这当然是违法的正确的写法是int arr[5];这就是简单文本替换的劣势所在。2.5 复杂类型的封装对于复杂的声明typedef具有压倒性的优势而#define几乎无法胜任可以参考2.4.2的例子。举两个例子定义一个大小为 10 的整型数组类型typedefintarray10[10];array10 arr;//等价于intarr[10];定义函数指针typedefvoid(*FuncPtr)(int);FuncPtr pmy_function;2.6 功能范围差异#define的功能更广它不仅能为类型取别名还能定义常量、定义带参数的宏函数比如#define MAX(a,b) ((a)(b)?(a):(b))、甚至控制条件编译。而typedef的功能单一只能用于类型。本文完。关注我我会持续更新干货。