我们知道,C语言是允许我们自己来创造类型的,这些类型就叫做——自定义类型。

自定义类型又包括结构体类型,联合体类型还有枚举类型。

今天的文章,我们就着重讲解这其中的结构体类型。

目录

结构体的声明

1.1结构的基础知识

1.2结构的声明

1.3 匿名结构体的情况

1.4结构的自引用

1.5重命名匿名结构体的情况

1.6结构体变量的定义和初始化

1.7结构体内存对齐

1.8为什么存在内存对齐” />1.9我们可以耍些小聪明达到节省空间的效果。

2.1修改默认对齐数

2.2 结构体传参

3.1位段

3.2 位段的内存分配

3.3 位段的跨平台问题

结构体的声明

1.1结构的基础知识

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

1.2结构的声明

struct tag { memberlist; }variablelist;

我们以这种方式来描述一个结构体。下面是简单的示范,我们来描述一个学生:

struct Stu{ char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号}; //分号不能丢

定义局部变量和全局变量的关系:

#define _CRT_SECURE_NO_WARNINGSstruct Stu{char name[20];//名字int age;//年龄char sex[5];//性别char id[20];//学号}s1,s2,s3; //全局变量int main(){struct Stu s4;struct Stu s5;//局部变量return 0;}

1.3 匿名结构体的情况

也可以省略不写结构体标签,不过这样会导致一个结果,结构体只能定义一次类型。

#define _CRT_SECURE_NO_WARNINGS#includestruct {char name[20];//名字int age;//年龄char sex[5];//性别char id[20];//学号}s1; //全局变量struct {char name[20];//名字int age;//年龄char sex[5];//性别char id[20];//学号}*ps; //全局变量int main(){s1.age = 1;printf("%d", s1. age);return 0;}

在上述的代码中,体现为定义结构体变量s1之后,无法再次定义诸如s2,s3等结构体类型。

不过要是你本来就准备只用一次结构体的话,定义一个匿名结构体也不错就是了。

上面的两个结构在声明的时候省略掉了结构体标签, 那么问题来了?

//在上面代码的基础上,下面的代码合法吗?

ps=&s1;

答案是否定的,及时两个结构体里面的元素都相同,编译器也会他们当成两个完全不同的类型,所以是非法的。

1.4结构的自引用

我们想要使用结构体实现类似于链表的功能。

在结构中包含一个类型为该结构本身的成员是否可以呢?

#includestruct Node{int data;struct Node n;};int main(){return 0;}

我们开动小脑筋,立马就发现了错误。

struct Node这个节点它所占用的空间有多大呢?

它不仅要存放一个整形,还要存放一个n。

这就无限循环下去了,struct Node里面还有一个struct Node。

大小是无法得出的,这是一个错误示范。

我们转变战略,用指针来实现。

#define _CRT_SECURE_NO_WARNINGS#includestruct Node{int data;//4struct Node *next;//4/8};int main(){struct Node n1;struct Node n2;n1.next = &n2;return 0;}

创建两个节点n1,n2,把它们像链条一样串起来。

编译器没有报错,这样的写法是正确的,同时我们发现,struct Node的大小可以轻而易举地算出,我们得出一个结论:

不是在自己的类型里面包含一个自己类型的变量,而是在自己的类型里面包含一个自己类型的指针。这样的实现方式才是可行的。

1.5重命名匿名结构体的情况

下面的代码是否可行呢?

#includetypedef struct {int data;}S;int main(){return 0;}

可行,不过S不再是匿名结构体的变量,而是变成了匿名结构体类型。

怎么用呢?这么用:

#includetypedef struct {int data;}S;int main(){S s;s.data = 1;printf("%d", s.data);return 0;}

能用这种方式模拟实现上面的链表呢?

这样写行吗?

typedef struct {int data;Node* next;}Node;

不行,在没有重命名出Node时就调用了Node。

在这种情况下,我们只能老老实实地写出类型名了!

typedef struct Node{int data;    struct Node* next;}Node;

1.6结构体变量的定义和初始化

有了结构体类型,那如何定义变量,其实很简单。

 int x; int y;}p1; //声明类型的同时定义变量p1struct Point p2; //定义结构体变量struct Point{p2//初始化:定义变量的同时赋初值。struct Point p3 = {x, y};struct Stu    //类型声明{ char name[15];//名字 int age;   //年龄};struct Stu s = {"zhangsan", 20};//初始化struct Node{ int data; struct Point p; struct Node* next; }n1 = {10, {4,5}, NULL}; //结构体嵌套初始化struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化

1.7结构体内存对齐

我们已经掌握了结构体的基本使用了。 现在我们深入讨论一个问题:计算结构体的大小。 这就到了本文的重中之重: 结构体内存对齐。

计算以下的结构体大小。

#includeint main(){struct S1{char c1;int i;char c2;};printf("%d\n", sizeof(struct S1));//练习2struct S2{char c1;char c2;int i;};printf("%d\n", sizeof(struct S2));//练习3struct S3{double d;char c;int i;};printf("%d\n", sizeof(struct S3));//练习4-结构体嵌套问题struct S4{char c1;struct S3 s3;double d;};printf("%d\n", sizeof(struct S4));}

运行结果如下:

是不是跟想的完全不一样?

没错,结构体的大小并不是成员大小的简单相加,而是有自己的一套规则的。

  1. 结构体的第一个成员永远是放在零偏移处。
  2. 从第二个成员开始,以后每个对齐成员都要对齐到某个对齐数的整数倍处。
  3. 这个对齐数是成员自身大小和默认对齐数的较小值。
  4. VS中默认的值为8
  5. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
  6. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。如果不够,则浪费空间来对齐。

我们以s1为例子来试验一下上述规则,如图所示。

因为从第二个成员开始,以后每个对齐成员都要对齐到某个对齐数的整数倍处。

所以1,2,3三个字节被浪费,int类型的存储从4开始到7,char类型存到8处。

最后结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

S1中最大对齐数为4,8正好是4的整数倍,所以结构体S1的总大小为8。

再看S4的情况:

白色为浪费部分。

1.8为什么存在内存对齐” />1.9我们可以耍些小聪明达到节省空间的效果。

让占用空间小的成员尽量集中在一起。

//例如:struct S1{ char c1; int i; char c2;};struct S2{ char c1; char c2; int i;};

S1S2类型的成员一模一样,但是S1S2所占空间的大小有了一些区别。

2.1修改默认对齐数

我们可以通过#pragma pack()指令来修改默认对齐数。

#include #pragma pack(1)//设置默认对齐数为1struct S1{char c1;int i;char c2;};int main(){//输出的结果是什么?printf("%d\n", sizeof(struct S1));return 0;}

可以看到,答案不再是12,默认对齐数确实被修改了。

想要取消的话就引入一个空指令。

#include #pragma pack(1)//设置默认对齐数为1#pragma pack()//取消设置的默认对齐数,还原为默认struct S1{char c1;int i;char c2;};int main(){//输出的结果是什么?printf("%d\n", sizeof(struct S1));return 0;}

2.2 结构体传参

下面print1和print2那个比较好?

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;}

上面的 print1 print2 函数哪个好些? 答案是:首选print2函数。 原因:

  • 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
  • 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

所以结构体传参数时,要传结构体的地址。

3.1位段

结构体讲完就得讲讲结构体实现 位段 的能力。

struct A{ int _a:2; int _b:5; int _c:10; int _d:30;};

A就是一个位段的类型,位段可以控制所给的空间大小,达到节省空间的目的。

它所占空间是多大?

#include struct A{int _a : 2;int _b : 5;int _c : 10;int _d : 30;};int main(){printf("%d\n", sizeof(struct A));return 0;}

它占了8*8=64个比特位。

从16个字节优化到8个字节,位段的功能可以说是十分强大。

3.2 位段的内存分配

1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型 2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。 3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

#include //一个例子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;//空间是如何开辟的?return 0;}
  • 首先做一个假设,假设内存中的比特位是由右向左使用的。
  • 一个字节内部,剩余的比特位不够使用时,直接浪费掉。

我们猜想是这个样子。

转换成16进制为:

62 03 04

我们来调试看看:

我们的猜想是正确的!

3.3 位段的跨平台问题

1. int 位段被当成有符号数还是无符号数是不确定的。 2. 位段中最大位的数目不能确定。( 16 位机器最大 16 32 位机器最大 32 ,写成 27 ,在 16 位机 器会出问题。 3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。 4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是 舍弃剩余的位还是利用,这是不确定的。 总结: 跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。

这篇博客旨在总结我自己阶段性的学习,要是能帮助到大家,那可真是三生有幸!如果觉得我写的不错的话还请点个赞和关注哦~我会持续输出编程的知识的!