文章目录

  • 1 #pragma 概念简介
    • 1.1 #pragma message 的用法
    • 1.2 #pragma once 的用法
    • 1.3 #pragma pack 的用法
      • 1.31 struct占用的内存大小如何计算
  • 2 总结

在学习 #pragma 之前 ,我们首先要明白一点, #pragma 的实现,在不同的编译器之间是不同的,所以使用它的代码,基本上不能移植代码。但是它也有它自己的用处,还是要学习以下。

1 #pragma 概念简介

  • #pragma 是唯一一个预处理器不处理的指令,它需要保留给编译器处理。
  • #pragma 用于指示编译器完成一些特殊的动作 (看后面就知道什么意思了)
  • #pragma 所定义的很多指示字,是编译器特有的
  • #pragma 在不同的编译器之间是不能移植的

因为不同的编译器的 #pragma 的实现是不同的。所以:

  1. 编译器将忽略它不认识的 #pragma 指令。
  2. 不同的编译器可能以不同的方式来解释同一条 #pragma 指令

一般用法为:

注意:不同的 parameter 参数的语法和意义各不相同

1.1 #pragma message 的用法

  • message的参数在大多数的编译器中都有相似的实现
  • message参数在编译时,输出消息到编译输出窗口中。注意是在编译时,不是程序运行时。
  • 如果将message用于条件编译中,可以提示代码的版本信息。如下图所示:

如下代码是 #pragma message的使用分析

  • 24-1-lyy.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!
#endifint main(){printf("%s\n", VERSION);return 0;
}

上面的 #error 的意思是如果没有定义上述的宏,就会将这句话在编译的时候打印出来,代表我们想要了解的错误。

  • 上述代码如果这样编译,不定义宏:gcc 24-1-lyy.c -o 24-1-lyy.out 在编译时将显示错误如下:

  • 如果这样编译,在命令行中定义宏 ANDROID23 ,gcc -DANDROID23 24-1-lyy.c -o 24-1-lyy.out 将在编译时显示如下信息:

注意一点,上述的信息是在编译的时候打印输出的,不是在程序运行的时候输出的。

1.2 #pragma once 的用法

首先说一下它的作用:

  • #pragma once 用于保证头文件只被编译一次。也就是可以避免重复包含头文件。这与上一篇文章使用条件编译避免重复包含头文件的作用是一样的:【C语言进阶深度学习记录】十八 条件编译的使用与分析
  • #pragma once 是编译器相关的,不一定被支持。

那么 #pragma once 与之前学的条件编译来避免重复包含头文件,这两种方式有什么区别呢?

  • #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 onceint g_value = 1;
  • 对上述代码进行编译运行:gcc 24-2.c -o 24-2.out

可以看出,虽然上面的代码包含了两次global头文件,但是编译并没有报错。因为在头文件中使用了#pragma once ,使得该头文件只能被编译一次,作用与在头文件中使用条件编译指令一样。

1.3 #pragma pack 的用法

什么是内存对齐?

  • 不同类型的数据在内存中按照一定的规则排列
  • 但是不一定是顺序的一个接一个的排列

例如下图中的两个结构体的大小是不一样的,因为它们的内存布局是不一样的:

它们的内存布局如下图:

至于为什么是上图这样的对齐方式,下一篇文章会进行学习。

先来加单的说一下为什么需要内存对齐:

  1. CPU对内存的读取不是连续的,而是分成块读取的,块的大小只能是1,2,4,8,16…字节
  2. 当读取的数据未对齐,则需要两次总线周期来访问内存才能将整个数据读完,这样会降低CPU性能
  3. 某些硬件平台只能从规定的相对地址处读取特定类型的数据,否则产生硬件异常
  • #pragma pack 用于指定内存的对齐方式。用于修改编译器的默认对齐方式

一般来讲,在Linux系统中,编译器的默认对我方式是4字节对齐。下图中的代码,可以将对齐方式修改为1字节对齐:

1.31 struct占用的内存大小如何计算

对于不同的内存对齐方式,上面的结构体在内存中的布局是不一样的,那么我们如何来计算不同的对齐方式在内存中的布局是什么样的呢?

需要根据以下三点:

  1. 第一个struct成员永远起始于 0偏移处

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

    2.1 偏移地址必须能被对齐参数整除
    2.2 如果一个结构体中有一个变量也是结构体,那么这个内部的结构体成员的对齐大小就按照其内部最大的数据成员作为其大小来计算

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

  • 只需要按照上述三点计算方法,就可以计算出所有结构体的大小以及内存布局的样式

如下面的代码:

  • 24-3.c
#include <stdio.h>#pragma pack(2)
struct Test1
{char  c1;short s;char  c2;int   i;
};
#pragma pack()#pragma pack(4)
struct Test2
{char  c1;char  c2;short s;int   i;
};
#pragma pack()int main()
{printf("sizeof(Test1) = %d\n", sizeof(struct Test1));printf("sizeof(Test2) = %d\n", sizeof(struct Test2));return 0;
}
  • 编译运行结果为:

sizeof(Test1) = 10
sizeof(Test2) = 8

  • 结果分析1–Test1:

对于结构体Test1,2字节对齐,按照上述三个计算条件有:

struct Test1
{              //对齐方式             大小         起始地址                占用的内存地址位置      char  c1;       2          大于     1            0                            0            short  s;       2          等于     2            2(被对齐参数2整除)            2~3           char  c2;       2          大于     1(对齐参数)   4                            4            int   i;        2(对齐参数) 小于     4            6(被对齐参数2整除)            6~9
};
  1. Test1的对齐方式是2字节。

然后根据:

  1. 每个成员按其类型大小和pack参数中较小的一个 进行对齐。比如上面的起始地址计算那里,都是被对齐参数2整除,这个2是类型大小和pack参数中较小的一个

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

这一条规则计算每个成员的起始地址。如上面的计算。

  1. 最后再看总体大小是否是对齐参数的整数倍。上面Test1 大小是10,是对齐参数2的整数倍
  • 结果分析2–Test2

对于结构体Test2,4字节对齐,按照上述三个计算条件有:

struct Test2
{              //对齐方式         大小           起始地址                   占用的内存地址位置      char  c1;       4      大于    1(对齐参数)       0                             0            char  c2;       4      大于    1(对齐参数)       1(被对齐参数1整除)             1           short s;        4      大于    2(对齐参数)       2(被对齐参数2整除)             2~3          int   i;        4      等于    4(对齐参数)       4(被对齐参数4整除)             5~7
};
  1. Test2 的对齐方式是4字节对齐

然后再根据:

  1. 每个成员按其类型大小和pack参数中较小的一个 进行对齐。比如上面的起始地址计算那里,对齐参数分别为1,1,2,4

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

这一条规则计算每个成员的起始地址。如上面的计算。

  1. 最后再看总体大小是否是对齐参数的整数倍。上面Test2 大小是8,是对齐参数4的整数倍

经过上面的两个结构体大小的计算,可以很容易的画出其内存图

再看一个复杂的结构体大小的计算

  • 代码 24-4.c
#include <stdio.h>struct S1
{short a;long b;
};struct S2
{char c;struct S1 d;double e;
};int main()
{printf("sizeof(struct S1) = %d\n", sizeof(struct S1));printf("sizeof(struct S2) = %d\n", sizeof(struct S2));return 0;
}

运行结果为:

sizeof(struct S1) = 8
sizeof(struct S2) = 20

S1很好分析。下面我们分析S2

struct S2         //对齐方式          大小                     起始地址    占用的内存地址位置
{char c;           4      大于     1(对齐参数)                0                0 struct S1 d;      4      等于     4(这个是S1中最大参数大小)   4(被4整除)       4~11(S1实际大小为8) double e;         4(对齐)小于     8                         12(被4整除)      12~19 };
  1. S2 的对齐方式是4字节对齐

然后再根据:

  1. 每个成员按其类型大小和pack参数中较小的一个 进行对齐。比如上面的起始地址计算那里,对齐参数分别为0,4,12

    2.1 偏移地址必须能被对齐参数整除
    2.2 如果一个结构体中有一个变量也是结构体,那么这个内部的结构体成员的对齐大小就按照其内部最大的数据成员作为其大小来计算

这一条规则计算每个成员的起始地址。如上面的计算。

  1. 最后再看总体大小是否是对齐参数的整数倍。上面S2 大小是20,是对齐参数4的整数倍

我所使用的编译器gcc 4.4.5 不支持8字节对齐方式

2 总结

  • #pragma 用于指示编译器完成以下特殊的动作
  • #pragma 对于不同的编译器,可能底层实现原理不太一样
    1. #pragma message 用于自定义编译消息
    2. #pragma once 用于避免重复包含头文件
    3. #pragma pack 用于指定内存对齐方式

【C语言进阶深度学习记录】十九 #pragma使用与分析相关推荐

  1. 【C语言进阶深度学习记录】九 C语言中const的详细分析

    文章目录 1 const的分析 2 const本质的分析实验 2.1 代码案例分析 3 const修饰函数参数和返回值时的情况 3.1 代码案例分析 4 总结 1 const的分析 不管是C语言还是C ...

  2. 【C语言进阶深度学习记录】三十五 程序中的堆、栈以及静态存储区(数据区)

    学习交流加 个人qq: 1126137994 个人微信: liu1126137994 学习交流资源分享qq群: 962535112 在我之前学习底层的知识的时候,也写过相关的内容.可以对比的学习:[软 ...

  3. 【C语言进阶深度学习记录】二十六 C语言中的字符串与字符数组的详细分析

    之前有一篇文章是学习了字符和字符串的,可以与之结合学习:[C语言进阶深度学习记录]十二 C语言中的:字符和字符串 文章目录 1 字符串的概念 1.1 字符串与字符数组 1.2 字符数组与字符串代码分析 ...

  4. 【C语言进阶深度学习记录】十六 静态库与动态库的创建与使用

    上一篇文章学习了编译的过程,点击链接查看:[C语言进阶深度学习记录]十五 编译过程简介,每一个C源文件编译后将会生成目标文件,那么这些目标文件,还需要链接起来,生成可执行文件. 文章目录 1 链接的意 ...

  5. 【C语言进阶深度学习记录】三十八 C/C++语言中的函数声明与函数定义

    文章目录 1 函数的声明和定义 1.1 代码分析 2 总结 1 函数的声明和定义 声明的意义在于告诉编译器程序单元的存在.只是告诉编译器它存在但是不在声明这里定义,有可能在当前文件中的其他地方或者其他 ...

  6. 【C语言进阶深度学习记录】八 C语言中void的分析

    文章目录 1 void的意义 1.1 不存在void变量 1.2 C标准 1.3 void指针的意义 1.4 通过void* 实现memset函数 2 总结 1 void的意义 void修饰函数的参数 ...

  7. 【C语言进阶深度学习记录】五 C语言中变量的属性

    上一篇文章学习了C语言中的类型转换,点击链接查看:[C语言进阶深度学习记录]四 C语言中的类型转换. 文章目录 1 C语言的变量属性 1.1 auto关键字 1.2 register关键字 1.3 s ...

  8. 【C语言进阶深度学习记录】十七 宏定义的使用与分析

    文章目录 1 C语言中的宏定义 1.1 定义宏常量 1.2 宏定义表达式 1.3 宏表达式与函数的对比 1.4 宏表达式的作用域 2 C语言中的内置宏 3 宏定义的代码综合示例 4 总结 1 C语言中 ...

  9. 【C语言进阶深度学习记录】一 数据类型的本质与变量的本质

    今天学习C语言中的数据类型的本质与变量的本质 文章目录 1 什么是数据类型 2 变量的本质 3 数据类型与变量的关系 4 自定义数据类型与创建变量 5 总结 1 什么是数据类型 数据类型可以理解为固定 ...

最新文章

  1. SLAM的前世今生 终于有人说清楚了
  2. 1w存银行一年多少利息_利息能拿上万?银行行长:20万存款这样存,一年躺着白白赚一万!...
  3. H.264入门级概念之I、B、P帧
  4. Linux ubuntu 装openCV,Ubuntu Linux下安装OpenCV2.4.1所需包
  5. [APIO2013]机器人(斯坦纳树)
  6. mysql sql汇总查询将两个结果集合并一行展示
  7. java 方法互斥_Java 两个互斥方法同时访问一个成员变量
  8. mysql 连接数和内存的关系_php-fpm进程数和mysql连接数之间的关系
  9. 手机网站注册页面html模板,手机网页登录注册自适应模版
  10. 信号完整性(SI)电源完整性(PI)学习笔记(一)信号完整性分析概论
  11. 两百行代码实现王校长大战鸡你太美
  12. Component name “XXX“ should always be multi-word vue/multi-word-component-names
  13. 李某同案律师下挑战书
  14. 图解通信原理与案例分析-1:开篇-通信系统大全与快速概览
  15. 拼多多远程删除用户照片事件
  16. 最详细的 Hadoop 入门教程
  17. c语言动态与静态分配内存空间的区别
  18. 【编辑器】unity自动化生成UI模板代码
  19. 删除win10系统默认微软输入法
  20. js(EcamaScript)

热门文章

  1. asic面试题目 英伟达_免笔试!不限量!全球可编程图形处理技术领袖英伟达2021校园招聘火热进行中!...
  2. 微信小程序:生命周期
  3. wifiwan口速率什么意思_无线路由器怎么设置wan口速率
  4. 自我总结篇之vue的组件通信(父传子 子传父 非父子)
  5. 使用PDFBox解析PDF文件
  6. 做移动端视频通话软件,大致看了下现有的开源软件(转)
  7. 如何构建积木式Web应用
  8. sharepoint安装心得_过程
  9. LetCode-MSSQL查找重复的电子邮箱
  10. react+redux+node报错Tapable.plugin is deprecated. Use new API on `.h ooks` instead