赞
踩
关于C语言,在指针学习过程中,在学习之余,做了一次总结,写成了一篇详解博客,也向大家分享的我对于指针的见解,我发现将所学的内容写成博客,不仅仅可以使和我一样的初学者们更快的了解相关知识点,还可以让我查缺补漏,弥补自己的短板,让我的基础更加扎实,所以在学完结构体以后,我也将结构体的内容整理成一篇博客,向大家分享。
当我们需要表达一个数据的时候,我们就需要用到变量,而变量又需要定义一个类型。我们通过之前的学习,知道了C语言中变量类型有:int、double、char、float等等基础类型,还有指针等等。但是如果我们想表达的数据比较复杂,不是一个数据,例如:日期(年、月、日)、学生信息(姓名、性别、年龄等等)、时间(时、分、秒)等等。而我们又想用一个整体来表达这些所有的数据,这个时候我们就需要用到一个自定义变量类型——结构体。
结构体是C语言中一种重要的数据类型,该数据类型由一组称为成员(或称为域,或称为元素)的不同数据组成,其中每个成员可以具有不同的类型。结构体通常用来表示类型不同但是又相关的若干数据。
注意:结构体是一种数据类型!!!
成员变量:结构是一些值的集合,这些值称为成员变量。
结构体的每个成员可以是不同类型的变量。
结构体的定义如下所示,struct为结构体关键字,tag为结构体的标志,member-list为结构体成员列表,其必须列出其所有成员;variable-list为此结构体声明的变量。
struct tag
{
member-list
}variable-list;
例1:描述一个学生信息
#include<stdio.h> //声明一个结构体类型 //声明一个学生类型,是想通过学生类型来创建学生变量(对象) //描述学生:属性·名字+电话+性别+年龄 struct Stu { char name[20];//名字 char tele[12];//电话 char sex[5];//性别 int age;//年龄 }s4,s5,s6;//分号不能丢 struct Stu s3;//s3,s4,s5,s6为全局变量 int main() { //创建结构体变量 struct Stu s1; struct Stu s2; //s1,s2为局部变量 return 0; }
易错提示:
一定不要忘记结束时的分号!!!
在声明结构的时候,可以不完全的声明
例2:匿名结构体类型
#include<stdio.h> struct { int a; char b; float c; }x; struct { int a; char b; float c; }* p; int main() { p = &x;//错误 E0513 不能将 "struct <unnamed> *" 类型的值分配到 "struct <unnamed> *" 类型的实体 return 0; }
上面的两个结构在声明的时候省略了结构体的标志(tag)。(是正确的)
上述代码的问题出现在:p = &x。//这是非法的。
上面的两个匿名结构体虽然各自的成员是一模一样的,但是在编译器看来,它们是两个不同的类型,所以出现了报错(部分编译器是警告)。
结构体的自引用就是指在结构体内部,包含指向自身类型结构体的指针。
在之前的学习中,我们知道了在函数中可以包含自己(即递归),那么在结构中包含一个类型为该结构体本身的成员是否可以呢?
例3:
#include<stdio.h>
struct Node
{
int data;//4
struct Node n;//4+套娃
};
int main()
{
sizeof(struct Node);
return 0;
}
运行结果:
错误 C2460 “Node::n”: 使用正在定义的“Node”
这是为什么呢?因为n定义中又有n,无限循环,系统无法确定该结构体的长度,会判定定义非法。
切记,结构体自引用,成员定义只能是指针。
例4:正确的自引用
#include<stdio.h>
struct Node
{
int data;//4
struct Node* n;//4/8
};
int main()
{
int a = sizeof(struct Node);
printf("%d", a);
return 0;
}
运行结果为:8(因为博主是32位)
这是为什么呢?因为我们在自引用时把结构的成员定义为指针,又指针的长度是确定的(上一节指针详解中提到过),所以此时结构体的长度也是确定的。
这时候可能有人又有疑问了,我们刚刚学了一种特殊的声明方式——匿名,那在自引用的时候,我们可不可以使用匿名了,就拿例4来举例子,因为这时候,我们使用了匿名结构,所以里边使用“Node* n;”不就好了吗?我们来看看,结构是怎样的:
例5:
#include<stdio.h>
struct
{
int data;
Node* n;
}Node;
int main()
{
int a = sizeof(struct Node);
printf("%d", a);
return 0;
}
运行结果为:一大堆报错
这是为什么呢?因为我们在声明结构体的内部,就使用了Node这个变量,但是我们的编译器是在声明结构体结束以后,才接收到Node这个变量,所以,在使用Node变量的时候,编译器无法识别,就自然会出现错误。
建议:在使用结构体自引用的时候最好不要使用匿名声明结构体。
前面我们了解了如何声明结构体的类型,现在我们有了结构体类型,那么我们要如何定义一个结构体变量以及初始化一个结构体变量呢?其实非常简单。
例6:
#include<stdio.h>
struct Stu
{
char name[20];
int age;
char sex[5];
}s1;
int main()
{
struct Stu s2 = { "lisi",18,"nan" };
printf("%s %d %s", s2.name[20], s2.age, s2.sex[5]);
return 0;
}
运行结果为:
lisi 18 nan
PS:这里博主用的是Visual Studio 2019
通过struct+结构体的标志(tag)+变量名,就完成了结构体的定义;而在{}内把结构体成员对应的类型用逗号隔开赋值给声明的结构体,我们就完成的结构体的初始化。
刚刚我们了解了结构体的自引用,了解了结构体内是可以存在结构体的,也就是结构体的嵌套,现在我们了解了单一的结构体如何定义和初始化,那有人就会想了,嵌套结构体如何进行定义和初始化呢?
例7:
#include<stdio.h> struct T { int c; double weight; }; struct Stu { char name[20]; struct T p; int age; char sex[5]; }; int main() { struct Stu s = { "lisi",{30,1.0},18,"nan"}; printf("%d %lf",s.p.c,s.p.weight); return 0; }
运行结果为:30 1.000000
在结构体中遇到结构体,我们在初始化的时候,同样的方法在外层结构体的{}内再添加一个{}即可。
注意:嵌套结构体在调用的时候,逐层调用。
通过前面的学习,我们已经掌握了结构体的基本使用了。
有人就又会问了,结构体是变量,那变量就有大小啊,我们如何计算结构体的大小呢?
这里就涉及到了一个热门的考点:结构体内存对齐。
先来做一道练习题:
例8:
#include<stdio.h> struct s1 { char c1; int a; char c2; }; struct s2 { char c1; char c2; int a; }; int main() { struct s1 s1 = { 0 }; printf("%d\n", sizeof(s1)); struct s2 s2 = { 0 }; printf("%d\n", sizeof(s2)); return 0; }
运行结果为:
12
8
大家第一次拿到这个题,肯定会想:这有什么好算的,不就是6、6吗?但是结构体的大小计算不是这样随便计算的,它需要符合一定的条件。
那么到底如何计算呢?我们需要利用结构体对齐规则:
①第一个成员在与结构体变量偏移量为0的地址处。
②其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值
提示:VS中默认的值为8
③结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
看完对齐规则,我们回到例8
㈠先看到struct s1这个结构体
第①步 结构体存放变量从偏移量为0的位置开始:
就是从图中0开始,又因为c1是一个字节,所以c1就是0 ~ 1。
第②步 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处:
对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值
a的对齐数为4,所以a要放到4的整数倍的地址处,所以a从4开始,又因为a是4个字节,所以a就是4 ~ 8;
到这里有人就会问了:那中间的1 ~ 4怎么办,中间这部分就浪费掉了。
c2的对齐数为1,c2是1个字节,所以从8开始,c2是8~9。
第③步 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
对于这个结构体中的成员:
c1的对齐数为1,a的对齐数为4,c2的对齐数为1,那么最大对齐数就是4;
而现在我们的一共用了9个字节,9不是4的整数倍,所以我们还要再浪费3个字节,达到4的整数倍12个字节。
㈡再看到struct s2这个结构体
第①步 结构体存放变量从偏移量为0的位置开始:
就是从图中0开始,又因为c1是一个字节,所以c1就是0 ~ 1。
第②步 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处:
对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值
c2的对齐数为1,所以c2要放到1的整数倍的地址处,所以c2从1开始,又因为c2是1个字节,所以c2就是1 ~ 2;
a的对齐数为4,所以a要放到4的整数倍的地址处,所以a从4开始,又因为a是4个字节,所以a就是4 ~ 8。
第③步 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
对于这个结构体中的成员:
c1的对齐数为1,c2的对齐数为1,a的对齐数为4,那么最大对齐数就是4;
现在我们一共用了8个字节,8是4的整数倍,所以这个结构体的大小就是8个字节。
趁热打铁,再来一道练习题:
例9:
#include<stdio.h>
struct s3
{
double a;
char b;
int c;
};
int main()
{
printf("%d\n", sizeof(struct s3));
return 0;
}
运行结果为:16
你做对了吗?如果没做对,没关系重新来过,再温习一遍例题;如果做对了,是不是成就感满满,但是别急,下面还有更难的!
在思考每一个问题的同时,不要忘记我们学过的结构体是可以嵌套的。但是不要担心,我们的对齐规则考虑到了这种情况:
④如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
来看一道例题:
例10:
#include<stdio.h> struct s3 { double a; char b; int c; }; struct s4 { char c1; struct s3 s3; double d; }; int main() { printf("%d\n", sizeof(struct s4)); return 0; }
运行结果为:32
还是按照步骤来解题:
第①步 结构体存放变量从偏移量为0的位置开始:
就是从图中0开始,又因为c1是一个字节,所以c1就是0 ~ 1。
第②步 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处,
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处:
对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值
对于struct s3这个结构体:
a的对齐数为8,b的对齐数为1,c的对齐数为4,所以最大对齐数为8;
所以s3要放到8的整数倍的地址处,所以s3从8开始,又因为s3是16个字节(例9),所以s3就是8~24;
d的对齐数为8,所以a要放到8的整数倍的地址处,所以a从24开始,又因为a是8个字节,所以a就是24 ~ 32。
第③步 结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
对于这个结构体中的成员:
c1的对齐数为1,s3的对齐数为8,d的对齐数为8,那么最大对齐数就是8;
现在我们一共用了32个字节,32是4的整数倍,所以这个结构体的大小就是32个字节。
①第一个成员在与结构体变量偏移量为0的地址处。
②其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值
提示:VS中默认的值为8
③结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
④如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
大家在对于对齐规则的学习中,肯定会有这样的疑问:
我们在对齐的过程中,浪费了那么多空间,那为什么还要存在内存对齐呢?
①平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
②性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于:为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
即 结构体的内存对齐是拿空间来换取时间的做法。
前面我们了解到:内存对齐是拿空间来换取时间的做法。
那么我们如何做到既要满足内存对齐,又要节省空间呢?
让占用空间小的成员尽量集中在一起。
举个例子:
例11:
struct s1
{
char c1;
int a;
char c2;
};
struct s2
{
char c1;
char c2;
int a;
};
这里s1和s2类型的成员是一模一样的,但是s2占用的空间比s1小。
在C语言中默认对齐数是可以修改的,利用 #pragma 这个预处理命令,就可以改变默认对齐数。
举一个例子:
例12:
#include<stdio.h> struct s1 { char c1; double a; }; #pragma pack(4) //设置默认对齐数为4 struct s2 { char c1; double a; }; #pragma pack() //取消设置的默认对齐数 int main() { printf("%d\n", sizeof(struct s1)); printf("%d\n", sizeof(struct s2)); return 0; }
运行结果为:
16
12
这里可以看到:
s1中,存放a的时候对齐数为8,所以a从8开始,又因为a是8个字节,所以a是8~16,所以s2是16个字节;
我们将默认对齐数修改为4的时候,s2中,存放a的时候对齐数为4,所以a从4开始,又因为a是8个字节,所以a是4~12,所以s2是12个字节。
offsetof()函数是用来返回结构体成员的偏移量。
使用offsetof()函数时,需要加上 #include<stddef.h> 这个头文件
offsetof(variable-list,member-list)
举个例子:
例13:
#include<stdio.h>
#include<stddef.h>
struct s
{
char c;
int a;
double b;
};
int main()
{
printf("%d\n", offsetof(struct s, c));
printf("%d\n", offsetof(struct s, a));
printf("%d\n", offsetof(struct s, b));
return 0;
}
运行结果为:
0
4
8
直接上例子:
例14:
#include<stdio.h> struct s { char c; int a; double b; }; void func1(struct s p) { p.a = 100; p.b = 3.14; p.c = 'w'; } void func2(struct s* p) { p->a = 100; p->b = 3.14; p->c = 'w'; } //传值 void print1(struct s tmp) { printf("%d %lf %c\n", tmp.a, tmp.b, tmp.c); } //传址 void print2(struct s* tmp) { printf("%d %lf %c\n", tmp->a, tmp->b, tmp->c); } int main() { struct s s = { 0 }; func1(s); print1(s); print2(&s); func2(&s); print1(s); print2(&s); return 0; }
运行结果为:
0 0.000000
0 0.000000
100 3.140000 w
100 3.140000 w
通过例14,我们可以看出func1进行传参,只是形参,func2进行传参,传的是地址;同样print1是传值,而print2是传址。两种传递方法都可以,但是我们更加提倡以地址的形式进行传递,因为这样是以指针的形式传递,无论结构体有多大,指针的大小均为4/8。
位段的声明和结构是类似的,有两个不同:
①位段的成员必须是int、unsigned int 、 signed int 或 char。
②位段的成员名后边有一个冒号和一个数字。
来做一道题:
例15:
#include<stdio.h> //位段 - 二进制位 struct s { int a : 2;//2个bit int b : 5;//5个bit int c : 10;//10个bit int d : 30;//30个bit }; int main() { struct s s; printf("%d\n", sizeof(s)); return 0; }
运行结果为:8
这里大家就会猜测说:2+5+10+30=47bit,那不应该是6个字节吗?为什么是8个字节啊,这是因为位段也有它的规则。
①位段的成员可以是int、unsigned int 、 signed int 或 char(属于整形家族)类型。
②位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。
③位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
现在我们再来看例15,:
a,b,c一共需要17个bit来存放,这时,需要开辟4个字节(32bit)的空间来存放;但是剩下的15个bit不足以存放d,所以就需要再开辟4个字节(32bit)的空间来存放d。(剩余的空间浪费了)
所以共需8个字节。
①int位段被当成有符号数还是无符号数是不确定的;
②位段中最大位的数目不能确定;(16位机器最大16,32位机器最大32)
③位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义;
④当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
跟结构体相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
概念:一一列举,把可能的取值一一列举。
举一个例子:
例16:一个人的性别
#include<stdio.h>
//枚举类型
enum Sex
{
//枚举的可能取值
MALE,//男性
FEMALE,//女性
SECRET//秘密
};
int main()
{
enum Sex s = MALE;
printf("%d %d %d\n", MALE, FEMALE, SECRET);
return 0;
}
运行结果为:0 1 2
注意:在定义枚举时,我们可以随意定义,但是如果没有赋值,会默认为0,1,2,……,同时枚举作为一个常量,我们无法在定义完成后进行修改。
①增加代码的可读性和可维护性;
②和 #define 定义的标识符比较枚举,枚举具有类型检查,更加严谨;
③防止了命名污染(封装);
④便于调试;
⑤使用方便,一次可以定义多个常量。
再举一个例子:
例17:
#include <stdio.h>
enum DAY
{
MON = 1, TUE, WED, THU, FRI, SAT, SUN
};
int main()
{
enum DAY day;
day = MON;
printf("%d", day);
return 0;
}
运行结果为:1
联合体也是一种特殊的自定义类型。这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
来看一个例子
例18:
#include<stdio.h> 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)); }
运行结果为:
4
004FFD3C
004FFD3C
004FFD3C
这也说明了:联合体的成员公用同一块空间。
联合体的成员是公用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合体至少得有能力保存最大的那个成员)。
前面的结构体和枚举都有自己的规则,那联合体也不例外:
①联合体的大小至少是最大成员的大小;
②当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
判断当前计算机的大小端存储。
例19:
#include<stdio.h> int check_sys() { int a = 1; //返回1表示小端 //返回0表示大端 return *(char*)&a; } int main() { int a = 1; int ret = check_sys(); if (1 == ret) { printf("小端\n"); } else { printf("大端\n"); } //int a = Ox11223344; // //低地址---------------------------------------------->高地址 //...[][][][11][22][33][44][][][][]... 大端字节序存储模式 //...[][][][44][33][22][11][][][][]... 小端字节序存储模式 //讨论一个数据,放在内存中的存放的字节顺序 //大小端字节序问题 // return 0; }
运行结果为:小端
制作学生管理系统
这个博主现在正在研究,也欢迎大家来交流,因为目前能力所限,这个到时候会再写一篇博客,专门说明这个。
不知不觉,结构体的内容已经结束了,博主从晚上七点奋战至凌晨四点,不得不感叹道:时间过的真快的。学习的时光总是美好的,每天能学到新的知识就会感到很充实,做这个博客的原因不光是想查缺补漏一下,更多的是想帮助那些初学者,让他们能够很快理解这些知识点,一起加油。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。