宏的作用:
- 很大程度上是为了提高代码的可移植性
- 增强代码的可读性,例如利用宏定义常量:
#define PI 3.14159 - 做函数功能无法完成的功能(也称之为母函数)
宏的作用域:
由#define的作用域从文件中的的定义点开始,直到用#undef指令取消宏为止或者直到文件尾为止
无参数的宏
#define LIMIT 1000 // 常见用法
#define GOOD // 空宏 带参数的宏
#define ADD(x,y) ((x) + (y)) // 加法运算
#define SQRT(x) ((x) * (x)) // 平方运算 可变参数宏
#define debug(...) printf(__VA_ARGS__)
宏展开过程
第1步:首先用实参代替形参,将实参代入宏文本中;
第2步:如果实参也是宏,则展开实参;
第3步:最后继续处理宏替换后的宏文本,如果仍包含宏,则继续展开;
注意:如果在第2步,实参代入宏文本后,实参之前或之后遇到#或##,实参不再展开,即宏只展开一次。
#define MACRO(x) x*2
#define PRINT(y) printf("Value is %d\n", y)
#define STR(z) #z
int main() {
int x = 5;
PRINT(MACRO(x)); // 输出: Value is 10
printf("%s\n", STR(MACRO(x))); // 输出: x*2
return 0;
} -
PRINT(MACRO(x))的展开过程:- 首先用实参
x代替MACRO中的形参x,得到x*2 - 由于
x不是宏,所以不再展开,直接代入PRINT中,得到printf("Value is %d\n", x*2) - 最后计算
x*2的值为10,输出Value is 10
- 首先用实参
-
STR(MACRO(x))的展开过程:- 首先用实参
x代替MACRO中的形参x,得到x*2 - 由于
x*2之前遇到了#,所以x不再展开,直接字符串化,得到"x*2" - 输出
x*2
- 首先用实参
宏的三种表达语句
1. 使用(),既可以用在函数外面,也可以用在函数内
括号的作用是确保宏展开后的表达式具有正确的求值顺序和优先级。
宏参数列表使用()括起来,例如:
#define MAX(a, b) ((a) > (b) ? (a) : (b)) 在上例中,如果不加括号,当a或b是复杂表达式时,求值顺序可能会出错。
2. 语句表达式({ ... }),只能用于函数内
语句表达式是GCC提供的一种扩展,它允许将一个复合语句作为一个值使用。主要有以下特点:
- 可以返回一个值,因此非常适合定义返回值的宏
- 可以定义局部变量,避免重复计算表达式
- 相当于一个匿名内联函数
例如,如果这个宏参数是一个函数,那么就有可能被调用多次从而达到不一致的结果
#define MAX( x, y ) ( ((x) > (y)) ? (x) : (y) )
#define MIN( x, y ) ( ((x) < (y)) ? (x) : (y) )
c = MIN(a,foo(b)); 这时foo()函数就被调用了两次。为了解决这个潜在的问题,我们应当这样写min(X,Y)这个宏:
#define min(X,Y) ({\
typeof (X) x_ = (X);\
typeof (Y) y_ = (Y);\
(x_ < y_) ? x_ : y_; }) ({...})的作用是将内部的几条语句中最后一条的值返回,它也允许在内部声明变量(因为它通过大括号组成了一个局部Scope)
3. do{}while(0),只能用于函数内
用do{}while(0)语句包含多语句防止错误
- 将多语句作为一个语句执行,避免分号遗漏导致的语法错误
- 使用break代替goto控制程序流程
- 定义空宏
定义多语句宏,避免语法错误:
#define DO(a,b) a+b; a++;
if(...)
DO(a,b); //产生错误
else
...
#define DO(a,b) do{a+b;a++;}while(0) 使用do{}while(0)可以确保宏展开后的代码块作为一个语句执行,避免了分号遗漏或者缺少大括号导致的语法错误。
避免使用goto控制程序流:
#define FOO(ptr) do { \
if (!ptr) break; \
/* 操作ptr */ \
} while (0) 使用break代替goto,可以避免goto导致的程序流程混乱。
定义空宏:
#define EMPTY() do {} while (0)
定义一个什么也不做的空宏,可用于占位或者调试。
预定义宏
ANSI C定义了许多宏。在编程中您可以使用这些宏,但是不能直接修改这些预定义的宏。
| 宏 | 描述 |
__DATE__ | 当前日期,一个以 "MMM DD YYYY" 格式表示的字符串常量。 |
__TIME__ | 当前时间,一个以 "HH:MM:SS" 格式表示的字符串常量。 |
__FILE__ | 这会包含当前文件名,一个字符串常量。 |
__LINE__ | 这会包含当前行号,一个十进制常量。 |
__STDC__ | 当编译器以 ANSI 标准编译时,则定义为 1。 |
__func__ | 函数名/非标准 |
__FUNC__ | 函数名/非标准 |
__PRETTY_FUNCTION__ | 更详细的函数信息/非标准 |
宏的常见案例
// 1,防止一个头文件被重复包含
#ifndef COMDEF_H // 如果没有定义COMDEF_H,则定义COMDEF_H
#define COMDEF_H
//头文件内容 ...
#endif
// 2,重新定义一些类型,防止由于各种平台和编译器的不同,而产生的类型字节数差异,方便移植。
typedef unsigned long int uint32; /* Unsigned 32 bit value */
// 3,得到指定地址上的一个字节或字
#define MEM_B( x ) ( *( (byte *) (x) ) )
#define MEM_W( x ) ( *( (word *) (x) ) )
// 4,求最大值和最小值
#define MAX( x, y ) ( ((x) > (y)) ? (x) : (y) ) // 求最大值
#define MIN( x, y ) ( ((x) < (y)) ? (x) : (y) ) // 求最小值
// 5,得到一个field在结构体(struct)中的偏移量
#define FPOS( type, field ) ( (dword) &(( type *) 0)-> field )
// 6,得到一个结构体中field所占用的字节数
#define FSIZ( type, field ) sizeof( ((type *) 0)->field )
// 7,按照LSB格式把两个字节转化为一个word
#define FLIPW( ray ) ( (((word) (ray)[0]) * 256) + (ray)[1] )
// 8,按照LSB格式把一个word转化为两个字节
#define FLOPW( ray, val ) \
(ray)[0] = ((val) / 256); \
(ray)[1] = ((val) & 0xFF)
// 9,得到一个变量的地址(word宽度)
#define B_PTR( var ) ( (byte *) (void *) &(var) )
#define W_PTR( var ) ( (word *) (void *) &(var) )
// 10,得到一个字的高位和低位字节
#define WORD_LO(xxx) ((byte) ((word)(var) & 255))
#define WORD_HI(xxx) ((byte) ((word)(var) >> 8))
// 11,返回一个比X大的最接近的8的倍数
#define RND8( x ) ((((x) + 7) / 8 ) * 8 )
// 12,将一个字母转换为大写
#define UPCASE( c ) ( ((c) >= 'a' && (c) <= 'z') ? ((c) - 0x20) : (c) )
// 13,判断字符是不是10进值的数字
#define DECCHK( c ) ((c) >= '0' && (c) <= '9')
// 14,判断字符是不是16进值的数字
#define HEXCHK( c ) ( ((c) >= '0' && (c) <= '9') || \
((c) >= 'A' && (c) <= 'F') || \
((c) >= 'a' && (c) <= 'f') )
// 15,防止溢出的一个方法
#define INC_SAT( val ) (val = ((val)+1 > (val)) ? (val)+1 : (val))
// 16,返回数组元素的个数
#define ARR_SIZE( a ) ( sizeof( (a) ) / sizeof( (a[0]) ) )
// 17,对于IO空间映射在存储空间的结构,输入输出处理
#define inp(port) (*((volatile byte *) (port)))
#define inpw(port) (*((volatile word *) (port)))
#define inpdw(port) (*((volatile dword *)(port)))
#define outp(port, val) (*((volatile byte *) (port)) = ((byte) (val)))
#define outpw(port, val) (*((volatile word *) (port)) = ((word) (val)))
#define outpdw(port, val) (*((volatile dword *) (port)) = ((dword) (val)))
// 18,使用一些宏跟踪调试
// ANSI标准说明了五个预定义的宏名.打印调试日志信息。
// 当定义了_DEBUG,输出数据信息和所在文件所在行
#ifdef _DEBUG
#define DEBUGMSG(msg,date) printf(msg);printf(“%d%d%d”,date,_LINE_,_FILE_)
#else
#define DEBUGMSG(msg,date)
#endif
// 19,宏定义防止使用是错误
// 用小括号包含。
// 例如:#define ADD(a,b) ((a) + (b))
// 用do{}while(0)语句包含多语句防止错误
// 例如:#difne DO(a,b) a+b;\
a++;
//应用时:
if(….)
DO(a,b); //产生错误
else
//解决方法:
#difne DO(a,b) do{a+b;\
a++;}while(0)
其他指令
#error指令
功能:通常与条件编译一起,用于检测正常编译过程中不应出现的情况。遇到#error指令预示程序中出现了严重错误,通常编译器会立即停止编译。
#if INT_MAX < 100000
#error int type is to small
#endif #line指令
- 用来改变程序行编号。
- #line n这条指令导致后续的编号为n、n+1、n+2…
- #line n “文件”,指令后面的行会被认为来自文件,行号由n开始。
#pragma指令
为啥用#pragma指令?
C和C++程序的每次执行都支持其所在的主机或操作系统所具有的一些独特的特点,例如有些程序需要精确控制数据存放的内存区域或控制某个函数接收的参数。#pragma为编译器提供了一种在不同机器和操作系统上编译以保持C和C++完全兼容的方法。而#pragma指令是由机器和相关的操作系统定义的,通常来说每个编译器是不同的。注意#pragma指令施加目标是编译器,能让编译器接受特殊的指令。
#pragma指令常见语法:
-
#pragma once保证所在头文件只会被包含一次,它是基于磁盘文件的,而#ifndef则是基于宏的。 -
#pragma warning允许有选择性的修改编译器的警告消息的行为。有如下用法:#pragma warning(disable:4507 34; once:4385; error:164) //等价于: #pragma warning(disable:4507 34) // 不显示4507和34号警告信息 #pragma warning(once:4385) // 4385号警告信息仅报告一次 #pragma warning(error:164) // 把164号警告信息作为一个错误 #pragma warning(default:176) // 重置编译器的176号警告行为到默认状态 //同时这个pragma warning也支持如下格式,其中n代表一个警告等级(1---4): #pragma warning(push) // 保存所有警告信息的现有的警告状态 #pragma warning(push,n) // 保存所有警告信息的现有的警告状态,并设置全局报警级别为n #pragma warning(pop) // #pragma warning(push) #pragma warning(disable:4705) #pragma warning(disable:4706) #pragma warning(disable:4707) #pragma warning(pop) // 在这段代码后,恢复所有的警告信息(包括4705,4706和4707)。 -
#pragma hdrstop表示预编译头文件到此为止,后面的头文件不进行预编译。BCB可以预编译头文件以 加快链接的速度,但如果所有头文件都进行预编译又可能占太多磁盘空间,所以使用这个选项排除一些头文件. -
#pragma message在标准输出设备中输出指定文本信息而不结束程序运行。用法如下:#pragma message("消息文本") // 当编译器遇到这条指令时就在编译输出窗口中将“消息文本”打印出来。 -
#pragma data_seg一般用于DLL中,它能够设置程序中的初始化变量在obj文件中所在的数据段。如果未指定参数,初始化变量将放置在默认数据段.data中,有如下用法:#pragma data_seg("Shared") // 定义了数据段"Shared",其中有两个变量a和b int a = 0; // 存储在数据段"Shared"中 int b; // 存储在数据段".bss"中,因为没有初始化 #pragma data_seg() // 表示数据段"Shared"结束,该行代码为可选的对变量进行专门的初始化是很重要的,否则编译器将把它们放在普通的未初始化数据段中而不是放在shared中。如上述的变量b其实是放在了未初始化数据段
.bss中。#pragma data_seg("Shared") // 定义了数据段"Shared",其中有个变量j int j = 0; // 存储在数据段"Shared"中 #pragma data_seg(push, stack1, "Shared2") // 定义数据段Shared2,并将该记录赋予别名stack1,然后放入内部编译器栈中 int l = 0; // 存储在数据段"Shared2"中 #pragma data_seg(pop, stack1) // 从内部编译器栈中弹出记录,直到弹出stack1,如果没有stack1,则不做任何操作 int m = 0; // 存储在数据段"Shared"中,如果没有上述pop段,则该变量将储在数据段"Shared2" -
#pragma code_seg它能够设置程序中的函数在obj文件中所在的代码段。如果未指定参数,函数将放置在默认代码段.text中,有如下用法:void func1() { } // 默认存储在代码段.text中 #pragma code_seg(".my_data1") // 存储在代码段.my_data1中void func2() { } #pragma code_seg(push, r1, ".my_data2") // 存储在代码段.my_data2中 void func3() { } #pragma code_seg(pop, r1) // 存储在代码段.my_data1中 void func4() { } -
#pragma pack用来改变编译器的字节对齐方式。常规用法为:#pragma pack(n) // 将编译器的字节对齐方式设为n,n的取值一般为1、2、4、8、16,一般默认为8 #pragma pack(show) // 以警告信息的方式将当前的字节对齐方式输出 #pragma pack(push) // 将当前的字节对齐方式放入到内部编译器栈中 #pragma pack(push,4) // 将字节对齐方式4放入到内部编译器栈中,并将当前的内存对齐方式设置为4 #pragma pack(pop) // 将内部编译器栈顶的记录弹出,并将其作为当前的内存对齐方式 #pragma pack(pop,4) // 将内部编译器栈顶的记录弹出,并将4作为当前的内存对齐方式 // r1为自定义的标识符 // 将内部编译器中的记录弹出,直到弹出r1,并将r1的值作为当前的内存对齐方式; // 当r1不存在,不做任何操作; #pragma pack(pop,r1) // 以如下结构为例: struct buffer { char a; WORD b; DWORD c; char d; }; // 在Windows32位默认结构大小: sizeof(struct) = 4+4+4+4=16; // 与#pragma pack(4)一样 // 若设为 #pragma pack(1), 则结构大小: sizeof(struct) = 1+2+4+1=8; // 若设为 #pragma pack(2), 则结构大小: sizeof(struct) = 2+2+4+2=10; // 在#pragma pack(1)时:空间是节省了,但访问速度降低了; // 有什么用处??? // 在系统通讯中,如和硬件设备通信,和其他的操作系统进行通信时等,必须保证双方的数据一致性。 -
#pragma comment将一个注释记录放置到对象文件或可执行文件中。其格式为:// comment-type是一个预定义的标识符,指定注释的类型,如:compiler,exestr,lib,linker,user之一。 #pragma comment( comment-type [,"commentstring"] ) #pragma comment(lib,“ .../Debug/Test.lib ”) // 表示链接Test.lib文件 #pragma comment(linker,"/ENTRY:main_function") // 表示指定链接器选项/ENTRY:main_function // compiler:放置编译器的版本或者名字到一个对象文件,该选项是被linker忽略的。 // exestr:在以后的版本将被取消。 /* lib: 放置一个库搜索记录到对象文件中,这个类型应该与commentstring(指定Linker要搜索的lib的名称和路径)所指定的库类型一致。在对象文件中,库的名字跟在默认搜索记录后面;linker搜索这个这个库就像你在命令行输入这个命令一样。你可以在一个源文件中设置多个库搜索记录,它们在obj */ // 文件中出现的顺序与在源文件中出现的顺序一样。 // 如果默认库和附加库的次序是需要区别的,使用/Zl编译开关可防止默认库放到object模块中。 // linker:指定一个连接选项,这样就不用在命令行输入或者在开发环境中设置了。只有下面的linker选项能被传给Linker: // 1. /DEFAULTLIB // 2. /EXPORT // 3. /INCLUDE // 4. /MANIFESTDEPENDENCY // 5. /MERGE // 6. /SECTIONLinker参数详解介绍:
(1)/DEFAULTLIB:library /DEFAULTLIB选项将一个library添加到LINK在解析引用时搜索的库列表。用/DEFAULTLIB指定的库在命令行上指定的库之后和obj文件中指定的默认 库之前被搜索。 忽略所有默认库(/NODEFAULTLIB)选项重写/DEFAULTLIB:library。如果在两者中指定了相同的library名称,忽略库(/NODEFAULTLIB:library)选项 将重写/DEFAULTLIB:library。 (2)/EXPORT:entryname 使用该选项,可以从程序导出函数以便其他程序可以调用该函数,也可以导出数据。通常在DLL中定义导出。 entryname是调用程序要使用的函数或数据项的名称。ordinal为导出表的索引,取值范围在1至65535;如果没有指定ordinal,则LINK将分配一个。 NONAME关键字只将函数导出为序号,没有entryname。DATA 关键字指定导出项为数据项。客户程序中的数据项必须用extern __declspec (dllimport)来声明。 有三种导出定义的方法,按照建议的使用顺序依次为: 1. 源代码中的__declspec(dllexport) 2. .def文件中的EXPORTS语句 3. LINK命令中的/EXPORT规范 所有这三种方法可以用在同一个程序中。LINK在生成包含导出的程序时还要创建导入库,除非在生成过程中使用了.exp 文件。 LINK使用标识符的修饰形式。编译器在创建obj文件时修饰标识符。如果entryname以其未修饰的形式指定给链接器(与其在源代码中一样),则LINK 将试图匹配该名称。如果无法找到唯一的匹配名称,则LINK发出错误信息。当需要将标识符指定给链接器时,请使用Dumpbin工具获取该标识符的修饰 名形式。 (3)/INCLUDE:symbol /INCLUDE选项通知链接器将指定的符号添加到符号表。若要指定多个符号,请在符号名称之间键入逗号(,)、分号(;)或空格。在命令行上,对每个符号需指定一次/INCLUDE:symbol。 链接器通过将包含符号定义的对象添加到程序来解析symbol。该功能对于添加不会链接到程序的库对象非常有用。 用该选项所指定的符号将覆盖通过/OPT:REF对该符号进行的移除操作。 (4)/MANIFESTDEPENDENCY:manifest_dependency /MANIFESTDEPENDENCY允许你指定位于manifest文件的<dependency>段的属性。/MANIFESTDEPENDENCY信息可以通过下面两种方式传递给LINK: 直接在命令行运行/MANIFESTDEPENDENCY 通过#pragma comment (5)/MERGE:from=to /MERGE选项将第一个段(from)与第二个段(to)进行联合,并将联合后的段命名为to的名称。 如果第二个段不存在,LINK将段(from)重命名为to的名称。 /MERGE选项对于创建VxDs和重写编译器生成的段名非常有用。 (6)/SECTION:name /SECTION选项用来改变段的属性,当指定段所在的obj文件编译的时候重写段的属性集。 可移植的可执行文件(PE)中的段(section)与新可执行文件(NE)中的节区(segment)或资源大致相同。 段(section)中包含代码或数据。与节区(segment)不同的是,段(section)是没有大小限制的连续内存块。有些段中的代码或数据是你的程序直接定义和 使用的,而有些数据段是链接器和库管理器(lib.exe)创建的,并且包含了对操作系统来说很重要的信息。 /SECTION选项中的name是大小写敏感的。 不要使用以下名称,因为它们与标准名称会冲突,例如,.sdata是RISC平台使用的。 .arch .bss .data .edata .idata .pdata .rdata .reloc .rsrc .sbss .sdata .srdata .text .xdata 为段指定一个或多个属性。属性不是大小写敏感的。对于一个段,你必须将希望它具有的属性都进行指定;如果某个属性未指定,则认为是不具备这个属 性。如果你未指定R,W或E,则已存在的读,写或可执行状态将不发生改变。 要对某个属性取否定意义,只需要在属性前加感叹号(!)。 E:可执行的 R:可读取的 W:可写的 S:对于载入该段的镜像的所有进程是共享的 D:可废弃的 K:不可缓存的 P:不可分页的 注意K和P是表示否定含义的。 PE文件中的段如果没有E,R或W属性集,则该段是无效的。 ALIGN=#选项让你为一个具体的段指定对齐值。 user:放置一个常规注释到一个对象文件中,该选项是被linker忽略的。 -
#pragma section创建一个段,其格式为:#pragma section( "section-name" [, attributes] )section-name是必选项,用于指定段的名字。该名字不能与标准段的名字想冲突。可用/SECTION查看标准段的名称列表。
attributes是可选项,用于指定段的属性。可用属性如下,多个属性间用逗号(,)隔开:
read:可读取的
write:可写的
execute:可执行的
shared:对于载入该段的镜像的所有进程是共享的
nopage:不可分页的,主要用于win32的设备驱动程序中nocache:不可缓存的,主要用于win32的设备驱动程序中discard:可废弃的,主要用于
win32的设备驱动程序中remove:非内存常驻的,仅用于虚拟设备驱动(
VxD)中如果未指定属性,默认属性为read和write。
在创建了段之后,还要使用
__declspec(allocate)将代码或数据放入段中。例如:
//pragma_section.cpp #pragma section("mysec",read,write) // 创建段mysec,并设置属性:read,write int i = 0; // i入了默认的数据段中 __declspec(allocate("mysec")) // 声明数据段mysec打开 int j = 0; // j放入了mysec数据段中 int main(){ return 0; } -
#pragma push_macro与#pragma pop_macro前者将指定的宏压入栈中,相当于暂时存储,以备以后使用;后者将栈顶的宏出栈,弹出的宏将覆盖当前名称相同的宏。例如:#include <stdio.h> #define X 1 #define Y 2 int main() { printf("%d",X); printf("\n%d",Y); #define Y 3 // C4005 #pragma push_macro("Y") #pragma push_macro("X") printf("\n%d",X); #define X 2 // C4005 printf("\n%d",X); #pragma pop_macro("X") printf("\n%d",X); #pragma pop_macro("Y") printf("\n%d",Y); }运行结果:
1 2 1 2 1 3