C语言宏定义用法总结

前言

最近在看源代码与开发项目的时候经常会遇到一些特殊的宏用户,接接触时感觉有点奇怪,其实是自己没有全面的熟悉宏的用法。在查阅完相关的材料后,写下这一篇总结,以期待以后忘记的时候可以重新打开回忆起里面的内容来。本博文主要是总结#define的用法,包括定义符号与定义宏macro。

预处理器的作用

编译一个C语言程序的第一步骤就是预处理阶段,这一阶段就是宏发挥作用的阶段。C预处理器在源代码编译之前对其进行一些文本性质的操作,主要任务包括删除注释、插入被#include进来的文件内容、定义和替换由#define 定义的符号以及确定代码部分内容是否根据条件编译(#if )来进行编译。”文本性质”的操作,就是指只是简单粗暴的由一段文本替换成另外一段文本,而不考虑其中任何的语义内容。

#define 定义符号

我们最常使用到的define的场景就是用#define来定义一个符号,比如

#define MAX 0x7FFFFFFF

定义了一个符号 MAX,每当后面的地方有MAX 符号出现的时候,预处理器就会把它替换成 0x7FFFFFFF.
注意了,上面的替换只会发生在MAX是作为一个符号出现的地方。什么是以符号出现?简单的来说是就以类似于标识符出现的场景。比如

#define MAX 0xFF
int MAXX=MAX;

是不会将MAXX前面的”MAX”替换的,因为这里”MAX”是后面的’X’一起构成了一个符号MAXX,并不是单独以MAX出现的;同理,printf("MAX"); 输出”MAX”字符串,而不会输出0xFF.
下面是一些常见的例子:

#define reg register
#define do_forever for(;;)
#define CASE break;case

第一个为关键字“register”定义了一个短的别名,这样做的好处是即可以对齐代码,又易于维护。比如 如果那一天想把原来所有的register类型变量该为使用auto ,那么就可以直接在宏定义这里改就可以啦。
第二个则是定义了一个更具描述性的符号来代替死循环。
第三个则是定义了一个可以自动break的”关键字” case.这样可以不用手动输入break;了

另外如果一个define的第二部分特别长的话,可以使用把它分成几行来写,出了最后一行之外,每行的末尾都要加一个反斜杠,例如:

#define DEBUG_PRINT printf("File %s line %d :"\
                                                "x=%d,y=%d,z=%d",\
                                                __FILE__,__LINE,x,y,z)

上面还使用了两个相邻字符串会被拼接成一个字符串的技巧。如果一个字符串字面量太长时也可以使用反斜杠分成几行来写,但是每行都需要使用双引号将字符串引起来再加反斜杠,否则字符串将会把每一行前面的空格包含进去。
例如:

#define str "123\
                            456"
#define str2 "123"\
                        "456"      

str与str2的区别在于,str的上下两部分子串之间会有许多空格符,而这可能不是我们想要的。而str2因为两个子符串是相邻的,所以会被编译器处理为同一个字符串,里面就不会出现多余的空格。
另外 值的注意的是,如果在宏定义的时候在尾部加了分号”;”,那么就不要在使用DEBUG_PRINT的后面加上分号了,因为这样很容易出错。
例如:

#define DEBUG_PRINT printf("File %s line %d :"\
                                                "x=%d,y=%d,z=%",\
                                                __FILE__,__LINE,x,y,z);

然后在后面有一处使用:

if(...)
    DEBUG_PRINT;
else
···

这样子是会报错的,因为这里替换后有结果是:

if(...)
    printf("File %s line %d :x=%d,y=%d,z=%d",__FILE__,__LINE,x,y,z);;
else
···

会报 else没有匹配if的错误,原因就是第二个;将if隔断了,编译器误认为if语句就此结束。
因此在这样使用宏的时候,千万要注意分号只留一个就可以。

#define 机制还允许把参数替换到文本中,这种实现机制被为宏(macro),2333千呼万唤使出来,原来这个带参数的才叫宏呀,上面说过的define充其量就是一个”定义”。

宏的声明方式:

#define name(parameter-list) stuff

其中parameter-list是一个由逗号隔开的参数列表,它们可能出现在stuff中。参数列表的左括号必须与name紧邻。如果两者之间有任何空白,参数列表就会被解释位stuff的一部分。
当宏被调用时,名字后面是一个由逗号分隔的值的列表,每个值都与宏定义中的一个参数相对应。当参数出现在程序中,与每个参数对应的实际值都将被替换到stuff中。

但是需要注意的是,所有用于数值表达式进行求值的宏定义都要将stuff里面出现参数的地方加上括号,同时将stuff加上括号,这么做为了保证运算优先级

例如:

#define SQUARE(x) x*x
int a =5;
int b =SQUARE(a+1);

b的结果是多少呢?文本替换大法后的结果为:

int b =a+1*a+1;

显然这不是我们想要的结果。这是因为宏是文本替换,并没有语义上的操作。如果SQUARE是一个函数,那么SQUARE(a+1)传入的是a+1的结果6,而不是以a+1这个整体进行操作。
解决上面问题,只需要修改成:

#define SQUARE(x) (x)*(x)

这样b的初始化就等价于:

int b =(a+1)*(a+1);

这正是我们想要的。
然而这么做仍然有一个问题,例如:

#define SQUARE(x) (x)*(x)
int a =10;
int b =SQUARE(a+1);
int c =121/SQUARE(a+1);

我们想将c的值初始化为121除以a+1的平方,就是将c初始化为1。然而···
我们手动替换一下,发现事实不是我们想像的:

int c =121/(a+1)*(a+1);//int c =121/11*11;

这么做的结果就是,c的值是121并不是我们想要的1,造成这种结果的本质原因是没有将宏里面的所有运算操作强制转为一个整体。其解决方法是将宏定义改为:

#define SQUARE(x) ((x)*(x))

如此即可。

宏与函数

宏相比函数而言的优势主要在于:
1. 宏因为是文本替换,没有函数栈的维护代价
2. 宏参数不带类型,可以做函数不能做的工作。

解释一下,第一点 函数调用是利用函数栈帧来实现的,这个需要一定的系统资源。但是宏是直接文本替换,无函数调用过程。
第二点是因为函数的参数列表要求每个参数必须带类型,当我们想实现把类型也当作参数传入时,函数是无法做到。例如下面的宏:

#define MALLOC(size,type) ((type*)malloc(sizeof(type) * (size)))
int * p =MALLOC(10,int);

这个宏有点厉害了,传入大小和类型两个参数,然后动态分配出所需要的内存。这个肯定是无法用函数实现的。这个是不是与C++的模版有点相像?模版能够实现的技术基础就是能够把参数类型当作一种参数,而宏正好就支持!!!

然而,宏的文本替换大法这么厉害,为什么还会有函数的存在嘞?
最主要的原因是:宏的文本替换大法在处理带副作用的宏参数时会导致不可预料的后果。

带副作用的宏参数

什么叫带副作用的宏参数?副作用是指某些运算符在求值的同时也会永久地改变操作数,比如:
i+1
这个表达式求值没有副作用,这个式子不管运算多少次,式子的值不变。但是:
++i
却不一样了,它运算100次与运算1次的结果是不一样的,因为每次运算都会永久改变i的值。
那当副作用遇上宏参数会发生什么呢?看一个经典例子:

#define MAX(a,b) ((a)>(b)?(a):(b))
int a=0;
int b =1;
int c =MAX(a++,b++);

根据宏替换大法,我们看c被预编译器处理后的样子:

int c =((a++)>(b++)?(a++):(b++));

ok,副作用一目了然。因为a++

int max(a,b)
{
    return a>b? a:b;
}
int a =0;
int b =1;
int c =max(a++,b++);

这么执行的结果就是
c=1,a=1,b=2;这是我们想要的。
上面两者的区别在于,使用函数时,只在参数列表里面进行一次带副作用的运算;使用宏时,在宏的列表,宏的内容都执行了带副作用的运算。而我们通常只希望副作用运算执行一次。

define 替换字符串(符号)里面的内容

上面提到,当预处理器搜索#define定义的符号时,字符串常量或符号的子串内容并不进行替换。但是如果想要把字符串或符号里面的宏参数进行替换,主要有两种方法,这两种方法的核心思想都是想办法把原字符串(符号)分多个部分,每部分其实都是一个宏参数,通过对每一段进行替换来达到字符串(符号)替换目的:

1. 利用相邻字符串被自动拼接成一个长字符串,这样就可以手动地将字符串分成多个小部分啦。举个例子:

#define PRINT(FORMAT,VALUE) printf("The value is " FORMAT"\n",VALUE)
PRINT("%d",x+3);

将产生如下的输出:

The value is 20;

这么做的可行性在于:”The value is ” FORMAT “\n”,是三个字符串,但是由于他们是相邻的,所以在预处理后会拼接成一个长的字符串。而三个子字符串却可以提供很大的操作性。
但是这个要求FORMAT是一个字符串才行。
如果我们想打印宏参数的内容怎么办呢?
可以使用 #arguement ,这种结构被预处理器翻译为”arguement”。编写下面的代码:

#define PRINT(FORMAT,VALUE)\
printf("The value of"#VALUE"is " FORMAT"\n",VALUE)
printf("%d",x+3);

它将输出:The value of x+3 is 20
这是因为”The value of”#VALUE”is ” FORMAT”\n”实际上是包含了”The value of “,#VALUE,”is “,FORMAT,”\n” 五部分字符串,其中VALUE和FORMAT被宏参数的实际值替换了。

2. 使用## 结构,这个用于处理符号。

##作用在于把位于它两边的符号连接成一个符号,通过这个就可以把分离的文本片段重新拼接成一个符号(标识符)
例如:

#define ADD_TO_SUM(sum_number,value)\
sum##sum_number+=(value)
int sum1=0;
int sum2=1;
int sum3=2;
ADD_TO_SUM(2,100);

最后一句就相当于在执行:sum2+=(100);,因为##的前面是sum,后面是sum_number+=(value).sum没有替换,sum_number以一个独立的符号出来,刚好就是宏参数的形式,所以就替换了。
还是有点厉害。

  • 28
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
以下是一个使用 C 语言宏定义实现的哈希表(类似于 uthash)的示例代码: ```c #include <stdio.h> #include <stdlib.h> /* 定义哈希表结构体 */ #define HASH_TABLE(name, key_type, value_type) \ typedef struct name##_entry { \ key_type key; \ value_type value; \ struct name##_entry *next; \ } name##_entry; \ typedef struct { \ int size; \ name##_entry **table; \ } name /* 初始化哈希表 */ #define HASH_TABLE_INIT(table, size) \ do { \ (table)->size = size; \ (table)->table = calloc(size, sizeof(*(table)->table)); \ } while (0) /* 释放哈希表 */ #define HASH_TABLE_FREE(table) \ do { \ int i; \ for (i = 0; i < (table)->size; i++) { \ name##_entry *entry = (table)->table[i]; \ while (entry) { \ name##_entry *next = entry->next; \ free(entry); \ entry = next; \ } \ } \ free((table)->table); \ } while (0) /* 查找键对应的 */ #define HASH_TABLE_GET(table, key, default) \ ({ \ int index = (key) % (table)->size; \ name##_entry *entry = (table)->table[index]; \ while (entry) { \ if ((entry)->key == (key)) { \ break; \ } \ entry = entry->next; \ } \ (entry) ? (entry)->value : (default); \ }) /* 设置键对应的 */ #define HASH_TABLE_SET(table, key, value) \ do { \ int index = (key) % (table)->size; \ name##_entry *entry = (table)->table[index]; \ while (entry) { \ if ((entry)->key == (key)) { \ (entry)->value = (value); \ break; \ } \ entry = entry->next; \ } \ if (!entry) { \ entry = calloc(1, sizeof(*entry)); \ entry->key = (key); \ entry->value = (value); \ entry->next = (table)->table[index]; \ (table)->table[index] = entry; \ } \ } while (0) /* 删除键对应的 */ #define HASH_TABLE_DEL(table, key) \ do { \ int index = (key) % (table)->size; \ name##_entry *entry = (table)->table[index]; \ name##_entry *prev = NULL; \ while (entry) { \ if ((entry)->key == (key)) { \ if (prev) { \ prev->next = entry->next; \ } else { \ (table)->table[index] = entry->next; \ } \ free(entry); \ break; \ } \ prev = entry; \ entry = entry->next; \ } \ } while (0) /* 示例用法 */ HASH_TABLE(my_hash_table, int, char *); int main() { my_hash_table table; HASH_TABLE_INIT(&table, 10); HASH_TABLE_SET(&table, 1, "one"); HASH_TABLE_SET(&table, 2, "two"); HASH_TABLE_SET(&table, 3, "three"); printf("%s\n", HASH_TABLE_GET(&table, 1, "not found")); printf("%s\n", HASH_TABLE_GET(&table, 2, "not found")); printf("%s\n", HASH_TABLE_GET(&table, 3, "not found")); HASH_TABLE_DEL(&table, 2); printf("%s\n", HASH_TABLE_GET(&table, 1, "not found")); printf("%s\n", HASH_TABLE_GET(&table, 2, "not found")); printf("%s\n", HASH_TABLE_GET(&table, 3, "not found")); HASH_TABLE_FREE(&table); return 0; } ``` 这个示例代码定义了一个名为 `my_hash_table` 的哈希表类型,其中键的类型为 `int`,的类型为 `char *`。通过使用宏定义来实现,可以方便地在其他代码中使用这个哈希表类型,并调用宏定义中定义的函数来操作哈希表。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值