目录
1、#include指令
2、#define和#undef指令
3、#ifdef、#ifndef、#else、#elif和#endif指令
4、#error指令
5、编译器预置宏__FILE__、__LINE__和__FUNCTION__
6、#pragma指令
6.1、#pragma once指令
6.2、#pragma message指令
6.3、#pragma warning指令
6.4、#pragma comment指令
6.5、#pragma hdrstop指令
6.6、#pragma resource指令
6.7、#pragma code_seg指令
6.8、#pragma data_seg指令
6.9、#pragma init_seg指令
6.10、#pragma pack指令
C/C++代码会使用到各式各样的预编译指令,预编译指令一般是以#号开头的,这些指令是在编译期间识别的,由编译器去负责解析这些指令并做出对应的处理。今天我们来介绍一下C/C++中常用的预编译指令。
VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931
该指令用来包含头文件,头文件中可能有结构体、枚举体、类或函数的声明,使用到这些对象时需要包含声明这些对象的头文件,比如:
#include “stdio.h”
包含头文件有#include
1)对于#include
,编译器在编译时从标准库路径开始搜索filename.h,所以在包含C/C++标准库时需要使用该方式,比如#include 、#include 。
2)对于#include “filename.h”,编译器从用户的工作路径开始搜索filename.h。对应非标准库的用户自定义头文件,使用这种包含方式。
#define通常用来定义一个宏,比如代码中多处使用的某个固定数值,我们可以用将固定值定义成宏,比如我们在用sqlite数据库存放数据时会设定单个数据库文件的大小:
#define MAX_CHAT_RECORD_DB_FILE_SIZE 1024*1024*500 // 单个db文件的大小上限是200MB
将一些常量或表达式定义成宏的好处是,如果要修改该数值,只要修改宏定义的地方即可,不要去每个使用到的地方去修改。
定义的宏可以是一个数值,也可以是个表达式,也可以是一段经常使用的代码。比如我们在delete指针时,可以封装成这样的宏:
/*lint -emacro(717, SAFE_DELETE)*/
#define SAFE_DELETE(p) \
do \
{ \
if ((p) != NULL) \
{ \
delete (p); \
(p) = NULL; \
} \
} while (0)
对于上述宏,在编译器编译时,会将使用到这些宏的地方直接替换成宏的内容。
除了将一个数值、一个表达式或一段代码定义成宏,可以直接定义一个没有具体内容的宏,仅仅表示代码中定义了该宏。只是起到一个标识的作用,代码中可以使用#ifdef或#ifndef来判断有没有定义该宏。
#ifdef RTC_VER
// ... // 定义了RTC_VER宏时的处理代码
#else
// ... // 没定义RTC_VER宏时的处理代码
#endif
通过添加宏,去控制代码的逻辑和走向,也可以通过宏去确定当前模式是否支持某些功能。
#undef指令则用于取消之前定义的宏。
这些是预编译指令中的条件判断指令,和C/C++代码中if、else、else if等是类似的。这些指令主要处理与宏相关的判断。比如我们经常在头文件中添加如下的一段预编译指令,防止头文件内容被多次包含:
#ifndef VERSION_CONTROL_H
#define VERSION_CONTROL_H
// ... // 头文件的内容
#endif
上述预定义指令的含义是,如果没定义VERSION_CONTROL_H宏,则定义该宏,并展开头文件的内容。如果已经定义了VERSION_CONTROL_H宏,则不会展开头文件的内容,会直接跳过头文件中的内容。
项目中肯定有多个头文件,这些头文件中用于被用来防止头文件被重复包含的宏名称是不能重复的,重复了会有问题的。在微软Visual Studio开发工具中,可以使用#pragma once指令来替代。
为啥会出现头文件多次包含的问题呢?其实很好理解,比如头文件header2.h中包含header1.h头文件(假设头文件header1.h中没有添加防止被重复包含的预编译指令),test.cpp中既包含了header1.h头文件,也包含了header2.h头文件,这样就会出现两次包含header1.h的问题,就会出现头文件中的内容被定义两次的问题(包含头文件的地方可以想象成直接替换成头文件中的内容),所以要添加防止头文件中的内容被重复定义的问题。特别是再大型项目中,有多个头文件和cpp文件,很容易出现同一个头文件被包含多次的问题,所以必须要在头文件中添加防止头文件中的内容被重复定义的预编译指令。
再就是,根据是否定义个某个宏去做不同的处理:
#ifdef RTC_VER
// ... // 定义了RTC_VER宏时的处理代码
#else
// ... // 没定义RTC_VER宏时的处理代码
#endif
使用该指令可以在编译器编译期间在IDE的输出窗口中打印出一段字符串。比如我在头文件中添加一条这样的指令:
#error 1111111111111111111111111
编译时在输出窗口会输出上述字符串:
该命令可以用在这样的场景下,比如一个项目代码的路径中有两个头文件都定义了同一个名字的宏(宏名称一样,宏值可能不一样),但不确定到底使用的是哪个头文件中定义的宏,就可以在一个头文件中添加:
#error 1111111111111111111111111
在另一个头文件中添加:
#error 2222222222222222222222222
然后重新编译代码,编译时就会在编译信息输出窗口中就会看到上述指令输出的字符串,输出了哪个字符串,就使用了对应的头文件。
__FILE__用于指示本行代码所在源文件的文件名(完整路径),__LINE__用于指示本行代码所在源文件中的位置(行数),__FUNCTION__用于指示本行代码所在函数(函数名)。这几个宏在编译期间编译器就将之替换成对应的内容了,在打印日志时比较有用。
使用这几个宏的测试代码如下:
void TestFunc()
{
cout << "Current File: " << __FILE__ << endl; //Current File: d:\testdlg.cpp
cout << "Current Line: " << __LINE__ << endl; //Current Line: 688
cout << "Current Function: " << __FUNCTION__ << endl; //Current Function: TestFunc
}
该类指令用来设定编译器的状态或者指示编译器完成一些特定的动作,它支持不同的操作参数。注意该部分指令只有微软的C++编译器才支持,才能识别。
在头文件的开始处添加这条指令,是为了保证头文件只被编译一次,以解决头文件被多次包含时的问题。也可以使用#ifndef/#define/#endif实现同样的效果,如下:
#ifndef VERSION_CONTROL_H
#define VERSION_CONTROL_H
// ... // 头文件的内容
#endif
该指令能够让编译器在执行到该条指令时在编译信息输出窗口中输出一段信息,其使用方法为:#pragma message(“消息文本”)。通过这条指令我们可以方便地记录在是否在源代码中定义过某个宏,比如:
#define RTC_VER // 定义了宏RTC_VER
// ......
#ifdef RTC_VER
#pragma message("Macro RTC_VER is defined") // 如果定义了宏RTC_VER,就输出:Macro RTC_VER is defined
#endif
该指令可以将指定编号的警告信息屏蔽掉。比如我们编写了如下的代码:
char szBuf[100] = { 0 };
strcpy( szBuf, "123456");
使用Visual Studio编译,会报出如下警告:
提示strcpy函数不安全,建议使用安全版本的字符串拷贝函数strcpy_s。
如果我们代码写的没问题,我们就要使用strcpy,则可以将该类警告信息屏蔽掉,如下:
#pragma warning(disable : 4996) // disable bogus deprecation warning
diasable后面跟的就是警告信息的类型编号。
该指令将一个注释记录放入一个对象文件或可执行文件中,其使用方法为:
#pragma comment(comment-type ,["commentstring"])
其中,comment-type 是一个预定义的标识符,指定注释的类型,包括compiler,exestr,lib,linker。比如我们在使用dll库之前需要将dll库对应的lib文件:(程序链接时使用)
#pragma comment(lib, "zlibwapi.lib")
该指令表示预编译头文件到此为止,后面的头文件不进行预编译。
该指令表示把指定文件中的资源加入工程,如
#pragma resource "*.dfm"
该指令能够设置程序中函数代码存放的代码段,开发驱动程序的时候会使用到。使用方法为:
#pragma code_seg(["section-name" [,"section-class"] ])。
该指令建立一个新的数据段并定义共享数据。一般用于DLL中,在DLL中定义一个共享的有名字的数据段,这个数据段中的全局变量可以被多个进程共享,否则多个进程之间无法共享DLL中的全局变量。其使用方法为:
#pragma data_seg("SharedData")
int nSharedVal; //共享数据
#pragma data_seg()
该指令用于控制全局对象的初始化顺序,其使用方法为:
#pragma init_seg({ compiler | lib | user | "section-name" [, func-name]} )
其中,init_seg后面括号中包含的参数有compiler编译器、 lib库、 user用户,和 user_defined_segment_name四种,前三个指令的初始化优先次序依次降低,但都先于普通的全局变量构造,如cout就是使用compiler级别构造的。比如:
#pragma init_seg(compiler)
#pragma init_seg(lib)
#pragma init_seg(user)
#pragma init_seg("user_defined_segment_name")
pragma init_seg(compiler)是保留给微软C/C++ 运行库使用的,我们不应该使用它!在我们的代码里,如果希望一些对象先于其他对象初始化,我们可以在对应的cpp文件中使用 #pragma init_seg(lib) 指令,比如:
#pragma init_seg( lib )
CImage::CInitGDIPlus CImage::s_initGDIPlus;
CImage::CDCCache CUIImage::s_cache;
void CImage::ReleaseGDIPlus()
{
s_initGDIPlus.ReleaseGDIPlus();
}
但要注意,一个源文件只能出现一次init_seg 指令。
该指令用来指定当前头文件中定义的结构体数据在内存中的对齐长度,比如:
#pragma pack(1)
struct TestStruct
{
char a;
int b;
};
cout << sizeof(TestStruct) << endl;
上述代码是输出TestStruct结构体的长度,如果不使用#pragma pack指定对齐长度,则是8字节。此处添加了#pragma pack(1),则是1字节对齐,那结构体的长度就是5字节了。