一 结构体类型
1.1 结构体类型的声明
结构体的作用是将不同类型的数据整合在一起
结构体的结构包括:关键字struct ,我们设定的结构体名student, 用来描述属性的结构体成员。
struct student {//struct 是关键字
//student是结构体类型名
//结构体类型名不同于结构体变量名
char name[20];//结构体成员
int age; //结构体成员
};
1.2 结构体变量的创建与初始化
//创建一个结构体变量,分为局部变量与全局变量,在函数之外的是全局,之内为局部
变量的创建:
struct student {
char name[20];
int age;
}stu1; //stu1是全局变量
struct student stu3;//stu3也是全局变量
int main() {
struct student stu2;//stu2是局部变量
return 0;
}
结构体全局变量有两种创建方式,局部变量只有一种创建方式,如代码所示!
变量的初始化:
结构体变量的初始化形式:
//第一种需要对应结构体类型赋值
struct student s1 = {"张三",18};
//第二种:
struct student s2 = { .age = 20,.name = "wangwu" };
//成员单独赋值
s2.name = "zhangsi";
2 结构体的特殊声明
省略掉结构体标签即类型名的声明
2.1 匿名结构体如何使用
struct
{
int a;
char b;
double c;
}s1 = {6,'a',6.0};//可以
int main(){
//struct s = { 4,'a',3.0 };//这种使用方式是错误的
// s1 = { 4,'a',3.0 }; //也不行
//s1.a = 4; //可以
printf("%d\n", s1.a);
return 0;
}
2.2 无标签的结构体不能与无标签的结构体进行运算,自身定义的除外
struct
{
int a;
char b;
double c;
}s1 = {6,'a',6.0},*p1;
struct
{
int a;
char b;
double c;
}*p2;
int main(){
p1 = &s1; //可以使用
printf("%d", (*p1).a);
//*p2 = &s1;//错误代码
//因为系统不能识别两个无符号的结构体是不是同一类型
return 0 ;
}
2.3 可以再为无标签结构体设置名字
typedef struct
{
int a;
char b;
double c;
}stu; //将名字设成stu
3 结构体的自引用
struct student {
char name[10];
struct student* Node; //链表的实现,可以指向下一个结构体类型
}; //不可以用结构体名连接下一个结构体变量,
//否则拿单个结构体中的数据时,
// 会拿出一连串的结构体类型变量
4 访问结构体变量中的成员
访问分直接访问与间接访问两种:
struct student {
char name[20];
int age;
}stu1;
int main(){
stu1 = {"张三",19};
//直接访问操作符. 操作数为结构体变量名与结构体成员名
printf("%s\n", stu1.name);
printf("%d\n", stu1.age);
//间接访问操作符 ->,操作数为指针与结构体成员名
struct student* p = &stu1;
printf("%s\n", p->name);
printf("%d\n", p->age);
//也可以访问指针再用直接访问操作符,
//直接访问操作符比解引用的操作符优先级高,故需要+()
printf("%s\n", (*p).name);
printf("%d\n", (*p).age);
return 0 ;
}
5 typedef对结构体的操作
//在声明结构体后,当我们创建结构体变量或结构体类型指针时,往往名字过长,导致代码可读性变差
struct Student {
char name[10];
int age;
};
int main() {
//当每次创建变量与指针时,都必须加上struct
struct Student s1 = { 0 };
struct Student* p1 = NULL;
return 0;
}
我们可以用**typedef**来修改结构体类型名与关键字struct ,组合成一个标识符
也可以来修改结构体指针类型名为一个标识符
#include<stdio.h>
//typedef 修改名称的两种形式:
//第一种:在结构体的声明中重设名称(结构体名与struct的组合)或者(结构体指针类型)
typedef struct Student {
char name[10];
int age;
} Student,* PStudent;
// 重设结构体名与struct的组合 为:Student
// 重设结构体指针类型 : PStudent //省去了*
// 第二种:
// 重设结构体名与struct的组合 为:Student
typedef struct Student Student ;
// 重设结构体指针类型 : PStudent // 这样省去了*
typedef struct Student* PStudent
// 第一种与第二种的修改名称的形式作用等同。
6 计算结构体类型的大小
(1)题
struct str {
char a;
int b;
char c;
};
int main() {
//判断结构体的类型的空间大小
struct str s = { 0 };
size_t sz = sizeof(s);
printf("%zd\n", sz);
return 0;
}
按照一般的思维,结构体类型的大小为各成员类型大小的总和,此例应为6个字节,真是如此吗?
之所以出现这个情况,要搞清楚结构体类型在内存中的存储情况
(2) 结构体在内存中的存储
结构体内存存储的规则:
1 第一个成员放置在偏移量为0的空间
2 第二个成员放置在偏移量是采用对齐数整数倍的地址
3 往后的成员依次按照第二个成员放置的规则
4 结构体的总大小为最大采用对齐数的整数倍
5 如果出现了嵌套了结构体的情况,即结构体变量作为结构体成员的情况
嵌套结构体对齐自己成员的最大采用对齐数的整数倍,
本结构体的大小为所有采用对齐数中(包括嵌套结构体中各成员的采用对齐数)
最大采用对齐数的整数倍
什么是偏移量?
以结构体首个地址为原点,首地址与原点的偏移量为0
,第二个地址与原点的偏移量为1
往后的地址依次类推。
什么是对齐数?
每一个结构体成员皆对应一个对齐数,对齐数的大小为结构体成员的大小,单位为字节
在VS编译器中,默认对齐数是8
Linus中gcc没有默认对齐数,大小直接为该成员的大小
在每个成员使用对齐数时,采用默认对齐数与成员自身对应的对齐数较小的值,得出
采用对齐数!
比如:char类型的大小为1个字节,对应的对齐数为1,而VS默认的对齐数为8,故采用对齐数1
(3)举例
举例1:
struct str {
char a;
int b;
char c;
};
举例2:
关于嵌套结构体的情况
struct str {
char a;
int b;
char c;
};
struct student {
char name;
int age;
struct str st1;
};
int main() {
struct student stu1 = { 0 };
size_t sz = sizeof(stu1);
printf("%zd\n", sz);
}
//此结果是如何得出:
(4)为什么结构体要采用这种对齐原则?
某些系统不能访问所有的地址,而只能在某些特定的地址一段一段地访问
这导致有时访问某一成员时需要访问多次,这浪费了时间
而将结构体成员存放到自身大小整数倍的地址处,这有效的减少了内存碎片
(有时访问的空间中有不是要成员的数据)的产生
通过填充字节,提高了成员访问的效率
本质上来讲,对齐原则是用空间换取了时间
(5)如何修改编译器中默认的对齐数
修改对齐数用的命令:#pragma pack(1) ,()中的数据是要设定的默认对齐数
#pragma pack() ,()中什么都不写,则还原为默认对齐数
#pragma pack(1) //将默认对齐数设置为1 ,相当于顺序存放
struct stu1 {
char a;
int b;
char c;
};
#pragma pack() //取消设置的对齐数,还原为默认!
struct stu2 {
char a;
int b;
char c;
};
int main(){
size_t len1 = sizeof(struct stu1);
printf("%zd\n", len1);
size_t len2 = sizeof(struct stu2);
printf("%zd\n", len2);
return 0 ;
}
要注意的是当#pragma pack(1)作用第一个结构体类型后,即使再用#pragma pack()还原默认对齐数,也不会改变第一个结构体的大小
7结构体传参
与数组传参时相同,如果将形参设成结构体,则形参会申请与实参同样的空间
这会 造成大量资源的使用,如果将形参设置成结构体指针,则不会出现这种情况
二 位段
(1) 什么是位段
位段(或称为位域)是结构体类型的一种形式,与常规结构体的差别在于成员不同
在c99之前的版本,位段只能是 int, unsigned int, signed int 或者是 char
(属于整形家族)类型
在c99版本中所有的整型家族均可作为成员,比如long ,short等
位段中的成员格式:举例:int a : 2;
// 其中int 代表位段成员类型 a代表成员名 :后面跟2,代表分配给此成员2个二进制位
位段的意义在于以比特位为单位,减少了内存空间的浪费
我们存储0,1,2,3时
00 01 10 11 两个比特位足矣,而在之前的结构体变量中存放一个整型数据就要申请32位空间
代码:
//有位段名(域名)的位段
struct stu1 {
int a1 : 10; //为成员a1分配10个比特位
int a2 : 8; //为成员a2分配8个比特位
int a3 : 30; //为成员a3分配30个比特位
};
//无位段名(域名)的位段
struct {
int a1 : 10; //为成员a1分配10个比特位
int a2 : 8; //为成员a2分配8个比特位
int a3 : 30; //为成员a3分配30个比特位
}s1;
(2)位段在内存中的存储
位段根据首成员的类型,申请相对应的空间大小
1)位段的成员类型相同时:
分配申请的空间给成员,剩余的空间不足矣满足下一成员的需求时,则重新申请一块空间
(问题1,不足的空间是浪费掉,还是继续利用)
(在VS编译器上不足的空间是浪费掉)
(问题2,在对申请的空间分配比特位时,是从低地址到高地址分配,还是从高地址到低地址分配?)
(在VS编译器上对申请的空间是从高地址到低地址分配空间),(注意这并不是数据在内存中存储的方式)
举例:
struct stu1 {
char a1 : 3; //为成员a1分配10个比特位
char a2 : 4; //为成员a2分配8个比特位
char a3 : 6; //为成员a3分配30个比特位
};
int main(){
struct stu1 s1;
size_t len = sizeof(s1);
printf("%zd\n", len);
return 0 ;
}
(3)位段的跨平台问题
位段的可移植性非常差,原因如下:
1 当位段的成员int型数据,在不同的机器上,int的大小可能不同,比如在32位机器上,int为4个字节, 在16位机器上,int为2个字节,如果我们给int 设置27位比特位,那么就会报错
2 在分配空间时,我们不能确定是从低地址到高地址分配,还是从高地址到低地址分配
3 不能确定不足的空间是浪费掉,还是继续利用
4 int 位段被当作有符号数还是无符号数,这个不能确定。
(4) 位段的应用:
在ip数据报中,一些数据段属性很小,但较多,所以可以用位段方式
来存放数据,节省空间!
(5)位段使用的注意事项
存储位段成员的基本单位是比特位,而地址对应的是字节,这就导致我们不能
通过 地址来访问数据,比如说scanf函数,操作符&,只能通过变量名,=
. ->等方式