18. typedef 与 #define 的区别:它们真的可以互换吗?

说实话,这个问题我面试过不少人,十有八九会答错。

很多人觉得 typedef 和 #define 不就是给类型起个别名吗?一个用关键字,一个用宏,差不多吧?

嗯,如果你也这么想,那今天这章就是为你准备的。

先看一个“翻车”现场

我记得有一次,团队里一个新同事写了这样一段代码:

#define PINT int*
typedef int* TINT;

PINT a, b;   // 你猜 b 是什么类型?
TINT c, d;   // 那 c 和 d 呢?

他信心满满地说:“a、b、c、d 都是 int* 啊,这有什么好问的?”

结果一编译,b 的类型是 int,不是 int*。

为什么会这样?

关键区别:#define 是文本替换,typedef 是类型定义。

展开来看:

// #define 展开后:
int* a, b;
// 等价于:
int *a;
int b;      // b 是 int,不是指针!

// typedef 展开后:
TINT c, d;
// 等价于:
int *c;
int *d;     // 两个都是指针

说白了,#define 就是个“复制粘贴”的活儿。预处理器看到 PINT,直接换成 int*,然后编译器再去解析。而 typedef 是编译器亲自处理的,它知道 TINT 就是一个完整的类型。

第二个坑:指针常量和常量指针

我个人习惯用 typedef 给复杂类型起别名,尤其是函数指针。但用 #define 做同样的事,很容易出问题。

#define CONST_PTR const int*
typedef const int* TCONST_PTR;

const int x = 10;
CONST_PTR p1 = &x;   // p1 是 const int*,没问题
// const CONST_PTR p2 = &x;  // 展开后:const const int* p2,编译报错

TCONST_PTR p3 = &x;  // p3 是 const int*
const TCONST_PTR p4 = &x;  // p4 是 const int* const,常量指针

你看,#define 在组合 const 时,会搞出重复的 const 关键字。而 typedef 能正确处理这种嵌套。

避坑指南:我曾经在一个通信协议栈的项目里,用 #define 定义了回调函数指针类型。结果在 const 修饰时出了诡异的问题,排查了整整一个下午。后来全部改成 typedef,世界清净了。

第三个区别:作用域

#define 从定义点开始,到文件结束都有效。除非你用 #undef 取消定义。

typedef 遵循 C 语言的作用域规则——在函数内定义,只在函数内有效;在文件作用域定义,全局有效。

void func() {
    #define LOCAL_INT int
    typedef int LocalInt;
    
    LOCAL_INT a = 5;   // 可以
    LocalInt b = 10;   // 可以
}

void another_func() {
    LOCAL_INT c = 3;   // 可以!宏仍然有效
    // LocalInt d = 4; // 编译错误!typedef 已超出作用域
}

嗯,这里要注意:宏没有作用域的概念,它就是个“全局的文本替换”。而 typedef 是真正的语言元素,受作用域约束。

第四个区别:调试体验

我用 GDB 调试的时候,typedef 定义的类型能直接显示出来。比如:

typedef struct {
    int x;
    int y;
} Point;

Point p = {10, 20};
// GDB 中:print p 显示 (Point) {x=10, y=20}

但 #define 定义的“假类型”,调试器根本不认识。它只知道展开后的原始类型。

小技巧:如果你在写一个库,需要对外暴露类型,优先用 typedef。这样用户调试时能看到有意义的类型名,而不是一堆 int、char* 的原始组合。

一张图看懂核心区别

typedef vs #define 核心区别 #define 🔹 预处理阶段处理 🔹 纯文本替换(复制粘贴) 🔹 无作用域概念 🔹 调试器不识别 🔹 可被 #undef 取消 🔹 不能用于复杂类型组合 ⚠ 容易产生意外展开 typedef 🔹 编译阶段处理 🔹 真正的类型别名 🔹 遵循作用域规则 🔹 调试器可识别类型名 🔹 不能取消定义 🔹 支持指针、数组、函数指针 ✅ 类型安全,可预测

什么时候用 #define?什么时候用 typedef?

我的经验是:

  • 定义类型别名 → 用 typedef。比如结构体、联合体、函数指针。
  • 定义常量 → 用 #define。比如 #define MAX_BUF_SIZE 1024。
  • 定义宏函数 → 用 #define。比如 #define MIN(a,b) ((a)<(b)?(a):(b))。
  • 条件编译 → 用 #define。配合 #ifdef 使用。

一句话总结:typedef 是给类型起名字,#define 是给文本起名字。它们根本就不是一个层面的东西,谈不上“互换”。

一个实战中的教训

我曾经维护过一个老项目,里面大量使用了 #define 来定义类型:

#define UINT8  unsigned char
#define UINT16 unsigned short
#define UINT32 unsigned int

看着挺像那么回事,对吧?

但后来要移植到 64 位平台,问题来了——unsigned int 在 64 位系统上还是 32 位,而 unsigned long 变成了 64 位。如果当初用 typedef:

typedef unsigned char  UINT8;
typedef unsigned short UINT16;
typedef unsigned int   UINT32;

移植时只需要改 typedef 那一行,所有代码自动适配。而 #define 的方式,你得满世界找那些“隐式依赖”的地方。

血的教训:别用 #define 假装定义类型。它只是文本替换,不是类型系统的一部分。你省的那点打字时间,将来会在调试和移植上十倍百倍地还回来。

总结一下

typedef 和 #define 能不能互换?

答案很明确:不能

它们的设计目的、处理阶段、行为特性完全不同。typedef 是给 C 语言类型系统服务的,#define 是给预处理系统服务的。你非要用 #define 干 typedef 的活,就像用螺丝刀钉钉子——不是不行,但迟早要翻车。

嗯,记住一句话:定义类型用 typedef,定义常量/宏用 #define。各司其职,别混着来。


公众号:蓝海资料掘金营,微信deep3321