目录
1.结构体
1.1结构
1.1.1基础知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
我们学过那些结构呢?
数组也是一种结构,类比结构体,数组的成员变量是相同的,结构体的成员变量是不同的。
1.2结构体类型的定义
先看看语法结构:
struct tag
{
member-list;
}variable-list;
假设要描述一个学生,比如他的姓名,年龄,性别等等怎么做呢?
struct student
{
//学生的相关属性(成员列表)
char name[20];
int age;
char sex[5];
};
这样就创建了一个结构体类型。
可能有人要问了:不需要给这些成员属性初始化吗?
{ 打个比方,1.这就好比设计师设计出的建筑图纸,而刚刚上面这段代码正是这个图纸,并没有真正取去建造这个建筑;2.好比 int a = 10; 这段代码里的int一样,仅仅只是个类型,不过以上是你自己定义的类型而已 }
那如何真正创建一个结构体变量呢?看以下代码
1.2.1结构体全局变量
struct student
{
//学生的相关属性(成员列表)
char name[20];
int age;
char sex[5];
}s1, s2;
{ 这里的s1或s2才是真正你建造出的建组,相当于int a = 10;这个代码里的变量a,但要注意,这里的s1,s2是两个结构体的全局变量(在main函数外面) }
那么局部变量也不难想到 ->
1.2.2结构体局部变量
struct student
{
//学生的相关属性(成员列表)
char name[20];
int age;
char sex[5];
}s1, s2;//全局变量
int main()
{
struct student s3;//局部变量
return 0;
}
1.3匿名结构体类型
先来看看这段代码有什么不一样
struct
{
//学生的相关属性(成员列表)
char name[20];
int age;
char sex[5];
}s1;
值得注意的是,匿名结构体类型只能用一次,在声明结构体类型的时候去创建这个变量s1,之后就再也不能创建了。
1.3.1匿名结构体类型的特性
看如下这段代码,你有什么发现?
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;//这里的*p是一个结构体指针变量
有没有发现这两个匿名结构体类型的成员都一样?
接着看:
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;//这里的*p是一个结构体指针变量
int main()
{
p = &x;
return 0;
}
这合理吗?
看样子,欸?好像是对的呀,p是一个结构体指针变量,用来存放结构体变量
可是编译这段代码你会发现报警告了:
从* 到 * 的类型不兼容?
不都是结构体吗?成员不都一样吗?
这就是匿名结构体的特性:
在编译器看来,这就是两种不一样的类型~(但实际上,语法上支持这么写😂)
1.4结构体的自引用
再来看以下代码合理吗?
struct Node
{
int data;
struct Node next;
};
如果你感觉可以,那么sizeof(struct Node)是多少呢?
你会发现struct Node里面还有一个stuct Node,里面还有…然后就一直套娃下去(doge)…
正确的结构体自引用如下:
struct Node
{
int data;
struct Node* next;
};
成员有两个一个是数据域,一个是指针域,指针域负责记录下一个结构体变量的地址,依此联系起来,学过链表的同学就明白啦,本文重点不在此,后期会出。
你可能见过有些同学这样去写:
typedef struct Node
{
int data;
Node* next;
}Node;
有没有想过这段代码到底是重命名在先还是在后?在后的话,Node*next就合不合理
想想代码还没有进行到重命名你就开始用,肯定是不合理的。
(先有鸡还是先有蛋的问题~doge)
正确写法:
typedef struct Node
{
int data;
struct Node* next;
}Node;
1.5结构体变量的定义和初始化
咱一步步分析(盖房子),现在你就是设计师!
第一步:盖房子总要有设计图纸吧~
来!给你!(下面代码)
struct Point
{
int x;
int y;
};
第二步:图纸设计好了,咱这就起身盖房子!(创建p1结构体变量)
struct Point
{
int x;
int y;
}p1;
第三步:想让咱家看着温馨住着舒服就来写家具吧!(初始化赋值)
struct Point
{
int x;
int y;
}p1 = {2, 3};
手工!
这时就创建好了一个结构体变量p1,并且p1中的x,y被分别赋值为2,3
咱来看看房间内部构局:
当然你也可以这样创建:
struct stu
{
char name[20];
int age;
char sex[5];
};
int main()
{
struct stu s1 = {"jay chou", 18, "man"};
return 0;
}
这就是对s1这个对象进行了一个简单的初始化;
当然结构体也可以嵌套使用:
struct score
{
int n;
char a;
};
struct stu
{
char name[20];
int age;
struct score s;
};
int main()
{
struct stu s1 = { "jay chou", 18 , { 100, 'w' } };
return 0;
}
注意:结构体初始化需要用大括号括起来;
综上所述,结构体定义和初始化的过程简而言之就是通过设计图纸建造房间,这个房间才是真实的物理存在,占用土地,空间。
怎么用这些信息呢?好比我需要打印这些信息怎么做?咱接着往下看~
1.6结构体变量的使用
上代码,一目了然~
struct score
{
int n;
char a;
};
struct stu
{
char name[20];
int age;
struct score s;
};
int main()
{
struct stu s1 = { "jay chou", 18 , { 100, 'w' } };
printf("%s %d %d %c \n", s1.name, s1.age, s1.s.n, s1.s.a);
return 0;
}
点’ . ‘符号可以通过结构体变量找到成员哦;
1.7结构体内存对齐
掌握了结构体的基本使用,接下来深入讨论一个问题:
1.7.1计算结构体大小(热门考点)
看下面这段代码,你认为这个结构体类型有多大?
例一:
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
是1+4+1 = 6个字节吗?
运行结果:
好奇宝宝有要问了,为什么是12呢?
结构体类型这样写是不占空间的,所以创建一个结构体变量
咱一起分析一波:
首先给出结构体的计算方法
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
第一条什么意思?看下图
第一个成员char c1在偏移量为0的地址处,并且char类型占1个字节的空间,如上图;
第二条什么意思?看下图
要对齐到某个数字(对齐数)的整数倍的地址处,int 类型占4个字节,VS的默认对齐数是8,要取较小值是4,所以4就是他的对齐数,在哪里开始占用空间呢?在对齐数4的整数倍数处开辟空间,所以在偏移量4的位置初开始开辟空间(如上图),int类型占4个字节空间,所以是4个黄格子;
第三条什么意思?看下图
结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍,当为c2开辟空间后,结构体的总大小为9个字节,而这三个成员变量中最大对齐数是4,9是4的倍数吗?显然不是,所以要继续向下浪费3个字节的空间,如上图,故此时,这个结构体的总大小为12,满足要求;
想知道自己掌握计算方法了没?来,试试就知道
例二:
struct S2
{ //对齐数
double d; //8
char c; //1
int i; //4
};
int main()
{
printf("%d\n", sizeof(struct S2));
return 0;
}
运行结果:
图解:
0~15是16个数,所以占16个字节;
第四条什么意思呢?既然是嵌套结构体,咱就举个栗子~
例三:
struct S2
{
double d;
char c;
int i;
};//由例二计算出大小为16
struct S3
{ //对齐数
char c1; //1
struct S2 s2; //8
double d; //8
};
int main()
{
printf("%d\n", sizeof(struct S3));
return 0;
}
运行结果:
嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
怎么理解这句话?
char c1在零偏移处占1个字节,下一个偏移量是1处,struct S2 s2由例二可知占16个字节的空间,由于结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍,可是VS的默认对齐数是8,取较小值,所以对齐数是8,1是8的倍数吗?显然不是,所以继续往后,直到偏移量为8的地址处,8是8的倍数,所以从这里开始开辟空间,struct S2 s2占16个字节,所以从此处向后开辟16个字节的空间,即下一个偏移量为24的地址处;double d的大小为8个字节,VS默认对其数也是8,取较小值,故对齐数为8,24是8的倍数吗?显然,所以继续从偏移量为24的地址处开辟8个字节的空间,最后偏移量0~31共32个数,所以占用32个字节。
图解:
你们是否发现了一个问题?
1.7.2结构体存在空间浪费?(为什么存在内存对齐?)
大部分的参考资料都是如是说的:
{
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
}
总体来说:
{
结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。
}
1.8修改默认对齐数
通过#pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。
假设我要修改默认对齐数为8,怎么做呢?
#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
反悔了怎么办(doge)下图方法论
#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
来点有意思的(如下代码):
#pragma pack(1)//设置默认对齐数为1
struct S2
{
char c1;
int i;
char c2;
};
如果创建这样一个结构体变量,他的大小是多少呢?
对齐数取较小值,任何数都可以是一的倍数,那么你就可以自信一点,无脑一点(doge)一个一个加起来就是了 1 + 4 + 1 = 6;
再来点刺激的 (如下代码):
#pragma pack(-1)//设置默认对齐数为-1
struct S2
{
char c1;
int i;
char c2;
};
一句老话叫做:一个想写BUG的程序员,你怎么都拦不住~
1.9结构体传参
传参分 传值 和 传址 ,学过函数因该都了解,咱来看看下面这段代码😶🌫️
#include<stdio.h>
struct student
{
char name[20];
int age;
char sex[5];
};
void Add_student(struct student* stu)
{
;
}
void Sub_student(struct student stu)
{
;
}
int main()
{
struct student stu;
Sub_student(stu);
Add_student(&stu);
return 0;
}
Sub_student(stu);
Add_student(&stu);
这两个函数,哪个好?
答案是:首选Add_student(&stu);
原因:函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的
下降。
可能又有好奇宝宝会问道:那如果不想修改实参的值,传值操作会形成一份临时拷贝,再怎么修改也不会影响实参,传值岂不是更安全?
哈哈哈,那是不是忘了可以用const 修饰以下指针,也能达到相同的作用,如下代码:
#include<stdio.h>
struct student
{
char name[20];
int age;
char sex[5];
};
void Add_student(const struct student* stu)
{
;
}
int main()
{
struct student stu;
Add_student(&stu);
return 0;
}
这时,就算你想改也改不辽~
结论:结构体传参的时候,要传结构体的地址。
2.位段
结构体讲完就得讲讲结构体实现 位段 的能力。
2.1基础知识
🍔位段与结构体类似,可以有很多类型,可以是int,char…
🌭位段的成员名后边有一个冒号和一个数字。
例如:
struct student
{
int a : 4;
int b : 8;
int c : 20;
int d : 30;
};
位段位段,位是什么,位实际上就是比特位,就是以上代码冒号后面的数字,例如int a : 4;都知道int 占4个字节的大小,而这句代码的意思就是从这4个字节(32个比特位)中拿出4个比特位的空间给整形变量a。
想象一下,如果我只需要给a赋一个值12,12用2进制表示就是1 1 0 0 ,只需要四个比特位就够了,如果写成int a = 12; 会给 a 32个比特位的空间,多出来28个比特位的空间岂不是浪费了?但如果通过位段来写,(如上代码)只给a分配32个比特位的其中4个,就做到了空间上的节省。
2.2位段的内存分配
例子:空间是如何开辟的?
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;//1010
s.b = 12;//1100
s.c = 3;//011
s.d = 4;//100
return 0;
}
如下图:(以下为VS2013测试)
写成16进制因该是62 03 04
看一下内存布局:
2.3 位段的跨平台问题
1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
舍弃剩余的位还是利用,这是不确定的。
简而言之,跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
2.4 位段的应用
后面写计算机网络的时候细讲~
3.枚举
不难理解,其实就是一一列举的意思
比如一周的七天就可以分别列举出来:周一,周二…
再比如一年的十二个月也可以一一列举…
😶🌫️这些情况都可以用到枚举;
3.1枚举类型的定义
直接上代码:
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
这些枚举成员都是有值的,默认从上倒下依次赋值位0,1,2,3…
当然在定义的时候也可以赋初值
enum Day//星期
{
Mon = 1,
Tues = 2,
Wed = 3,
Thur = 4,
Fri = 5,
Sat = 6,
Sun = 7
};
但请注意,如果你这样赋值:
enum Day//星期
{
Mon = 1,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
Mon后面的枚举成员会根据上一个元素的大小一次加一,此时Tues就被赋值为 2,Wed被赋值为3…
怎么用呢?
写一个程序,输入数字1~7,告诉你周几的任务是什么?
enum Day//星期
{
Mon = 1,
Tues = 2,
Wed = 3,
Thur = 4,
Fri = 5,
Sat = 6,
Sun = 7
};
int main()
{
int input = 0;
scanf("%d ", &input);
switch (input)
{
case Mon:
//任务一
break;
case Tues:
//任务二
break;
case Wed:
//任务三
break;
case Thur:
//任务四
break;
case Fri:
//任务五
break;
case Sat:
//任务六
break;
case Sun:
//任务七
break;
default:
printf("输入非法,请重新输入!");
break;
}
return 0;
}
这样写有什么好处呢?往下走~
3.1.1为什么使用枚举?
1. 增加代码的可读性和可维护性
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
3. 防止了命名污染(封装)
4. 便于调试
5. 使用方便,一次可以定义多个常量
如何理解第二点呢?
enum Color
{
RED,//0
GREEN,//1
BLUE//2
};
#define RED 0
#define GREEN 1
#define BLUE 2
//用起来有什么区别
#define定义的RED实际上是一个符号而已,未来如果在代码里面遇到RED会直接替换成0的,而枚举里面的成员RED是有类型的(Color类型),更加严谨.
例如:
enum Day//星期
{
Mon = 1,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
int main()
{
enum Day a = 1;
return 0;
}
这合理吗?
显然1这个类型和a类型显然不匹配,如果给a赋值Mon就可以,这样写虽然再.c文件里不会报错,但是如果将文件改成C++,语法更加严谨,就会报错(如下图):
4.联合(共用体)
🎈联合也是一种特殊的自定义类型
🎇这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间
4.1 联合类型的声明和定义
例子
union Un//声明
{
char c;
int i;
};
//定义
int main()
{
union Un u;
return 0;
}
可能有人要问了,这不和结构体类型差不多嘛~
那你知道sizeof(union Un)是多少吗?
4.2联合大小的计算
例一如果按照结构体的大小计算,他是8个字节么?
实际不然,看一下运行结果:
哎呀,怎么是4呢?
如下代码:
union Un
{
char c;
int i;
};
int main()
{
union Un u;
printf("%d\n", sizeof(u));
printf("%p\n", &u);
printf("%p\n", &(u.c));
printf("%p\n", &(u.i));
return 0;
}
咱不妨再来观察一下他们的地址!
好家伙,原来三个地址是一样的,这时候可能你心里已经有想法了,三个地址一样,说明访问他们的起始位置的地点是一样的,共用一块空间;
实际上他们是这样开辟空间的:(如下图)
也正因为联合体也叫共用体,是因为他们也共用一块空间
可以思考这么一个问题:这两个成员变量可以一起使用吗?
{ 不可以,正因为他们共用同一块空间,更改c的值,i就变了,更改 i 的值c就变了 }
所以这里只占用4个字节。
但也要注意,联合体也存在内存对齐~
看下面这个例子:
union Un
{
char a[5];
int i;
};
//若创建这种类型的联合体变量,有多大?
注 意:这里的char a[5];只需要看他的类型char 类型即可,char类型占一个字节,默认对齐数是8,取较小值,所以对齐数是1,而i变量为int 类型,占4个字节,对齐数是4,但由于char a[5]占5个字节,联合体的大小要为最大对齐数的整数倍,故大小是8;(图解如下图)
4.3联合体的特点
🍞联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)
4.4面试题(判断机器大小端)
当你看见这样一段代码
int main()
{
int a = 0x11223344;
return 0;
}
当a要存这么一个数据,会将这个数据分成4分(11 22 33 44)分别是一个字节
可以回忆起大小端如下:
int main()
{
int a = 0x11223344;
//低地址--------------------高地址
//...[][][11][22][33][44][][][][][]...大端字节序存储模式
//...[][][44][33][22][11][][][][][]...小端字节序存储模式
//论一个数据,放在内存中的字节顺序
return 0;
}
解法一
那到底该怎么判断机器的大小端呢?
只需要取到这个变量的第一个字节的内容就可以判断,如果第一个字节内容是44,就是小端存储,如果是11就是大端存储;
但是怎么拿到第一个字节的内容呢?
不卖关子了, 实际上并不难,只需要拿出这个变量的地址,然后强制类型转化成char*类型,我们知道,指针类型决定访问的空间大小,所以强转成char*类型再解引用即可访问第一个字节的内容;
代码实现如下:
由于上述例子设计进制转化,这里可以换成一个操作起来容易的数;
这是博主曾经再写数据的存储这个文章提到的一种解决方法~
int check_sys()
{
int a = 1;
//00 00 00 01大端
//01 00 00 00小端
if (*(char*)&a == 0)
{
return 0;
}
else
{
return 1;
}
}
int main()
{
int ret = check_sys();
if (ret == 0)
{
printf("大端\n");
}
else
{
printf("小端\n");
}
return 0;
}
还有一种很好的解决方法:
解法二
😶🌫️ 回想刚刚写联合体定义与声明的这段代码
union Un//声明
{
char c;
int i;
};
//定义
int main()
{
union Un u;
return 0;
}
i 变量占4个字节,c占一个字节,并且共用一块空间;
那咱能不能巧妙利用以下这个结构呢?
看下图:
代码实现如下:
int check_sys()
{
union Un
{
char c;
int i;
} u;
u.i = 1;
return u.c;//直接返回第一个字节的内容
}
int main()
{
int ret = check_sys();
if (ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
欸你看,这个方法妙不妙~
码字不易~
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/124383.html