进阶C语言——再识结构体


1 结构的基础知识

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

2 结构的声明

c 复制代码
struct tag
{
 member-list;
}variable-list;

如果下面来描述一个学生的话,我们会想到学生的姓名,成绩,性别等,那我们就可以用结构体来创建这样的一个变量包含这些内容。

c 复制代码
struct stu
{
	char name[20];
	int age;
	char sex[5];
	double score;
};

3 特殊的声明

在声明结构的时候,可以不完全的声明。

比如

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

这些结构体表示只能使用一次,因为没有结构体标签导致无法构成类型,所以匿名结构体只能使用一次。
注意!!!

匿名结构体竟然只能使用一次了,那我们有些操作就不能进行,比如

c 复制代码
//在上面代码的基础上,下面的代码合法吗?
p = &x;

编译器就会把他们当成两个完全不同的类型,所以p不能存放x的地址

4 结构的自引用

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

这里要注意不是包含结构体变量,而是包含该类型的结构体指针(是指针嗷,睁大你们的卡姿兰大眼睛看看),不能一个结构体里同样有这个类型的结构体!!!

那下面来看个例子吧

c 复制代码
//代码2
struct Node
{
 int data;
 struct Node* next;
}

next是结构体指针,指针类型是 struct Node。

错误代码

c 复制代码
struct Node
{
 int data;
 struct Node;
}

这就是错误的结构体里有结构体,我们是指针,不是变量。

下面在看一个代码

c 复制代码
//代码3
typedef struct
{
 int data;
 Node* next;
}Node;
//这样写代码,可行否?

这样相当于重定义一个匿名结构体,就相当于古代一位隐居诗人,又被你拉出来去打仗,你说这合理吗,虽然好像可能会合理,但肯定是少数,对吧!!

也相当于是先有鸡还是先有蛋,我们这个结构体还没创建好,就里面有指针,连结构体的类型也没有,我们就进行重命名,所以这肯定是错误的
正确代码

c 复制代码
typedef struct Node
{
 int data;
 struct Node* next;
}Node;

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

声明类型并创建变量

c 复制代码
struct stu
{
	char name[20];
	int age;
	char sex[5];
	double score;
}s1,s2;

声明结构体的时候就创建变量s1,s2.
初始化的时候赋值

c 复制代码
struct stu
{
	char name[20];
	int age;
	char sex[5];
	double score;
}s1,s2;
int main()
{
	struct stu s3 = { "张三",18,"男",99.9 };
	return 0;
}

结构体包含结构体的方法

c 复制代码
//初始化:定义变量的同时赋初值。
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 };//结构体嵌套初始化

6 结构体内存对齐

这是本章节最重要的内容,我会很详细的讲方法,并且举例子来说明

先看规则

如何计算?

首先得掌握结构体的对齐规则:

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

举个例子

c 复制代码
#include<stdio.h>
struct S1
	
{
	char c1;
	int i;
	char c2;
};
int main()
{
	printf("%zd\n", sizeof(struct S1));
}

因为我们的开始都是从零开始的,那结构体中的char就是只占一个字节,然后根据我们的第二条规则,整型的最小对齐数是4,那我们整型开始必须是4的倍数才行,那我们在4的位置往下四个字节大小到7的的位置,再来一个char类型刚刚好到8的位置,但是我们总的大小必须是最大对齐数的整数倍,这里的对齐数是结构体的最大对齐数,所以就是4的倍数,那就是12,刚刚好到11的位置,所以所占大小就是12.

我们看看结果

那我们再来一个例子更深刻的理解

c 复制代码
struct S3
{
	double d;
	char c;
	int i;
};
#include<stdio.h>
int main()
{
	printf("%d\n", sizeof(struct S3));
}


和上面的解释其实一样,一开始都是从0开始的,然后就是double是八个字节大小的内存,因为和我们的默认对齐数是一个值,那对齐数就是8,一个char占一个字节,接下来的int 最小对齐数是4,所以我们要再12的我位置开始,刚刚好总共16个字节,也是我们最大对齐数double的整数倍,所以答案就是16.

再讲一个嵌套结构体算大小,相信大家肯定也会算了

c 复制代码
struct S3
{
	double d;
	char c;
	int i;
};

struct S4
{
	char c1;
	struct S3 s3;
	double d;
};
#include<stdio.h>
int main()
{
	printf("%d\n", sizeof(struct S4));
}


所以到31的位置举例0刚刚好是32个,是我们最大对齐数8的倍数,所以答案就是32

为什么存在内存对齐?

我们分为两种原因,一个是平台问题,一个是性能问题

  1. 平台原因(移植原因):
    不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特
    定类型的数据,否则抛出硬件异常。
  2. 性能原因:
    数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
    原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访

结构体的内存对齐是拿空间来换取时间的做法

那我们在设计结构体的时候应该怎样设计才能保证既节省空间又节省内存呢

我们可以这样设计,将小的字节放在前面,比如char类型的先放,其次是short这种,这样就能节省空间了

比如下面两个结构体的成员列表是相同的,但是他们的字节大小却不同

c 复制代码
//例如:
struct S1
{
 char c1;
 int i;
 char c2;
};
struct S2
{
 char c1;
 char c2;
 int i;
};

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

7 修改默认对齐数

c 复制代码
#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;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
	//输出的结果是什么?
	printf("%d\n", sizeof(struct S1));
	printf("%d\n", sizeof(struct S2));
	
		return 0;
}

我们一般设置都是2的几次方,而不是像1 3这种完全没有规律的,就比如在我们32位机器的平台下,我们一次读四个字节大小的,如果是5这种没有规律的反而要读两次,这样效率没有提升。

8 结构体传参

结构体传参的时候其实和我们的函数传参有异曲同工的作用,在传参的时候也是在栈上开辟空间,也遵循我们经常说的形参是实参的一份临时拷贝

c 复制代码
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好

答案是2好

解释

因为我们在结构体传参,如果是print的话,会继续开辟空间来存放,这会占掉很多空间,而我们的print2的话空间最多是8在64位平台上的话,因为我们传的是一个地址,地址就是一个指针变量的大小,那就是4个字节或者八个字节,所以传地址更好

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。

如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的

下降。

今天的分享就到这里,我们下期再见,谢谢大家!!!

相关推荐
Fu_Lin_几秒前
《Qt嵌入式从零基础到精通》前言与阅读指南
开发语言·qt
名字还没想好☜4 分钟前
Go 的 select 实战:超时、非阻塞收发与优雅退出的三个套路
开发语言·数据库·golang·go·并发
geovindu5 分钟前
java: Backtracking Algorithm
java·开发语言·windows·后端·算法·回溯算法
Scott9999HH9 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社10 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海10 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖11 小时前
JDK 26 新特性详解
java·开发语言
马优晨11 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区13 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大13 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf