一 情景介绍
假如学校新招有3个学生,学校学生系统需要录入他们的信息,那么我们是不是使用下面的方法录入呢:
//录入第一个学生的信息
int a_name;
int a_age;
int a_number;
scanf("%d%d%d",&a_name,&a_age,&a_number);
//录入第二个学生的信息
int b_name;
int b_age;
int b_number;
scanf("%d%d%d",&b_name,&b_age,&b_number);
//录入第三个学生的信息
int c_name;
int c_age;
int c_number;
scanf("%d%d%d",&c_name,&c_age,&c_number);
很明显,这样的确可以在学生系统中对他们的身份信息进行录用。
路人A:你这个代码怎么这么多一样的,是不是偷懒了?
程序员:你放屁,这可是大工程,我一个一个输入可累死我了。
这时程序员就想对代码进行简化,既让外行人看这些代码显得迷糊,又方便了对学生信息的录入。
这时程序员脑洞大开:
将学生的信息类比一个存放数据的仓库,而这个仓库里面又分为3个小仓库,第一个小仓库存放的是姓名的信息,第二个小仓库存放的是年龄的信息,第三个小仓库存放的是学号的信息,我们存入学生A的数据时,可以将数据命名为A,那么将数据A的姓名信息存放在第一个仓库,年龄信息存放在第二个仓库,学号信息存放在第三个仓库,而当需要取出学生A的某个信息时,只需要去学生A的仓库找就行了。
二 它是啥?(结构的初始化)
没有声音看不懂上面的文字?那就看代码吧:
struct stu {
char name[10];
int age;
int number;
};
stu相当于存放学生的信息仓库,char name[10]相当于仓库中存放姓名信息的小仓库,int age相当于仓库中存放年龄信息的小仓库,int number相当于仓库中存放学号信息的小仓库。其实这种方式就是通过“结构体”进行存放变量,结构体里面存放的变量称作“结构体变量”。
很好,很有精神,现在你已经知道这仓库是怎么声明的了,下面可以对仓库塞数据了:
那么问题来了,怎么将信息塞进去?想访问结构体的成员,我们可以通过 . 或者 -> 操作符。
那么我们先将A同学的信息进行录入:
//按照结构体成员的顺序初始化
struct stu A={"小明",18,1234 };
//按照指定的顺序初始化
struct stu A = { .age = 18,.name = "小红",.number=2345 };
上面的代码创建了一个名为A的结构体,并对结构体成员变量进行赋值,这种操作称作初始化 。
也可以理解为创建这种类型的仓库,毕竟仓库都还没有创建,你往里面塞信息找得到吗?
欸,可能还真找得到。
三 你的名字?(匿名结构体)
下面的结构体没有进行结构体类型重命名,这种结构体称作匿名结构体,基本上只能使用一次。
往匿名结构体内的int age成员变量存放数据后,另外两个成员变量就无法使用了。
struct {
char name[10];
int age;
char sex[5];
}x;//x是结构体的全局变量
struct {
int a;
char b;
float c;
}*p;//指针p也是全局变量
为什么基本上只能使用一次呢,我们可以这样理解,匿名结构体是重命名结构体的蓝图,你将数据放入 匿名结构体,就破坏了原本的蓝图,那么这个蓝图就失去了蓝图的权威性,没人认可它,你就算后续再往里面存放数据,数据也不想进入这个结构体。
四 我用我自己?(结构的自引用)
在结构体中,不能包含一个类型为该结构本身的成员。
struct node {
int data;
//struct node next;//这样是错误的自引用,结构体中包含结构体,结构体的大小就会变的无穷大
struct node* next;
//正确的引用方法:可包含同类型结构体的指针,给它个地址自己找去,那样就不需要担心结构体的大小
};
五 它多大?(结构体的大小,默认对齐数)
问:结构体的内存大小是多少?
答:如:int 占4个字节,char占一个字节,算一下有多少个成员变量,将它们的大小加起来完事。
错,大错特错,怎么肯直接加起来就行了?它要内存对齐的啊。
那么啥是内存对齐:
1.结构体第一个成员变量需要对齐到结构体变量起始位置偏移量为0的地址处。
2.其他成员变量要对齐到对齐数的整数倍的地址处。
ps: vs中默认对齐数为8,Linux中对齐数就是变量成员自身大小。
ps2: 一个成员变量的对齐数与默认对齐数相比较,哪个小哪个就是它的对齐数
3.结构体的总大小为最大对齐数的整数倍。
4.如果结构体中嵌套了其他结构体,那么这个嵌套结构体成员变量的对齐到自己的成员变量中对打对齐数的整数倍处。
按照对齐规则,我们可以对结构体的大小进行推测:
struct stu {
int a; //4-8-4
char b; //1-8-1
int c; //4-8-4
};
那么结构体的大小真的是12吗?不多说,直接去VS验证
可以看到,结果真是12,那么对于嵌套结构体的情况是不是也可以自己推算呢?
struct stu {
char a; //1-8-1
char b; //1-8-1
int c; //4-8-4
};//大小为8
struct stu2 {
int a; //4-8-4
char b; //1-8-1
struct stu s;//4-8-4
};
根据在内存中成员变量的内存分配,结构体在内存中是存在内存浪费的,因此我们设计结构体时要既要满足对齐,又要节省空间。
如果你说,你想修改默认对齐数,那当然是可以的,毕竟代码可是很人性化的:
#pragme pack(1)//将默认对齐数设置为1
#pragme pack()//取消设置的对齐数,还原为默认
六 我?传参?(结构体传参)
毫无疑问,结构体也能作为参数传递给函数
就像下面的代码:
struct stu {
int data;
int num;
};
void printf1(struct stu s) {
printf("%d %d\n", s.data, s.num);
}
void printf2(struct stu* s) {
printf("%d %d", s->data, s->num);
}
int main() {
struct stu s1 = { s1.data = 2000,s1.num = 100 };
printf1(s1);//直接传结构体
printf2(&s1);//传结构体地址
//首选传递地址,函数传参时,是需要压栈的,
//如果传递的参数是整个结构体,那么结构体过大时,
//参数压栈系统会开销过大
}
七 结构体实现位段
位段的声明与结构体的声明相似度高达百分之九十九。
但是有两个不同:
位段的成员必须是:int, char, signed int ,unsigned int
位段的成员名后有一个冒号和一个数字(数字表示它所占据的比特位)
struct stu { char a : 2; //0010 char b : 3; //0011 char c : 5;//00101 char d : 4;//0100 }; //一个字节有8个比特位,那么上述位段的内存: // 从左向右分配还是从右到左分配标准尚未定义,假如我们从右往左分配 // 00000000 先分配一个字节 // 00000010 a // 00001110 b // 00001110 00000000 剩下的空间不足,再申请一个字节 // 00001110 00000101 c // 00001110 00000101 剩下空间不足,再申请一个字节 // 00001110 00000101 00000100 int main() { struct stu s = { 0 }; s.a = 2; s.b = 3; s.c = 5; s.d = 4; return 0;//0e 05 04 }
对于位段,其实是有很多问题的:
在跨平台时:
int不确定被当成是有符号数还是无符号数
位段中最大位的数目不能确定
位段成员在内存中分配的方向不确定
当第一个位段成员剩余的位置无法容纳第二个位段成员时,剩余的空间是否利用不确定