ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
【71.1 结构体的内存生效。】 上一节讲到结构体有三道标准工序“造模”和“生成”和“调用”,那么,结构体在哪道工序的时候才会开始占用内存(或者说内存生效)?答案是在第二道工序“生成”(或者说定义)的时候才产生内存开销。第一道工序仅“造模”不“生成”是不会产生内存的。什么意思呢?请看下面的例子。 第一种情况:仅“造模”不“生成”。 struct StructMould //“造模” { unsigned char u8Data\_A; unsigned char u8Data\_B; }; 分析:这种情况是没有内存开销的,尽管你已经写下了数行代码,但是C编译器在翻译此代码的时候,它会识别到你偷工减料仅仅“造模”而不“生成”新变量,此时C编译器会把你这段代码忽略而过。 第二种情况:先“造模”再“生成”。 struct StructMould //“造模” { unsigned char u8Data\_A; unsigned char u8Data\_B; }; struct StructMould GtMould\_1; //“生成”一个变量GtMould\_1。占用2个字节内存 struct StructMould GtMould\_2; //“生成”一个变量GtMould\_2。占用2个字节内存 分析:这种情况才会占用内存。你“生成”变量越多,占用的内存就越大。像本例子,“生成”了两个变量GtMould\_1和GtMould\_2,一个变量占用2个字节,两个就一共占用了4个字节。结论:内存的占用是跟变量的“生成”有关。 【71.2 结构体的内存对齐。】 什么是对齐?为了确保内存的地址能整除某个“对齐倍数”(比如4)。比如以4为“对齐倍数”,在地址0存放一个变量a,因为地址0能整除“对齐倍数”4,所以符合“地址对齐”,接着往下再存放第二个变量b,紧接着的地址1不能整除“对齐倍数”4,此时,为了内存对齐,本来打算把变量b放到地址1的,现在就要更改挪到地址4才符合“地址对齐”,这就是内存对齐的含义。“对齐倍数”是什么?“对齐倍数”就是单片机的位数除以8。比如8位单片机的“对齐倍数”是1(8除以8),16位单片机是2(16除以8),32位单片机是4(32除以8)。本教程所用的单片机是8位的51内核单片机,因此“对齐倍数”是1。1是可以被任何整数整除的,因此,8位单片机在结构体的使用上被内存对齐的“干扰”是最小的。 为什么要对齐?单片机内部硬件层面一条指令处理的数据宽度是固定的,比如,因为一个字节是8位,所以,8位的单片机一次处理的数据宽度是1个字节(8除以8等于1),16位的单片机一次处理的数据宽度是2个字节(16位除以8位等于2),32位的单片机一次处理的数据宽度是4个字节(32位除以8位等于4),如果字节不对齐,本来单片机一个指令能处理的数据可能就要分解成2个指令甚至更多的指令,所以C编译器为了让单片机处于最佳状态,在某些情况就会涉及内存对齐,结构体就涉及到内存对齐。 结构体的内存对齐表现在哪里呢?请看下面两个例子: 第一个例子:8位单片机。 struct StructMould\_1 //“造模” { unsigned char u8Data; //一个unsigned char占用1个字节。 unsigned long u32Data; //一个unsigned long占用4个字节。 }; struct StructMould\_1 GtMould\_1; //占用多少个字节内存呢? 分析:GtMould\_1这个变量占用多少个内存字节呢?假设GtMould\_1的首地址是0,那么地址0就存放成员u8Data,u8Data占用1个字节,所以接下来的地址是1(0+1),问题来了,地址1能直接存放占用4个字节的成员u32Data吗?因为8位单片机的“对齐倍数”是1(8除以8),那么地址1显然是可以整除“对齐倍数”1的,因此,地址1是可以果断存储u32Data成员的。因此,GtMould\_1占用的总字节数是5(1+4),也就是u8Data和u32Data两者所占字节数之和。 第二个例子:32位单片机。 struct StructMould\_1 //“造模” { unsigned char u8Data; //一个unsigned char占用1个字节。 unsigned long u32Data; //一个unsigned long占用4个字节。 }; struct StructMould\_1 GtMould\_1; //占用多少个字节内存呢? 分析:GtMould\_1这个变量占用多少个内存字节呢?假设GtMould\_1的首地址是0,那么地址0就存放成员u8Data,u8Data占用1个字节,所以接下来的地址是1(0+1),那么问题来了,地址1能直接存放占用4个字节的成员u32Data吗?不能。因为32位单片机的“对齐倍数”是4(32除以8),那么地址1显然是不可以整除“对齐倍数”4的,因此,就要把地址1更改挪到地址4这里才符合“地址对齐”,这样,就意味着多插入了3个“填充的字节”,因此,GtMould\_1占用的总字节数是8(1+3+4),也就是“1个字节u8Data,3个填充字节,4个u32Data”三者所占字节数之和。那么问题又来了,如果把结构体内部成员u8Data和u32Data的位置顺序更改一下,内存容量会有所改变吗?位置顺序更改后如下。 struct StructMould\_1 //“造模” { unsigned long u32Data; //一个unsigned long占用4个字节。 unsigned char u8Data; //一个unsigned char占用1个字节。 }; struct StructMould\_1 GtMould\_1; //占用多少个字节内存呢? 分析:更改u8Data和u32Data的位置顺序后,u32Data在前u8Data在后,GtMould\_1这个变量占用多少个内存字节呢?假设GtMould\_1的首地址是0,那么地址0就存放成员u32Data,u32Data占用4个字节,所以接下来的地址是4(0+4),那么问题来了,地址4能直接存放占用1个字节的成员u8Data吗?能。因为32位单片机的“对齐倍数”是4(32除以8),那么地址4显然是可以整除“对齐倍数”4的,因此,地址4是可以果断存储u8Data的。那么,是不是GtMould\_1就占用5个字节呢?不是。因为结构体的内存对齐,还包括另外一条规定,那就是“一个结构体变量所占的内存总容量必须能整除该单片机的“对齐倍数”(单片机的位数除以8),如果不能,C编译器就会擅自在最后一个成员的后面插入若干个“填充字节”来满足这个规则”,根据这条规定,计算所得的总容量5是不能整除“对齐倍数”4的,必须再额外填充3个字节补足到8,才能整除“对齐倍数”4,因此,更改顺序后,GtMould\_1还是占用8个字节(4+1+3),前4个字节是u32Data,中间1个字节是u8Data,后3个字节是“填充字节”。 因为本教程采用的是8位的51内核单片机,因此,在上述这个例子中,GtMould\_1所占的字节数是符合“第一个例子”的情况,也就是占用5个字节。内存对齐是遵守几条严格的规则的,我只列出其中最关键的两条给大家大致阅读一下,有一个印象即可,不强求死记硬背,只需知道“结构体因为存在内存对齐,所以实际内存容量是有可能大于内部各成员类型字节数相加之和,尤其是16位或者32位这类单片机”就可以了。 第(1)条:结构体内部某个成员相对结构体首地址的偏移地址必须能整除该单片机的“对齐倍数”(单片机的位数除以8),如果不能,C编译器就会擅自在各成员之间插入若干个“填充字节”来满足这个规则。 第(2)条:一个结构体变量所占的内存总容量必须能整除该单片机的“对齐倍数”(单片机的位数除以8),如果不能,C编译器就会擅自在最后一个成员的后面插入若干个“填充字节”来满足这个规则。 【71.3 如何获取某个结构体变量的内存容量?】 结构体存在内存对齐的问题,就说明它的内存占用情况不会像普通数组那样一目了然,那么,我们编写程序的时候怎么知道某个结构体变量占用了多少个字节数?答案是:用sizeof宏函数。比如: struct StructMould\_1 { unsigned long u32Data; unsigned char u8Data; }; struct StructMould\_1 GtMould\_1; unsigned long a; //此变量用来获取结构体变量GtMould\_1所占用的字节总数 void main() //主函数 { a=sizeof(GtMould\_1); //利用宏函数sizeof获取结构体变量所占用的字节总数 } 【71.4 结构体之间的赋值。】 结构体之间的赋值有两种,第一种是成员之间“一对一”的赋值,第二种是整个结构体之间“面对面”的整体赋值。第一种成员赋值像普通变量赋值那样,没有那么多套路和忌讳,数据传递安全可靠。第二种整个结构体之间赋值在编程体验上带有“一键操作”的快感,但是要注意避开一些“雷区”,首先,整体赋值的前提是必须保证两个结构体变量都是同一个“结构体模板”造出来的变量,不同“模板”的结构体变量之间禁止“整体赋值”,其次,哪怕是“同一个模板”的结构体变量,也并不是所有的“同模板结构体”变量都能实现整个结构体之间的直接赋值,只有在结构体内部成员比较简单的情况下才适合“整体赋值”,如果结构体内部包含有“指针”或者“字符串”或者“其它结构体中的结构体”,这类情况就比较复杂,这时建议大家绕开有“雷区”的“整体赋值”而直接选用安全可靠的“成员赋值”。什么是“成员赋值”什么是“整体赋值”?请看下面两个例子。 第一种:成员赋值。把结构体变量GtMould\_2\_A赋值给GtMould\_2\_B。 struct StructMould\_2 //“造模” { unsigned long u32Data; unsigned char u8Data; }; struct StructMould\_2 GtMould\_2\_A; //生成第1个结构体变量 struct StructMould\_2 GtMould\_2\_B //生成第2个结构体变量 void main() //主函数 { //先给GtMould\_2\_A赋初值。 GtMould\_2\_A.u32Data=1; GtMould\_2\_A.u8Data=2; //通过“成员赋值”,把结构体变量GtMould\_2\_A赋值给GtMould\_2\_B。 GtMould\_2\_B.u32Data=GtMould\_2\_A.u32Data; //成员之间“一对一”的赋值 GtMould\_2\_B.u8Data=GtMould\_2\_A.u8Data; //成员之间“一对一”的赋值 } 第二种:整体赋值。把结构体变量GtMould\_2\_A赋值给GtMould\_2\_B。 struct StructMould\_2 //“造模” { unsigned long u32Data; unsigned char u8Data; }; struct StructMould\_2 GtMould\_2\_A; //生成第1个结构体变量 struct StructMould\_2 GtMould\_2\_B //生成第2个结构体变量 void main() //主函数 { //先给GtMould\_2\_A赋初值。 GtMould\_2\_A.u32Data=1; GtMould\_2\_A.u8Data=2; //通过“整体赋值”,把结构体变量GtMould\_2\_A赋值给GtMould\_2\_B。 GtMould\_2\_B=GtMould\_2\_A; //整体之间“一次性”的赋值 } 上述例子中的整体赋值,是因为结构体内部的数据比较“简单”,没有包含“指针”或者“字符串”或者“其它结构体中的结构体”这类数据成员,如果包含这类成员,建议大家不要用整体赋值。比如遇到以下这类结构体就建议大家直接用安全可靠的“成员赋值”: struct StructMould //“造模” { unsigned char u8String\[\]=”String”; //字符串 unsigned char \*pu8Data; //指针 struct StructOtherMould GtOtherMould; //结构体中的结构体 }; 【71.5 例程练习和分析。】 现在编写一个练习的程序: /\*---C语言学习区域的开始。-----------------------------------------------\*/ struct StructMould\_1 //“造模” { unsigned long u32Data; //一个unsigned long占用4个字节。 unsigned char u8Data; //一个unsigned char占用1个字节。 }; struct StructMould\_2 //“造模” { unsigned char u8Data; unsigned long u32Data; }; struct StructMould\_1 GtMould\_1; //占用多少个字节内存呢? struct StructMould\_2 GtMould\_2\_A; struct StructMould\_2 GtMould\_2\_B; unsigned long a; //此变量用来获取结构体变量GtMould\_1所占用的字节总数 void main() //主函数 { a=sizeof(GtMould\_1); //利用宏函数sizeof获取结构体变量GtMould\_1所占用的字节总数 //先给GtMould\_2\_A赋初值。 GtMould\_2\_A.u32Data=1; GtMould\_2\_A.u8Data=2; //通过“整体赋值”,把结构体变量GtMould\_2\_A赋值给GtMould\_2\_B。 GtMould\_2\_B=GtMould\_2\_A; //整体之间“一次性”的赋值 View(a); //把a发送到电脑端观察 View(GtMould\_2\_B.u32Data); //把结构体成员GtMould\_2\_B.u32Data发送到电脑端观察 View(GtMould\_2\_B.u8Data); //把结构体成员GtMould\_2\_B.u8Data发送到电脑端观察 while(1) { } } /\*---C语言学习区域的结束。-----------------------------------------------\*/ 在电脑串口助手软件上观察到的程序执行现象如下: 开始... 第1个数 十进制:5 十六进制:5 二进制:101 第2个数 十进制:1 十六进制:1 二进制:1 第3个数 十进制:2 十六进制:2 二进制:10 分析: GtMould\_1所占的字节数a为5。 GtMould\_2\_B的结构体成员GtMould\_2\_B.u32Data为1。 GtMould\_2\_B的结构体成员GtMould\_2\_B.u8Data为2。 【71.6 如何在单片机上练习本章节C语言程序?】 直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。