一:结构体
(1)结构体类型的声明
//结构的声明:关键字+标签/结构体名称 struct STU//STU标签可以省,但不建议省,起名应该做到见名知义 { //聚合数据类型:数组和结构体,c中的结构体成员不能为空,这些成员类型相同或者不同 char name[20];//姓名 int age;//年龄 char sex[5];//性别 char id[20];//学号 }y[20],*z;//分好不能丢 //这个声明创建了y和z两个结构体变量,对于结构体变量建议省,按需所取。y是一个数组,它包含了20个结构,z是一个指针,它指向这个类型的结构。
(2)结构成员
结构的成员可以是标量、数组、指针、甚至是结构体
结构体成员的访问并不是像数组一样通过下表访问的,而是通过操作符(.)访问的,这是属于一般访问的
#include<stdio.h> #include<windows.h> struct S { char a; int age; struct simple *sp; }s;//这个声明创建了一个名叫x的变量,它包含三个成员:一个字符,一个整型数,一个结构体指针 int main() { struct S A; A.a = 'A'; A.age=18; system("pause"); return 0; }
结构体成员的间接访问:
#include<stdio.h> #include<windows.h> struct S { char a; int age; struct simple *sp; }s;//这个声明创建了一个名叫x的变量,它包含三个成员:一个字符,一个整型数,一个结构体指针 int main() { struct S A; A.a = 'A'; A.age = 18; struct S *p;//访问结构体指针变量 p = &A; //(*p).a = 'a';//点的操作符优先级高于间接访问的操作符 //(*p).age = 20; p->a = 'a';//箭头操作符,它接受两个操作符,但左操作数必须是一个指向结构的指针 p->age = 20; system("pause"); return 0; }
(3)结构的自引用(注意这个是用了typedef的,创建了一种新类型)
#include<stdio.h> #include<windows.h> typedef struct S { char a; int age; struct S *next;//结构的自引用必须是*next,必须是指针,它指向的是同一种类型的不同结构 }s; int main() { s n;//这个技巧和声明一个结构标签的效果几乎相同,此时的s现在是一个类型名而不是结构标签,所以后面的声明必须是以下的 s x; s y[20] , *z; n.a = '5'; n.age = 22; printf("%d\n", sizeof(struct S)); system("pause"); return 0; }
(4)结构的不完整声明(注:struct B;在不声明的时候可以编译运行,只是由于编译器问题而已,但是原则上不允许只这样,必须声明)
#include<stdio.h> #include<windows.h> struct B;//A的成员列表需要标签B的不完整声明,一旦A被声明之后,B的成员列表也可以被声明 struct A { char a; int b; struct B *next; }; struct B { char a; int b; struct A *next_o; }; int main() { printf("%d\n", sizeof(struct A)); system("pause"); return 0; }
(5)结构体变量的定义和初始化(可以类比数组,可以整体初始化,不允许整体赋值)
#include<stdio.h> #include<windows.h> struct B; struct A { char a; int b; struct B *next; int c[5]; }x = { '5', 10, { NULL }, {0} };//方式一 struct B { char a; int b; struct A *next_o; }; int main() { struct A obj = { '5', 10, { NULL }, { 0 } };//方式二 //obj = { '5', 10, { NULL }, { 0 } };//错误 printf("%d\n", sizeof(struct A)); system("pause"); return 0; }(6)结构体内存对齐问题
我们现在使用的算机中内存空间都是按照字节(Byte)划分的,理论上说,似乎对任何类型的变量的访问可以从任意地址开始,但实际情况则是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型的数据按照一定的规则在内存空间上排列,而不是顺序的一个接一个地排放,这就是对齐。
默认对齐数
Linux 默认#pragma pack(4)
window 默认#pragma pack(8)
注:可以通过预编译命令#pragma pack(n) ,n=1,2,4,8,16来改变这一系数,其中的n就是指定的“对齐系数”。
内存对齐原则
1.第一个成员在与结构体变量地址偏移量为0的地址处(即第一个成员存放在结构体的首地址处);
2.其它成员变量要对齐到某个数字(对齐数)的整数倍地址处。对齐数=编译器默认的对齐数与该成员大小的较小值(我们会在后面详细讲解);
3 .结构体总大小为最大对齐数的整数倍(通过第二条我们知道,结构体中每一个成员都有一个自己对应的对齐数);
4.如果有嵌套结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(包含嵌套结构体)的整数倍。
为什么存在内存对齐
1.平台原因
不是所有的硬件平台都能访问任意地址上的任意数据;某些硬件平台只能在某些地址处取某些特定类型的数据,否则会抛出硬件异常。
2.性能原因
数据结构(尤其是栈)应该尽可能在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要做两次内存访问;而对其的内存访问仅需要一次访问。
总的来说,结构体内存对齐就是拿空间换取时间的做法。
#include<stdio.h> #include<windows.h> struct A { char a;//对齐数1,因为1<8,偏移量0.结构体第一个成员的地址是对齐的 int b;//对齐数4,起始偏移量1,但是不能整除4,所以偏移量为1+3 int c[5];//对齐数4,总大小:20+4+1+3=28,偏移量8 }x; //此时最大对齐数为4,而28刚好能整除4,所以对齐数为28 //结构体的总大小为最大对齐数的整数倍。 int main() { printf("%p %p\n", &x, &(x.a));//结构体的地址和结构体成员的第一个地址相同 printf("%d\n", sizeof(struct A));//28 system("pause"); return 0; }
接下来我们看一下将成员顺序一变,看有什么变化,两个程序将会将内存对齐问题很明显的显现
#include<stdio.h> #include<windows.h> struct A { char c;//对齐数:1,1+3 int b;//对齐数:4 char a;//对齐数:1 }x; //最大对齐数:4,结构总大小:1+3+4+1=9,结构总大小要是4的整数倍,即为12 int main() { printf("%p %p\n", &x, &(x.c)); printf("%d\n", sizeof(struct A));//12 system("pause"); return 0; }
#include<stdio.h> #include<windows.h> struct A { char a;//对齐数:1 char c;//对齐数:1 int b;//对齐数:4 }x; //最大对齐数:4,结构总大小:1+1+4=6,结构总大小要是4的整数倍,即为8 int main() { printf("%p %p\n", &x, &(x.a)); printf("%d\n", sizeof(struct A));//8 system("pause"); return 0; }
接下来我们讨论结构体嵌套问题
#include<stdio.h> #include<windows.h> struct B { double c;//对齐数8 char d;//对齐数1 float e;//对齐数4 };//最大对齐数:8,总大小:(8+1+3+4+x)%8==0,所以结构的总大小为:16 struct A { char a;//对齐数:1 struct B p;//对齐数:16,16+3+1 double b;//对齐数:8 }x; //最大对齐数:16,结构总大小:(1+16+3+8+x)%16==0,结构总大小要是16的整数倍,即为32 int main() { printf("%d\n", sizeof(struct B));//16 printf("%d\n", sizeof(struct A));//32 system("pause"); return 0; }
关于内存对齐,我们用图来分析(CPU能从4的整数倍处读取数据)
浪费了空间,换取了时间,那么在设计的时候,我们既要满足对齐,又要节省空间,所以我们可以尽可能的让占用空间小的成员尽量集中在一起。
接下来我们修改编辑器默认的对齐数,然后来实践一下,看看我们的结构的大小的变化
#include<stdio.h> #include<windows.h> #pragma pack(1)//修改编译器的默认对齐数,编译器默认为8,如果是#pragma pack(0)或者空,表示恢复编译器默认值,并且其参数只能是1,2,4.6,8,16这样的偶数 struct B { double c;//对齐数1,因为长度8>1对齐数,只能是两者中的较小值 char d;//对齐数1,1=1 float e;//对齐数1,1<4(长度) };//最大对齐数:1,总大小:(8+1+4+x)%1==0,所以结构的总大小为:13 struct A { char a;//对齐数:1,(长度)1=1 struct B p;//对齐数:1,13>1 double b;//对齐数:1,1<长度8, }x; //最大对齐数:1,结构总大小:(1+13+8+x)%1==0,结构总大小要是1的整数倍,即为32 int main() { printf("%d\n", sizeof(struct B));//13 printf("%d\n", sizeof(struct A));//22 system("pause"); return 0; }
(7)结构体传参
注:* 数组,指针在传参时会发生降维,而结构体在传参时不会发生降维,但是仍会形成临时变量,只要传参就会形成临时变量
*尽量不要传结构体变量,必须传结构体指针(原因:函数在传参的时候,参数是要压栈的,如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降)
#include<stdio.h> #include<windows.h> struct B { double c; char d; int e; }; struct B s = { 85964587, 'A', 20 }; void print(struct B *ps) { printf("%d\n", ps->d);//65 } int main() { print(&s);//传参传结构体地址,不能传结构体 system("pause"); return 0; }
(8)结构体实现位段
什么是位段:①位段的成员必须是int ,unsigned int或signed int。
②位段的成员名后面有一个冒号和一个数字。
如下,在vs中的位段运行结果:
#include<stdio.h> #include<windows.h> struct B { char a :3;//a占char类型的3个bit char b :4;//b占char类型的4个bit char c :5;//c占char类型的5个bit char d :4;//d占char类型的4个bit }; int main() { struct B s = { 0 }; s.a = 10; s.b = 12; s.c = 3; s.d = 4; printf("%d\n", sizeof(struct B)); system("pause"); return 0; }
然后我们用图示的方法来看一下它在内存中的分布
位段的内存分配:
1:位段的成员可以是int 、unsigned int、signed int 或者char(属于整型家族)类型;
2:位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的;
3:位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
位段的跨平台问题:
1:int 位段被当成有符号数还是无符号数是不确定的;
2:位段中最大位的数目不确定(16位机器最大16,32位机器最大32,写成27,在16位机器中会出问题);
3:位段中的成员在内存中从左往右分配,还是从右往左分配尚未定义;
4:当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总结位段:跟结构体相比,位段可以达到相同的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
二:枚举
(1):枚举类型的定义
下面我们用代码来举例说明它的使用
#include<stdio.h> #include<windows.h> enum DAY { MON = 4, TUES = 5, SUN = 8, SAT,//9 FRI//10 }; enum color {//下面{}里面的内容是枚举类型的可能取值,即枚举常量,这些可能取值都是有值的,默认从0开始,依次递增,当然在定义的时候也可以赋值(enum DAY) RED,//0 BLACK,//1 YELLOW,//2 GREEN,//3 BLUE//4 }; int main() { enum color c; enum DAY p; c = RED; p = MON; printf("%d\n", MON); printf("%d\n", SUN); printf("%d\n", FRI); printf("%d\n", RED); printf("%d\n", BLACK); printf("%d\n", BLUE); system("pause"); return 0; }
(2):枚举的优点
1:增加代码的可读性和可维护性
2:和#define定义的标识符比较枚举有类型检查,更加严谨
3:防止了命名污染(封装)
4:便于调试
5:使用方便,一次可以定义多个常量
(3):枚举的使用(看枚举类型的定义下面的代码)
三:联合(共用体)
(1):联合类型的定义(联合变量也可以进行初始化,但初始值必须与联合第一个成员的类型匹配)
下面我们用代码来详细分析联合的特点以及声明
#include<stdio.h> #include<windows.h> typedef union A { int a;//对齐数4 char b;//对齐数1 double c;//对齐数8, char d[9];//对齐数1, }un_t; //最大对齐数;8,联合大小:9,9不能被8整除,所以联合大小为16 int main() { un_t x; x.a = 0x11223344; x.b = 0x55; printf("%p ,%p ,%p ,%p ,%p \n", &x, &(x.a), &(x.b), &(x.c), &(x.d));//联合体所有成员的地址相同=总体空间的第一个字节的地址=联合体的地址 printf("%d\n", sizeof(un_t));//联合的大小至少是最大成员的大小(16) printf("%x\n", x.a);//11223355(大小端问题)此时为小端 system("pause"); return 0; }
我们估计会好奇x.a的结果,这是大小端问题,此时这台机器为小端
单步调试后
(2):联合的特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)
(3):联合大小的计算
①联合的大小至少是最大成员的大小;
②当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
注:如有错误,请各位大佬留言,谢谢