C/C++ 中的宏 (#define) 与预处理 (#if/#ifdef/#pragma) 的使用方法大全、使用技巧

原文:C/C++ 中宏与预处理使用方法大全 (VC)
作者:Breaker <breaker.zy_AT_gmail>


C/C++ 中的宏 (#define) 与预处理 (#if/#ifdef/#pragma) 的使用方法大全、使用技巧

开发环境:VC 2005

关键字:宏, 预定义宏, 预处理, 预编译头, VC, #pragma, 编译选项, 程序区段

RTFM: Read The F__king Manual/MSDN

目录


VC 中的宏使用方法参考 MSDN: Macros (C/C++)

C/C++ 预定义宏^

__LINE__: 当前源文件的行号,整数
__FILE__: 当前源文件名,char 字符串,使用 /FC 选项产生全路径
__DATE__: 当前编译日期,char 字符串,格式 Aug 28 2011
__TIME__: 当前编译时间,char 字符串,格式 06:43:59
__STDC__: 整数 1,表示兼容 ANSI/ISO C 标准,配合 #if 使用
__TIMESTAMP__: 最后一次修改当前文件的时间戳,char 字符串,格式 Sun Aug 28 06:43:57 2011
__cplusplus: 以 C++ 方式而非 C 语言方式编译时定义,VC 2005 中定义为 199711L,配合 #ifdef 使用

例子:C/C++ 预定义宏的取值^

MacroTest.h 中定义函数 PrintSourceInfo() 和 PRINT_SOURCE_INFO(),在 MacroTest.cpp include=> MacroTest.h,并调用它们

输出结果

(1). 使用函数 PrintSourceInfo(),无论 Debug/Release 方式编译,无论是否 inline 化 PrintSourceInfo(),输出结果相同,均是 MacroTest.h 的信息:

File: d:\source\macrotest\macrotest.h, Line: 64, Date: Aug 28 2011, Time: 06:43:59, Timestamp: Sun Aug 28 06:43:57 2011, ANSI/ISO C: NO, C++: YES

(2). 使用宏 PRINT_SOURCE_INFO(),Debug/Release 方式编译输出结果大致相同,均是 MacroTest.cpp 的信息,只是 Debug 输出的 __FILE__ 是全路径,而 Release 输出的是相对路径:

File: d:\source\macrotest\macrotest.cpp, Line: 14, Date: Aug 28 2011, Time: 07:42:30, Timestamp: Sun Aug 28 07:38:25 2011

说明

(1). __FILE__、__DATE__、__TIME__ 是 char 字符串,而不是 wchar_t 宽字符字符串,需配合 _T()、_t 系列函数使用

(2). 如果在函数 PrintSourceInfo() 中使用宏,则 __FILE__、__LINE__、__TIME__ 等表示的是 PrintSourceInfo() 所在文件,即例 1 中的 MacroTest.h 的信息;如果在宏 PRINT_SOURCE_INFO() 中使用宏,因为宏 PRINT_SOURCE_INFO() 嵌套展开的缘故,__FILE__ 等表示的是 PRINT_SOURCE_INFO() 展开所在文件,即 MacroTest.cpp 的信息

(3). 无论使用 PrintSourceInfo() 还是 PRINT_SOURCE_INFO(),__LINE__ 总是文件 .h/.cpp 的固有行号,而非 [MacroTest.cpp include=> MacroTest.h] 预处理展开后的行号

(4). 在 VC 2005 中,上述编译方式下没有定义 __STDC__,要使 __STDC__ = 1,应同时满足以下条件:

  • (a). 以 C 方式编译
  • (b). 使用编译选项 /Za,表示禁止 Microsoft C/C++ 语言扩展,从而兼容 ANSI C/C++
C/C++ 预定义宏用途:诊断与调试输出^

参考 VC CRT 和 MFC 的代码,注意:需要在宏中使用 __FILE__、__LINE__,原因见上面“说明 (2)”

CRT 的诊断与调试输出:assert, _ASSERT/_ASSERTE, _RPTn/_RPTFn/_RPTWn/_RPTFWn^

CRT 的诊断宏 assert()、_ASSERT()/_ASSERTE()

CRT 的调试输出宏 _RPTn()/_RPTFn(),n: 0 ~ 5
_RPTWn()/_RPTFWn() 是宽字符版

MFC 的诊断与调试输出:ASSERT/VERIFY, ASSERT_VALID, TRACE/TRACEn^

MFC 的诊断宏 ASSERT()/VERIFY()、ASSERT_VALID()

MFC 的调试输出宏 TRACE()/TRACEn(),n: 0 ~ 3

MFC 的调试版 new^

CRT 和 C 标准库中的宏^

VC CRT 和 C 标准库中的宏参考 MSDN: Global Constants

NULL 空指针^

NULL 在 stddef.h, stdio.h, stdlib.h 等多个头文件中定义,是地址/指针类型的 0,如下:

C++ 中的 0 是类型自动的,所以用 0 定义 NULL;而 C 中 0 是确定的 int 类型,所以需要强制

C++ 中,当 NULL 的相关操作数,如:对比操作 ptr == NULL,或函数的形参是指针类型时,或者能够“从指针类型隐式转换”时,0 被自动转换为指针类型

例子:NULL 隐式转换和 0 是类型自动的^

limits.h 整数类型常量^

在 limits.h 中定义,定义了各种 int 类型 (unsigned, char, short, long, __int64) 的最小、最大值,如 SCHAR_MAX (signed char MAX)、UCHAR_MAX (unsigned char MAX)、USHRT_MAX (unsigned short MAX) 等。编译时,如果 int 字面量超出这些范围,会编译出错

参考 MSDN: Integer Limits

float.h 浮点类型常量^

在 float.h 中定义,定义各种浮点类型 (float, double, long double) 的极限值,如最小、最大值,最小浮点差量 (epsilon) 等

参考 MSDN: Floating Limits

例子:浮点数极限值:判断浮点数是否相等^

math.h 数学常量^

数学计算常用的浮点数常量,如 M_PI (pi), M_E (e), M_SQRT2 (sqrt(2)) 等。这些数学常量不是标准 C/C++ 的一部分,而是 Microsoft 的扩展,使用前需要定义 _USE_MATH_DEFINES:

EOF 常量^

EOF (end-of-file) 常量,定义为 (-1),有宽字符版 WEOF ((wint_t)(0xFFFF)),EOF 和 WEOF 在 stdio.h 中定义,还有 _TCHAR 版 _TEOF,在 tchar.h 中定义。EOF 在流、I/O 操作中表示到达流、文件末尾(EOF 条件),也用来表示发生错误情况

例子:标准输入的 EOF^

测试输出,用 Ctrl + Z 产生 EOF 信号:

errno.h 错误代码^

在 errno.h 中定义,是测试全局量 errno 的值,errno 在 VC 中实现为线程安全的函数,而非全局变量。错误代码以 E 打头如 EINVAL:不合法的参数错误

错误代码具体值参考 MSDN: errno Constants errno, _doserrno, _sys_errlist, and _sys_nerr

locale 类别^

locale 类别 (Categories),在 locale.h 中定义,如 LC_ALL、LC_CTYPE

_MAX_PATH 等文件名与路径长度限制^

包括全路径与各部分路径的限制,即 FILENAME_MAX、_MAX_PATH、_MAX_DRIVE、_MAX_EXT、_MAX_FNAME、_MAX_DIR,在 stdlib.h 中定义。最大全路径长度限制在 260,和 Windows 的 MAX_PATH 相同,这是为了兼容 Windows 98 FAT32 文件系统。CRT 支持 32767 长度的文件名,方法和 Windows API 相同,即使用 "\\?\" 路径前缀,并调用 Unicode 宽字符版的 CRT 函数

RAND_MAX 随机数最大值^

在 stdlib.h 中定义为 32767,rand() 函数会产生 0 ~ RAND_MAX 之间的伪随机 int 值

例子:用 RAND_MAX 产生某个范围内的随机数^

va_arg/va_start/va_end 访问变长函数参数^

用于访问类似 printf(const char* format, ...) 等变长函数参数的辅助宏,在 stdarg.h 中声明,参考 MSDN: va_arg, va_end, va_start

宏实现的 CRT 函数^

在 VC CRT 中有些函数以宏和函数两种方式实现,如 getchar(),并优先使用宏版本,

强制使用函数版的方法:

(1). 调用时给函数名加括号,如 (getchar)()
(2). 调用前,取消宏版本的定义,如 #undef getchar

两种实现方式的比较见 MSDN: Recommendations for Choosing Between Functions and Macros

Microsoft 预定义宏^

VC C/C++ 和 Microsoft 预定义宏参考 MSDN: Predefined Macros

这些宏可以分类如下:

平台与系统类^

_M_IX86: IA32/x86 平台
_M_IA64: IA64/IPF (Itanium Processor Family) 64bit 平台
_M_X64: x64/x86-64/AMD64 平台
WIN32, _WIN32: Win32 和 Win64 程序开发都会定义
_WIN64: Win64 程序开发
_CONSOLE: 控制台 Windows 程序开发,链接 Console 子系统:/SUBSYSTEM:CONSOLE
_WINDOWS: 非控制台 Windows 程序开发,链接 Windows 子系统:/SUBSYSTEM:WINDOWS

版本号类^

通常定义为数字,配合 #if (XXX >= 1000) 使用,启动、禁用特定部分的代码、特性

_MSC_VER: VC 编译器 cl 版本号。VC 2003 编译器版本号 13.10 (_MSC_VER = 1310),VC 2005 编译器版本号 14.00 (_MSC_VER = 1400)。用 cl /? 查看编译器版本号
_MFC_VER: MFC 版本号
_ATL_VER: ATL 版本号
__CLR_VER: CLR 版本号
WINVER: 目标 Windows 版本号
_WIN32_WINNT: 目标 Windows NT 版本号
_WIN32_WINDOWS: 目标 Windows 9x 版本号
_WIN32_IE: 目标 IE 版本号

工程配置管理类^

_DEBUG, NDEBUG: Debug/Release 编译方式
UNICODE, _UNICODE, _MBCS: ANSI/UNICODE/MBCS 字符集支持
_AFXDLL: 动态链接 MFC (DLL)
_ATL_STATIC_REGISTRY, _ATL_DLL: 静态/动态链接 ATL
_DLL: 动态链接 CRT (DLL),对应 /MD、/MDd 编译选项
_MT: CRT 多线程支持,目前 4 种 CRT 链接方式 /MD、/MDd、/MT、/MTd 都支持多线程(VC 2005 已没有单线程版 CRT),加上创建 DLL 模块的 /LD、/LDd,都定义 _MT
_MANAGED: 以 /clr、/clr:pure、/clr:safe 托管方式编译时,定义为 1
__cplusplus_cli: 以 /clr、/clr:pure、/clr:safe 方式编译时定义,VC 2005 中定义为 200406L

上面 1、2、3 类宏通常和条件编译预处理指令 #if/#ifdef/#ifndef 配合使用

辅助类^

__VA_ARGS__: 在函数式宏中,代表变长部分参数 (...),参考 MSDN: Variadic Macros

__COUNTER__: include 展开编译单元后,编译时第一次遇到 __COUNTER__ 替换为 0,以后在这个编译每遇到一次 __COUNTER__ 自增一。不同的编译单元之间 __COUNTER__ 不互相积累叠加,均从 0 开始计数,但预编译头 .pch 文件会记录 __COUNTER__ 的历史值,则每个编译单元均从历史值 + 1 开始计数。__COUNTER__ 支持宏的嵌套展开

__FUNCTION__, __FUNCDNAME__, __FUNCSIG__: 表示所在函数的函数名的 char 字符串。例如,对于 void test_funcname_macro() 函数原型,它们的值如下:

(1). __FUNCTION__ = test_funcname_macro: 函数的原始名/非修饰名 (undecorated)
(2). __FUNCDNAME__ = ?test_funcname_macro@@YAXXZ: 函数的修饰名 (decorated),可用工具 undname "decorated_name" 得出函数原型和调用规范,即 __FUNCSIG__ 所表示的
(3). __FUNCSIG__ = void __cdecl test_funcname_macro(void): 函数的 signature 名,即调用约定、返回值类型、参数类型

例子:用 __VA_ARGS__ 打印跟踪函数调用^

这个 CALL_TRACE 功能不实用,只为说明 __VA_ARGS__ 用法:

例子:用 __VA_ARGS__ 格式化 std::string^

例子:用 __COUNTER__ 计数值定义掩码常量^

这种方法限制很多,并不实用,如 MyMask 之后再定义另一个掩码列举型时,会从 __COUNTER__ 的历史值而非 0 开始:

例子:用 __FUNCTION__ 打印跟踪函数调用^

Windows API 中的注释性宏^

注释性宏,即是否使用它们不影响编译结果,通常定义为空

目的:

(1). 在源代码中起到注解 (annotation) 和标注 (marker) 作用,便于阅读和理解代码功能
(2). 指导 lint 等静态代码检查工具检查代码缺陷
(3). 指导文档自动生成工具扫描源文件,生成类、函数/API 参考文档

如 WinDef.h 中定义的 IN、OUT、OPTIONAL 用来说明函数参数或类型成员的传入、传出、可选性质

sal.h 中有更完整和复杂的注释性宏,SAL (Source code Annotation Language) 参考 sal.h 源文件和 MSDN: SAL Annotations

Windows API 和 CRT 都用 SAL 注释,几个常用的如下:

__in: 传入参数
__out: 传出参数
__inout: 传入且传出参数
__in_opt, __out_opt, __inout_opt: 可选参数,可以为 NULL

如 CreateFileW() 的声明:

Windows API 中的常用宏^

Windows API 包含大量旗标、掩码、状态码、错误码等常量式宏

函数式宏最常用的有:

类型辅助类^
GDI 类^

另外,BITMAP_WIDTHBYTES(bits) 不在 Windows API 中,但比较常用于位图:

错误处理类^

标记没有使用的参数、变量辅助宏^

UNREFERENCED_PARAMETER(P)
DBG_UNREFERENCED_PARAMETER(P)
DBG_UNREFERENCED_LOCAL_VARIABLE(V)

让没有使用的参数、变量不产生编译警告,并且关闭 lint 缺陷检查报告

错误码、状态码^

Windows 有三大错误码、状态码空间:

(1). Win32 状态码:GetLastError() 所返回,DWORD 类型,WinError.h 中定义
(2). COM 状态码:COM 函数用,HRESULT 类型,WinError.h 中定义
(3). 内核状态码:内核函数和低级 API 用,NTSTATUS 类型,ntstatus.h 中定义

状态码有关的宏:

Win32 状态码没有类似 MAKE_HRESULT 的宏,自定义 Win32 状态码时可以用 mc (Message Compiler) 工具处理 .mc 脚本,自动生成含自定义 Win32 状态码的头文件,同时生成用于 FormatMessage() 的状态码文本描述,参考 MSDN:Message Compiler

也可以自定义用于 Win32 状态码的 MAKE_WINERR():

调用规范类^

调用规范/约定参考 MSDN: Calling Conventions

Windows API 使用的调用规范名称宏,在 WinDef.h 中定义:

COM 常用的调用规范辅助宏:

国际化类^
资源类^
网络类^

字符串化操作符 #^

将代码中某个名字转换为字符串字面量,即“加引号”,参考 MSDN: Stringizing Operator

用 # 操作构造字符串化宏 STRINGIZE^

说明:

(1). # x 产生的是 char 字符串,非 wchar_t 字符串,需配合 _T() 使用

(2). _MACRO() 再次调用 __MACRO() 是一种针对 # 和 ## 操作的常用编写技巧。因为 #、## 操作比较特殊,当它处于宏体中时,不会进行嵌套展开,如 __TSTRINGIZE(NULL) 展开为 "NULL" 而非 "0",要想嵌套展开,再定义一层 _STRINGIZE() 调用 __STRINGIZE() 即可,_TSTRINGIZE(NULL) 展开为 "0"

CRT 中的 STRINGIZE 定义^

CRT 中有类似上面的 STRINGIZE(),以及宽字符化字面量宏 _CRT_WIDE() 的定义:

STRINGIZE 的展开规则^

1. 如果 _STRINGIZE() 的参数是宏,那么宏代表的实际值也将被展开,即嵌套展开

例子:用 STRINGIZE 查看宏的展开结果^

查看某个宏在当前编译配置 (Debug/Release, ANSI/Unicode) 下,实际表示的东西,如某个 _t 系列函数、Windows API 究竟表示哪个函数,可以利用 _STRINGIZE():

输出结果:

2. 如果 _STRINGIZE() 的参数单纯的变量、函数、类型、const、enum 常量,那么只是将 _STRINGIZE() 括号中的东西加引号而已,如下:

输出结果:

拼接操作符 ##^

将代码中两个名字拼接到一起,形成一个名字。## 操作“不加引号”,参考 MSDN: Token-Pasting Operator

## 与 # 一样对其操作数不进行嵌套展开,所以 __CONCAT(aaa, __CONCAT(bbb, ccc)) 的展开结果是 aaa__CONCAT(bbb, ccc),而 _CONCAT(aaa, _CONCAT(bbb, ccc)) 的展开结果是 aaabbbccc。## 的结果是名字拼接,而不是字符串字面量,即不是 "aaabbbccc"

通常用 ## 操作拼接构造类型、变量、函数的名字

例子:_T() 的定义^

例子:Windows API 通用句柄类型的定义^

例子:用 ## 构造函数名^

TCHAR 统一字符类型和处理^

_TCHAR、_T()、_t 系列函数等东西叫做 Generic-Text Mapping,即使用宏进行统一字符类型编写,在不同的字符集编码工程配置 ANSI/UNICODE/MBCS 下替换为不同的实际函数或类型,参考 MSDN:Generic-Text Mappings,Using Generic-Text Mappings, Using TCHAR.H Data Types with _MBCS

工程的字符集配置的宏定义:

ANSI (SBCS, ASCII): _UNICODE 和 _MBCS 均未定义,使用 char 单字节字符集编码
UNICODE: _UNICODE 定义,使用 wchar_t 宽字符集编码,VC 默认 wchar_t 2 字节
MBCS: _MBCS 定义,使用 char 变长字符集编码,一个字符占一个或多个 char

_TCHAR, _TEXT()/_T(), _t 系列函数^

根据 _UNICODE、_MBCS 的定义,调用 ANSI/UNICODE/MBCS 不同字符集版本的 CRT 函数,或产生字面量,多在 tchar.h 中声明。_t 字符操作函数参考 MSDN:String Manipulation (CRT)

TCHAR, LPTSTR/LPCTSTR, TEXT(), A/W 版本 Windows API^

根据 UNICODE 的定义,调用 ANSI/UNICODE 不同字符集版本的 Windows API,或产生字面量,多在 WinBase.h、Windows.h 中声明

不成文约定:带 _ 前缀的代码,通常对应 CRT,而不带 _ 前缀的东西,通常对应 Windows API。A/W 版本 API 都是接收字符串参数的函数,但并非所有接收字符串的 API 都有 A/W 两个版本,如 GetProcAddress() 只是 A 版本函数,因为 DLL 中的导出符号用 ASCII 英文足够了

宏的缺点和替代方法^

宏是预编译行为,所做的是名字替换,它的缺点和替代方法如下:

宏难于调试^

编译时,宏不会产生用于调试的名字符号。如 #define MAX_PATH 260,在调试时,无法找到其符号名 MAX_PATH,而只是 260 数字

常量式宏可以用 const 和 enum 代替,在调试中可以查看 const、enum 的符号名,并且 const、enum 和宏的运行时开销是相同的(有使用 const、enum 时才会分配内存):

另外,在 VC 2005 中,进行 C/C++ 源码级别调试时,函数式宏无法像 inline 或普通函数一样使用 Step into 进入宏定义体的代码,宏调用被视为一条语句,只能使用 Go To Definition (F12) 跳转到宏定义处查看代码,而不能调试

宏的使用缺陷^

(1). 宏以字面形式展开,有副作用,典型的有两种:

(a). 宏参数不加括号展开时改变逻辑,如 #define RECT_AREA(x, y) (x * y)

解决方法:定义宏时给参数的使用加上括号,如 #define RECT_AREA(x, y) ((x) * (y))

(b). 宏体为多行语句,如果放到判断语句中,并且不加 {} 包起来,只有第一句在判断语句下执行,其它在判断语句外,如下例:

解决方法:定义宏时用 {} 或 do {} while(0) 包起来,如下:

(2). 宏对参数没有类型检查,宏的返回也不具有类型

(3). 函数式宏,不是函数,不能将其宏名作为函数指针,即不能进行函数回调;也不能进行递归调用

函数式宏大多能用 inline 函数 + 函数 template 的方式代替,并保持相同的运行时开销。但因为 inline 函数是一种 尽力而为 (Try My Best) 的编译器指示(inline 函数不一定 inline 化,inline 化的程度也不同),实际的开销根据 inline 函数调用复杂程度(是否有递归、作为函数指针)、不同编译器、不同的工程配置(Debug/Release、编译选项、编译优化级别),inline 化有所不同

参考 MSDN: Inline Functions versus Macros

宏造成全局名字空间污染^

宏是全局名字空间的,容易造成名字污染、干扰,可用 const、enum、inline 解决。如下:

输出结果:

后面定义的宏 MACRO_DEF_VAL 的值将前面的覆盖了:

优先使用宏的情况^

不是所有的宏都能用 const、enum、inline 函数代替:

(1). 对于一些不对应单个函数、变量、常量,并且编码量大、结构重复的整块代码,宏是最合适的选择,如 MFC 的 RTTI 支持和消息映射结构

(2). 如 例子:C/C++ 预定义宏的取值 “说明 (2)”中所示,需要嵌套展开 __FILE__、__LINE__、__FUNCTION__ 等预定义宏的情况,必需用宏,而不能用 inline 函数

条件编译^

#if/#else/#elif/#ifdef/#ifndef/#if defined/#if !defined

#ifdef XXX 等价于 #if defined (XXX)
#ifndef XXX 等价于 #if !defined (XXX)

参考 MSDN: The #if, #elif, #else, and #endif Directives

例子:注释大量代码^

例子:MFC 中的调试版代码示例^

见上文“MFC 的调试版 new”

AssertValid() 参考 MSDN: MFC ASSERT_VALID and CObject::AssertValid

例子:DLL 工程导出符号^

工程 DllProj 中导出符号(变量、函数、类)的方法:在工程中定义宏 DLLPROJ_EXPORTS (/D "DLLPROJ_EXPORTS"),并在使用工程中保证没有定义 DLLPROJ_EXPORTS

DllProj 导出符号的声明文件 DllProj.h 如下,在使用工程中 #include 该文件:

例子:用 #undef 解决 wxWidgets 自定义事件链接 BUG^

BUG 参考:wxEvent derived event,Custom Events

BUG 触发条件:以 DLL 方式使用 wxWidgets Windows 版本,即定义了 WXUSINGDLL,使用 DECLARE_EVENT_TYPE() 定义自定事件

BUG 表现:以 __declspec(dllimport) 修饰事件标识,实际上事件标识应该是一个模块内变量,而非导入变量,出现链接问题。MinGW GCC 4 报链接错误,VC 2005 报链接警告:warning C4273: inconsistent dll linkage。BUG 的具体原因请跟踪 wxWidgets 源码 event.h 中的 DECLARE_EVENT_TYPE() 和 dlimpexp.h 中的 WXDLLIMPEXP_CORE 定义

BUG 典型工程:开源下载器 MultiGet svn version 3

BUG 解决方法

方法 1. 不使用旧的 DECLARE_EVENT_TYPE() 而使用 DECLARE_LOCAL_EVENT_TYPE() 定义自定事件
方法 2. 使用 DECLARE_EVENT_TYPE() 前后包含 undefine_WXDLLIMPEXP_CORE.h、redefine_WXDLLIMPEXP_CORE.h 头文件,以便取消和重定义 WXDLLIMPEXP_CORE

方法 2 中的 undefine_WXDLLIMPEXP_CORE.h、redefine_WXDLLIMPEXP_CORE.h 以及使用方法如下:

说明

(1). #undef 是 #define 的反操作,取消宏定义,而不是将宏定义为空。取消后的宏名可以再次定义,而不产生重定义问题
(2). 大量嵌套的 #if 条件编译结构,可使用这种预处理缩进方法

预编译头文件^

预编译头文件 PCH (Precompiled Header) 是对某个编译单元的编译结果 (.pch),通常这个编译单元命名为 [stdafx.cpp include => stdafx.h](VC 工程标准)或 [common.cpp include => common.h]。与常规的编译结果 (.obj) 不同的是,如果 .pch 的编译单元源码在两次工程编译期间不改变,则重新编译工程时,不会重新编译 .pch

PCH 的特点使它的源码如 stdafx.h,适合放入很少更改的代码,如标准库、运行时库、系统 API、第三方库的头文件,以及工程全局的设置和名字符号,在重新编译工程时,这些代码便不会重新编译,以加快编译速度

使用 PCH 的编译命令^

以 VC 工程标准的 [stdafx.cpp include => stdafx.h] 预编译头文件编译单元为例,产生、使用 PCH 的编译命令选项如下:

  • 对除了 stdafx.cpp 之外的其它编译单元 .c/.cpp(没有 .h/.hpp,.h/.hpp 是通过 #include 展开到 .c/.cpp 形成编译单元的),使用如下编译选项(Debug 工程配置),如果使用 VC IDE 则在工程属性页中设置:

    /Yu"stdafx.h" /Fp"Debug\ProjName.pch"
    

    /Yu 表示通过 stdafx.h 使用 PCH,/Fp 指定使用的 PCH 路径为 Debug\ProjName.pch

  • 对 stdafx.cpp,如果使用 VC IDE 则在 stdafx.cpp 的属性页中设置,使用如下编译选项:

    /Yc"stdafx.h" /Fp"Debug\ProjName.pch"
    

    /Yc 表示通过 stdafx.h 产生 PCH,/Fp 指定产生的 PCH 路径为 Debug\ProjName.pch

PCH 的详细方法参考 MSDN: Creating Precompiled Header Files

例子:典型的 MFC 工程预编译头 stdafx.h 代码^

常用预处理指令^

VC 支持的预处理指令参考 MSDN: Preprocessor Directives

#error 产生人工编译错误^

#error 产生 fatal error,编译器输出 #error 后的提示文本。指示该源文件必需使用 C++ 方式编译,如下:

以 C 语言方式编译上面源文件 (/Tc test.cpp) 时报错:

fatal error C1189: #error :  MUST use C++ compilation
#line 改变行号和源文件名^

#line 改变 __FILE__ 和 __LINE__ 的取值,例如:

输出:

File: test_02.cpp, Line: 1
# 空指令^

没有作用的合法预处理指令行正则表达式:[\t ]*#[\t ]*

#pragma 预处理指令^

#pragma 是一组编译器特定的预处理指令,每种编译器的 #pragma 的子指令都有所不同。VC 的 #pragma 指令参考 MSDN: Pragma Directives and the __Pragma Keyword

常用的 #pragma 指令如下:

#pragma once 只包含一次头文件^

对头文件只包含一次,如下:

它和传统的 #ifndef 只包含一次技巧的功能相同:

在源文件中多次 #include 包含 test.h 时,不会出现 redefinition 错误

#pragma message 编译时输出消息^

#pragma message 在编译过程中,向标准输出或 VC 的 Output 窗口打印指定消息,作用:(1) 告知程序员代码编译和使用的注意事项 (2) 用于查看和诊断实际的编译代码

例子:用 #pragma message 和 STRINGIZE 查看宏的展开结果^

例 11 是用 STRINGIZE 在运行时输出宏的展开结果,其实在编译时也可以用 #pragma message 输出,诊断编译的实际代码:

在标准输出或 VC 的 Output 窗口输出:

#pragma push_macro/pop_macro 保存和恢复宏定义^

#pragma push_macro/pop_macro 用来解决宏命名冲突问题,如下:

#pragma message 输出如下:

#pragma warning 禁用和启用编译警告^

例子:

#pragma comment 目标文件注释和编译选项传递^

#pragma comment 的作用是在编译或链接过程中,向 COFF 二进制目标文件或 PE 可执行文件 (.obj/.lib/.exe/.dll) 中插入字符串注释。目的:

(1). 将版本、版权等信息插入到 COFF/PE 文件中,以便发布
(2). 插入的字符串会作为后续编译阶段(如链接)的选项,以便支持如 auto-link 等在源码中设置编译、链接选项

例子:用 #pragma comment(lib) 实现库的 auto-link^

以例 17 DllProj 工程为基础,DllProj 定义有 DLLPROJ_EXPORTS 宏,DllProj.h 如下:

在使用工程中 #include 该文件,并设置库搜索路径 /LIBPATH,不必指定链接导入库 DllProj.lib

#pragma comment(linker) 传递链接选项^

#pragma comment(linker, "link_option")

link_option 只能为以下链接选项:

/DEFAULTLIB
/EXPORT
/INCLUDE
/MANIFESTDEPENDENCY
/MERGE
/SECTION

#pragma comment(linker, "/SECTION") 设置区段属性^

设置区段属性的方法有:

(1). 使用模块定义文件 .def 的 SECTIONS 定义,见 MSDN: SECTIONS (C/C++)

(2). 使用 /SECTION 链接选项,见 MSDN: /SECTION (Specify Section Attributes)

(3). 使用 #pragma section 指令创建区段

对于上面的方法 (2),可用 #pragma comment(linker) 指定链接选项

例子:#pragma comment(linker, "/SECTION") 设置可读写、共享区段

#pragma 区段操作^

区段属性和标准区段名参考 /SECTION 链接选项。查看区段偏移地址 (RVA)、起始地址和属性,用 dumpbin 工具:

dumpbin /SECTION:secname xxx.exe|xxx.dll

自定义区段名不能和标准区段名冲突。自定义区段名长度限制为 8 个字符,超过 8 个会截断

#pragma section 在目标文件中创建区段^

#pragma section 在.obj 中创建区段,并设置属性 read, write, execute, shared, nopage, nocache, discard, remove

#pragma section 新建的区段不包括任何内容,需向新建的区段放置数据或代码:

(1). 对于数据区段(全局变量)

(a). __declspec(allocate("secname")) 修饰
(b). #pragma bss_seg/const_seg/data_seg 指令

(2). 对于代码区段(函数)

(a). #pragma alloc_text 指令
(b). #pragma code_seg 指令

例子:#pragma section 创建可读写、共享区段

#pragma alloc_text 将 C 链接约定的函数放置到区段^

#pragma alloc_text 只能应用于 C 链接约定的函数,即对于 C++ 编译方式,需用 extern "C" 声明函数。所以 #pragma alloc_text 不支持重载函数和类成员函数

#pragma alloc_text 在函数声明与函数定义体之间使用

例子:在可执行、非分页区段中,用 #pragma alloc_text 放置 C 链接约定函数

#pragma code_seg 将函数放置到代码区段^

#pragma code_seg/bss_seg/const_seg/data_seg 会新建区段,之前不必有 #pragma section;如果之前有 #pragma section,会使用 #pragma section 设置的区段属性

如果省略参数,会放置到标准区段:

#pragma code_seg(): .text
#pragma data_seg(): .data
#pragma bss_seg(): .bss
#pragma const_seg(): .rdata

例子:在可执行、非分页区段中,用 #pragma code_seg 放置函数

#pragma data_seg/bss_seg/const_seg 将数据放置到区段^

数据性质和区段有对应关系,如果放置区段和数据性质冲突,则不会实际放到该区段中:

(1). .data 标准区段,放置初始化非 0 全局数据。用 #pragma data_seg 放置初始化数据,必需显示初始化其中变量(可以初始化为 0),否则不会放入 #pragma data_seg 指定的区段

(2). .bss 标准区段,放置未初始化、默认或显式初始化为 0 的全局数据。注意链接时 .bss 会向 .data 中合并,所以在 .exe/.dll 中看不到 .bss 区段,可查看 .obj 中的 .bss 区段。用 #pragma bss_seg 放置未初始化数据,必需不初始化其中变量(也不能初始化为 0),否则不会放入 #pragma bss_seg 指定的区段

(3). .rdata 标准区段,放置只读的全局常量数据。const 数字类型会编码到代码中(指令立即数),所以不放到 .rdata 中。用 #pragma const_seg 放置只读常量数据

例子:自定义区段和数据性质冲突

以下错误编译器不会报错,但实际没有放置到期望的区段中

#pragma pack 设置成员字节对齐^

设置 struct, union, class 成员字节对齐的方法:

(1). 编译选项 /Zp,x86 缺省为 /Zp8,即以 8 byte 对齐,参考 MSDN: /Zp (Struct Member Alignment)

(2). 使用 __declspec(align(#)) 修饰,参考 MSDN: align (C++)

(3). 使用 #pragma pack

#pragma pack(): 不带参数的 #pragma pack() 表示恢复到编译选项 /Zp 设置的字节对齐

#pragma pack(show): 产生一条 C4810 编译警告,报告当前的字节对齐:

Test.cpp(140) : warning C4810: value of pragma pack(show) == 8

例子:使用 #pragma pack 设置成员字节对齐

#pragma inline 函数设置^

inline 函数修饰,参考 MSDN: inline, __inline, __forceinline

inline 函数编译优化选项,参考 MSDN: /Ob (Inline Function Expansion)

#pragma auto_inline 禁用和启用 auto-inline^

例子:

使用 /O2 编译优化选项,含 /Ob2:启动 auto-inline

#pragma inline_depth 设置函数调用的 inline 化深度^

#pragma inline_depth(n) 作用于 inline、__inline 和 /Ob2 选项下的 auto-inline 化函数,不作用于 __forceinline 函数。需要 /Ob1 或 /Ob2 编译选项

n: 0 ~ 255,255 表示无限制调用深度 inline 化,0 表示禁止 inline 化,省略参数 #pragma inline_depth() 时 n = 254

递归函数 inline 化的最大调用深度为 16 次调用

#pragma inline_recursion 禁用和启用递归函数的 inline 化^

作用于 inline、__inline 和 /Ob2 选项下的 auto-inline 化函数。需要 /Ob1 或 /Ob2 编译选项

默认为 #pragma inline_recursion(off),这时一个可 inline 化的递归调用函数只 inline 展开一次。如果 #pragma inline_recursion(on),则 inline 展开深度由 #pragma inline_depth 限制,并不超过 16 次

#pragma 优化指令^

编译优化选项 /O,参考 MSDN: /O Options (Optimize Code)

#pragma optimize 禁用或启动特定优化^

#pragma optimize("gp(s|t)y", on|off)

优化参数和编译优化选项之间的对应关系:

g: /Og
p: /fp:precise 浮点数一致性
s: /Os 生成最小代码
t: /Ot 生成最快代码
y: /Oy

#pragma intrinsic 使用 intrinsic 函数^

使用 intrinsic 函数编译选项 /Oi,参考 MSDN: /Oi (Generate Intrinsic Functions)

#pragma intrinsic,参考 MSDN: intrinsic

#pragma function 使用普通函数^

和 #pragma intrinsic 对应,改变 /Oi 选项或之前的 #pragma intrinsic 设置,使用指定函数名的普通函数版本

#pragma deprecated 声明废弃函数^

#pragma deprecated 用来声明废弃的函数、类型、宏,编译器产生 C4995 警告

__declspec(deprecated) 修饰也可用来声明废弃的函数、类型,编译器产生 C4996 警告

例子:

#pragma omp 使用 OpenMP 指令^

指令形式:

#pragma omp omp_directive

用于多线程、并发编程的 OpenMP 指令,子指令 omp_directive 参考 MSDN: OpenMP Directives

#pragma region/endregion 折叠代码块^

标记一整块代码,在 VC 编辑器可折叠成一行 (+) 和展开,见 VC 的 Edit->Outlining 菜单

VC Outlining 常用快捷键:

Ctrl + M, Ctrl + L: 折叠或展开所有的代码块
Ctrl + M, Ctrl + M: 折叠或展开光标所在的代码块

#pragma setlocale 设置源代码中字符串字面量的编码^

#pragma setlocale("locale")

#pragma setlocale() 使用的 locale 参数和 CRT 函数 setlocale() 的相同,参考 MSDN: Language Strings,如 简体中文 "chs" (GBK),繁体中文 "cht" (BIG5),日文 "jpn" (JIS)。注意:GBK 包括简体中文、繁体中文、日文,所以繁体中文的源文件不一定是 BIG5,也可能是 GBK,要看实际的编码

例子:

默认源代码的设置为 #pragma setlocale(""),"" 表示 Windows 用户默认 ANSI 代码页,在控制面板中区域和语言选项中设置,默认简体中文系统为 GBK,繁体中文系统为 BIG5 等。所以在简体系统下编写简体字面量代码,或在繁体系统下编写繁体字面量代码等,无需设置源文件的 #pragma setlocale

在简体中文 Windows 下源文件使用 BIG5 编码源文件,代码中有 L"xxx" 的宽字符字面量,且 "xxx" 在 BIG5 - ASCII 的字符集范围,则应当使用 #pragma setlocale("cht")

#pragma include_alias 定义头文件别名^

预处理相关编译选项^

/D 定义宏^

/D: 定义宏,作用类似 #define,但会去掉选项中的引号
/U: 取消指定的预定义宏,类似 #undef,如 /U _DEBUG
/u: 取消所有的预定义宏。/U 和 /u 都不能取消在源码中用 #define 定义的宏

定义数字^

  • /DTESTMACRO: 等价 #define TESTMACRO 1,整数
  • /DTESTMACRO=1: 同上
  • /DTESTMACRO="1": 同上
  • /DTESTMACRO=3.14: 等价 #define TESTMACRO 3.14,浮点数
  • /DTESTMACRO="3.14": 同上

定义字符串^

  • /DTESTMACRO="abcdef": 等价 #define TESTMACRO abcdef,非字符串字面量(没有引号)
  • /DTESTMACRO=\"abcdef\": 等价 #define TESTMACRO "abcdef",字符串字面量
  • /DTESTMACRO="\"abcdef\"": 同上

空定义^

  • /DTESTMACRO=: 等价 #define TESTMACRO
  • /DTESTMACRO="": 同上
  • /DTESTMACRO=\"\": 等价 #define TESTMACRO "",非空定义,而是空字符串

CL 环境变量使用 /D^

SET CL=/DTESTMACRO#1: 用 # 代替 =,等价 /DTESTMACRO=1,即 #define TESTMACRO 1

/E, /EP, /P 预处理选项^

/E: 预处理源文件,结果输出到标准输出,去掉注释,在 #inlcude 展开和条件编译周围产生 #line 行号指示
/EP: 和 /E 相似,结果输出到标准输出,但不产生 #line
/P: 和 /E 相似,产生 #line,结果输出到文件 (test.cpp => test.i),相当于 cl /E test.cpp > test.i
/P /EP 联用: 结果输出到文件 test.i,且不产生 #line。/E、/EP、/P 不能和预编译头 PCH 联用
/C: 预处理时保留注释,和 /E、/P、/EP 联用

例子:预处理展开源文件^

源文件 test.cpp:

预处理编译命令:

cl /P /C /DTESTMACRO test.cpp

预处理输出到 test.i:

例子:过滤查看预处理展开结果^

用这种方法可以查看编译过程中,实际的宏展开、预处理结果

以上面的 test.cpp 为例,预处理编译命令和 grep 过滤:

cl /EP /C /DTESTMACRO test.cpp 2>NUL | egrep -A 5 -B 5 "MARK: TESTMACRO"

2>NUL: 用于屏蔽输出 VC 编译器 banner 和提示、错误信息,用 /nologo 选项也可以
egrep -A 5 -B 5: 表示输出匹配正则表达式前后 5 行

输出结果如下:

/showIncludes 输出头文件列表^

输出源文件的 #include 的头文件列表到 stderr,包括嵌套 #include

例子:查看 #include 头文件列表^

以上面 test.cpp 为例,编译命令:

cl /nologo /showIncludes /EP test.cpp >NUL

输出头文件列表,由实际 VC 安装路径而定。嵌套 #include 用空格缩进表示,如 stdio.h include=> crtdefs.h:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值