接下来我们来了解C语言中很重要的内容:结构体。虽然到现在我们可以创建常量,变量,数组,但是存储的都是相同类型的数据,如果我们需要写入不同数据类型的信息怎么办,例如常见的身份证上的信息,有身份证号,有地址,有名字,有照片。又比如一个学生的学习,有学号,姓名,年龄,等等。这样的话,如果我们还是以前那样一个数据创建一个的话,岂不是很麻烦,当我们需要将不同数据类型存储在一起的时候这就引出了 结构体。
大家可以先看一下下面的图片,大概了解结构体长什么样子:
struct Stu
//struct创建结构体的必要前提
//stu结构体名字
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};//结尾必须这样写
int main()
{
struct Stu s = { "张三", 20, "男", "20230818001" };//按照结构体成员的顺序初始化
printf("name: %s\n", s.name);
printf("age : %d\n", s.age);
printf("sex : %s\n", s.sex);
printf("id : %s\n", s.id);
return 0;
}
这样大家大概知道结构体是什么样子了吧。
结构体的创建与初始化
那接下来我们就正式的开始学习今天的主题吧,首先我们要学习如何创建结构体。但其实结构体的创建是很简单的,首先你需要在你要引用结构体的前面创建(这个肯定都能理解,毕竟要用肯定要有,才能使用)接着就是写出以下的内容:
我们只需要依照上面的图片一样,先写出结构体的标志struct然后一个{}(注意必须在结尾的括号后添加一个; 表示结构体到这里就结束了),在{}中写入结构体(也就是需要的多种类型)。
看了上面的内容大家应该知道结构体的创建了吧,接下来我们就学习如何将创建的结构体初始化。
这样大家应该了解的差不多了吧,将结构体的第一行抄下来后在后面再写一个名字后就可以结构体赋值了,但是需要注意的是赋值的顺序必须与创建结构体的顺序一样。
结构体可以不完全声明
学习了上面的知识后,大家应该认为结构体都是这样的了吧,但其实嘞,结构体还有特殊的。大家可以看一下下面的代码,大家认为是否有问题。
struct
{
int a;
char b;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
其实这些代码是没有问题的,开始我们讲了在struct后写的是结构体的名字,但是,大家知道现在名字可以改的吧。结构体有名字那么肯定也可以改吧。所以在结构体的最后的括号后再的话就可以重新命名。以上是结构体不完全声明。上⾯的两个结构在声明的时候省略掉了结构体标签(tag)。虽然都是没有错误的。匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次。所以我们写结构体的时候尽量按规定创建结构体,不要炫技。
结构体里面包含另外一个结构体
好了,当我们了解结构体不完全声明后的利害之处后。我们大家想一下结构体可不可以包含另外一个结构体,像数组那样。答案当然是肯定的,在一个结构体中可以包含另外一个结果体,并且可以同时对两个结构体初始化。
这里大家看到了吧,在一个结构体中引入另外一个结构体,首先需要将被包含的结构体写在包含结构体的前面。接着在包含结构体中写入结构体,在写被包含结构体的时候,需要将被包含结构体初始化的名字写出来。不知道大家是否了解了。
那么接下来的结构体初始化的话,其实与我们平常的结构体初始化是差不多的,我们只需要在要初始化被包含结构体的时候,再写一个{},然后在其中初始化被包含结构体就可以了。
结构体自引用
好了,我们学习了结构体引用引用结构体,那我们可不可以结构体自己引用自己嘞。这就好比说,自己说自己好帅,自己一会就会捡到100块钱一样。那么结构体可以自引用吗?当然是可以的。
struct Node
{
int data;
struct Node next;
};
大家觉得这个正确吗?如果正确的话。sizeof(struct Node)结果是什么样的嘞。如果真的去尝试的话,大家会发现,系统会报错的。因为⼀个结构体中再包含⼀个同类型的结构体变量,这样结构体变量的大⼩就会⽆穷的⼤,是不合理的。所以上面的引用是错误的。正确的自引用方法是:
struct Node
{
int data;
struct Node *next;
};
提前学习过的应该知道一个指令typedef(这是一个修改名字的指令)如:
经过上面的操作后,我们就给int取了一个名字mun了,当然int还是可以继续使用的。那么我们可以将这个指令引入结构体中吗?答案是可以的,虽然可以使用但却很容易出现问题。比如大家可以看一下下面的图片大家思考一下下面的图片是否正确:
typedef struct
{
int data;
Node* next;
}Node;
大家认为这个代码是正确的吗?大家可以看到我在使用typedef的时候并未对结构体命名,那么这个结构体就是前面说的匿名结构体。Node是对前⾯的匿名结构体类型的重命名产⽣的,但是在匿名结构体内部提前使⽤Node类型来创建成员变量,所以这是不⾏的。
那么解决犯法是怎么搞呢?其实我们只需要定义结构体不要使⽤匿名结构体了,那具体在结构体使用typedef的正确方法是什么样子的嘞:
typedef struct Node
{
int data;
struct Node* next;
}Node;
结构体大小计算
我们学习到现在已经掌握了结构体的基本使用了,那我们就进一步,来讨论如何计算结构体的大小。计算结构体大小的话,就不得不了解一个知识点:结构体的内存对齐。那么结构体内存对齐规则大致是下面这四条:
我们直接举一个例子来分析吧,大家看一下下面的结构体,并且可以猜一下下面的结构体大小是多少?
大家想到了吗,答案是24,大家可能会想啊,不就是1+8+1=10吗。怎么等于24啊。这可不是我瞎说的啊。大家先看一下代码运行下来是什么样子的
是吧,结果就是24,那为什么会是24呢?结构体的对齐规则说过在结构体中用结构体成员中最大的当对齐数,如果大于了默认的对齐数的话,结构体对齐数就变成了默认数。我们看上面的代码,最大的是double(8)而默认数是也是8.那么这个对齐数不会改变。那么接下来我们来排一下,因为对齐规则说过,堆放都是从0开始,那么char一个字节,那么占第一个,但是对齐数个字节呀,1,2,3,4,5,6,7都不是8的倍数啊,所以double只有在第8个位置安放才符合规则。然后double本身8个字节那么现在就用了16个字节了,但有7个字节是空的,因为对齐规则跳过了。最后还有一个char,17.18.19.20.21.22.23也不是8的倍数,那么char只有在第24个字节的时候储存,那么现在就是24个字节,但是浪费了14个字节。大家可以看一下下面的图片
这样大家是否了解了一些了,那我们再举一个例子:
大家可以思考一下上面的结果是多少?因为这个与我们第一个讲的是差不多的只是换了个位置,但是结果却不一样哈。好了,答案是16。为什么嘞,我在给大家讲一下。1. 第一个是char类型,后面每个数据成员存储的起始位置要从该成员大小的整数倍开始算(也就是1的倍数,故char num2 的起始位置是1,double num3的起始位置是2)。\n2.判断当前总内存(10)是否是最大成员内存(8)的整数倍,如果不是需要补齐到满足最大成员内存的最小整数倍(2倍,16)。\n3.故double num3的起始位置修改为8,char num1,char num2,补齐到8个内存,故结构体所占内存为16。这次大家应该就知道了吧。
好了,我们再讲一个特殊一点的,嵌套结构体如何计算例如:
规则中将嵌套结构体的对齐数,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。也就是说上面的对齐数是8,。如果s3的最大改为4,那么对齐数也会改变。那么这个代码结果是多少嘞?
那我们分析一下,1.struct S1的内存为16(char b,int c一起补齐8个,共补3个)。\n2.S2中嵌套结构体S1(结构体成员要从其内部最大元素大小的整数倍地址开始存储),当前S1中最大元素大小为8,S2中double也是8,故S2最大对齐数是8的倍数\n3. char num补齐到8,结构体的总内存为(8+16+8=32)。好了那么大家应该知道如何计算结构体大小了吧
为什么存在内存对齐?
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对⻬是拿空间来换取时间的做法。反正呀,我个人认为知道有这个事情就可以了,大佬的事情我们不知道我们也不敢问。
修改对齐数
#pragma pack,这个预处理指令,可以改变编译器的默认对⻬数。那我们也不废话直接上代码
在代码的前面#pragma pack(),括号里面就是修改的对齐数。并且我们都知道做事要有始有终,我们既然修改对齐数,那么我们后面肯定要将对齐数改回来呀,所以我们在结构体末尾写上#pragma pack(),那么对齐数就变回去了。
结构体传参
我们创建了结构体,使用肯定不能像开头那样直接打印一下,我们可以将结构体内容传出去,就是传参嘛。大家可以看一下下面的代码,虽然结构是一样的,但是大家认为哪一种更好一些嘞?
肯定是选着第二个代码,为什么嘞?因为函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。
总结:结构体传参的时候,要传结构体的地址。
位段
大家应该很少听过这个词汇吧。
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
上面的A就是⼀个位段类型。,那么位段是定义是什么嘞
我们如何计算位段大小,大家可以看到上面代码后面都跟着一个数字,那个就是给成员分配的比特位,比如说a就分配了5个比特位,b就是5,c是10,d是30。
位段的内存分配
1.位段的成员可以是 int unsigned int signed int 或者是 char 等类型
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。
位段的跨平台问题
1. int位段被当成有符号数还是⽆符号数是不确定的。
2. 位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会
出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃
剩余的位还是利⽤,这是不确定的。
总结:
跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
位段的应⽤
下图是⽹络协议中,
IP数据报的格式,我们可以看到其中很多的属性只需要⼏个bit位就能描述,这⾥使⽤位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报⼤⼩也会较⼩⼀些,对⽹络的畅通是有帮助的。
位段使⽤的注意事项
位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。
所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊放在⼀个变量中,然后赋值给位段的成员。
struct A
{
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
struct A sa = {0};
scanf("%d", &sa._b);//这是错误的
//正确的⽰范
int b = 0;
scanf("%d", &b);
sa._b = b;
return 0;
}
好了,以上就是鄙人想与大家分享的关于结构体的知识了,肯定还有很多不足甚至错误的地方,希望大家可以在评论区中写出来,以便改正!