我的C实践(1):宏的应用
2016-07-29 00:00
190 查看
1、为了调用宏时能得到正确结果,在宏体中建议对宏的每个参数用括号括起来,并且当宏体是一个表达式时整个宏体也用括号括起来。
2、用宏来插入任意语句。
注意,第2个调用中逗号表达式语句要用括号括起来,否则预处理器会认为给宏传了两个实参,由于没有两个实参的incr,因此会报错。
3、为了使函数式宏能像真正的函数一样工作,建议用do{ }while(0)语句包住宏体的代码。
如果用注释中定义那个swap,则if {...};后面会一个分号,单独的分号是一个空语句,这导致if与else之间有两个单独的语句不合法。而用do{ }while(0)套住语句时则不会有这样的问题。
4、用宏来包装语句。
5、用内置宏来编写兼容标准C和非标准C实现的程序。
6、用可变参数列表来定义出错处理宏。
在宏体中我们用__VA_ARGS__来访问形参尾部的可变参数列表(可变参数列表只能出现在尾部),注意__VA_ARGS__只能出现在参数表中包括省略号的宏定义中。
7、用内置宏来编写兼容非标准C和标准C,同时兼容C++的C语言头文件。
上面的条件指令会使导致我们要重复编写很多代码,因为C函数的原型声明一般只需加上extern "C"就可以正确地在C++使用了。传统非标准C中的函数原型声明不能有参数列表,而标准C中则需要参数列表。因此用下面的方式控制更方便。
注意在宏定义时,宏名__P与左括号之间不能有空格,否则就不是函数式宏了。而在函数声明或调用时,函数名与参数列表之间可以有空格。由于使用了传统的原型,在编写函数定义时我们就要用传统的语法,即void myfunc(a,b) int a,b;{ ... },标准C语言也支持传统的语法。
8、用宏来跟踪一个函数的所有调用。
注意函数的定义必须出现在跟踪宏的定义之前,因为在宏体中使用了实际的函数,因此必须先看到其定义。在预处理时,预处理器发现main中的各个调用有同名的宏,并且参数匹配,因此会作宏展开,在实际的函数前插入了一个printf来跟踪这个调用的位置。
9、用宏来获取结构中成员的地址偏移字节量。
注意很多C语言实现允许在间接选择运算符->的左边使用null指针,因为C标准中没有显式地允许或禁止这种做法。这样,对运算的结果取地址后转换成整型,就可以获得结构中成员的字节偏移量。从输出结果中(Linux下的gcc编译器编译)可以看出char型成员a被对齐为4个字节,因此整个S结构的长度为16个字节。当然这种获取成员地址偏移量的用法不一定所有的C语言实现都支持。若不支持,则可用预定义非null指针和从结构的基址中减去成员地址的方法来计算偏移量。这个OFFSET宏类似于stddef.h文件中的offsetof宏。
10、用宏来实现位掩码。位掩码是个整数,由指定的二进制0和1序列组成。位掩码可以用来掩掉一个整数的二进制表示中的某些位(只要让整数与位掩码作与运算即可),比如Linux/Unix中的umask就是文件默认访问权限的位掩码,网络的子网掩码就是IP地址的位掩码(可以掩掉其主机地址部分,以获得网络地址部分)。
注意这些宏的实现是可移植的,它们并不依赖于整数使用多少位表示或计算机使用高位存储法还是低位存储法。
/* c1.c:将两个数相乘 */ #define product(x,y) ((x)*(y)) #include <stdio.h> int main(){ int a=1,b=2,c=3,d=4,x=0; x=product(a+3,b)+product(c,d); /* 若宏体中没有使用括号,则得不到 你想要的结果 */ printf("%d/n",x); return 0; }
2、用宏来插入任意语句。
/* c2.c:插入任意语句 */ #define insert(stmt) stmt /* 插入任意语句 */ #include <stdio.h> int main(){ int a,b; insert({ a=1;b=1; }) /* 这是一个复合语句 */ insert({ a=1,b=1; }) /* 如果逗号表达式不用圆括号括起,则预处理器会认为 有两个实参,报错 */ printf("a=%d, b=%d/n",a,b); return 0; }
注意,第2个调用中逗号表达式语句要用括号括起来,否则预处理器会认为给宏传了两个实参,由于没有两个实参的incr,因此会报错。
3、为了使函数式宏能像真正的函数一样工作,建议用do{ }while(0)语句包住宏体的代码。
/* c3.c: 交换两个整型变量的值 */ /* #define swap(x,y) { int temp=x; x=y; y=temp; } */ #define swap(x,y) / do { int temp=x; x=y; y=temp; } while(0) #include <stdio.h> int main(){ int x=4,y=3; if(x>y) swap(x,y); /* 用第一个swap时会出错,导致{ }后面有一个分号, 用第二个swap则没问题 */ else x=y; printf("x=%d, y=%d/n",x,y); return 0; }
如果用注释中定义那个swap,则if {...};后面会一个分号,单独的分号是一个空语句,这导致if与else之间有两个单独的语句不合法。而用do{ }while(0)套住语句时则不会有这样的问题。
4、用宏来包装语句。
/* c4.c:打印1到20的立方表,用宏来包装循环语句 */ #define incr(v,low,high) / for((v)=(low); (v)<=(high); (v)++) #include <stdio.h> int main(){ int j; incr(j,1,20) printf("%2d %6d/n",j,j*j*j); return 0; }
5、用内置宏来编写兼容标准C和非标准C实现的程序。
/* c5.h:用内置宏来编写兼容标准C和非标准C实现的程序头文件 */ #ifdef __STDC__ #if defined(__STDC_VERSION__) && __STDC_VERSION__>=19901L /* 符合C99标准的代码 */ #elif defined(__STDC__VERSION__) && __STDC__VERSION__>=199409L /* 符合C89及增补1标准的代码 */ #else /* 符合C89但不包括增补1的标准代码 */ #endif #else /* __STDC__没有定义,说明是非标准的C编译器 */ /* 非标准的C代码 */ #endif
6、用可变参数列表来定义出错处理宏。
/* c6.h:用可变参数列表来定义出错处理宏 */ #ifdef DEBUG #define myprintf(...) fprintf(stderr,__VA_ARGS__) #else #define myprintf(...) printf(__VA_ARGS__) /* 可以这样使用这个宏:myprintf("x=%d/n",x); */
在宏体中我们用__VA_ARGS__来访问形参尾部的可变参数列表(可变参数列表只能出现在尾部),注意__VA_ARGS__只能出现在参数表中包括省略号的宏定义中。
7、用内置宏来编写兼容非标准C和标准C,同时兼容C++的C语言头文件。
/* c7.1.h:用内置宏来编写兼容非标准C和标准C,同时兼容C++的C头文件 */ #ifdef __cplusplus /* C++编译器会定义这个宏 */ /* 兼容C++的C代码 */ #else #ifdef __STDC__ /* 符合标准C的C代码 */ #else /* 非标准的C代码 */ #endif #endif
上面的条件指令会使导致我们要重复编写很多代码,因为C函数的原型声明一般只需加上extern "C"就可以正确地在C++使用了。传统非标准C中的函数原型声明不能有参数列表,而标准C中则需要参数列表。因此用下面的方式控制更方便。
/* c7.2.h:用内置宏来编写兼容非标准C和标准C,同时兼容C++的C头文件 */ #ifndef __P #ifdef __STDC__ /* 若用标准的C编译器编译本代码 */ #define __P(arg) arg /* 标准C中,函数原型声明需要带参数列表 */ #else #define __P(arg) () /* 传统的非标准C中,函数原型声明不能带参数列表 */ #endif #endif #ifdef __cplusplus /* 在C++中使用本代码时会增加extern "C" { */ extern "C" { #endif /* 下面是C代码 */ extern void myfunc __P((int,int)); /* 标准C中会展开成带参数列表,非标准C中会展开成 不带参数列表,注意有两个括号 */ /* ... */ #ifdef __cplusplus /* extern "C"的结束花括号 */ } #endif
注意在宏定义时,宏名__P与左括号之间不能有空格,否则就不是函数式宏了。而在函数声明或调用时,函数名与参数列表之间可以有空格。由于使用了传统的原型,在编写函数定义时我们就要用传统的语法,即void myfunc(a,b) int a,b;{ ... },标准C语言也支持传统的语法。
8、用宏来跟踪一个函数的所有调用。
/* g1.c:用宏来跟踪一个函数的所有调用。 */ #include <stdio.h> void func(int *a,int *b){ int t=*a; *a=*b; *b=t; } #define func(x,y) / (printf("func's invoking point: %s, %d/n",__FILE__,__LINE__), func((x),(y))) int main(){ int a[]={0,1,2,3,4,5,6,7,8}; printf("a[0]=%d,a[1]=%d/n",a[0],a[1]); func(&a[0],&a[1]); printf("a[0]=%d,a[1]=%d/n/n",a[0],a[1]); printf("a[2]=%d,a[3]=%d/n",a[2],a[3]); func(&a[2],&a[3]); printf("a[2]=%d,a[3]=%d/n/n",a[2],a[3]); printf("a[4]=%d,a[5]=%d/n",a[4],a[5]); func(&a[4],&a[5]); printf("a[4]=%d,a[5]=%d/n/n",a[4],a[5]); return 0; }
注意函数的定义必须出现在跟踪宏的定义之前,因为在宏体中使用了实际的函数,因此必须先看到其定义。在预处理时,预处理器发现main中的各个调用有同名的宏,并且参数匹配,因此会作宏展开,在实际的函数前插入了一个printf来跟踪这个调用的位置。
9、用宏来获取结构中成员的地址偏移字节量。
/* g2.c:用宏来获取结构中成员的地址偏移字节量 */ #include <stddef.h> /* 用到size_t */ #include <stdio.h> #define OFFSET(type,field) / ((size_t)&((type*)0)->field) struct S{ char a; int b; double c; } x; int main(){ printf("member a's offset: %d/n",OFFSET(struct S,a)); printf("member b's offset: %d/n",OFFSET(struct S,b)); printf("member c's offset: %d/n",OFFSET(struct S,c)); printf("sizeof(struct S): %d/n",sizeof(struct S)); return 0; }
注意很多C语言实现允许在间接选择运算符->的左边使用null指针,因为C标准中没有显式地允许或禁止这种做法。这样,对运算的结果取地址后转换成整型,就可以获得结构中成员的字节偏移量。从输出结果中(Linux下的gcc编译器编译)可以看出char型成员a被对齐为4个字节,因此整个S结构的长度为16个字节。当然这种获取成员地址偏移量的用法不一定所有的C语言实现都支持。若不支持,则可用预定义非null指针和从结构的基址中减去成员地址的方法来计算偏移量。这个OFFSET宏类似于stddef.h文件中的offsetof宏。
10、用宏来实现位掩码。位掩码是个整数,由指定的二进制0和1序列组成。位掩码可以用来掩掉一个整数的二进制表示中的某些位(只要让整数与位掩码作与运算即可),比如Linux/Unix中的umask就是文件默认访问权限的位掩码,网络的子网掩码就是IP地址的位掩码(可以掩掉其主机地址部分,以获得网络地址部分)。
/* bit_mask.h:位掩码的实现 */ #ifndef BIT_MASK_H #define BIT_MASK_H /* 一个字的低n位都是0,其余位都是1。注意n的值不能大于int型的宽度 */ #define low_zeroes(n) (-1<<n) /* 一个字的低n位都是1,其余位都是0 */ #define low_ones(n) (~(-1<<n)) /* 一个字的低offset位都是1,接着的width位都是0,其余位都是1 */ #define mid_zeros(width,offset) / (-1<<(width+offset) | ~(-1<<offset)) /* 一个字的低offset位都是0,接着的width位都是1,其余位都是0 */ #define mid_ones(width,offset) / (~(-1<<(width+offset) | ~(-1<<offset))) #endif
注意这些宏的实现是可移植的,它们并不依赖于整数使用多少位表示或计算机使用高位存储法还是低位存储法。
相关文章推荐
- libxml2剖析(1):功能特性
- Martin Fowler的《持续集成》
- C标准库源码解剖(14):通用函数stdlib.h
- RPM软件包的制作
- 回溯法
- dbm数据库源代码分析(1):概述
- vc 保存界面上控件为图片
- 最大子序列问题
- Java集合框架官方教程(6):算法
- 深入理解Java国际化
- Linux内核启动过程分析
- 我的C++实践(7):模板元编程实战
- VMWare组网攻略
- 搭建thrift服务
- 从ping和ping6说起
- 浏览器的渲染原理简介
- 第4部分:资源
- SQLite剖析(4):数据类型
- SQLite剖析(3):C/C++接口介绍
- 千万级并发实现的秘密:内核不是解决方案,而是问题所在!