赞
踩
结构体作为由程序员自己来定义的类型,其本身相对其他类型来说是复杂的,如相对int,char,long,float……等来说,下面这个结构体就可以描述更为复杂的对象’人‘:
- typedef struct
- {
- char name;
- int age;
- int height;
- }or;
这段代码定义了一个包含name、age、height的三个成员变量的结构体类型or,而or足以片面的描述一个人,描述的复杂度是任意单一基本数据类型都无法做到的。
可以这么说:
结构体也是一种数据类型,它由程序员自己定义,可以包含多个其他类型的数据。像 int、float、char 等是由C语言本身提供的数据类型,不能再进行分拆,我们称之为基本数据类型;而结构体可以包含多个基本类型的数据,也可以包含其他的结构体,我们将它称为复杂数据类型或构造数据类型。
一个结构体可能由四部分构成:结构体关键字、结构体名、成员变量和基本语法框架(可能会加入typedef类型重定义一下,这里就不算如其中了)。
- struct or //"struct"结构体关键字//“or”结构体名
- {
- char name; //成员变量1
- int age; //成员变量2
- int height; //成员变量3
- }; //“{ }”和“;”为基本语法框架
struct是结构体关键字,是说明定义为结构体的关键;
由程序员根据情况和需要自由命名,需要和其他类型名区别开来,同时和struct共同构成完整的结构体类型,"struct 结构名"类比基本数据类型“int”。
指结构体所包含的元素,即它的“成员”,如上例。
基本的语法,分别为“{ }”和“;”构成。
- struct 结构名
- {
- 成员变量;
- ……;
- };
- struct or
- {
- char name;
- int age;
- int height;
- }ren,liu;
sturct or此时已经是一个结构体类型,后缀一个变量名当然可以创建一个变量,至于其是全局变量,还是局部变量就要看声明的位置了。
- typedef struct or
- {
- char name;
- int age;
- int height;
- }ren;
- typedef struct
- {
- char name;
- int age;
- int height;
- }run;
typedef(类型重定义)可以将类型重定义为我们喜闻乐见的样子,其用法大家还是很清楚的,忘了的话可以再复习一下。
类似于2.2,typedef将struct or重定义为ren。
- typedef struct Node
- {
- int data;
- struct Node* next;
- }node;
- struct
- {
- char name;
- int age;
- int height;
- }ren;
匿名结构体顾名思义,是没有名字的结构体类型,而在结构体构成讲过,完整的结构体类型由struct+名 构成,单凭struct本身是无法单独表示一个结构体类型的。
上述代码中,匿名结构体类型仅仅作为一次性的类型使用,无法再创建出和ren一样的结构体变量,即便是声明时内容一样。
下面将通过两个例子证实本观点:
①无法再创建,仅作为一次性类型使用
错误信息为:
错误(活动)E0020未定义标识符
错误(活动)E0040应输入标识符
②异父异母双胞胎
- struct
- {
- char name;
- int age;
- int height;
- }* ren;
-
- struct
- {
- char name;
- int age;
- int height;
- }liu;
-
- int main()
- {
- ren == &liu;
- return 0;
- }
警告表明,本质上ren和liu的类型并不相同。
最后还是要说一下,匿名变量本身的适用范围相当狭窄,尽量不要使用为好。
再简单不过的操作,和创建其他变量一样,类型+名;即可。
- sturct or ret;//正常
- red liu;//typedef后
- struct
- {
- char name;
- int age;
- int height;
- }liu;//匿名
- struct or
- {
- char name;
- int age;
- int height;
- //struct ret s;
- }liu;
按照成员变量的默认顺序依次初始化,默认初始化:
- //变量 = {成员1,成员2……};
- liu = {"zhangsan",24,180};
对指定成员初始化,指定初始化:
- //变量 = {.成员 = 初始化,.成员 = 初始化,……}
- liu = {.age = 24, .height 180,.name = "zhangsan"};
结构体变量内含一个成员结构体变量(非自引用),结构体嵌套初始化:
- //很简单再用一个{}将成员结构体的数据包裹起来
- liu = {"zhangsan",24,180,{1,2,"niuma"}};
不知道你们是否想过,在结构中包含一个类型为该结构本身的成员是否可以呢?
- struct Node
- {
- int data;
- struct Node next;
- };
上述代码如果可行,那sizeof(struct Node)是多少?
想象一下,struct Node类型内有一个struct Node类型的成员变量,成员变量内又有一个struct Node类型的成员变量,无限套娃下去空间该有多大。
- struct Node
- {
- int data;
- struct Node* next;
- };
虽然无法自引用,但其存一个自身类型的指针还是没有问题的,因为指针的空间大小是一定的,且不会无限向下套娃,那么此时就可以计算出sizeof(struct Node)是多少了。
和*类似,结构体有两个相关操作符分别是“.”和“->”。
- struct Stu
- {
- char name[15];
- int age;
- }s;
①“.”如何访问
- strcpy(s.name, "zhangsan");//使用.访问name成员
- s.age = 20;//使用.访问age成员
② "->"如何访问
- void print(struct Stu* ps)
- {
- printf("name = %s age = %d\n", (*ps).name, (*ps).age);
- //使用结构体指针访问指向对象的成员
- printf("name = %s age = %d\n", ps->name, ps->age);
- }
- int main()
- {
- struct Stu s = {"zhangsan", 20};
- print(&s);//结构体地址传参
- return 0;
- }
总结:指针用“->”,变量用“.”。
因为不确定大家对结构体的存储是否有过了解,这里我在正文内容前先加入一些导入内容,以方便对后面内容的理解。
- #include <stdio.h>
- #include <string.h>
- struct liu
- {
- int a;
- char name;
- float c;
- };
- struct ma
- {
- char b;
- char name;
- int abb;
- };
- struct cdd
- {
- short a;
- short b;
- int c;
- };
-
- int main()
- {
- printf("%d ", sizeof(struct liu));
- printf("%d ", sizeof(struct ma));
- printf("%d ", sizeof(struct cdd));
- return 0;
- }
上述代码中,分别用不同的类型或顺序来声明了三种不同的结构体,并在主函数中计算出其大小后打印出来,大家不妨猜测一下打印的结果是什么。
常规思路下,我们将结构体类型申请的空间默认为紧密相连的一份内存,其数据存放是一个挨着一个,没有”缝隙“和内存浪费的,即下图:
最后得出的结果也无非是,9、6、8,这样的求解思路是完全情有可原的,在未了解本质的情况下,还是很难想象到其真实存储状况的,接下来直接放结果。
cdd很可能猜对了,但其他两个的结果是否让你大吃一惊,对于结构体在内存中究竟是如何存储的,还请接着往下阅读正文部分。
偏移量是结构体存储的重要依据,结构体的每个成员变量该存在哪里,本质上就是它说了算。
在了解编译器是如何依据偏移量对结构体”排兵布阵“前,我们首先需要了解偏移量是什么,以及在结构体中偏移量又该如何计算。
偏移量,顾名思义,自然是偏移一定”根“的量,可以简单理解为偏移的值就是偏移量。
在c语言,偏移量在指针运算中得到了很好的体现,如对不同类型指针进行加减,其”步幅“表现完全不同。
- #include <stdio.h>
- int main()
- {
- int a = 0;
- char b = 'a';
- int* as = &a;
- char* bs = &b;
- printf("&a = %x as + 1 = %x\n&b = %x bs + 1 = %x\n", &a,as + 1, &b,bs + 1);
- return 0;
- }
指针as的”步幅“为4字节,”根“为&a,as+1相对&a的偏移量为4;
指针bs的”步幅“为1字节,”根“为&b,bs+1相对&b的偏移量为1。
找到”根“才能确认偏移量,而结构体变量的首地址便是结构体的”根“,其偏移量以1个字节为单位。
- struct liu
- {
- int a;
- char name;
- float c;
- } ret;
对于结构体ret,假设其首地址为‘0XEFFFEE93’,首地址所在空间对应的偏移量便为0,偏移一个字节的地址‘0XEFFFEE94’所对应空间的偏移量应该为1,以此类推……
了解了相关的偏移量知识,就可以根据下述原则来确定不同成员变量存储的位置,及计算出结构体所分配空间的总大小:
① 第一个成员在与结构体变量偏移量为0的地址处。
② 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
③ 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
④如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,嵌套结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
⑤遵循内存合理浪费原则
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS编译器中默认的值为8
可能单看规则还有点懵,没关系,下面我会逐条进行演示,演示以结构体ret为例。
- struct liu
- {
- char f;
- int a;
- char name;
- float c;
- } ret;
即:首个成员变量以偏移量为0(首地址)所在位置开始存储。
若首个成员为char类型变量,便自首地址起占用其所需要的1字节空间。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值
部分1:编译器默认的一个对齐数
不同的编译器默认对齐数不同,以VS(visual studio)编译器为例,其默认对齐数为8。
部分2: 该成员大小的较小值
成员大小,即成员变量的大小,以字节为单位,如char类变量的大小为1。
(手————动————分————割)
该规则的意思是,存放成员的起始点的偏移量必须为其对齐数的整数倍。
如ret的第二个成员的大小是4,默认对齐数为8,所以它必须从4的整数倍的偏移量位置开始存储。
ret的第三个成员的大小是1,默认对齐数为8,所以它必须从1的整数倍的偏移量位置开始存储。
当存储完全部数据,如下:
此时该结构体包括浪费的空间在内一共占用了16个字节,依照本条规则,结构体总体大小必须为结构体中最大的对齐数的整数倍。
对于ret,成员f对齐数为1,成员a对齐数为4,成员name对齐数为1,成员c对齐数为4,其中最大的对齐数是4。
对齐数的整数倍为16,这和结构体本身已占用的16个字节契合,因此不需要再浪费额外的空间给ret满足对其条件。
如果再给ret结构体增加一个成员变量,如char d;那么结构体存储完全部成员用了17字节,此时结构体ret不再满足对齐条件,需浪费3字节的空间来不足,最终ret被分配到的空间会是20字节。
虽然无法自引用,但一个结构体内是可以嵌套其他结构体的,而这个嵌套结构体的对齐数就是其成员对齐数中最大的那个,其占用内存的大小也是遵循前面几条规则,必须是本身最大对齐数的整数倍。
该规则演示换例子ret的内容:
- typedef struct aff
- {
- int a;
- double b;
- }aff;
- struct abb
- {
- char a;
- int b;
- aff c;
- }ret;
先来看嵌套结构体c,经计算得其占内存16字节,最大对齐数为8(来自double b;)。
再看ret,成员a和b占用了偏移量0~7的空间:
根据”嵌套结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍“,所以c在ret中也应占16字节;最大对齐数为8,根据“嵌套的结构体对齐到自己的最大对齐数的整数倍处”,所以对齐到8的整数倍的偏移量,8正好满足,即c占用偏移量8~23的空间。
ret当前占用空间为24字节,其最大对齐数也就是嵌套结构体c的最大对齐数8,24字节满足8的整数倍,无需补足,最终得结构体ret的大小为24字节。
编译器为对齐而浪费的空间遵循最小浪费原则。
如:对齐数为4,则偏移量能4则4,能8则8。
① 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
②性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
如对于32位机器来说,其32位地址线一次性可读取32bit,也就是4byte,对于对齐的结构体读取:
对于未对齐结构体的读取:
这两种对齐在访问成员b的时候需要的次数分别为1次和2次。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法
对结构体内存的优化主要要做到两点,即在设计结构体的时候,我们既要满足对齐,又要节省空间,也就是空间我们要,时间我们也要。
做到这一点主要靠调整成员变量的排列顺序做到,合理利用对齐数来最大程度上利用内存。
如对于一个结构体类型s1:
- struct S1
- {
- char c1;
- int i;
- char c2;
- };
我们可以这样调整……
- struct S2
- {
- char c1;
- char c2;
- int i;
- };
尽管只是节约了4字节的空间,但当这种操作执行次数够多,其省下的空间也可以是个庞大的数量。
通过预处理指令#pragma来设置默认对齐数
- #pragma pack(8)//设置默认对齐数为8
- #pragma pack()//取消设置的默认对齐数,还原为默认
- #include <stdio.h>
- #pragma pack(8)//设置默认对齐数为8
- struct S1
- {
- char c1;
- int i;
- char c2;
- };
- #pragma pack()//取消设置的默认对齐数,还原为默认
- #pragma pack(1)//设置默认对齐数为1
- struct S2
- {
- char c1;
- int i;
- char c2;
- };
- #pragma pack()//取消设置的默认对齐数,还原为默认
- int main()
- {
- //输出的结果是什么?
- printf("%d\n", sizeof(struct S1));
- printf("%d\n", sizeof(struct S2));
- return 0;
- }
当设置默认对齐数为1时,任意偏移值都满足对齐条件,所有成员紧密排列;当设置默认对齐数为3……为5……等时……
这种看似省空间的做法却会破坏原本默认对齐时省时间这一重要属性,所以设置默认对齐数时一定要考虑清楚。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。