“少年不识愁滋味,爱上层楼。爱上层楼,为赋新词强说愁。
而今识尽愁滋味,欲说还休。欲说还休,却道天凉好个秋。”
猛戳订阅🍁🍁 👉 纯C详解数据结构专栏 👈 🍁🍁
这不是目录
前言
一、结构体基础知识
1. 结构声明
2.结构成员的直接访问
3.结构成员的间接访问
4.结构体的自引用
5.结构的初始化
二、结构、指针和成员
结构体内存分配(面试重点)
1.如何计算结构体大小?
2.为什么存在内存对齐?
3.offsetof宏
三、作为函数参数的结构
四、位段
1.什么是位段
2.位段的内存分配
五、枚举
1.枚举的优点
六、联合
1.为什么要用联合体?
2.联合大小的计算
前言
最近在学数据结构并且写了一些数据结构的题。发现结构体和数据结构有关联。觉得有必要对结构体进行一下深入的总结。
一、结构体基础知识
C语言提供了两种类型的数据类型:数组和结构
数组:数组是相同类型元素的集合。他通过下标引用或指针间接访问其中的元素的。
结构:结构是不同或相同类型元素的集合。因为结构成员的长度不同,所以他是通过名字来访问结构体成员的
1. 结构声明
完全声明:声明结构需要加上标签,尽量不要不加,标签(tag)允许多个声明使用同一个成员列表,并且创建同一种类型的结构。
不完全声明:不加标签称为结构体的不完全声明,没有标签声明的结构体只能用一次,也就是创建一次变量。第二次创建的变量和第一次创建的变量类型就不同了。
struct tag//标签 { //成员列表 member-list; }//变量列表variable-list;
声明结构时一种良好的技巧是用typedef创建一种新的类型。这样可以简化书写。
2.结构成员的直接访问
结构变量的成员是通过操作符(.)来访问的。点操作符接收两个操作数,做操作数就是结构体变量的名字,有操作数就是需要访问的成员的名字。
例如:创建一个学生变量Student,可以通过Student.age来访问学生的年龄。Student.name来访问学生的名字。
struct Stu { char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号 }Student; //分号不能丢
3.结构成员的间接访问
结构成员的间接访问操作使用操作符(->)箭头来实现的。前提是你必须有个指向结构的指针。这是什么时候用点操作符和箭头操作的区别。
举个例子:创建一个指向结构体的指针p。
void fun(struct Stu* p);
可以用p->age来访问学生的年龄,也可以用(*p).age来访问。
(->)操作符的出现是因为要访问一个结构体成员你必须先对指针解引用然后再用(.)操作符访问结构体成员,这样比较麻烦,所以前辈将解引用操作内置在箭头操作符中。然后使用起来就方便了。
4.结构体的自引用
在一个结构体内部包含一个类型为结构本身的成员是非法的。
举例:
struct Node { int data; struct Node next; };
这样就就像一个永远不会终止的程序。但下面的声明是合法的。
struct Node { int data; struct Node* next; };
这个声明只是将next换为了指针类型。现在next是一个指针,而不是一个结构。编译器在结构的长度确定之前就已经知道了指针的长度。所以是合法的。(因为指针在32位机器上是4个字节,因为指针就是地址)。
5.结构的初始化
结构的初始化方式和数组的初始化方式很相似。一个位于一对花括号内部、由逗号分隔的初始值列表可用于结构各个成员的初始化。这些值根据结构成员列表的顺序写出。如果初始列表的值不够,剩余的结构成员将使用缺省值进行初始化。
举例:
struct Node { int data; struct Point p; struct Node* next; }n1 = {10, {4,5}, NULL}; //结构体嵌套初始化 struct Node n2 = {20, {5, 6}, NULL};
二、结构、指针和成员
直接或间接用指针访问结构成员的操作是非常简单的,但是当他们应用于复杂的情形时就有可能引起混淆。
例如如下结构:
typedef struct student { int a; short b[2]; }stu; typedef struct school { int a; char b[3]; stu c; struct school* d; }sch;
实际结构图。
对结构体初始化并创建一个指针p指向这个结构体。
sch x = {10, "Hi", {5,{-1,25}, 0}; sch* p = &x;
结构体内存分配(面试重点)
实际在为结构体成员分配内存时,要满足结构体成员边界的对齐要求。
现在我们深入讨论一个问题:计算结构体的大小。
这也是一个特别热门的考点: 结构体内存对齐
1.如何计算结构体大小?
首先得掌握结构体的对齐规则三步曲:
第一个成员在与结构体变量偏移量为0的地址处。
其他成员变量要对齐到对齐数的整数倍的地址处。
而 对齐数 = 编译器默认的一个对齐数 与 该结构成员大小的较小值。
VS中默认的值为8
结构体总大小为每个成员变量的最大对齐数的整数倍。
注: 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
例子:
struct S1 { char a; int b; char c; }; printf("%d\n", sizeof(struct S1));
系统禁止编译器在结构的起始位置跳过几个字节来满足边界对齐要求,因此所有结构的起始位置必须是结构中边界最大的数据类型所要求的位置。因此,成员a必须被存储于一个能被4整除的地址。结构的下一个成员b是一个整型值,所以它必须跳过3个字节(灰色方块)才能存储b的四个字节。在b之后是c,c占了一个字节,但刚好是四的倍数所以c在8的位置。最后根据第三条规则,结构体总大小为每个成员变量的最大对齐数的整数倍,所以总大小为4的倍数。也就是12
2.为什么存在内存对齐?
大部分的参考资料都是如是说的:
1.平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问
举例:
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间
:让占用空间小的成员尽量集中在一起。
注意:当程序将创建几百个甚至几千个结构时,减少内存浪费的要求就比程序的可读性更为几波。在这种情况下,在声明中增加注释可能避免可读性方面的损失。
3.offsetof宏
sizeof操作符能够得出一个结构的整体长度,包括因边界对齐而跳过的那些字节。如果你必须确定某个成员的实际位置,应该考虑边界对齐因素,可以使用offsetof宏(定义于stddef.h)
offsetof(type, memeber);
type就是结构的类型,member就是结构成员名。表达式的结果返回一个size_t的值,表示这个指定成员开始存储的位置距离结构开始存储的位置偏移几个字节。
三、作为函数参数的结构
下面的 print1 和 print2 函数哪个好些?
struct S { int data[1000]; int num; }; struct S s = {{1,2,3,4}, 1000}; //结构体传参 void print1(struct S s) { printf("%d\n", s.num); } //结构体地址传参 void print2(struct S* ps) { printf("%d\n", ps->num); } int main() { print1(s); //传结构体 print2(&s); //传地址 return 0; }
答案是:首选print2函数。
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降
结论:
结构体传参的时候,要传结构体的地址。
四、位段
1.什么是位段
位段的声明和结构类似,但它的成员是一个或多个位的字段。位段的声明和任何普通的结构成员声明相同。但是有几个规则
1.位段的成员必须是 int、unsigned int 或signed int 。
2.位段的成员名后边有一个冒号和一个数字。
冒号后面的数字指定该位段所占用的位的数目。
struct A { int _a:2; int _b:5; int _c:10; int _d:30; };
2.位段的内存分配
位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
一个例子//空间是如何开辟的?
struct S { char a:3; char b:4; char c:5; char d:4; }; struct S s = {0}; s.a = 10; s.b = 12; s.c = 3; s.d = 4;
如图所示
五、枚举
枚举顾名思义就是一一列举。把可能的取值一一列举。
比如我们现实生活中:一周的星期一到星期日是有限的7天,可以一一列举。
enum Day//星期 { Mon, Tues, Wed, Thur, Fri, Sat, Sun };
注意:花括号里面的成员都是常量,叫做枚举常量。这些成员都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。
例如
enum Color//颜色 { RED=1, GREEN=2, BLUE=4 };
1.枚举的优点
我们可以使用 #define 定义常量,为什么非要使用枚举?
枚举的优点:
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较枚举有类型检查,更加严谨。
- 防止了命名污染(封装)
- 便于调试
- 使用方便,一次可以定义多个常量
六、联合
与结构相比,联合是另一种动物。联合虽然与结构相似,但它的行为方式和结构不同。
1.为什么要用联合体?
因为联合中的所有成员引用的是内存中的相同位置。当你想在不同的时刻把不同的东西存储于同一个位置时,就可以使用联合。
//联合类型的声明 union Un { int i; char c; }; union Un un; // 1.下面输出的结果是一样的吗? printf("%d\n", &(un.i)); printf("%d\n", &(un.c)); //2.下面输出的结果是什么? un.i = 0x11223344; un.c = 0x55; printf("%x\n", un.i);
对于问题1:下面输出的结果是一样的吗?
答案:一样,原因是因为i和c用的是同一块地址。可以说用的是相同的位(bit)。
对于问题2:下面输出的结果是什么?
答案:输出的结果是0x11223355。为什么是55呢?
首先在内存空间中为i开辟了一块四个字节的地址。接着赋值了0x1122334,接着un.c = 0x55;的操作根据小端字节序覆盖了最低位的一个字节。所以答案为0x11223355
2.联合大小的计算
1.联合的大小至少是最大成员的大小。
2.当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
union Un1 { char c[5]; int i; }; union Un2 { short c[7]; int i; }; //下面输出的结果是什么? printf("%d\n", sizeof(union Un1));//8 printf("%d\n", sizeof(union Un2));//16