目录
1.结构体类型的声明
1.1结构体的概念
结构是值的集合,这些值称为成员变量,每个成员可以是不同类型的变量
类型:
1.内置类型:char short int等
2.自定义类型:结构体、联合、枚举
1.2结构声明
struct tag { memeber-list;//成员 }variable-list;//结构体变量的名字 描述学生: struct stu { char name[20]; int age; char sex[5]; char id[20]; }; int main() { struct stu s1={"ss",20,98.5f}; struct stu s2={.age=22,.name="cc",.score=55.5f}; printf("%d",s1.name); return 0; }
1.3特殊声明
可以不完全声明
//匿名结构体类型,也就是省略了结构体的名字 struct { int a; char b; float; }x;//这里是指用这个结构体类型创建了一个全局变量x struct { int a; char b=0; float c; }a[20],*p; int main() { struct Stu s1,s2,s3;//这里是根据结构体Stu类型创建了3个变量 return 0; }
p=&x; 因为x变量的类型即结构体类型和p指针的类型即结构体类型是完全不一样的,因此x的地址赋给p是非法 匿名结构体若不重命名,则基本上只能使用一次
1.4结构体自引用
struct Node { int data; struct Node next; } //这样自引用是错误的,结构体占据空间是无限大 正确自引用是: struct Node { int data; struct Node*next;//下一个节点的地址,从而串联一串在内存中分散的数据 }
typedef struct { int data; Node*next; }Node; 这里是非法的,因为提前在结构体内使用了Node 类型,struct后面没有写名字 也就没有重命名。
如果要用typedef,不要用匿名结构体
typedef struct Node { int data; strct Node*next; }Node;
2.结构体变量创建和声明
struct point { itn x; int y; }p1;//声明的同时定义一个类型为结构体point的全局变量p1; struct point p2;定义结构体变量p2 struct point p3={x,y};//定义变量的同时初始化值 struct stu { char name[15]; int age; }; struct stu s={"sdsda",20};//初始化 struct Node { int data; struct point p; struct Node* next; }n1={10,{4,5},NULL};//结构体嵌套初始化;
c99的指示器初始化,可以不按照成员顺序初始化
struct stu { char name[15]; int age; }; struct stu s={.age=20,.name="ssd"};
3.结构成员访问操作符
结构体变量.成员变量名 结构体指针->成员变量名 struct Stu { char name[15];//名字 int age; //年龄 }; void print_stu(struct Stu s)//传递的是变量s的值 { printf("%s %d\n", s.name, s.age); } void set_stu(struct Stu* ps)//传递的结构体变量的地址 { strcpy(ps->name, "李四"); ps->age = 28; } int main() { struct Stu s = { "张三", 20 }; print_stu(s); set_stu(&s); print_stu(s); return 0; }
4.结构体内存对齐
同样的的结构体,内部成员定义的顺序不一样,整个结构体类型的大小也不一样了。
4.1对齐规则
4.1.1
结构体的第一个成员对齐到相对结构体变量起始位置偏移量为0的地址处
4.1.2
其他成员要对齐到某个数字(对齐数)的整数倍的地址处
对齐数=编译器默认的一个对齐数与该成员变量大小的较小值(比如vs的8对齐数和变量的4大小,小的是4,那对齐数就是4),vs中默认8,linux没有默认,对齐数就是成员自身大小
4.1.3
结构体总大小为最大对齐数,(结构体中每个成员都有一个对齐数,所有对齐数中最大的)整数倍
4.1.4如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍
//练习1 struct S1 { char c1; int i; char c2; }; printf("%d\n", sizeof(struct S1)); 这里结果是8,首先c1在偏移量0的位置 由于int类型,4比8,取4,i放在偏移量4的地方由于int占4字节, 所以要占4个偏移量,达到了8偏移量 接着c2是char类型,放偏移量9的地方,占一个偏移量 最大对齐数是4,9偏移量不是倍数处, 直到偏移量12才是4倍数处,所以最终结构体大小是12 浪费6字节 //练习2 struct S2 { char c1; char c2; int i; }; printf("%d\n", sizeof(struct S2)); 这里结果是8,首先c1在偏移量0的位置 接着因为1比8,取1,所以c2放在偏移量1的位置(1是1的倍数) 再接着i,int类型,4比8,取4,i放在4倍数处, 也就是偏移量4的地方,由于int占4字节,所以要占4个偏移量,达到了8偏移量 最后整体大小,其中最大对齐数是4,8偏移量正好是4的倍数,所以整体大小是8 浪费2字节 //练习3 struct S3 { double d; char c; int i; }; printf("%d\n", sizeof(struct S3)); 首先double占8字节,所以达到了偏移量7(0-7), 接着char类型占1字节,所以达到了偏移量8(8) int类型是占4字节,所以从偏移量12位置开始,达到了偏移量15(12-15) 由于最大对齐数是8,所以16正好是8的倍数处,所以16是结构体整体大小 //练习4-结构体嵌套问题 struct S4 { char c1; struct S3 s3; double d; }; printf("%d\n", sizeof(struct S4)); 首先c1是占偏移量0的字节 接着S3结构体,按该结构体最大对齐数,则从偏移量8开始,后面16个字节都是s3, 然后是double类型 从偏移量24开始(24是8的倍数),占8个字节,达到偏移量31, 最后整个结构体的大小,由于本身不含嵌套结构体的最大对齐数是8, 嵌套结构体的最大对齐数也是8,32正好是8的倍数,所以整个结构体就是32大小
offsetof()宏,用来计算结构体成员,相较于起始位置的偏移量
4.2为什么存在内存对齐
4.2.1平台原因(移植原因):
不是所有硬件平台都能访问任意地址上任意数据,有些硬件平台只能在某些地址处取某些特定类型的数据,否则会报出硬件异常
4.2.2性能原因:
数据结构(尤其是栈)应该尽可能在自然边界上对齐。为了访问未对齐内存,处理器要2次内存访问,对齐仅需要一次访问,假如处理器总是拿8个字节,地址必须是8的倍数,比如把double类型数据的地址都以8倍数对齐,那每次都能完整拿到一个数据,如果不对齐,可能一个double数据放在2个8倍数地址中,那处理器就需要读取2次内存,从而凑到完整的数据
因此内存对齐是拿空间换时间,提高效率
设计结构时,要将占空间小的成员尽量凑到一起
struct S2 { char c1; char c2; int i; };//尽量用这种 struct S1 { char c1; int i; char c2; };
4.3修改默认对齐数
#pragma 这个预处理民指令,可以改变编译器的默认对齐数
#include <stdio.h> #pragma pack(1)//设置默认对⻬数为1 struct S { char c1; int i; char c2; }; #pragma pack()//取消设置的默认对⻬数,还原为默认 int main() { printf("%d\n", sizeof(struct S)); return 0; }
5.结构体传参
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; } //但由于函数传参,是需要创建临时空间给传递来的数据的 如果直接给结构体的值,假如结构体很大,那么这时候就会浪费时间和空间 如果给地址,就能节省时间和空间
6.结构体实现位段
6.1什么是位段
位段的声明和结构是类似的
位段的成员必须是int、unsigned int 或signed int,在c99中位段成员的类型也可以选择其他类型
位段的成员名后边有一个冒号和一个数字
struct a { int _a:2; int _b:10; int _c:5; int _d:30; }//这就是一个a位段,其中成员必须是int 或unsigned int 或者signed int 数字指的是二进制位,也就是存储了几位二进制位,数字不能超过变量类型规定的大小
6.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; }; struct s S={0}; S.a=10; S.b=12; S.c=3; S.d=4; 在vs的环境下 10=1010,但a只占3位,所以截取低位,也就是010,放入第一个字节的低位; 接着12是1100,b占4位,所以正好全部存进去,放在010的左边; 3是00011,第一个字节不够存,舍弃剩余空间,申请新的一个字节, 然后将5位00011放入新字节的低位,剩余3位不够d变量存储, 于是再申请一个新的字节,4是0010,放入新字节的低位, 于是,将3个字节变成十六进制,于是在内存监控中我们就能看到, 内存中存储的是0x62 03 04
6.3跨平台问题
首先是,int位段被当成无符号数还是有符号数是不确定的
位段最大位的树木不确定,因为16位机器最大是16,32位机器是32,如果写成30,16位机器运行会出问题
位段成员在内存中从左向右还是从右向左分配是不确定的
如果该字节内剩余空间不足以后续的类型存放,那么将会申请一个新的字节
,但旧的字节中剩余的空间是否被使用也是不确定的结构虽然相比位段庞大些,但可以跨平台,但位段不能跨平台,除非不同机器使用不同的位段
6.4位段的应用
网络协议中,ip数据包的格式,我们可以看到其中很多的属性只需要几个bit位就能描述,这里使用位段,能够实现想要的效果,也节省了空间,这样网络传输的数据报大小,也会较小一些,对网络的畅通是有帮助的
6.5位段使用注意事项
由于位段中一些成员会公用一个字节,且一个字节有一个地址,但字节中的位是没有独立地址的,所以用scanf不能直接给位段成员赋值,必须先将值赋给一个变量,在将变量赋值给位段成员
struct A { int _a : 2; int _b : 5; int _c : 10; int _d : 30; }; int main() { struct A sa = {0}; int b = 0; scanf("%d", &b); sa._b = b; return 0; }