🌱 1.结构体
🍀🍀1.1结构体概述
🌼🌼🌼1.1.1结构体概念
结构体(struct)是由一系列具有相同类型或不同类型的数据构成的数据集合,也叫结构,它就将不同类型的数据存放在一起,作为一个整体进行处理。
🌼🌼🌼1.1.2 结构体的声明与使用
🌼结构体的声明:
struct Book { char name[20]; char author[20]; int price; };
这个声明描述了一个由两个字符数组和一个int变量组成的结构体。
它将这些变量封装成一个整体,代表了一本书(含有书名,作者名,价格)。
但是注意,它并没有创建一个实际的数据对象,而是描述了一个组成这类对象的元素。
因此,我们有时候也将结构体声明叫做模板,因为它勾勒出数据该如何存储,并没有实例化数据对象。
🌼结构体的使用(一共3种创建方法)
用结构体创建全局变量
struct Book { char name[20]; char author[20]; int price; }b1,b2; struct Book b1;
这里创建的b1,b2,b3 是完全等价的,都是全局变量。
- 用结构体创建局部变量
struct Book { char name[20]; char author[20]; int price; }; int main() { struct Book b4; return 0; }
在main函数中创建的结构体变量就是局部变量
🌼结构体变量的定义和初始化
🌱1. 初始化:定义变量的同时赋初值。
🌱2. 结构体的初始化要使用大括号。
struct Stu //类型声明 { char name[15];//名字 int age; //年龄 }; struct Stu s = {"zhangsan", 20};//初始化
🌱3. 结构体嵌套初始化:
在结构体中又包含了一个结构体
struct Point { int x; int y; }p1 = { 1,2 }, p2 = {3,4}; struct Point p3 = { 5,6 }; struct Node { int data; struct Point p; struct Node* next; char name[20]; }n1 = {10, {4,5}, NULL}; struct Node n2 = {20, {5, 6}, NULL, "zhangsan"};
🌱4. 对于嵌套结构体的访问
struct Point { int x; int y; }p1 = { 1,2 }, p2 = { 3,4 }; struct Point p3 = { 5,6 }; struct S { int data; struct Point p; char name[20]; }n1 = { 10, {4,5}, }; struct S n2 = { 20, {5, 6}, "zhangsan" }; int main() { struct Point p4 = {1,2}; struct S s = { 20,{7,8} }; printf("%d", s.data); printf("%d %d", s.p.x, s.p.y); return 0; }
🌱5. 对于结构体变量中的整型数组如何循环打印:
struct S { int a[10]; }n1 = { {1,2,3} }; int main() { int i = 0; struct S s = { {7,8,9} }; for (i = 0; i < 10; i++) { printf("%d ", s.a[i]); } return 0; }
🌼🌼🌼1.1.3匿名结构体的声明及使用
struct { int a; char c; double b; }s1,s2;
匿名结构体可以无结构体的名字,要创建变量的时候只能在大括号的后面直接创建s1,s2
。
匿名结构体只能使用一次。
struct { int a; char c; double b; }s1,s2;//第一个 struct { int a; char c; double b; }*ps;//第二个 int main() { ps = &s1; return 0; }
以上创建了两个匿名的结构体类型,但编译器会认为他们是不同的,因此第二个结构体创建的匿名结构体指针无法指向第一个匿名结构体。
非法赋值使编译器报错。
🌼🌼🌼1.1.4 结构体的自引用
链表就如同车链子一样,head指向第一个元素:第一个元素又指向第二个元素;……,直到最后一个元素,该元素不再指向其它元素,它称为“表尾”,而链表的实现就需要用到结构体的自引用。
struct Node { int data; struct Node* n; };
上述创建了一个链表,data是数据域,n为指针域。
结构体自引用:能够找到通过地址找到自己同类型的下一个结点。
🌼🌼练习一:
struct Node { int data; struct Node next; }; //可行否? 如果可以,那sizeof(struct Node)是多少?
这样不行,会造成死循环,因为无法确定结构体的大小
🌼🌼练习二:
typedef struct { int data; Node* next; }Node; //这样写代码,可行否?
分析如下:定义了一个匿名结构体,并且用typedef
将这个匿名结构体重命名为Node
,但Node的产生必须要先在内存中创建一个int型,和Node指针大小的内存空间,但是此时还未重命名,所有是错误的
正确写法:
typedef struct Node { int data; struct Node* next; }Node;
🍀🍀 1.2结构体对齐及其大小计算
🌼🌼🌼1.2.1偏移量
定义:把存储单元的实际地址与其所在段的段地址之间的距离称为段内偏移,也称为"有效地址或偏移量"。
先简单举个例子来说:
创建的s1 和s2 结构体变量的内存大小是多少呢:
struct S1 { char c1; int i; char c2; }; struct S2 { char c1; char c2; int i; }; int main() { struct S1 s1 = { 'x',100,'y'}; struct S2 s2 = { 'x','y'100}; printf("%d", sizeof(struct S1)); //12 printf("%d", sizeof(struct S2)); //8 return 0; }
结果发现两者的内存大小不同,原因是因为涉及了内存对齐。
🌱内存对齐的规则
结构体第一个成员永远放在结构体变量偏移量为0的地址处。
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8
结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
🌱为什么存在内存对齐?
平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特
定类型的数据,否则抛出硬件异常。
性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访
问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
🌼🌼🌼1.2.2结构体大小计算
🌱对于S1来说:
struct S1 { char c1; int i; char c2; };
3个变量最大的占4个字节,而VS的默认对齐数是8,因此该结构体的默认对齐数是4
🌱对于S1来说:
struct S2 { char c1; char c2; int i; };
struct S3 { double d; char c; int i; }; printf("%d\n", sizeof(struct S3));
因此一共16个字节
🌼🌼🌼1.2.3修改默认对齐数
我们使用#pragma
修改默认对齐数
#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));
分析:
此时因为默认对齐数被设置成1,最小值,因此所有的对齐数都为1,相当于取消内存对齐,无空间浪费。
🌱百度笔试题:
写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明
考察: offsetof 宏的实现
在 <stddef.h>
中定义了个 offsetof(s,m)
宏,这个宏用来取得结构体中元素的偏移量很方便,下面是此宏的具体定义:
#define offsetof(s, m) (size_t)&(((s *)0)->m)
offsetof(s, m) 其中,s 是结构体名,m 是它的一个成员。s 和 m 同是宏 offsetof() 的形参,这个宏返回的是结构体 s 的成员 m 在结构体中的偏移地址。
(s *)0 : 这里的用法实际上是欺骗了编译器,使编译器认为 “0” 就是一个指向 s 结构体的指针(地址),即 s 结构体就是位于 0x0 这个地址处。
(s *)0-> m :指向这个结构体的 m 元素。
&((s *)0)->m : 表示 m 元素的地址。这里,如上面所说,因为编译器认为结构体 s 被认为是处于 0x0 地址处,所以 m 的地址自然的就是 m 在 s 中的偏移地址了。
最后将这个偏移值转化为 size_t 类型。
#include<stdio.h> #include<stddef.h> #define offsetof(s, m) (size_t)&(((s *)0)->m) struct S { char c1; int a; char c2; }; int main() { printf("%u\n", offsetof(struct S, c1)); //0 printf("%u\n", offsetof(struct S, a)); //4 printf("%u\n", offsetof(struct S, c2)); //8 return 0; }
🌼🌼🌼1.2.4结构体传参
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; }
上面的 print1 和 print2 函数哪个好些?
答案是:首选print2函数。
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
结论:
结构体传参的时候,要传结构体的地址。
🍀🍀 1.3结构体与位段
🌼🌼🌼1.3.1位段
位段的声明和结构是类似的,有两个不同:
位段的成员必须是 int、unsigned int 或signed int 。
位段的成员名后边有一个冒号和一个数字。
位段可以节省空间。
int _a:2;表示_a只需要2个比特位
比如:下方的A就是一个位段类型。那位段A的大小是多少?
struct A { int _a:2; int _b:5; int _c:10; int _d:30; }; printf("%d\n", sizeof(struct A));
🌱分析:
位段一次开辟一个整型(4字节==32比特位)
_a + _b + _c一共用了17个比特位,之后的_c不够用了,于是又开辟了4个字节
因此一共8个字节
🌱总结
位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
🌼🌼🌼1.3.2位段实现结构体
🌱练习:
以下的空间是如何开辟的?
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;
分析:
- 由于是char位段,因此是一个字节一个字节开辟的
🌱位段的跨平台问题
int 位段被当成有符号数还是无符号数是不确定的。
位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
器会出问题。
位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
舍弃剩余的位还是利用,这是不确定的。
🌱总结:
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。