一.结构体
- 数据经常会以组的形式存在,例如:用结构体描述一个复杂对象的基本信息–学生,这些值能够存储在一起,访问起来就会简单一些,但是由于这些值的类型互不相同,则无法使用数组存储,因此便有了结构体
1-1结构体类型的声明
用结构体描述一个复杂对象的基本信息:学生 struct Stu {//Stu是结构体标签,struct Stu才是结构体类型,相当于int char name[20];名字 int age;年龄 char sex[2];性别 char id[20];学号 };分号不能丢
数组元素可以通过下标访问,是因为数组的元素长度相同,但是结构体的成员变量的类型不同,因此不能使用下标访问结构体的成员变量
1-2结构体的自引用
正确的结构体自引用:结构体的 struct Stu1 { int a; char b; struct Stu1* c;结构体指针变量 }; 错误的结构体自引用: struct Stu2 { int a; char b; struct Stu2 c;结构体 };
结构体包含一个类型为该结构体本身的成员是非法的,有点像永不终止的递归程序; 但是结构体内部包含一个指向该结构体本身的指针是合法的,在单链表中很常见
单链表中结构体中包含结构体指针的使用: typedef struct Node { int date; struct Node* ps;但不能写成ListNode* ps //因为这个typedef声明的类型名直到声明的末尾才被定义 }ListNode;
此处的typedef是给struct Node进行了类型重命名,也类似于int,以后就可以直接用Node定义结构体变量
typedef类型重命名前定义结构体变量: struct Node newnode; typedef类型重命名后定义结构体变量:(是不是少些了个struct,适当偷懒) ListNode newnode;
1-3结构体变量的定义和初始化
结构体的初始化方式和数组的初始化方式很像,结构体成员是否包含数组分别对于一维和二维数组的初始化很像
struct Stu { char name[20]; int age; char sex[2]; }student1={{宋小宝},18,男};
- 结构体的成员变量不能是结构体本身,但是可以是另一个结构体
typedef struct date { int year; int month; int day; }Date; struct Stu { Date a; int number; };
图解:
1-4结构体内存对齐(求结构体所占字节数)
什么是偏移量和对齐数?
- 偏移量:相对于第一位置存放开始,到第二个位置存放开始之间的地址差
偏移量可以使用offsetof宏来求得(定义与stddef.h)
offsetof(type,member)例子即是offsetof(struct Stu,age)
对齐数:该成员变量的字节数和编译器默认对齐数(VS默认是8)中的最小值
每一个成员变量都有自己的对齐数
结构体内存对齐规则:
第一个成员在与结构体变量偏移量为0的地址处
从第二个成员开始的每个成员变量要对齐到对齐数的整数倍处
结构体的整体大小就是最大对齐数的整数倍
如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍中,结构体的整体大小就是所有最大对齐数的整数倍
概念麻烦,实际简单的一批
给你举个例子画个图吧 struct AKIGN { char a;第一个数不用考虑内存对齐,直接放 int b;第二个数放的位置(偏移量)要是int字节(4)的整数倍,也就是4 char c;第三个数放的位置(偏移量)要是char字节(1)的整数倍,也就是9 };
由于结构体的整体大小就是所有最大对齐数(这里是4)的整数倍,所以结果总共是占12个字节
- 图解:
- 总之就是要综合考虑数据类型和偏移量,匹配整数倍就可以啦,是不是很简单
为什么存在内存对齐?
1.1、平台原因:不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据
2、性能原因:,对于访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问,使得处理起来速度快。
本质:
空间换时间\
怎样设计可以减少空间浪费?
使得空间小的成员尽量集中在一起
学会了?来几道测试题?
// 练习2 struct S2 { char c1; 1 char c2; 1-2 int i; 4-8 }; printf("%d\n", sizeof(struct S2));//要是double(8)个字节的整数倍,答案:占8个字节 //练习3 struct S3 { double d; 1-8 char c; 9 int i; 12-16 }; printf("%d\n", sizeof(struct S3));//要是double(8)个字节的整数倍,答案:占16个字节 //练习4-结构体嵌套问题 //如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍中 struct S4 { char c1; 1 struct S3 s3; 8-16 double d; 16-24 }; printf("%d\n", sizeof(struct S4));//要是double(8)个字节的整数倍,答案:占24个字节
修改默认对齐数
#include <stdio.h> #pragma pack(4)//设置默认对齐数为4 struct S1 { char c1; double i;默认对齐数(4)和成员变量的字节数(8)比较取最小的为默认对齐数(4):4个字节 char c2; }; #pragma pack()//取消设置的默认对齐数,还原为默认 #pragma pack(1)//设置默认对齐数为8 struct S2 { char c1; int i; char c2; }; #pragma pack()//取消设置的默认对齐数,还原为默认 int main() { //输出的结果是什么? printf("%d\n", sizeof(struct S1));答案是16个字节 printf("%d\n", sizeof(struct S2));答案是6个字节 return 0; }
1-5结构体传参
结构体传参的两种方式:(不考虑效率而言)
1.传结构体本身: print_struct(struct Stu); 2.传结构体地址: print_struct(&struct Stu);
关于效率高的传参方式:传结构体指针
函数形参要压栈,而结构体指针相对占用栈内存小
如果希望传过去修改成员,则只有传结构体指针
如果不希望被修改也可以使用const修饰,进行保护
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; }
1-6结构体实现位段(bit field)
有的时候我们存放的一个数据并不需要一个字节那么大的空间,而只需要几个比特位就够了,比如我们用二进制表示灯泡的开关,只需要0和1就足够表示这两种状态,那么怎么实现这样减少空间的浪费呐?这就需要用到我们强大的位段。
位段位段,位顾名思义就是二进制位,段就是路段,长度,综合就是二进制位的长度
位段和结构体的声明的区别:
他的成员后面相对于结构体多一个冒号和整数,这个整数指定该位段所占用的位的数目。
位段成员必须声明为int ,unsigned int ,signed int 或char类型
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)); }
注意:注重可移植性的程序应该避免使用位段:(理由如下)
int 位段被当成有符号数还是无符号数是不确定的。
位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
器会出问题。
位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的
联合体:联合体就是共用同一块内存
#include<stdio.h> union UN//如果省去UN就是匿名结构体类型,只能使用一次 { int a; char ch; }; int main() { //判断猜测:共用同一块内存 union UN u; printf("%p\n", &(u.ch));//地址1 printf("%p\n", &(u.a));//地址1 printf("%d\n", sizeof(u));//4 u.a = 1; printf("%d\n", u.a);//1 //通过共用一块内存通过改变u.c就能改变u.a u.ch = 0; printf("%d\n", u.ch);//0 //判断是大端还是小端 printf("%d\n", u.ch);//0 return 0; }
判断机器是大端机还是小端机:
union un { int a; char c; }; int test1() { int n = 1;; return *(char*)&n; } int test2() { union un u; u.a = 1; return u.c==1; } //这里都是使得int类型的变量赋值为1,也就是十六进制的都是00 00 00 01,然后通过让char或者联合本身的特点访问其中的低地址的内容,如果最低的字节内容是1,则是整数的最低字节的内容放在了低地址处,则是小端,反之。。 int main() { int ret=test1(); if (ret == 0) printf("大端\n"); else if(ret==1) printf("小端\n"); }
计算联合体的大小: