编程学习过程中越是痛苦的时候,学到的东西就会越多

在STM32的RTC开发中,编程学习过程中越是痛苦的时候,学到的东西就会越多

1.结构体有何作用

691730cd35c74cf4a7634c36ef4dd8ce.png

三个月前,教研室里一个学长在华为南京研究院的面试中就遇到这个问题。当然,这只是面试中最基础的问题。如果问你你怎么回答?我的理解是这样的,C语言中结构体至少有以下三个作用:

(1)有机地组织了对象的属性。

比如,在STM32的RTC开发中,我们需要数据来表示日期和时间,这些数据通常是年、月、日、时、分、秒。如果我们不用结构体,那么就需要定义6个变量来表示。这样的话程序的数据结构是松散的,我们的数据结构最好是“高内聚,低耦合”的。所以,用一个结构体来表示更好,无论是从程序的可读性还是可移植性还是可维护性皆是:

typedef struct //公历日期和时间结构体

{

vu16 year;

vu8 month;

vu8 date;

vu8 hour;

vu8 min;

vu8 sec;

}_calendar_obj;

_calendar_obj calendar; //定义结构体变量

312c4ab339eca0543a72a819d47799d7.png

(2)以修改结构体成员变量的方法代替了函数(入口参数)的重新定义。

如果说结构体有机地组织了对象的属性表示结构体“中看”,那么以修改结构体成员变量的方法代替函数(入口参数)的重新定义就表示了结构体“中用”。继续以上面的结构体为例子,我们来分析。假如现在我有如下函数来显示日期和时间:

void DsipDateTime( _calendar_obj DateTimeVal)

那么我们只要将一个_calendar_obj这个结构体类型的变量作为实参调用DsipDateTime()即可,DsipDateTime()通过DateTimeVal的成变量来实现内容的显示。如果不用结构体,我们很可能需要写这样的一个函数:

void DsipDateTime( vu16 year,vu8 month,vu8 date,vu8 hour,vu8 min,vu8 sec)

显然这样的形参很不可观,数据结构管理起来也很繁琐。如果某个函数的返回值得是一个表示日期和时间的数据,那就更复杂了。这只是一方面。

另一方面,如果用户需要表示日期和时间的数据中还要包含星期(周),这个时候,如果之前没有用机构体,那么应该在DsipDateTime()函数中在增加一个形参vu8 week:

void DsipDateTime( vu16 year,vu8 month,vu8 date,vu8 week,vu8 hour,vu8 min,vu8 sec)

可见这种方法来传递参数非常繁琐。所以以结构体作为函数的入口参数的好处之一就是函数的声明void DsipDateTime(_calendar_obj DateTimeVal)不需要改变,只需要增加结构体的成员变量,然后在函数的内部实现上对calendar.week作相应的处理即可。这样,在程序的修改、维护方面作用显著。

typedef struct //公历日期和时间结构体

{

vu16 year;

vu8 month;

vu8 date;

vu8 week;

vu8 hour;

vu8 min;

vu8 sec;

}_calendar_obj;

_calendar_obj calendar; //定义结构体变量

(3)结构体的内存对齐原则可以提高CPU对内存的访问速度(以空间换取时间)。

并且,结构体成员变量的地址可以根据基地址(以偏移量offset)计算。我们先来看看下面的一段简单的程序,对于此程序的分析会在第2部分结构体成员变量内存对齐中详细说明。

#include

int main()

{

    struct //声明结构体char_short_long

    {

        char c;

        short s;

        long l;

    }char_short_long;

    struct //声明结构体long_short_char

    {

        long l;

        short s;

        char c;

    }long_short_char;

    struct //声明结构体char_long_short

    {

        char c;

        long l;

        short s;

    }char_long_short;

printf(" \n");

printf(" Size of char = %d bytes\n",sizeof(char));

printf(" Size of shrot = %d bytes\n",sizeof(short));

printf(" Size of long = %d bytes\n",sizeof(long));

printf(" \n"); //char_short_long

printf(" Size of char_short_long = %d bytes\n",sizeof(char_short_long));

printf(" Addr of char_short_long.c = 0x%p (10进制:%d)\n",&char_short_long.c,&char_short_long.c);

printf(" Addr of char_short_long.s = 0x%p (10进制:%d)\n",&char_short_long.s,&char_short_long.s);

printf(" Addr of char_short_long.l = 0x%p (10进制:%d)\n",&char_short_long.l,&char_short_long.l);

printf(" \n");

printf(" \n"); //long_short_char

printf(" Size of long_short_char = %d bytes\n",sizeof(long_short_char));

printf(" Addr of long_short_char.l = 0x%p (10进制:%d)\n",&long_short_char.l,&long_short_char.l);

printf(" Addr of long_short_char.s = 0x%p (10进制:%d)\n",&long_short_char.s,&long_short_char.s);

printf(" Addr of long_short_char.c = 0x%p (10进制:%d)\n",&long_short_char.c,&long_short_char.c);

printf(" \n");

printf(" \n"); //char_long_short

printf(" Size of char_long_short = %d bytes\n",sizeof(char_long_short));

printf(" Addr of char_long_short.c = 0x%p (10进制:%d)\n",&char_long_short.c,&char_long_short.c);

printf(" Addr of char_long_short.l = 0x%p (10进制:%d)\n",&char_long_short.l,&char_long_short.l);

printf(" Addr of char_long_short.s = 0x%p (10进制:%d)\n",&char_long_short.s,&char_long_short.s);

printf(" \n");

return 0;

}

程序的运行结果如下(注意:括号内的数据是成员变量的地址的十进制形式):

7dd4a0ecc443bdec9812cba3e0ca3748.png

2.结构体成员变量内存对齐

首先,我们来分析一下上面程序的运行结果。前三行说明在我的程序中,char型占1个字节,short型占2个字节,long型占4个字节。char_short_long、long_short_char和char_long_short是三个结构体成员相同但是成员变量的排列顺序不同。并且从程序的运行结果来看,

Size of char_short_long = 8 bytes

Size of long_short_char = 8 bytes

Size of char_long_short = 12 bytes //比前两种情况大4 byte!

并且,还要注意到,1 byte (char)+ 2 byte (short)+ 4 byte (long) = 7 byte,而不是8 byte。

所以,结构体成员变量的放置顺序影响着结构体所占的内存空间的大小。一个结构体变量所占内存的大小不一定等于其成员变量所占空间之和。如果一个用户程序或者操作系统(比如uC/OS-II)中存在大量结构体变量时,这种内存占用必须要进行优化,也就是说,结构体内部成员变量的排列次序是有讲究的。

结构体成员变量到底是如何存放的呢?

在这里,我就不卖关子了,直接给出如下结论,在没有#pragma pack宏的情况下:

·原则1结构(struct或联合union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储)。

·原则2结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。

·原则3结构体作为成员时,结构体成员要从其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct b,b里有char,int,double等元素时,那么b应该从8的整数倍地址处开始存储,因为sizeof(double) = 8 bytes)

这里,我们结合上面的程序来分析(暂时不讨论原则3)。

先看看char_short_long和long_short_char这两个结构体,从它们的成员变量的地址可以看出来,这两个结构体符合原则1和原则2。注意,在 char_short_long的成员变量的地址中,char_short_long.s的地址是1244994,也就是说,1244993是“空的”,只是被“占位”了!

成员变量

成员变量十六进制地址

成员变量十进制地址

char_long_short.c

0x0012FF2C

1244972

char_long_short.l

0x0012FF30

1244976

char_long_short.s

0x0012FF34

1244980

可见,其内存分布图如下,共12 bytes:

290b0ea9b1f02878ec968709d30642a6.png

首先,1244972能被1整除,所以char_long_short.c放在1244972处没有问题(其实,就char型成员变量自身来说,其放在任何地址单元处都没有问题),依据原则1,在之后的1244973~1244975中都没有能被4(由于sizeof(long)=4bytes)整除的,1244976能被4整除,所以char_long_short.l应该放在1244976处,那么同理,最后一个.s(sizeof(short)=2 bytes)是应该放在1244980处。

是不是这样就完毕了?不是,还有原则2。依据原则2的要求,char_long_short这个构造体所占的空间大小应该是其占内存空间最大的成员变量的大小的整数倍。假如我们到此就完毕了,那么char_long_short所占的内存空间是1244972~1244981共计10bytes,不合乎原则2,所以,必需在最后补齐2个 bytes(1244982~1244983)。

至此,一个构造体的内存布局完成了。

下面我们依照上述原则,来验证这样的分析是不是正确。按上面的分析,地址单元1244973、1244974、1244975以及1244982、1244983都是空的(至少char_long_short未用到,只是“占位”了)。假如我们的分析是正确的,那么,定义这样一个构造体,其所占内存也应该是12 bytes:

struct //声明构造体char_long_short_new

{

char c;

char add1; //补齐空间

char add2; //补齐空间

char add3; //补齐空间

long l;

short s;

char add4; //补齐空间

char add5; //补齐空间

}char_long_short_new;

可见,我们的分析是正确的。至于原则3,大家能够自己编程验证,这里就不再探讨了。

所以,没论你是在VC6.0还是Keil C51,还是Keil MDK中,当你须要定义一个构造体时,只有你稍微留心构造体成员变量内存对齐这一现象,就能够在很大程度上节约MCU的RAM。这一点不仅仅应用于实际编程,在很多大型公司,假如IBM、微软、百度、华为的笔试和面试中,也是常见的。

这三大块硬骨头是进修C语言的绊脚石,下功夫拿掉根本上C语言的大动脉就打通了,那么再去进修别的内容就相比照较简略了。编程进修过程中越是痛苦的时候,学到的东西就会越多,克服过去就会自己的技能,放弃了前面的付出的时长都将清零。越是难学的语言在入门之后,在入门之后越觉得过瘾,而且还容易上瘾。你上瘾了没?还是放弃了?

a8ceab73de0601d4587f86381c87e62c.png20d3122a5f59ff04702cf4e0f9999b81.png

猜你喜欢

转载自blog.csdn.net/danpianji777/article/details/124947546