1.结构体
1.1 结构体类型的声明
结构是某些值的集合,这些值被称为成员变量,结构的成员类型没有特殊的规定
struct tag//tag 是结构体标签,可以理解为所创建的结构体名称 { member - list;//成员列表,所有类型变量的总称 }variable-list;//有结构体名称所创造的变量,即结构体变量
描述一位大学生
struct student { char name[20];//姓名 int age;//年龄 char sex[10];//性别 char number[10];//学号 };
特殊声明–无结构体标签
匿名结构体类型
struct { int i; char c; double d; }a;
这种结构体的生命只能使用一次,即在创建完结构体变量a之后就无法再次使用。如果对此变量进行取地址操作同样是非法的。
1.2 结构体的自引用
对于结构体本身包含一个类型为该结构体本身的成员,这样的想法是否可行呢?
答案是:当然是可以的啦
观察下列代码,自引用是否正确
struct student { char name[20]; int age; char sex[10]; struct student next; }; int main() { printf("%d", sizeof(struct student)); return 0; }
这里出现的问题就是结构体中的next没有明确的数目,不清楚到底有多少个学生,陷入无限的死循环中去。
修改之后
struct student { char name[20]; int age; char sex[10]; struct student* next; };
修改之后,只是将指向下一个学生的地址的指针放在结构体中,可以通过指针进而访问下一个学生的信息
结构体自引用还会出现另一个问题–先有鸡还是先有蛋
typedef struct student { char name[20]; int age; char sex[10]; stu* next; }sut;
这里说不清楚到底是先有 结构体变量stu 还是先有 成员stu
为了避免这种问题的出现,可以进行以下修改
typedef struct student { char name[20]; int age; char sex[10]; struct stu* next; }sut;
1.3 结构体变量的定义和初始化
结构体也是一种类型,是类型就需要定义变量,那么该如何去定义变量呢?
类型就像是图纸,通过类型去创建变量,就像是拿着图纸去造房子
struct student//结构体类型声明 { char name[20]; int age; char sex[10]; }stu;//定义结构体变量stu
初始化就是在创建变量的同时赋以数值
stu = { "zhangsan",20,"nan" };
1.4 结构体内存对齐
创建变量之后就会遇到另一个问题:结构体变量的大小该如何计算?
这里就需要引入:结构体内存对齐的概念
首先介绍结构体的对齐规则
1.第一个成员在与结构体变量偏移量为0的地址处 2.其余成员变量要对齐到对齐数的整数倍的地址处 对齐数==编译器默认的一个对齐数 与 该成员大小的较小值 (vs默认对齐数的值是8) 3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍 4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处, 结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
内存对齐存在的原因
1.平台原因
不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则就会出现硬件异常。
2.性能问题:
数据结构应该尽可能地在自然边界上对齐
为了访问未对齐的内存,处理器需要做两次内存访问;对于对齐的内存仅需要一次访问。
总结
结构体的内存对齐是拿空间换取时间的做法
例如
struct student { char c; int i; }s;
在内存未对齐的情况下,结构体变量s访问整型变量i时需要两次;
而在内存对齐的情况下,只需要跳过字符变量c,直接一次访问变量i
尝试算一算结构体的大小
练习一
struct s1 { char c1; int i; char c2; }s1; int main() { printf("%d", sizeof(s)); return 0; }
第一个成员char c1,在偏移量为0的地址处; 第二个成员 int i,整型自身是4个字节,vs默认对齐数是8个字节, 所以int i的对齐数是4个字节,对齐在偏移量为4的地址处; 第三个成员char c2,字符大小是4个字节,vs默认对齐数是8个字节, 所以char c2对齐数是1个字节,对齐到地址为8的地址处 此时结构体大小是9个字节 因为结构体总大小为最大最对齐数的大小,三个成员最大的对齐数是4, 所以结构体总大小是12
练习二–嵌套结构体类型
struct s1 { char c1; int i; char c2; }; struct s2 { char c3; struct s1 s1; }s2; int main() { printf("%d", sizeof(s2)); return 0; }
第一个成员char c3在偏移量为0的地址处 第二个成员是结构体struct s1 s1, 嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体struct s1 s1 对齐在偏移量在4的地址处, 此时结构体大小是15 结构体总大小是最大对齐数的整数倍,最大对齐数是4,所以大小为16
修改默认对齐数
通过 #pragma预处理指令,改变默认对齐数
#include<stdio.h> #pragma pack(1)//将默认对齐数修改为1 struct s1 { char c1; int i; char c2; }; #pragma pack()//恢复默认对齐数 //修改默认对齐数只限定于预指令之间的范围 int main() { printf("%d", sizeof(struct s1)); return 0; }
将默认对齐数改为1,可以理解为此时没有结构体内存对齐,所以
结构体大小就是各成员大小总和
总结 结构在对齐方式不合适时,自己可以修改默认对齐数
1.5 结构体传参
观察下面的代码
#include<stdio.h> struct student { char c; int i; }; struct student s = { 'm',20 }; void print1(struct student s) { printf("%d\n", s.i); } void print2(struct student* ps) { printf("%d\n", ps->i); } int main() { print1(s);//传结构体 print2(&s);//传地址 return 0; }
对于函数 print1和 print2哪个更好些?
答案是:首选 print2函数
函数传参时,参数是需要压栈,有时间和空间上的系统开销 如果传递结构体时,结构体过大,参数压栈的系统开销比较大,会导致性能的下降
所以结构体传参时,首选传结构体的地址
1.6 结构体实现位段
位段与结构体都可以减少内存空间的浪费
位段的声明和结构体相似,但也存在两个不同点
1.位段的成员必须是int,unsigned int ,signed int或者char 2.位段的成员名后边有一个冒号和一个数字
例如
struct A { int a : 1; int b : 2; int c : 3; int d : 4; }; int main() { printf("%d", sizeof(struct A)); return 0; }
A是一个位段类型,既然是个数据类型,那么大小是非常重要的,所以位段A的大小是多少呢?
运行之后是4,对于这个结果大家可能会很困惑,为什么呢?
这里就需要先介绍位段的内存分配,才能解决大家的疑问
位段的内存分配
1.位段的成员可以是`int,unsigned int,signed int`或者是`char`(整形家族)类型 2.位段的空间是按照需要以4个字节(`int`)或者1个字节(`char`)的方式来开辟 3.位段涉及很多不确定因素,位段是不跨平台的。
struct M { char a : 3; char b : 4; char c : 5; char d : 4; }; int main() { struct M m = { 0 }; m.a = 5; m.b = 10; m.c = 15; m.d = 4; printf("%d\n", sizeof(m)); return 0; }
若是一次所开辟的空间没有使用完,且不能够存下另一个变量
则只能按照类型重新开辟空间使用
之所以位段大小是3个字节,是因为首先是以1个字节`char`开8个byte, 存下`char a`和`char b`,然后有开辟1个字节存放`char c`, 最后开辟1个字节用于存放`char d`. 1个char=8个byte char a : 3; char b : 4; 1个char=8个byte char c : 5; 1个char=8个byte char d : 4;
位段存在跨平台问题
1. `int`类型的位段不清楚是被当成 `signed`类型还是 `unsigned`类型 2. 位段中最大位的数目不能确定 3. 位段中的成员在内存中没有定义从左向右还是从右向左分配 4.