【C语言】自定义类型:结构体
前言
在C语言中有许多内置的数据类型,如int、char、short等,我们可以直接拿来用。但是在有些情况下,比如描述一个人,这个人的基本数据有名字,性别和年龄等,内置的类型并不能满足我们的使用需求,那我们就需要自定义类型。这篇文章会简单介绍一下自定义类型中的结构体。
一、结构体是什么
简单来说,结构体就是一些值的集合,这些值被称为成员变量,成员变量可以是不同类型
结构体和数组相似,数组是一些类型相同的元素的集合
二、结构体的声明
struct tag
{
member-list;
}variable-list; //分号不可以丢
struct 是关键字,tag 是我们自定义的名字。struct tag 连在一起是类型名,使用时不要把 struct 丢掉了 ,比如创建一个结构体变量 a :
struct tag a
member - list 是成员变量,它们可以是不同类型,如
int a
char b
等variable-list 是结构体变量名,相当于
int a
中的 a ,我们可以在声明结构体时创建结构体变量,也可以不创建,使用时再创建
这里我们描述一个学生:
struct Stu
{
char name[20];//姓名
int age;//年龄
char sex[5];//性别
char id[20];//学号
};//分号不可以丢
三、特殊的声明
结构体的声明,可以不完全声明:
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
如上代码,不完全声明的结构没有名字,因此当我们创建变量时,只能在声明结构时创建
这两个结构虽然成员相同,但在编译器眼中,就是两个不同的结构体类型
因此以下代码是非法的
p = &x;
四、结构体的自引用
说到结构体的自引用,就不得不提一下数据结构中的链表了
那什么是链表呢?
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的
如图,共有四个节点,每个节点都是一个结构体
每个节点在存储数据的同时还能指向下一个节点,形成了一个链式结构
那我们为了完成上述功能,声明一个结构体
struct Node
{
int data;
struct Node next;
};
这样写看似没什么问题,但是当我们计算结构体大小时就会发现问题
这个结构体的大小是 data 的大小加上 next 的大小,data 大小为 4 个字节,next 的大小呢?next 的大小又是 data 大小加上 next 大小…如此循环,这个结构体的大小就无法计算。
为了解决这个问题,我们将下节点的地址放到上一个节点,这样就可以通过上个节点找到下个节点,而指针的大小是固定的,结构体的大小也就可以计算出来了
struct Node
{
int data;
struct Node* next;
};
五、结构体变量的定义和初始化
定义
我们可以在声明结构体的同时进行结构体变量定义,也可以先声明,等用的时候再定义结构体变量
struct Point
{
int x;
int y;
}p1;//p1为全局变量
int main()
{
struct Point p2;//p2为局部变量
return 0;
}
初始化
结构体的初始化要用 {} ,成员之间用逗号隔开
struct Point
{
int x;
int y;
}p1 = { 1, 2 };
struct Student
{
char name[20];
int age;
}s1 = { "张三", 18 };
int main()
{
struct Point p2 = { 2,3 };
struct Student s2 = { "李四", 19 };
return 0;
}
同时,结构体支持嵌套初始化
struct Point
{
int x;
int y;
}p1 = { 1, 2 };
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = { 10, {4,5}, NULL };
六、结构体成员的访问
结构体变量访问成员
访问形式:变量名.成员名
struct Stu
{
char name[20];
int age;
};
int main()
{
struct Stu s = { "张三",18 };
printf("%s\n", s.name);
printf("%d\n", s.age);
}
结构体指针变量访问成员
结构体指针变量想访问结构体成员需要先解引用再正常进行访问,或者直接 结构体指针变量->成员名
struct Stu
{
char name[20];
int age;
};
int main()
{
struct Stu s = { "张三",18 };
struct Stu* p = &s;
printf("%s\n", (*p).name);
printf("%d\n", p->age);
}
七、结构体内存对齐
1.引出
结构体的基本使用我们已经了解,下面我们来探讨如何计算结构体的大小
结构体的大小并不是单纯地把成员的大小相加,我们来看下面的代码
struct s1
{
char c1;
char c2;
int i;
};
struct s2
{
char c1;
int i;
char c2;
};
int main()
{
printf("s1的大小:%d\n", sizeof(struct s1));
printf("s2的大小:%d\n", sizeof(struct s2));
return 0;
}
s1 和 s2 这两个结构的成员相同,只是成员的排列顺序不同,那么它们的大小如何?
可以看到,s1 和 s2 的大小并不相同,这就涉及到结构体的内存对齐了
在内存对齐之前,我们先引入一个宏 offsetof,这个宏是干什么的?
offsetof 可以计算结构体成员相较于起始位置的偏移量
参数:结构体类型,结构体成员名
头文件:<stddef.h>
下面我们计算一下 s2 的成员的偏移量
struct s1
{
char c1;
char c2;
int i;
};
struct s2
{
char c1;
int i;
char c2;
};
int main()
{
//printf("s1的大小:%d\n", sizeof(struct s1));
//printf("s2的大小:%d\n", sizeof(struct s2));
printf("%d\n", offsetof(struct s2, c1));
printf("%d\n", offsetof(struct s2, i));
printf("%d\n", offsetof(struct s2, c2));
return 0;
}
运行结果
什么意思呢?我们画图表示一下,假设内存中的存储如下:
c1 是起始位置,所以它的偏移量是 0
而 i 的偏移量是 4 ,这表示它跳过了 3 个字节,直接存到了 4 的位置
c2 的偏移量是 8 ,存到了 8 的位置
此时三个数据都存了起来,s2 大小是 9 个字节,但是上面我们通过 sizeof 计算 s2 的大小是 12,这说明 s2 在 9 字节的基础上,又额外占用了 3 字节,这样一算直接浪费了 6 个字节
这就是内存对齐,那么内存对齐是怎么实现的?又为什么要浪费空间进行内存对齐?
2.如何对齐
对齐规则
1.第一个成员存放在与结构体变量偏移量为 0 的地址处
2.其他成员存放在 对齐数 的整数倍的地址处
对齐数 = 编译器默认对齐数 与 该成员大小的 较小值
例如:vs默认对齐数为 8 ,一个 int 成员大小为 4,那么对齐数就是 43.结构体总大小为最大对齐数(每个成员都有对齐数)的整数倍
4.如果嵌套了结构体,那么嵌套的结构体存放在自己最大对齐数的整数倍处,结构体的总大小为所有最大对齐数(包括嵌套结构体的对齐数)的整数倍
详细步骤
1.第一个成员存放在与结构体变量偏移量为 0 的地址处
2.其他成员存放在 对齐数 的整数倍的地址处
第二个成员是 i ,自身大小为 4 ,默认对齐数为 8 ,取较小值 4 为对齐数,所以 i 存放在 4 的整数倍的地址处
第三个成员 c2,自身大小为 1,默认对齐数为 8 ,取较小值 1 为对齐数,所以 c2 存放在 1 的整数倍的地址处
3.结构体总大小为最大对齐数(每个成员都有对齐数)的整数倍
此时的结构体总大小为 9 ,而所有对齐数为 1 ,4 ,1 ,最大对齐数为 4,所以总大小为 4 的倍数 12
4.如果嵌套了结构体,那么嵌套的结构体存放在自己最大对齐数的整数倍处,结构体的总大小为所有最大对齐数(包括嵌套结构体的对齐数)的整数倍
在此声明结构体 s3,s3 嵌套了 s2
struct s2
{
char c1;
int i;
char c2;
};
struct s3
{
char c;
struct s2 s2;
};
成员 c 依然存放在偏移量为 0 的地址处,而结构体 s2 存放在自己最大对齐数,即 4 的整数倍处
此时结构体总大小为 16,符合条件:结构体的总大小为所有最大对齐数(包括嵌套结构体的对齐数)的整数倍
所以最终大小为 16
3.为什么对齐
我们已经学习了如何进行内存对齐,可以发现,内存对齐是存在空间浪费的,那为什么还要进行内存对齐呢?
1.平台原因
并不是所有硬件都可以访问任意地址的任意类型数据的,某些硬件平台只能访问特定地址的特定类型数据
2.性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
未对齐
为了访问 i ,需要作两次访问
对齐
访问 i ,只需要访问一次就可以
总结:结构体的内存对齐是拿空间来换取时间的做法。
八 、结构体传参
我们知道函数传参时,如果是传值,那么会形成形参,会有时间和空间上的消耗
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的
下降。
struct S
{
int data[1000];
int num;
};
//传值
void print1(struct S s)
{
printf("%d\n", s.num);
}
//传址
void print2(struct S* s)
{
printf("%d\n", s->num);
}
int main()
{
struct S s = { {1,2,3},1000 };
//传值
print1(s);
//传址
print2(&s);
return 0;
}
上述代码,打印函数优先选择 print2 ,具有更高的效率
总结:结构体传参的时候,要传结构体的地址。
九、位段
位段的出现就是为了节省空间
1.什么是位段
位段与结构类似,但又不同
位段的成员类型必须是 int、unsigned int 或signed int
位段的成员名后边有一个冒号和一个数字
例:
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
表示 _a 大小为 2 个比特位,_b 的大小为 5 比特位,以此类推
那我们就可以算出 A 的大小为 47 比特位,也就是 6 字节
那到底是不是呢?我们来验证一下
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
printf("%d", sizeof(struct A));
return 0;
}
结果:
结果与我们预料的并不一样,这涉及到位段的内存分配
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;
};
我们推测结构体类型 S 的内存分配可能是这样的:
先开辟一个字节的空间,分配存放 a 和 b 的空间,C语言标准并未规定位段是从左向右分配还是从右向左分配,我们这里假设从右向左分配
分配完 a、b之后,还剩下一个比特位,给 c 分配肯定是不够了,只能再开辟一字节空间,那么剩下的一个比特位是接着用还是浪费掉不用了?我们猜测是浪费了
剩下 3 个比特位,不够给 d,只能再开一字节,浪费这 3 个比特位
至此,空间分配完毕
如果没浪费空间,那结构体类型就是 2 个字节
如果浪费了,那结构体类型就是 3 字节
验证:
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
printf("%d\n", sizeof(struct S));
}
结果是3,说明浪费了空间
我们假设位段是从右向左分配空间,现在来验证
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
printf("%d\n", sizeof(struct S));
}
10的二进制位是 1010,由于 a 只有三个比特位的空间,所以只能存下 010;
12的二进制位是 1100,b 有四个空间,存下1100
以此类推,c 存 00011,d 存 0100
换算为十六进制:
大概在内存中存的就是这么个数
验证:
结果一致,至少说明在vs中,位段分配空间就是从右向左分配
3.位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
器会出问题。- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
舍弃剩余的位还是利用,这是不确定的。
总结:跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
4.位段的应用
位段在网络中应用很多
我们日常发消息时,不是单纯地把要发的消息发到网上,而是封装成一个数据包
数据表包中的信息可以把消息准确发送给别人
而位段可以减小包体体积,避免数据包过大,造成网络拥堵
结束,再见:D