天天看点

【C语言进阶剖析】24、#pragma 使用分析

1 #pragma 简介

2 pragma message

3 #pragma once

4 #pragma pack

5 sizeof(struct) 32位系统与64位系统区别

6 小结

今天学习一个非常重要的预处理指示字 #pragma,在实际工程开发中这个预处理指示字用的非常多,我们以前却接触的非常少,为什么呢,因为 #pragma 是 c 语言留给编译器生产厂商对 c 语言进行扩展了一个特殊的预处理指示字。这也就导致了一个问题,

#pragma 在不同的编译器之间可能是无法移植的,这里我们学习几个常用的功能。

#pragma 用于指示编译器完成一些特定的动作

#pragma 所定义的很多指示字是编译器特有的(后面的参数决定)

#pragma 在不同的编译器间是不可移植的

预处理器将忽略它不认识的 #pragma 指令

不同编译器可能以不同的方式解释同一条 #pragma 指令

就是因为不同的编译器生产厂商对 #pragma 的实现可能不同,某一指令,可能一个编译器有,另一个没有,怎么处理这种情况呢,那就把不认识的 #pragma 指令直接删除就行了。 也可能出现两个编译器都有这个指令,但是功能不同。

下面看一下用法:

【C语言进阶剖析】24、#pragma 使用分析

下面看一些具体的指令吧

message 参数在大多数的编译器中都有相似的实现

message 参数在编译时输出消息到编译输出窗口中

message 用于条件编译中可提示代码版本信息

下面看一个例子:

【C语言进阶剖析】24、#pragma 使用分析
上面的代码功能是:如果定义了宏 android20,就在编译时就将消息 compile android sdk 2.0…输出到窗口。这里并不表示编译出错,仅仅是输出一条消息而已

我们来用编译器编译一下试试,代码如下:

上面的代码表示如果定义了对应的宏,就打印对应的信息,并且将宏 version 定义为对应的版本信息 #error 表示如果没有定义宏,就编译出错,并生成编译错误信息。

先不定义宏,编译一下:

【C语言进阶剖析】24、#pragma 使用分析
可以看到 #error 生成一条编译错误信息

在命令行中定义宏,再次编译,可以看到打印了一条编译信息(这里仅仅是打印了一条编译消息),并生成了可执行文件,结果如下:

【C语言进阶剖析】24、#pragma 使用分析

上面使用的是 gcc 编译器,下面我们再用 vs 编译器尝试编译一下:

在此之前,先说一下怎么在 cmd 中用 vs 编译器

vs的cmd使用: win+r 打开运行窗口 输入“cmd”,回车打开 dos 窗口 找到 vs 软件所在路径 \vc\bin 在文件夹中找到 vcvars32.bat 将这个 bat 文件拖拽到 dos 窗口中,回车 切换磁盘(如 e:),回车 通过 cd 更改操作路径(如 cd practice),回车 输入 cl -dandroid23 24-1.c -o 24-1(android23 是命令行定义的宏,24-1.c 是本执行的cpp文件名,请根据实际进行替换),回车 输入24-1.exe,回车 大功告成

下面就来用 vs 编译器编译运行一下,结果如下:

【C语言进阶剖析】24、#pragma 使用分析

可以看到 gcc 编译器和 vs 编译器对#pragma message 的处理略有区别:

gcc 编译器:#pragma message(“compile android sdk 2.3…”)

vs 编译器:compile android sdk 2.3…

这说明这两个编译器对 #pragma message 都有实现,但是实现略有差异。

#pragma once 用于保证头文件只被编译一次

#pragma once 是编译器相关的,不一定被支持

前面我们学习了【c语言进阶剖析】22、条件编译使用分析,其中说到使用条件编译可以保证头文件只被编译一次。

【C语言进阶剖析】24、#pragma 使用分析

二者有什么区别呢,#ifndef 这种方式是被 c 语言所支持的,实际上并不是只包含一次头文件,而是包含多次,但是我们使用宏保证只被嵌入一次到源代码中,虽然只嵌入一次,但是还是包含了多次,编译器还是要多次处理。

#pragma once 告诉预处理器当前头文件只被编译一次,只要 #include 一次,后面的 #include 相同的头文件都不起作用,不会被处理,所以 #pragma once 效率更高。更详细的区别请看#pragma once 和 #ifndef 的区别

但是实际工程中 #ifndef 使用的更多,这是因为 #ifndef 是被 c 语言所支持的,所有的编译器都可以编译,但是对于 #pragma once,有些编译器不支持。

下面通过一个例子说明:

上面的代码包含两次文件 global.h,由于使用了 #pragma once,文件 global.h 只会被处理一次

下面我们用不同的编译器来编译,首先是 gcc 编译器:

没有任何问题,再用 vs 编译器试试,结果如下:

【C语言进阶剖析】24、#pragma 使用分析

也没有问题,再试试 bcc 编译器,结果如下:

【C语言进阶剖析】24、#pragma 使用分析

bcc 编译器报错了,提示变量 g_value 被初始化了不止一次,也就是说 g_value 被多次定义。

从上面使用 gcc,vs,bcc 编译器来看。gcc 和 vs 编译器支持 #pragma once,bcc 编译器不支持 #pragma once,提示 g_value 重复定义了。由于不认识 #pragma once 这个指示字,怎么处理呢,就直接把文件 global.h 中的 #pragma once 删除即可。最终导致预处理后的源代码中 global.h 被包含两次,g_value 被重复定义。

由于有些编译器支持 #pragma once,有些不支持,怎么做既能保证高效又能保证多个编译器之间可以通用呢,那就是混合使用 #ifndef 和 #pragma once

将 global.h 的代码更改如下:

如果编译器支持 #pragma once,遇见该指示字后,后面的 #include 将不再处理,这样提高了效率,如果编译器不支持 #pragma once,#pragma once 将直接被删除,使用 #ifndef 来保证头文件中的代码只被嵌入到预编译后的源代码中一次

在说这个指示字之前,我们先说说内存对齐。

什么是内存对齐:

不同类型的数据再内存中按照一定的规则排序,而不一定是顺序的一个接一个的排序

下面看一个内存对齐的例子,下面两个结构体的大小相同吗。

【C语言进阶剖析】24、#pragma 使用分析

我们用编译器编译运行一下,代码如下:

编译运行结果如下:

结构体大小有如下规则:

结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍)

结构体大小必须是所有成员大小的整数倍,也即所有成员大小的公倍数。

求嵌套的结构体大小规则:

嵌套的结构体,需要将其展开。对结构体求sizeof时,上述两种原则变为:

展开后的结构体的第一个成员的偏移量应当是被展开的结构体中最大的成员的整数倍。

结构体大小必须是所有成员大小的整数倍,这里所有成员计算的是展开后的成员,而不是将嵌套的结构体当做一个整体。

【C语言进阶剖析】24、#pragma 使用分析

两个结构体在内存中的分布如图所示:test1 中放置了c1后,开始 s 的大小为 2,偏移量必须是成员大小的整数倍,所以从偏移量为 2 处开始存放,

为什么需要内存对齐呢?原因如下:

cpu 对内存的读取是不连续的,而是分块读取的,块的大小只能是1、2、4、8、16……字节

当读取操作的数据未对齐,则需要两次总线周期来访问内存,因此性能会大打折扣

某些硬件平台只能从规定的相对地址处读取特定类型的数据,否则产生硬件异常

#pragma pack 就是用于指定内存对齐方式

先来感受一下 #pragma pack 是如何改变内存对齐方式的

将上面24-3.c 的代码更改如下,重新编译运行。

可以看到内存对齐的方式已经改变了

上面的结果我们已经初步知道了 #pragma pack 可以改变内存对齐的方式,具体是如何影响的呢,下面具体说明:

struct 占用内存大小计算:

第一个成员起始于 0 偏移处

每个成员按其类型大小和 pack 参数中较小的进行对齐

偏移地址必须能被对齐参数整除

结构体成员的大小取其内部长度最大的数据成员作为去大小

结构体总长度必须为所有对齐参数的整数倍

注意:编译器在默认情况下按照 4 字节对齐,也就是说如果 #pragma pack() 不写,则和 #pragma pack(4) 效果是相同的

!!!注意:这里是针对 32 位系统,对于64 位系统而言,默认情况下按照 8 字节对齐

下面我们手动计算一下结构体的大小,首先是编译器默认的对齐方式

所以 test1 的大小为 12 字节。

下面我们再看一个例子,这是一个微软的面试题:

我们先来手动分析一下:

经过上面的分析,struct s1 的大小为 8 个字节,struct s2 的大小为 24个字节,真的是这样吗。我们来尝试一下。

先用 vs 编译器编译一下,结果如下,和我们手动计算的一样。

【C语言进阶剖析】24、#pragma 使用分析

再用 gcc 编译器编译一下,结果如下:

gcc 编译器的结果和我们分析的不一样呀,什么原因呢,gcc 编译器暂时不支持 8 字节对齐,碰见不支持的 #pragma pack(8),怎么处理呢,直接删除 #pragma pack(8) 和 #pragma pack(),这样就变成了默认四个字节对齐,所以 struct s2 的大小计算方法如下:

最后计算变量 e 的对齐参数时,对齐方式变成 4 字节对齐,偏移量为 18,所有结构体大小为 20。

这再次说明了#pragma 是编译器相关的。

32 位的编译器默认情况按照 4 字节对齐

对于64 位系统而言,默认情况下按照 8 字节对齐

64位系统结果如下:

【C语言进阶剖析】24、#pragma 使用分析
【C语言进阶剖析】24、#pragma 使用分析

32位系统结果如下:

【C语言进阶剖析】24、#pragma 使用分析
【C语言进阶剖析】24、#pragma 使用分析

32 位只有 4 个字节,最长对齐模数只能按 4 个字节来对齐,double 是分成了 2 个 4 字节。

指针大小:

32位系统:4字节

64位系统:8字节

1、#pragma 用于指示编译器完成一些特定的动作

2、#pragma 所定义的很多指示字是编译器特有的

#pragma message 用于自定义编译消息

#pragma pack 用于指定内存对齐方式