概述
C语言当中宏定义是非常常用的,在简单Hello world程序就含有了文件包含的宏定义,那么在C语言当中宏定义分为哪几类呢?
- 文件包含
文件包含指令,也就是#include指令,非常常见的一种,形式如 #include<stdio.h>/ #include "myfile.h".,该行在编译预处理时被文件名指定的文件内容替换。
- 宏替换
宏定义,也就是#define指令,形式如 #define 名字 替换文本。在编译预处理时,就出现名字的地方被替换文本天替换,替换文本可以任意字符串,其作用域从定义点开始到被编译的文件结束。
例子:
#define NULL 0
#define forever for( ; ; ) //创建新的关键词 forever
#define max(a,b) ((a) > (b) ? (a) : (b)) // 括号非常重要
#define putc(_ch, _fp) _IO_putc (_ch, _fp) //实现函数功能
#define swap(a,b) do{ unsign long t = a; a =b ; b=t; } while(0) //实现函数功能,注意do/while,很好技巧
- 条件包含
编译预处理进行条件控制,是提供了一种在编译工程中可以根据所求条件的值有选择地包含不同的代码手段。#if语句中包含了一个常量整型表达式(其中不包含sizeof,类型强制转换或枚举常量),若表达式的求值不为0时,执行后续各行,直到遇到#endif、#elif 或 #else为止。其中特殊表达式defined(名字),当名字已经定义了,则值为1,反之为0.譬如:
#if !defined(HDR) #define HDR /*hdr.h 文件内容*/ #endif
以下是 #if /#elif /#else/#endif综合例子:
#if SYSTEM == SYSV #define HDR "sysv.h" #elif SYSTEM == BSD #define HDR "bsd.h" #elif SYSTEM == MSDOS #define HDR "msdos.h" #else #define HDR "default.h" #endif
常见预处理指令
#define // 定义一个预处理宏 #undef //取消宏的定义 #include //包含文件命令 #include_next //与#include相似, 但它有着特殊的用途 #if //编译预处理中的条件命令, 相当于C语法中的if语句 #ifdef // 判断某个宏是否被定义, 若已定义, 执行随后的语句 #ifndef //与#ifdef相反, 判断某个宏是否未被定义 #elif //若#if, #ifdef, #ifndef或前面的#elif条件不满足, 则执行#elif之后的语句, 相当于C语法中的else-if #else //与#if, #ifdef, #ifndef对应, 若这些条件不满足, 则执行#else之后的语句, 相当于C语法中的else #endif //#if, #ifdef, #ifndef这些条件命令的结束标志. defined //与#if, #elif配合使用, 判断某个宏是否被定义 #line // 标志该语句所在的行号 # // 将宏参数替代为以参数值为内容的字符窜常量 ## //将两个相邻的标记(token)连接为一个单独的标记 #pragma //说明编译器信息 #warning //显示编译警告信息 #error //显示编译错误信息
与函数对比
上述可以看出宏定义能够实现函数功能,那么它与函数比对一下,
- 在代码编写上,相对比较复杂的功能,采用宏定义实现,代码会比较长,不利于阅读,而函数能够清晰表达含义
- 在编译之后文件大小量上,函数只定义一份,其他地方是重复调用,而宏定义是替换原则,编译预处理之后,引用的宏定义名字,全部替换了,从而使得编译之后,文件膨胀。譬如:
#include<stdio.h> #define TEST(a,b) printf(#a "<" #b "=%d\n",(a)<(b)) int main(){ TEST(2,3); TEST(7,8); return 0; }
采用编译预处理之后,gcc -E macro.c,产生的结果:
# 1 "macro.c" # 1 "<built-in>" # 1 "<command-line>" # 1 "macro.c" # 1 "/usr/include/stdio.h" 1 3 4 # 28 "/usr/include/stdio.h" 3 4 // 。。。头文件 int main(){ printf("2" "<" "3" "=%d\n",(2)<(3)); printf("7" "<" "8" "=%d\n",(7)<(8)); return 0; }
- 在类型角度来看,宏定义基本上属于弱类型的,而函数是强类型的,各有优缺点,譬如上述提到 max宏定义,基本整型、浮点型等可采用大于小于比较的都可以使用,而定义max函数的话,就需要n个了。
- 在性能角度来看,函数调用需要进栈出栈、跳转等等处理,相对宏定义的效率偏低一些
- 总结如下:
属性 | #define宏 | 函数 |
代码长度 | 每次使用时,宏代码都被插入到程序中。除了非常小的宏之外,程序的长度将大幅度增长 | 函数代码只出现于一个地方:每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数调用、返回的额外开销 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非它们加上括号,否则邻近操作符的优先级可能产生不可预料的结果。 | 函数参数只在函数调用时求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。 |
参数求值 | 参数用于宏定义时,每次都将重新求值,由于多次求值,具有副作用的参数可能会产生不可预测的结果。 | 参数在函数调用前只求值一次,在函数中多次使用参数并不会导致多次求值过程,参数的副作用并不会造成任何特殊问题。 |
参数类型 | 宏与类型无关,只要参数的操作是合法的,它可以用于任何参数类型。 | 函数的参数是与类型有关系的,如果参数的类型不同,就需要使用不同的函数,即使它们执行的任务是相同的。 |
以上是来源《C和指针》一书,当然,需要权衡什么时候需要使用宏定义,什么时候使用函数
常用招术
- 防止一个头文件被重复包含
#ifndef COMDEF_H #define COMDEF_H //头文件内容 #endif
- 重新定义一些类型,防止由于各种平台和编译器的不同,而产生的类型字节数差异,方便移植
typedef signed long int int32; /* Signed 32 bit value */ typedef signed short int16; /* Signed 16 bit value */ typedef signed char int8; /* Signed 8 bit value */
- 得到指定地址上的一个字节或字
#define MEM_B(x) (*((byte *)(x))) #define MEM_W(x) (*((word *)(x)))
- 內联函数功能
#define MAX(a,b) ((a) > (b) ? (a) : (b))
- 得到结构体成员变量的偏移量或字节数
#define offsetof(type,member) ((size_t) &((type *) 0) -> member) #define ssizeof(type,member) sizeof(((type *)0)->member)
- 灵活调用函数
#define s5(a) supper_ ## a #include <stdio.h> void supper_printf(const char* p ) { printf("this is supper printf:\n%s\n",a); } int main() { s5(printf)("hello owrld"); return 0; }
复杂宏定义
来自linux内核代码,如下所示:
#define offsetof(type,member) ((size_t) &((type *)0)->member) #define container_of(ptr,type,member) ({ const typeof( ((type *)0)->member) * mptr = (ptr); \ (type *) ((char*) mptr - offset(type,member));})
对于第一条宏定义,我们逐步分析如下:
type * p = ( type *)0; // 定义指针p,类型为type,值为0 int maddr = &(p->member);//取member成员的地址 size_t s = (size_t) maddr; //类型转换
那么实现功能很强大,求出MEMBER在TYPE中的偏移量。
type * p = ( type *)0; // 定义指针p,类型为type,值为0 typeof(p->member); //获取类型 const typeof(p->member) *mptr = (ptr); //定义指针,指向了ptr char * maddr = (char *)mptr//获得mptr地址 offset(type,member); //求出member载type中的偏移量 (char*) mptr - offset(type,member) ;//求出type首地址
简单分析如图所示:
type
|----------------|
|----------------|
|----------------|
|----------------|
ptr->| member -- |
|----------------|
|----------------|
|----------------|
|----------------|
那么实现功能,指针ptr指向结构体type中的成员member;通过指针ptr,返回结构体type的起始地址。