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


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

1 #pragma 简介

  • #pragma 用于指示编译器完成一些特定的动作
  • #pragma 所定义的很多指示字是编译器特有的(后面的参数决定)
  • #pragma 在不同的编译器间是不可移植的
    1. 预处理器将忽略它不认识的 #pragma 指令
    2. 不同编译器可能以不同的方式解释同一条 #pragma 指令

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

下面看一下用法:
在这里插入图片描述
下面看一些具体的指令吧

2 pragma message

  • message 参数在大多数的编译器中都有相似的实现
  • message 参数在编译时输出消息到编译输出窗口中
  • message 用于条件编译中可提示代码版本信息

下面看一个例子:
在这里插入图片描述

上面的代码功能是:如果定义了宏 ANDROID20,就在编译时就将消息 Compile Android SDK 2.0…输出到窗口。这里并不表示编译出错,仅仅是输出一条消息而已

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

// 24-1.c
#include<stdio.h>
#if defined(ANDROID20)
    #pragma message("Compile Android SDK 2.0...")
    #define VERSION "Android 2.0"
#elif defined(ANDROID23)
    #pragma message("Compile Android SDK 2.3...")
    #define VERSION "Android 2.3"
#elif defined(ANDROID40)
    #pragma message("Compile Android SDK 4.0...")
    #define VERSION "Android 4.0"
#else 
    #error Compile Version is not provided!
#endif

int main()
{
    printf("%s\n", VERSION);
    return 0;
}
  • 上面的代码表示如果定义了对应的宏,就打印对应的信息,并且将宏 VERSION 定义为对应的版本信息
  • #error 表示如果没有定义宏,就编译出错,并生成编译错误信息。

先不定义宏,编译一下:
在这里插入图片描述

可以看到 #error 生成一条编译错误信息

在命令行中定义宏,再次编译,可以看到打印了一条编译信息(这里仅仅是打印了一条编译消息),并生成了可执行文件,结果如下:
在这里插入图片描述
上面使用的是 gcc 编译器,下面我们再用 vs 编译器尝试编译一下:

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

VS的cmd使用

  1. Win+R 打开运行窗口
  2. 输入“cmd”,回车打开 dos 窗口
  3. 找到 VS 软件所在路径 \VC\bin
  4. 在文件夹中找到 vcvars32.bat
  5. 将这个 bat 文件拖拽到 dos 窗口中,回车
  6. 切换磁盘(如 e:),回车
  7. 通过 cd 更改操作路径(如 cd practice),回车
  8. 输入 cl -DANDROID23 24-1.c -o 24-1(ANDROID23 是命令行定义的宏,24-1.c 是本执行的cpp文件名,请根据实际进行替换),回车
  9. 输入24-1.exe,回车

大功告成

下面就来用 vs 编译器编译运行一下,结果如下:
在这里插入图片描述
可以看到 gcc 编译器和 vs 编译器对#pragma message 的处理略有区别:

  • gcc 编译器:#pragma message(“Compile Android SDK 2.3…”)
  • vs 编译器:Compile Android SDK 2.3…

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

3 #pragma once

  • #pragma once 用于保证头文件只被编译一次
  • #pragma once 是编译器相关的,不一定被支持

前面我们学习了【C语言进阶剖析】22、条件编译使用分析,其中说到使用条件编译可以保证头文件只被编译一次。
在这里插入图片描述
二者有什么区别呢,#ifndef 这种方式是被 C 语言所支持的,实际上并不是只包含一次头文件,而是包含多次,但是我们使用宏保证只被嵌入一次到源代码中,虽然只嵌入一次,但是还是包含了多次,编译器还是要多次处理。
#pragma once 告诉预处理器当前头文件只被编译一次,只要 #include 一次,后面的 #include 相同的头文件都不起作用,不会被处理,所以 #pragma once 效率更高。更详细的区别请看#pragma once 和 #ifndef 的区别

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

下面通过一个例子说明:

// 24-2.c
#include<stdio.h>
#include"global.h"
#include"global.h"
int main()
{
    printf("g_value = %d\n", g_value);
    return 0;
}
// global.h
#pragma once
int g_value = 1;

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

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

$ gcc 24-2.c -o 24-2
$ ./24-2
g_value = 1

没有任何问题,再用 vs 编译器试试,结果如下:
在这里插入图片描述
也没有问题,再试试 bcc 编译器,结果如下:
在这里插入图片描述
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 的代码更改如下:

// global.h
#ifndef _GLOBAL_H_
#define _GLOBAL_H_
#pragma once
int g_value = 1;
#endif

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

4 #pragma pack

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

什么是内存对齐:

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

下面看一个内存对齐的例子,下面两个结构体的大小相同吗。
在这里插入图片描述
我们用编译器编译运行一下,代码如下:

// 24-3.c
#include<stdio.h>
struct Test1
{
    char c1;
    short s;
    char c2;
    int i;
};
struct Test2
{
    char c1;
    char c2;
    short s;
    int i;
};
int main()
{
    printf("sizeof(Test1) = %ld\n", sizeof(struct Test1));
    printf("sizeof(Test2) = %ld\n", sizeof(struct Test2));
    return 0;
}

编译运行结果如下:

$ gcc 24-3.c -o 24-3
$ ./24-3
sizeof(Test1) = 12
sizeof(Test2) = 8

结构体大小有如下规则:

  1. 结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍)
  2. 结构体大小必须是所有成员大小的整数倍,也即所有成员大小的公倍数。

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

  1. 展开后的结构体的第一个成员的偏移量应当是被展开的结构体中最大的成员的整数倍。
  2. 结构体大小必须是所有成员大小的整数倍,这里所有成员计算的是展开后的成员,而不是将嵌套的结构体当做一个整体。

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

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

  • CPU 对内存的读取是不连续的,而是分块读取的,块的大小只能是1、2、4、8、16……字节
  • 当读取操作的数据未对齐,则需要两次总线周期来访问内存,因此性能会大打折扣
  • 某些硬件平台只能从规定的相对地址处读取特定类型的数据,否则产生硬件异常

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

先来感受一下 #pragma pack 是如何改变内存对齐方式的
将上面24-3.c 的代码更改如下,重新编译运行。

// 24-3.c
#include<stdio.h>
#pragma pack(1)
struct Test1
{
    char c1;
    short s;
    char c2;
    int i;
};
#pragma pack()

#pragma pack(1)
struct Test2
{
    char c1;
    char c2;
    short s;
    int i;
};
#pragma pack()

int main()
{
    printf("sizeof(Test1) = %ld\n", sizeof(struct Test1));
    printf("sizeof(Test2) = %ld\n", sizeof(struct Test2));
    return 0;
}
$ gcc 24-3.c -o 24-3
$ ./24-3
sizeof(Test1) = 8
sizeof(Test2) = 8

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

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

struct 占用内存大小计算:

  • 第一个成员起始于 0 偏移处
  • 每个成员按其类型大小和 pack 参数中较小的进行对齐
    • 偏移地址必须能被对齐参数整除
    • 结构体成员的大小取其内部长度最大的数据成员作为去大小
  • 结构体总长度必须为所有对齐参数的整数倍

注意:编译器在默认情况下按照 4 字节对齐,也就是说如果 #pragma pack() 不写,则和 #pragma pack(4) 效果是相同的
!!!注意:这里是针对 32 位系统,对于64 位系统而言,默认情况下按照 8 字节对齐

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

#pragma pack(4)
struct Test1
{					// 对齐参数         偏移地址    大小
    char c1;		// min(1, 4) = 1    0			1
    short s;		// min(2, 4) = 2	2			2
    char c2;		// min(1, 4) = 1	4			1
    int i;			// min(4, 4) = 4	8			4
};
#pragma pack()

#pragma pack(4)
struct Test2
{					// 对齐参数         偏移地址    大小
    char c1;		// min(1, 4) = 1    0			1
    char c2;		// min(1, 4) = 1    1			1
    short s;		// min(2, 4) = 2    2			2
    int i;			// min(4, 4) = 4    4			4
};
#pragma pack()

所以 Test1 的大小为 12 字节。

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

// 24-4.c
#include<stdio.h>

#pragma pack(8)
struct S1
{
    short a;
    long b;
};
struct S2
{
    char c;
    struct S1 d;
    double e;
};
#pragma pack()

int main()
{
    printf("sizeof(S1) = %ld\n", sizeof(struct S1));
    printf("sizeof(S2) = %ld\n", sizeof(struct S2));
    return 0;
}

我们先来手动分析一下:

#pragma pack(8)
struct S1
{					// 对齐参数         偏移地址    大小
    short a;		// min(2, 8) = 2    0			2
    long b;			// min(4, 8) = 4    4			4
};					// 4+4 = 8,8 是所有对齐参数的整数倍,所以大小为 8
struct S2
{					// 对齐参数         偏移地址    大小
    char c;			// min(1, 8) = 1    0			1
    struct S1 d;	// min(4, 8) = 4    4			8
    double e;		// min(8, 8) = 8    16			8
};					// 16+8 = 24,24 是所有对齐参数的整数倍,所以大小为 24
#pragma pack()

经过上面的分析,struct S1 的大小为 8 个字节,struct S2 的大小为 24个字节,真的是这样吗。我们来尝试一下。
先用 vs 编译器编译一下,结果如下,和我们手动计算的一样。
在这里插入图片描述
再用 gcc 编译器编译一下,结果如下:

$ gcc 24-4.c -o 24-4
$ ./24-4
sizeof(S1) = 8
sizeof(S2) = 20

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

struct S2
{					// 对齐参数         偏移地址    大小
    char c;			// min(1, 4) = 1    0			1
    struct S1 d;	// min(4, 4) = 4    4			8
    double e;		// min(4, 8) = 4    12			8
};					// 12+8 = 20,20 是所有对齐参数的整数倍,所以大小为 20

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

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

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

  • 32 位的编译器默认情况按照 4 字节对齐
  • 对于64 位系统而言,默认情况下按照 8 字节对齐
#include<stdio.h>
struct A {
    int   a;
    char  b;
    double c;
    char  d;
};
struct B {
    char  a;
    double b;
    char  c;
};
int main() {
    printf("int = %d, char = %d, double = %d\n", sizeof(int), sizeof(char), sizeof(double));
    printf("structA = %d, structB = %d\n", sizeof(struct A), sizeof(struct B));
    return 0;
}
  • 64位系统结果如下:
$ gcc 24-5.c -o 24-5
$ ./24-5
int = 4, char = 1, double = 8
structA = 24, structB = 24

在这里插入图片描述
在这里插入图片描述

  • 32位系统结果如下:
$ gcc 24-5.c -o 24-5
$ ./24-5
int = 4, char = 1, double = 8
structA = 20, structB = 16

在这里插入图片描述
在这里插入图片描述
32 位只有 4 个字节,最长对齐模数只能按 4 个字节来对齐,double 是分成了 2 个 4 字节。

指针大小:

  • 32位系统:4字节
  • 64位系统:8字节

6 小结

1、#pragma 用于指示编译器完成一些特定的动作
2、#pragma 所定义的很多指示字是编译器特有的

  • #pragma message 用于自定义编译消息
  • #pragma once 用于保证头文件只被编译一次
  • #pragma pack 用于指定内存对齐方式
  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值