c语言基础知识——结构体

时间:2023-04-03 13:51:39

目录

前言

一、结构体的声明

1.结构的声明

2.特殊的声明

二、结构体的定义和初始化

1.定义

(1)声明类型的同时定义变量

 (2)先声明,在后面需要时再定义

2.初始化

(1)定义变量的同时赋值

 (2)结构体嵌套初始化

 (3)无顺序初始化

3.结构的自引用

三、结构体内存计算

1.结构体内存对齐

(1)对齐规则

(2)为什么存在内存对齐?

2.计算方法

(1)普通类型

(2)结构体嵌套问题

3.修改默认对齐数


前言

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

一、结构体的声明

1.结构的声明

struct tag
{
        member - list ;
} variable - list ;

例如,一个学生可以视为一个结构体,它具有名字,年龄,性别,学号等基本信息。可以表示如下:

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

这里千万要注意分号不要丢~ 

2.特殊的声明

在声明结构体的时候,也可以不完全声明。即可以匿名声明结构体。如:

//匿名结构体类型
struct
{
     int a;
     char b;
     float c;
}x;
struct
{
     int a;
     char b;
     float c;
}a[20], *p;

上面两个结构在声明的时候直接省略了名称,即上面的标签tag。

那么就会出现这样的问题: 这两个结构体是否相同呢?

我们用代码来检验一下:

p = &x;

 把上述几个代码放在一起运行,编译器就会出现警告:

从“*_”到“*_”类型不兼容。

也就是说,编译器认为这两个结构体是不同的类型,所以这样使用是非法的

二、结构体的定义和初始化

1.定义

结构体类型创建完毕,怎样定义一个变量呢?可以有以下两种方式:

(1)声明类型的同时定义变量

struct Point
{
     int x;
     int y;
}p1; //声明类型的同时定义变量p1

 (2)先声明,在后面需要时再定义

struct Point
{
     int x;
     int y;
};
struct Point p2;

2.初始化

初始化也可以有以下几种方式:

(1)定义变量的同时赋值

struct Stu        //类型声明
{
     char name[15];//名字
     int age;//年龄
};
struct Stu s = {"zhangsan", 20};//初始化

 (2)结构体嵌套初始化

struct Point
{
     int x;
     int y;
};
struct Node
{
     int data;
     struct Point p;
     struct Node* next; 
}n1 = {10, {4,5}, NULL};

 (3)无顺序初始化

比如上面n1的初始化,还可以这样写

n2 = {.data = 10, .next = NULL, .p = {4,5}};

3.结构的自引用

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

我们尝试一下以下代码:

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

这样的代码是否可行呢?

其实不可以,运行时编译器会报出这样的错误:“next”使用未定义的 struct“Node”。这是为什么呢?

原来在声明这个结构体类型时,在第二个类型处定义一个结构体,这个结构体又需要开辟两个类型的空间,而第二个类型又是一个结构体,这样无限循环下去,结构体根本不能完全声明。

那有没有一种方法能够在结构体内部调用本身类型的结构体呢?

答案是肯定的。直接包含结构体类型会无限开辟空间,那么我们只需用一个指针,指向这种类型的结构体就可以了。这样指针变量很容易开辟空间,结构体就能很好地定义出来。如下:

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

好了,学完匿名声明和自引用,有些人可能就会想到,能不能用下面这种方式来定义呢?

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

答案是否定的。因为Node这个结构体是结构体类型在声明完之后才能定义的,所以在结构体类型定义的过程中出现Node ,计算机无法解析,认为是未定义的符号。

 所以,如果要使用结构体自引用,就不能使用匿名声明。

三、结构体内存计算

现在我们已经掌握了结构体的使用方法。那么结构体的大小是多大呢?是不是声明时定义了的类型大小之和呢?(我这样说了,那就肯定不是,要不然我后面没啥讲了嘿嘿嘿)要弄清楚这个问题,我们就要研究一下结构体的内存对齐。

1.结构体内存对齐

(1)对齐规则

  • 第一个成员在与结构体变量偏移量为0的地址处。
  • 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
  • 对齐数 = 编译器默认的一个对齐数 与 该成员大小 之中的较小值 
  • 每个成员变量都有一个对齐数,结构体总大小为最大对齐数的整数倍。
  • 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

(2)为什么存在内存对齐?

1. 平台原因(移植原因)

不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特
定类型的数据,否则抛出硬件异常。
2. 性能原因
数据结构 ( 尤其是栈 ) 应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
举个例子,比如说计算机从开头一次读取四个字节,而如果有一个数据存储在第一个四字节和第二个四字节之间,那么读取这个数据就需要读取两次。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。

 不太理解?我们用实际例子来看一下结构体大小是如何计算的。

2.计算方法

(1)普通类型

struct S1
{
     char c1;
     int i;
     char c2;
};

结果是多少呢?我们按照对齐规则来分析。

 首先第一个成员在与结构体变量偏移量为0的地址处: 

c语言基础知识——结构体

 第二个成员是int类型,大小4个字节,vs编译器默认对齐数为8,4<8,因此它的对齐数为4。所以要对齐到4的整数倍的地址处,即int要从”4“这个地方开始存放。

c语言基础知识——结构体

 第三个成员类型是char类型,大小1字节,1<8,所以对齐数为1,char可以从”8“处开始存储。

c语言基础知识——结构体

 最后,判断一下最大对齐数,1=1<4,三个成员最大对齐数为4,目前结构体大小是9(0~8),而结果必须是4的倍数,所以结构体大小是16.(不是4的倍数,要扩展空间直到达到4的倍数)。学废了吗?学废了!

(2)结构体嵌套问题

观察下面这个结构体:

struct S2
{
 double d;
 char c;
 int i;
};
printf("%d\n", sizeof(struct S2));
struct S3
{
 char c1;
 struct S2 s2;
 double d;
};
printf("%d\n", sizeof(struct S3));

 首先,我们可以根据上面的规则直接算出s3大小为12。接着,我们来看最后一条规则:

如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

什么意思呢?接下来我将通过计算帮助大家理解。

首先s3第一个成员是c1,直接从0开始存储:

c语言基础知识——结构体

 接着开始存储s2。

结构体s2的成员有三个,对齐数分别是8、1、4,最大对齐数是8,因此结构体s2应该对齐到8的倍数处(嵌套的结构体对齐到自己的最大对齐数的整数倍处)。而s3大小为12,那么嵌套的结构体就应该这样存储:

c语言基础知识——结构体

 然后,存储成员d。大小为8字节,默认对齐数是8字节,因此对齐数就是8字节。从24偏移量开始存储:

c语言基础知识——结构体

最后,我们发现目前大小为32,但是规则中还有一句话: 结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。c1对齐数为1,s2对齐数为8,d对齐数为8,因此最大对齐数为8,而32恰好是8的倍数,不需要再扩展,因此最终结果是32。

我看到有人问了,如果成员的排列方式不一样,有没有可能结构体大小也不一样?

问得好!!!就让我们来对比一下下面这两个结构体:

struct S1
{
     char c1;
     int i;
     char c2;
};
struct S2
{
     char c1;
     char c2;
     int i;
};

来吧,算一下它们的大小。。。。。。。。。

算完了吗?公布答案,s1大小为12,s2大小为8。恭喜你回答正确。

咦?这两个结构体的成员好像一模一样诶。没错,虽然我们无法改变结构体浪费内存的现象,但是在设计结构体的时候,可以让占用空间小的成员尽量集中在一起,这样就可以既满足对齐,又能节省空间。 

3.修改默认对齐数

这个部分比较简单,使用#pragma这个预处理指令,就可以修改我们的默认对齐数。如:

#pragma pack(1)//设置默认对齐数为1
struct S1
{
     char c1;
     int i;
     char c2;
};

此处的s1与前面的s1一样,但是前面的s1大小是8,但是这里修改默认对齐数为1,大小变成了6。

修改默认对齐数要看具体使用情景,在结构体对齐数不合适的时候,我们可以自己修改默认对齐数。 

四、结构体传参

直接来看代码:

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函数更好。 想一想,为什么?

原因:

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的 下降。
通俗点来说,就是结构体有可能会非常非常大,如果传值调用,就需要在栈空间创建另一个相同的临时变量,大大浪费了空间。
所以,结构体传参的时候,最好传结构体的地址。
欢迎大家讨论,如果有问题感谢大家批评指正,也感谢大家的点赞关注,接下来我还会继续写完这部分的其他内容,我们下次再见!