十年网站开发经验 + 多家企业客户 + 靠谱的建站团队
量身定制 + 运营维护+专业推广+无忧售后,网站问题一站解决
本节主要深度剖析一下预处理和里面的宏定义以及代码编写的一些固定格式的原理
我们提供的服务有:做网站、成都网站设计、微信公众号开发、网站优化、网站认证、柳江ssl等。为上千企事业单位解决了网站和推广的问题。提供周到的售前咨询和贴心的售后服务,是有科学管理、有技术的柳江网站制作公司
预编译又称为预处理,是做些代码文本的替换工作。
比如:处理#开头的指令,比如拷贝#include包含的文件代码,#define宏定义的替换,条件编译等,就是为编译做的预备工作的阶段,主要处理#开始的预编译指令,预编译指令指示了在程序正式编译前就由编译器进行的操作,可以放在程序中的任何位置。
c编译系统在对程序进行通常的编译之前,先进行预处理。 c提供的预处理功能主要有以下三种:
作用是为了:
宏定义开始,到文件结束(其他的文件包含宏定义的文件也可引用)。
为什么要使用宏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,显然这不是我们想要的结果,导致出错
比如 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 ,所以不会再重复执行”头文件内容“部分的代码。
C语言提供#include 命令来实现文件包含的操作,它实际是宏定义的延伸;
(1)#includeC 编译系统所提供的并存放在指定的子目录下的头文件。找到文件后,用文件内容替换该语句;
(2)#include “filename”
预处理应在当前目录中查找文件名为filename 的文件.
若没有找到,则按系统指定的路径信息,搜索其他目录。找到文件后,用文件内容替换该语句。
#include 是将已存在文件的内容嵌入到当前文件中
❗️include<>和 “” 区别🤨 尖括号<>和引号 “” 的区别在于,前者表示要包含的文件位于编译器的默认搜索路径中,而后者表示要包含的文件位于程序文件所在的目录或指定的搜索路径中。
也就是""先搜索当前目录的,如果找不到文件回到编译器的默认搜索路径重新搜索。
主函数要写main (int argc, char* argv[ ])
argc在C语言中表示运行程序时传递给main()函数的命令行参数个数
。
argv在C语言中表示运行程序时用来存放命令行字符串参数的指针数组
。
argc、argv用命令行编译程序时有用。主函数main中变量(int argc,char *argv[ ])的含义:
字符串参数的指针
,每一个元素指向一个参数。其中argv[0] 指向程序运行的全路径名,argv[1] 指向在DOS命令行中执行程序名后的第一个字符串,argv[2] 指向执行程序名后的第二个字符串,argv[argc]为NULL
。#pragma用于指示编译器完成一些特定的动作
下面讲解一下常用的几个#pragma 预处理命令
该指令将一个注释记录放入一个对象文件或可执行文件中
。常用的lib 关键字,可以帮我们连入一个库文件。比如:
#pragma comment(lib, "user32.lib")
linker:将一个链接选项放入目标文件中
,你可以使用这个指令来代替由命令行传入的
或者在开发环境中设置的链接选项,你可以指定/include 选项来强制包含某个对象。
例如:
#pragma comment(linker, "/include:__mySymbol")
#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
在头文件的最开始加入这条指令就能够保证头文件被编译一次
另外保证头文件只编译一次的方法:
#ifndef _FILENAME_H
#define _FILENAME_H
#endif
当程序中第一次 #include 包含该头文件时,由于 _NAME_H 这个宏还没有定义,所以会定义 _NAME_H 这个宏,并执行”头文件内容“部分的代码;
当发生多次 #include 时,因为前面已经定义了 _NAME_H ,所以不会再重复执行”头文件内容“部分的代码。
另一个使用得比较多的pragma 参数是code_seg。格式如:
#pragma code_seg( ["section-name"[,"section-class"] ] )
它能够设置程序中函数代码存放的代码段,当我们开发驱动程序的时候就会使用到它
能够在编译信息输出窗口中输出相应的信息,这对于源代码信息的控制是非常重要的。其使用方法为:
#pragma message(“消息文本")
当编译器遇到这条指令时就在编译输出窗口中将消息文本打印出来。 这对于我们进行源码控制,代码调试有帮助。
内存对齐是指在计算机系统中,为了提高存储器和处理器的访问性能,将数据项存储在特定的地址上,使得访问该数据项的时候会更快。 这些特定的地址通常是某个较小的数的倍数,这个较小的数被称为对齐粒度
。
例如,如果数据项是一个 32 位整数,那么对齐粒度就是 4 字节。如果将这个数据项存储在 4 字节的倍数的地址上,那么就是对齐的;否则,就是不对齐的。
对齐常常是为了让存储器访问更快。当处理器访问内存时,它通常是以块的形式一次性读取多个字节。如果数据项不对齐,那么处理器就必须两次访问内存,才能读取完整的数据项。这会降低访问效率。
不同的计算机系统有不同的内存对齐规则。有的系统要求所有数据项都必须对齐,有的系统则允许部分数据项不对齐。
“#pragma pack” 是一个编译指令,它可以用来设置编译器使用的内存对齐粒度
。它通常用于调整结构体成员在内存中的对齐方式。
例如,如果在结构体中定义了两个成员,一个是 8 位整数,另一个是 32 位整数,那么如果编译器使用的内存对齐粒度是 4 字节,那么这两个成员在内存中的布局就会是这样的:
8 位整数 | 32 位整数 |
---|---|
1 | 2 |
3 | 4 |
5 | 6 |
7 | 8 |
这种布局方式被称为对齐。
但是,有时候我们希望结构体的成员不要对齐,而是按照定义的顺序在内存中连续存储。这时候就可以使用 “#pragma pack” 指令来设置内存对齐粒度。
例如,如果在结构体定义之前加上 “#pragma pack(1)”,那么编译器就会使用 1 字节作为内存对齐粒度,这样结构体的成员就会按照定义的顺序在内存中连续存储,而不会对齐。
注意,“#pragma pack” 指令仅对当前编译单元有效,也就是说,一旦编译单元结束,内存对齐粒度就会恢复。
举个内存对齐的例子:
假设我们有以下的结构体:
struct Data {char a;
int b;
short c;
};
这个结构体中包含 3 个成员:一个 8 位整数、一个 32 位整数和一个 16 位整数。
如果编译器使用的内存对齐粒度是 4 字节,那么这个结构体在内存中的布局就会是这样的:
8 位整数 | 32 位整数 | 16 位整数 |
---|---|---|
1 | 2 | 3 |
4 | 5 | 6 |
7 | 8 | 9 |
10 | 11 | 12 |
注意到这种布局方式下,每个成员的地址都是连续的。
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 个字节对齐,即数组按照数组中的每个成员的类型对齐
(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的倍数的位置取得,那么这时就需要内存对齐,否则无法访问到该整型数据。
规则性能原因: 数据结构(尤其是栈):应该尽可能的在自然边界上对齐。原因在于,为了访问未对齐内存,处理器需要作两次内存访问;而对齐的内存访问仅需一次。
大对齐数
。首先,一般都是向较小的数取对齐数,例如,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元起,快前往官网查看详情吧