c语言中,#definetypedef 均是用来定义别名的符号,但又有明显的不同。 #define 定义的宏只是简单的文本替换,typedef 则是类型别名。

C语言中宏为预处理阶段的一种文本替换工具。从使用上分为

  • 对象类的宏

  • 函数类的宏。

可以将任何有效的标识符定义为宏,你甚至可以将c语言关键字定义为宏,你能这么做的原因是因为c预处理器 没有关键字这个概念。利用这个特性你可以将const关键字对不支持的编译器隐藏起来。

基础用法

对象类的宏仅仅为会被替换为定义的代码片段,被称为对象类的宏也是因为其在代码中以数据对象的形式存在。

  1. 标识符别名
#define BUFFER_SIZE 1024

这是最常用的用法,预处理阶段,BUFFER_SIZE会被替换为1024。按照惯例,宏名用大写字母表示。

若宏体过长,可以加反斜杠换行

#define NUMBERS 1,\
				2,\
				3

预处理阶段,int x[] = { NUMBERS };会被替换为int x[] = { 1, 2, 3 };

  1. 宏函数

带括号的宏被认为是宏函数。用法和普通函数一样,只不过其在预处理阶段就展开。

#define MIN(X, Y) ((X) < (Y) ? (X) : (Y))

需要注意的是,宏名和括号之间不能有空格!

高级用法

字符串化(Stringizing)

在写调试程序的宏函数时,我们希望可以将宏函数的参数转换为字符串嵌入字符常量中,这时我们可以使用符号#将其字符串化。

#define WARN_IF(EXP) \
do { if (EXP) \
		 fprintf(stderr, "Warning: " #EXP "\n");} \
while (0)

这时WARN_IF(x==0);会被扩展成:

do { if (x == 0)
    fprintf (stderr, "Warning: " "x == 0" "\n"); }
while (0);

上面宏函数体中的 do { } while (0) 是在宏中有多个语句时用到的。为了将宏代码和其他片段分割开来。 譬如以下的程序:

#define M() a(); b()

if (cond)
    M();
else
    c();

/* 预处理后 */

if (cond)
    a(); b();
else   /* <- else 没有对应的 if */
    c();

只用 {} 也不行:

#define M() { a(); b(); }

if (cond)
    M();
else
    c();

/* 预处理后 */

if (cond)
    { a(); b(); };  /* 最后的分号表示 if 语句结束 */
else   /* <- else 没有对应的 if */
    c();

do {} while(0) 才可以:

#define M() do { a(); b(); } while(0)

if (cond)
    M();
else
    c();

/* 预处理后 */

if (cond)
    do { a(); b(); } while(0);
else
    c();

连接字符串(Concatenation)

当宏中出现##时,会对 token 进行连接:

#define COMMAND(NAME)  { #NAME, NAME ## _command }

struct command
{
    char *name;
    void (*function) (void);
};

struct command commands[] =
{
    COMMAND (quit),
    COMMAND (help),
    ...
};

上述命令会被扩展为:

struct command commands[] =
{
    { "quit", quit_command },
    { "help", help_command },
    ...
};

变参数宏(Variadic Macros)

宏函数还可以像函数一样接受可变参数。语法和可变参数的函数一样:

#define eprintf(...) fprintf(stderr, __VA_ARGS__)

这时调用宏的所有参数都会代替__VA_ARGS__被展开:

eprintf("%s:%d: ", input_file, lineno)会被展开为fprintf(stderr, "%s:%d: ", input_file, lineno)

以上宏定义不够直观,我们可以指定参数名:

#define eprintf(fmt, ...) fprintf(stderr, fmt, __VA_ARGS__)

但是还有一个问题,调用上述宏定义时若省略可选参数,会报错。例如eprintf("success!\n",);会展开为fprintf(stderr, "success!\n", );,原因在于字符串末尾会多出来一个逗号。这时我们就需要用到前面提到的##了,将宏定义改写为:

#define eprintf(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__)

当省略参数时,多余的逗号就会被删除。

typedef

c语言中的 typedef 用来给数据类型取别名,目的是使代码易读和易理解。有简化声明的作用。

简化声明

定义了一个结构体如下:

struct _Point {
  double x, y;
};

每次创建一个 _Point 类型的变量,得这么写:

struct _Point a;

每次都要带上 struct 关键字增加击键数,这时可以利用 typedef 取别名简化变量声明。

C++ 中定义结构体已经隐含了这层含义,因此可以直接使用 _Point a; 来声明变量。

typedef struct _Point Point;

这样的话,定义新的变量可以写为

Point a;

更易阅读和理解。

与数组和指针一起使用

先来看几个例子:

typedef int iArr[6];

typedef struct Node Node;
struct Node {
  int data;
  Node *pNext;
};
typedef struct Node* pNode;

typedef int (*pfunc)(int a, int b);

这几个例子其实和c语言的复杂声明是一样的。去掉 typedef 关键字后就得到一个正常的 变量声明语句。

typedef int iArr[6]; 变为 int iArr[6];,表示声明一个包含6个元素的int数组。 而加上 typedef 后就得到了一个新的类型名,iArr 不再是变量名,而是新的类型名 iArr, 用 iArr 可以去定义与原来的 iArr 变量相同类型的变量。

iArr a;  /* a 为 包含6个元素的int数组 */

理解了这个后,剩下其他 typedef 就都好理解了。

pNode pNew;  /* pNew 为指向 struct Node 类型的指针 */

int add(int a, int b) {
  return a + b;
}
pfunc func = add;  /* func 为一个函数指针,其指向的函数有两个int参数,返回int */

另外,在 C++11 标准中,引入了新的为类型取别名的关键字 using,可以用来代替 typedef, 而且更好理解:

using iArr = int [6];
using pNode = Node *;
using pfunc = int (*) (int a, int b);

从可读性上 using 好于 typedef 。此外 typedefusing 不是完全等价的,using 可以用来 给模板取别买,typdef 则不行。

template <typename T>
using Vec = MyVector<T, MyAlloc<T>>;

// usage
Vec<int> ivec;

用起来非常自然。若是使用 typedef,则是这样:

template <typename T>
typedef MyVector<T, MyAlloc<T>> Vec;

// usage
Vec<int> vec;

编译的时候,会得到类似 error: a typedef cannot be a template 的错误信息。

总结起来,你只要弄懂了复杂声明,typedef 就很好理解!

此外,在 C++11 中推荐使用 using 代替 typedef

参考