资讯

精准传达 • 有效沟通

从品牌网站建设到网络营销策划,从策略到执行的一站式服务

预处理/宏定义/头文件(main)深度剖析-创新互联

本节主要深度剖析一下预处理和里面的宏定义以及代码编写的一些固定格式的原理

我们提供的服务有:做网站、成都网站设计、微信公众号开发、网站优化、网站认证、柳江ssl等。为上千企事业单位解决了网站和推广的问题。提供周到的售前咨询和贴心的售后服务,是有科学管理、有技术的柳江网站制作公司

预处理宏定义头文件main深度剖析
  • ❗️ 什么是预编译/预处理?🤔
    • 🏷️ 预处理功能
  • 宏定义#define
    • 宏定义的作用域
    • 为什么要使用宏
    • 为什么使用宏比调用函数更加高效❓️
    • 含参数的宏与函数的优缺点
    • 宏定义的时候,每部分都加上括号
    • 当宏出现在字符串中的时候,宏不会被替换
    • 宏定义函数的时候,函数标识符和参数之间不能有空格
    • 取消宏定义的符号 #undef
  • 条件编译
  • 文件包含#include
    • ❗️include<>和 “” 区别
  • mian函数参数argc和argv的作用
  • #pragma预处理
    • #pragma comment(...)
    • #pragma warning
    • #pragma once
    • #pragma code_seg
    • #pragma message
    • #pragma pack内存对齐
      • 内存对齐的原理:
      • #pragma pack 设置内存对齐粒度
      • 内存对齐例子
      • 内存对齐的规则
  • ‘#’在宏定义预处理的使用
    • “#”运算符
    • “##”运算符-粘合剂
  • 结构体内存对齐
    • 为什么要存在内存对齐?
    • 规则
    • 举例简单结构体的对齐数的计算
    • 结构体中包含联合体的结构体大小
      • 分支 联合体的大小计算:
      • 计算结构体中的联合体大小
  • 位段和对齐
    • 什么是位段?
  • 更多参考:📚


❗️ 什么是预编译/预处理?🤔

预编译又称为预处理,是做些代码文本的替换工作。

比如:处理#开头的指令,比如拷贝#include包含的文件代码,#define宏定义的替换,条件编译等,就是为编译做的预备工作的阶段,主要处理#开始的预编译指令,预编译指令指示了在程序正式编译前就由编译器进行的操作,可以放在程序中的任何位置。


🏷️ 预处理功能

c编译系统在对程序进行通常的编译之前,先进行预处理。 c提供的预处理功能主要有以下三种:

  • 1)宏定义
  • 2)文件包含
  • 3)条件编译

作用是为了:

  1. 总是使用不经常改动的大型代码体。
  2. 程序由多个模块组成,所有模块都使用一组标准的包含文件和相同的编译选项。在这种情况下,可以将所有包含文件预编译为一个预编译头。


宏定义#define

宏定义的作用域
宏定义开始,到文件结束(其他的文件包含宏定义的文件也可引用)。

为什么要使用宏

1) 提高代码的可读性和可维护性
2) 避免函数调用,提高程序效率

举例:

 #define ERROR_POWEROFF -1

     若不采用宏定义的方式,代码中出现-1 时,程序的可读性变差,代码中出现有具体的含义的单独的数字(比如上面-1) 称为魔鬼数,别人阅读代码的时候会抓狂,恐怕自己阅读的时候,也不知具体的含义


为什么使用宏比调用函数更加高效❓️

因为,使用宏就像使用头文件一样,就比如使用,使用头文件进行预编译之后,直接可以使用字符串进行定义使用;宏也是如此,在定义宏之后,编译器直接进行了预编译,这时候调用它,就是直接进行替换。

而调用其他函数时,要给他在内存中单独分配空间,普通变量分布在栈区,动态内存分布在堆区,静态变量在全局数据区(全局数据区也包括全局变量),字符常量在常量区,二进制指令(也就是函数体)分布在代码区。

执行这个函数时,要获取被调用函数指定的地址(被调用函数的地址有一个范围,起始地址就是函数的入口地址,被调用函数从起始地址开始一步步往下执行),之后程序会跳转到被调函数的第一条语句,一步步往下依次执行被调函数中的语句,直到函数执行结束。
所以,相比调用函数,宏的开销更小!


含参数的宏与函数的优缺点
带参 宏函数
处理时间编译时程序运行时
参数类型没有参数类型问题 定义实参形参类型
处理过程不分配内存分配内存
程序长度变长不变
运行速度不占运行时间调用和返回占用时间


宏定义的时候,每部分都加上括号

(1)#define SQR (x) x * x

当表达式 x = 10+1, SQR(x) * SQR(x) 替换为 10+1*10+1,显然这不是我们想要的结果,导致出错

(2)#define ADD (x) (x)+(x)

当表达式 x=5, ADD(x)*ADD(x) 替换为 (5)+(5) * (5)+5,显然这不是我们想要的结果,导致出错

  • 解析:避免这种错误,当宏定义的时候,每部分都加上括号:
    (1)#define SQR (x) ((x) *( x))
    (2)#define ADD (x) ((x)+(x))

当宏出现在字符串中的时候,宏不会被替换
比如 printf("ADD(x)");  打印的结果为 ADD(x) 而不是 (x)+(x)

宏定义函数的时候,函数标识符和参数之间不能有空格

比如 #define SQR (x) x * x , 宏将变成代码中用(x) x * x 替换代码中的SQR ;

但引用宏的时候可以有空格,比如 #define ADD (x) ((x)+(x)), 应用的时候 ADD (3) 和 ADD(3) 都是正确的

取消宏定义的符号 #undef
  • 取消宏定义的符号#undef,此符号之后的宏的定义将不再起作用


条件编译

有时候我们会在 代码中的头文件.h中看见ifndef/define/endif,那么他们的作用是什么?

#ifndef、#define、#endif 是预处理命令,它们一起用来根据不同情况编译不同代码、产生不同目标文件的机制,称为条件编译。

条件编译的形式之一:

(1)     #ifdef 标识符
                          程序段1
                  #else
                         程序段2
                  #endif
                  
  (2)    #if 常量表达式
                          程序段1
                #else
                         程序段2
                #endif

一个重要的作用是防止该头文件被重复引用。

一般可以用于防止头文件重复包含。格式如下:

#ifndef _NAME_H
#define _NAME_H
// 头文件内容
#endif

当程序中第一次 #include 包含该头文件时,由于 _NAME_H 这个宏还没有定义,所以会定义 _NAME_H 这个宏,并执行”头文件内容“部分的代码;

当发生多次 #include 时,因为前面已经定义了 _NAME_H ,所以不会再重复执行”头文件内容“部分的代码。


文件包含#include
  • 文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。

C语言提供#include 命令来实现文件包含的操作,它实际是宏定义的延伸;

(1)#includeC 编译系统所提供的并存放在指定的子目录下的头文件。找到文件后,用文件内容替换该语句;

   (2)#include “filename”

   预处理应在当前目录中查找文件名为filename 的文件.
   若没有找到,则按系统指定的路径信息,搜索其他目录。找到文件后,用文件内容替换该语句。

     #include 是将已存在文件的内容嵌入到当前文件中

❗️include<>和 “” 区别

🤨 尖括号<>和引号 “” 的区别在于,前者表示要包含的文件位于编译器的默认搜索路径中,而后者表示要包含的文件位于程序文件所在的目录或指定的搜索路径中。也就是""先搜索当前目录的,如果找不到文件回到编译器的默认搜索路径重新搜索。

  • 例如,#include “myheader.h” 指令会在编译时包含名为 myheader.h 的头文件。优先搜索当前目录的,如果找不到文件回到编译器的默认搜索路径重新搜索。


mian函数参数argc和argv的作用

主函数要写main (int argc, char* argv[ ])

argc在C语言中表示运行程序时传递给main()函数的命令行参数个数
argv在C语言中表示运行程序时用来存放命令行字符串参数的指针数组

argc、argv用命令行编译程序时有用。主函数main中变量(int argc,char *argv[ ])的含义:

  1. main(int argc, char *argv[ ], char **env)是UNIX和Linux中的标准写法。
  2. argc: 整数,用来统计你运行程序时送给main函数的命令行参数的个数
  3. argv[ ]: 指针数组,用来存放指向你的字符串参数的指针,每一个元素指向一个参数。其中argv[0] 指向程序运行的全路径名,argv[1] 指向在DOS命令行中执行程序名后的第一个字符串,argv[2] 指向执行程序名后的第二个字符串,argv[argc]为NULL
  4. argc、argv是在main( )函数之前被赋值的,编译器生成的可执行文件,main( )不是真正的入口点,而是一个标准的函数,这个函数名与具体的操作系统有关。


#pragma预处理

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

下面讲解一下常用的几个#pragma 预处理命令


#pragma comment(…)

该指令将一个注释记录放入一个对象文件或可执行文件中。常用的lib 关键字,可以帮我们连入一个库文件。比如:

 #pragma comment(lib, "user32.lib")
  • 该指令用来将user32.lib 库文件加入到本工程中。

linker:将一个链接选项放入目标文件中,你可以使用这个指令来代替由命令行传入的
或者在开发环境中设置的链接选项,你可以指定/include 选项来强制包含某个对象。

例如:

   #pragma comment(linker, "/include:__mySymbol")


#pragma warning
#pragma warning(disable: 4507 34; once: 4385; error: 164)
含义:不显示 4507 和34 号警告信息
4385号警告信息仅报告一次
把164号警告信息作为一个错误

#pragma warning( disable : 4507 34; once : 4385; error : 164 )

等价于:

  #pragma warning(disable:4507 34) // 不显示4507 和34 号警告信息

  #pragma warning(once:4385) // 4385 号警告信息仅报告一次

  #pragma warning(error:164) // 把164 号警告信息作为一个错误。

不过程序设计的时候,尽量少用disable,尽量在编码的时候,将warning问题解决掉,有的时候warning 也是潜在的bug


#pragma once
在头文件的最开始加入这条指令就能够保证头文件被编译一次

另外保证头文件只编译一次的方法:

#ifndef  _FILENAME_H

    #define _FILENAME_H

    #endif

当程序中第一次 #include 包含该头文件时,由于 _NAME_H 这个宏还没有定义,所以会定义 _NAME_H 这个宏,并执行”头文件内容“部分的代码;

当发生多次 #include 时,因为前面已经定义了 _NAME_H ,所以不会再重复执行”头文件内容“部分的代码。


#pragma code_seg

另一个使用得比较多的pragma 参数是code_seg。格式如:

#pragma code_seg( ["section-name"[,"section-class"] ] )

它能够设置程序中函数代码存放的代码段,当我们开发驱动程序的时候就会使用到它


#pragma message

能够在编译信息输出窗口中输出相应的信息,这对于源代码信息的控制是非常重要的。其使用方法为:

#pragma message(“消息文本")

当编译器遇到这条指令时就在编译输出窗口中将消息文本打印出来。 这对于我们进行源码控制,代码调试有帮助。


#pragma pack内存对齐

内存对齐的原理:

内存对齐是指在计算机系统中,为了提高存储器和处理器的访问性能,将数据项存储在特定的地址上,使得访问该数据项的时候会更快。 这些特定的地址通常是某个较小的数的倍数,这个较小的数被称为对齐粒度

例如,如果数据项是一个 32 位整数,那么对齐粒度就是 4 字节。如果将这个数据项存储在 4 字节的倍数的地址上,那么就是对齐的;否则,就是不对齐的。

对齐常常是为了让存储器访问更快。当处理器访问内存时,它通常是以块的形式一次性读取多个字节。如果数据项不对齐,那么处理器就必须两次访问内存,才能读取完整的数据项。这会降低访问效率。

不同的计算机系统有不同的内存对齐规则。有的系统要求所有数据项都必须对齐,有的系统则允许部分数据项不对齐。


#pragma pack 设置内存对齐粒度

“#pragma pack” 是一个编译指令,它可以用来设置编译器使用的内存对齐粒度。它通常用于调整结构体成员在内存中的对齐方式。

例如,如果在结构体中定义了两个成员,一个是 8 位整数,另一个是 32 位整数,那么如果编译器使用的内存对齐粒度是 4 字节,那么这两个成员在内存中的布局就会是这样的:

8 位整数32 位整数
12
34
56
78

这种布局方式被称为对齐。

但是,有时候我们希望结构体的成员不要对齐,而是按照定义的顺序在内存中连续存储。这时候就可以使用 “#pragma pack” 指令来设置内存对齐粒度。

例如,如果在结构体定义之前加上 “#pragma pack(1)”,那么编译器就会使用 1 字节作为内存对齐粒度,这样结构体的成员就会按照定义的顺序在内存中连续存储,而不会对齐。

注意,“#pragma pack” 指令仅对当前编译单元有效,也就是说,一旦编译单元结束,内存对齐粒度就会恢复。


内存对齐例子

举个内存对齐的例子:

假设我们有以下的结构体:

struct Data {char a;
    int b;
    short c;
};

这个结构体中包含 3 个成员:一个 8 位整数、一个 32 位整数和一个 16 位整数。

如果编译器使用的内存对齐粒度是 4 字节,那么这个结构体在内存中的布局就会是这样的:

8 位整数32 位整数16 位整数
123
456
789
101112

注意到这种布局方式下,每个成员的地址都是连续的。


struct TestStruct1
                   {   char c1;
                           short s;
                           char c2;
                           int i;
                    };

解析:此结构体在内存中的布局为 1*,11,1*******,1111 (1 代表占用内存,* 代表为内存对齐补的内存空间)

   所以 sizeof(TestStruct1)  为12


#pragma pack (n),编译器将按照n 个字节对齐
#pragma pack (),编译器将取消自定义字节对齐方式

例如:

   #pragma pack(8)
           struct TestStruct4
           { char a;
                     long b;
           };
   struct TestStruct5
       { char c;
                 TestStruct4 d;
                 long long e;
        };
      #pragma pack()
解析:

    TestStruct4 内存布局: 1***,1111

    TestStruct5 内存布局:  1***,1***,1111****,11111111

    所以 sizeof(TestStruct4) 为 8,sizeof(TestStruct5)为 24


内存对齐的规则

(1)每个成员分别按自己的方式对齐,并能最小化长度

(2)复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度

(3)对齐后的长度必须是成员中大的对齐参数的整数倍,这样在处理数组时可以保证每一项都边界对齐

(4)对于数组,比如:char a[3];它的对齐方式和分别写3 个char 是一样的.也就是说它还是按1 个字节对齐,即数组按照数组中的每个成员的类型对齐

  • 如果写: typedef char Array3[3];Array3 这种类型的对齐方式还是按1 个字节对齐,而不是按它的长度

(5)不论类型是什么,对齐的边界一定是1,2,4,8,16,32,64…中的一个


‘#’在宏定义预处理的使用

“#”运算符

1、字符串中包含宏参数,可以使用“#”,可以把语言符号转化为字符串。

#define SQR(x)  printf("The square of  " #x " is %d. \n", ((x) * (x)));
SQR(8)

输出:The square of 8 is 64.

“##”运算符-粘合剂

1、也可以用于宏函数的替换部分,这个运算符把两个语言符号组合成单个语言符号

#define XNAME(n)  x ## n
XNAME(8)

被展开为:x8
“ ## ” 就是个粘合剂,将前后两部分粘合起来。


结构体内存对齐

为什么要存在内存对齐?

平台原因(移植原因) : 不是所有的硬件平台都能访问任意地址上的任意数据的;某些平台只能在某些地址处取得某些特定类型的数据,否则抛出硬件异常。比如,当一个平台要取一个整型数据时只能在地址为4的倍数的位置取得,那么这时就需要内存对齐,否则无法访问到该整型数据。

性能原因: 数据结构(尤其是栈):应该尽可能的在自然边界上对齐。原因在于,为了访问未对齐内存,处理器需要作两次内存访问;而对齐的内存访问仅需一次。

规则
  1. 结构体的第一个成员直接对齐到相对于结构体变量起始位置为0处偏移。
  2. 从第二个成员开始,要对齐到某个【对齐数】的整数倍的偏移处。
  3. 结构体的总大小,必须是大对齐数的整数倍。每个结构体成员都有一个对齐数,其中大的对齐数就是大对齐数
  4. 如果嵌套了结构体的情况。嵌套的结构体对齐到自己的大对齐数的整数倍处,结构体的整体大小就是所有大对齐数(含嵌套结构体的对齐数)的整数倍。

举例简单结构体的对齐数的计算

首先,一般都是向较小的数取对齐数,例如,int大小为4,系统指定的对齐数为8. 8 >4,所以取4为对齐数,就像:

在这里插入图片描述
红色和绿色的是存了的地址,白色的就是浪费的空间,所以说对齐方式很浪费空间,可是按照计算机的访问规则,这种方式提高了效率。

从上可以看出,该结构体的大小为:1 + 4 + 1 + 3(浪费的空间(白色)) = 9,然后通过法则三知道9是不行的,要偏移到12,因为总大小要是大对齐数的整数倍。

在这里插入图片描述综上 结构体的大小为:1 + 4 + 1 + 3 + 4(偏移的大小) = 12.

结构体中包含联合体的结构体大小 分支 联合体的大小计算:

在这之前咱先了解一下联合体大小计算规则:联合体中大成员所占内存的大小且必须为大类型所占字节的最小倍数。

在这里插入图片描述

计算结构体中的联合体大小

联合体在结构体里面比较特殊,他可以作为大的对齐数,联合体大小为8,系统指定的对齐数为8,所以大对齐数为8,然后可以根据上面的内存格子数一数。

在这里插入图片描述uoion U先取大类型 64位 8 字节double ,char[7]占7个字节,所以8字节够用了。

综上结构体的大小为:1 + 3 + 4 + 8 + 1 + 7(偏移量) = 24


位段和对齐

什么是位段?


更多参考:📚
  • 《c语言深度剖析》整理–预处理

  • C语言深度解剖 – 预处理

  • Daily-C-Study(17):C语言文件包含#include

  • C语言重难点:内存对齐和位段

  • 结构体对齐计算(超详细讲解,一看就会)

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


新闻名称:预处理/宏定义/头文件(main)深度剖析-创新互联
网址分享:http://cdkjz.cn/article/dicdcd.html
多年建站经验

多一份参考,总有益处

联系快上网,免费获得专属《策划方案》及报价

咨询相关问题或预约面谈,可以通过以下方式与我们联系

大客户专线   成都:13518219792   座机:028-86922220