一篇文章让你彻底区分#define和typedef

在 C 和 C++ 中,#define 和 typedef 都可以用来给现有的类型起一个别名,但其实他们两个有着本质上的区别,本文将带你彻底区分 #define 和 typedef 。

1. 核心本质差异

我们先讲最干货的,让你知道他们在底层到底有什么不同。

#define:

#define 本质上只是简单的文本替换。

预处理阶段 ,也就是说编译器真正开始编译代码之前,预处理器 就会把代码中所有出现宏名称的地方,按照宏定义替换成相应的内容。

typedef:

typedef 本质上是为一个 已有的数据类型 创建一个新的别名。它不仅仅是简单的文本替换,它实际上产生了一个新的类型名称。

编译阶段编译器 会将用 typedef 定义的类型别名视为全部类型的一种进行语法和语义分析。

2. 实际使用场景分析

在了解 #definetypedef 的核心本质差异之后,本章我们从实际的使用场景入手,结合代码分析二者的差异。

2.1 连续声明指针

这个应该是大家最熟悉的一点,也是 #definetypedef 在处理指针别名时的最大区别。请看下面 C 代码:

c 复制代码
#define PTR_INT int*
typedef int* Ptrint;
​
PTR_INT a, b;
Ptrint  x, y;

我们先来分析 #define:第一章中说过,#define 只是简单的文本替换,因此 PTR_INT a, b 在预处理阶段进行替换之后会变成 int* a, b ,我们都知道 * 只与 a 结合,这就导致 a整型指针 ,而 b 却是一个整型变量

再看 typedefPtrint 被编译器视为一个确确实实存在的 类型 ,而不是文本替换。因此 Ptrint x, y 声明了两个变量 xy,它们的类型都是 Ptrint,因此xy 都是整型指针。

我们进行实际测试,将上面的 C 代码文件命名为 demo.c,然后用下面命令将 C 代码分别进行预处理和编译:

bash 复制代码
gcc -E demo.c -o demo.i
gcc -S demo.i -o demo.s

产生的 demo.i 代表预处理之后的文件,demo.s 代表编译之后的文件。

我们先来看 demo.i,这是预处理之后的文件:

可以看到,PTR_INT 确实被替换成 int * 了。并且,如我们第一章所说,#define 的处理发生在预处理阶段,而 typedef 的处理发生在编译阶段,因此 Ptrint 并没有发生变化,这也是符合预期的。

再来看看编译后的文件 demo.s:

这里只截取了我们需要注意的部分,并没有截取全部内容。还要先说明一下,在我用的 64 位 Ubuntu 虚拟机中,指针大小为 8 字节,int 的大小为 4 字节。

我们可以看到截图中:

arduino 复制代码
.size a, 8
.size b, 4
.size x, 8
.size y, 8

这就说明,除了 b 只分配了 4 个字节之外,别的 axy 都分配了 8 个字节,刚好证明 b 的类型是 int,而其他三个都是 int *

2.2 与const结合使用

当与 const 结合使用时,二者的表现也完全不同。请看下面代码:

c 复制代码
#define PTR_INT int*
typedef int* PtrInt;
​
const PTR_INT p1;
const PtrInt  p2;

对于 #define:预处理之后是 const int* p1,这里 const 修饰的是 int,表示 p1 是一个 指向常量 的指针,也就是说 指针的指向可以变,但不能通过指针修改值

对于 typedefPtrInt 是一个指向 int指针 。当用 const 修饰它时,const 修饰的是 指针本身 ,所以 p2 会解析为 int* const p2 ,即 p2 是一个 常量指针 ,也就是说 指针的指向不能变,但是可以通过指针修改其指向的值

我们用下面代码进行编译测试:

c 复制代码
#define PTR_INT int*
typedef int* PtrInt;
​
int main()
{
    int num[2] = {2,5};
​
    const PTR_INT p1 = &num[0];
    const PtrInt  p2 = &num[1];
​
    p1 = &num[1];//改变p1指向
​
    *p1 = 10;//改变p1指向位置的值
​
    p2 = &num[0];
​
    *p2 = 20;
​
    return 0;
}

编译结果如下:

可以看到,指出了存在错误的地方,也正如我们上述所说,p1 可以改变指向,但不能通过解引用修改值,p2 相反。

2.3 作用域的差异

#define:一旦在一个文件中定义,从定义的那一点开始,直到文件结束,所有的宏都会被替换,并且它可以无视大括号 {} 构成的块。

typedef:他遵循块作用域的规则,如果定义在一个 {} 内部,那么别的 {} 内是不可以使用的。

比如下面代码:

c 复制代码
void func1() 
{
    #define MY_INT int
    typedef float MY_FLOAT;
    MY_FLOAT f = 1.0;
}
​
void func2() 
{
    MY_INT i = 10;
    MY_FLOAT f;//这里会报错
}

我们编译一下:

可以看到,它提示 func2 中有个不认识的类型名称 MY_FLOAT

2.4 类型检查相关

#define: 预处理阶段就完成了替换,不进行任何类型检查。如果宏定义有语法错误,预处理器不会报错,错误会被带入编译阶段,但编译器的报错会指向替换后的代码,而不是宏定义本身,一旦出错极难调试。

typedef: 在编译阶段处理,有严格的类型检查 。如果类型不匹配或有语法错误,编译器会明确指出 typedef 存在的问题,有利于调试

2.4.1 #define错误用法示例

如下代码,加入我们把 unsigned 误写成 unsign

c 复制代码
#define UINT unsign int  
​
int main() 
{
    UINT a = 10;  
    UINT b = 20;  
    UINT c = 30;  
​
    return 0;
}

尝试编译一下:

可以看到,报错的内容比代码本身都长的多,看起来就让人头大,正如上面所说,预处理器只是做简单的文本替换,并不检查错误,这就导致编译器会产生许多无法准确定位问题根源的报错,导致难以调试。

2.4.2 一个稍复杂的语法错误

当你想当然的写下下面的代码,问题就产生了:

c 复制代码
#define ARRAY_5 int[5]
​
int main() 
{
    ARRAY_5 arr; 
    
    arr[0] = 100;
    return 0;
}

大家可以看看编译后的报错信息:

报错信息极其难懂:"在 [ 之前期望一个标识符或 ("。拿着这种报错信息你怎么排查错误?

为什么报错是这样呢?归根结底还是预处理器的无脑替换,替换之后第 5 行变成了:int[5] arr;

而在 C 语言中这当然是违法的,正确的写法是 int arr[5];

这就是简单文本替换的劣势所在。

2.5 复杂类型的封装

对于复杂的声明,typedef 具有压倒性的优势,而 #define 几乎无法胜任,可以参考2.4.2的例子。

举两个例子:

定义一个大小为 10 的整型数组类型:

c 复制代码
typedef int array10[10];
array10 arr; //等价于intarr[10];

定义函数指针:

c 复制代码
typedef void (*FuncPtr)(int); 
FuncPtr p = &my_function;

2.6 功能范围差异

#define 的功能更广,它不仅能为类型取别名,还能定义常量、定义带参数的宏函数,比如#define MAX(a,b) ((a)>(b)?(a):(b))、甚至控制条件编译。

typedef 的功能单一,只能用于类型。


本文完。

关注我,我会持续更新干货。

相关推荐
星辰_mya2 小时前
数据库运维与数据安全:备份恢复、日志分析与故障排查
运维·数据库·后端·面试·架构师
重庆小透明3 小时前
【java基础内容】ArrayList与LinkedList的区别及ArrayList源码解析
java·开发语言·后端·面试·职场和发展
一只叫煤球的猫3 小时前
芋道源码,拉黑我,改变不了你还在搬运别人文章的事实
java·后端·面试
野犬寒鸦4 小时前
计网复习Day01
服务器·后端·网络协议·面试
张元清4 小时前
不用 WebSocket 库,在 React 中构建实时功能
前端·javascript·面试
im_AMBER6 小时前
手撕代码之事件委托
前端·javascript·面试
梦里花开知多少6 小时前
深入解析Launcher3 中的 CellLayout
android·面试
星辰_mya6 小时前
高可用与扩展:主从复制、读写分离与分库分表
spring·面试·系统架构