【C语言学习】结构体详解

一.结构体类型和结构体变量

结构体和结构体变量是两个不同的概念(一个抽象、一个具体)。结构体类型是一种数据类型,它和int、char、double是一样的,只不过这个类型需要我们人为进行定义(比如结构体名称、结构体里面变量名、变量类型)。
结构体变量是我们根据我们先创建的结构体类型来创建的,它里面存放的是具体数据,这些数据和结构类型里面的数据类型是相对应的。

结构体类型 结构体变量
不同类型的数据组合形成一个整体 基于结构体类型创建的具体变量,具体实例
不占用实际的内存空间 会占用实际的内存空间

二.结构体类型声明

类型声明就是定义结构体类型的过程,定义结构体需要用到关键字struct。

2.1 结构体类型的一般声明

c 复制代码
//定义一个名为student的结构体类型
struct student
{
	char name[20];//姓名
	int age;//年龄
	float height;//身高
	float weight;//体重
}; //;一定不能忘

2.2 结构体的特殊声明

特殊声明有两种:匿名结构体、嵌套结构体

2.2.1 匿名结构体声明

c 复制代码
struct 
{
	int a;
	char b;
	float c;
}x; //这里的x就是结构体变量,并且是全局变量

对于匿名结构体,如果类型重命名的话,基本上只能使用一次。

c 复制代码
struct
{
	int a;
	char b;
	float c;
}x;
struct
{
	int a;
	char b;
	float c;
}* p;

int main()
{
	p = &x;
	return 0;
}

运行结果:

编译器会把上面的两个声明当成完全不同的两个类型(显然p和&x的类型应该是一样的),所以是非法的。

2.2.2 嵌套结构体声明

c 复制代码
//嵌套结构体,顾名思义,就是在定义一个结构体类型时,里面包括另外类型的结构体变量
//先定义一个名为Date的结构体类型
struct Date
{
	int year;
	int month;
	int day;
};

//再定义一个名为person的结构体类型
struct person
{
	char name[20];//名字
	int age;//年龄
	float height;//身高
	float weight;//体重
	struct Date birthday;//名为birthday的结构体变量,类型是struct Date
};

2.2.3 结构体自引用

结构体自引用是指在定义结构体类型时,使用该结构体本身的类型作为成员类型,从而形成一种递归或者循环的数据结构。包括直接自引用和间接自引用两种形式,但是直接自引用通常不可以,因为编译器要确定结构体的大小,直接自引用会让结构体无限递归下去。

c 复制代码
struct Node
{
	int data;
	struct Node next;//无法确定大小
};
c 复制代码
//修改后:
struct Node
{
	int data;
	struct Node* next;//指针大小为4/8个字节
};

三、结构体变量创建和初始化

c 复制代码
//全局变量
struct student
{
	char name[20];//姓名
	int age;//年龄
	float height;//身高
	float weight;//体重
} x = {{"xiao ming"}, {18}, {170.25}, {59.6}}; //x是结构体变量名,等号右边是初始值

struct
{
	char name[20];//姓名
	int age;//年龄
	float height;//身高
	float weight;//体重
} y = {{"yao ming"}, {20}, {181.2}, {60.7}};
//y是结构体变量名,等号右边是初始值

//局部变量
struct student
{
	char name[20];//姓名
	int age;//年龄
	float height;//身高
	float weight;//体重
};

int main()
{
	struct student s = {{"xiao ming"}, {18}, {170.25}, {59.6}};
	//s是结构体变量名,等号右边是初始值
	return 0;
}

//如果嫌结构体类型名字太长
//采用typedef重新定义
typedef struct student
{
	char name[20];//姓名
	int age;//年龄
	float height;//身高
	float weight;//体重
} Su;
Su s = {{"xiao ming"}, {18}, {170.25}, {59.6}}; //s是结构体变量,且为全局变量

三、结构体成员访问

两种访问形式:
结构体变量名.成员变量名

c 复制代码
#include <stdio.h>

struct student
{
	char name[20];//姓名
	int age;//年龄
	float height;//身高
	float weight;//体重
};

int main()
{
	struct student s = {{"xiao ming"}, {18}, {170.25}, {59.60}};
	printf("%s",s.name);      //xiao ming
	printf("%d",s.age);       //18
	printf("%.2f",s.height);  //170.25
	printf("%.2f",s.weight);  //59.60
	return 0;
}

运行结果:

结构体指针->成员变量名

c 复制代码
#include <stdio.h>

struct student
{
	char name[20];//姓名
	int age;//年龄
	float height;//身高
	float weight;//体重
};

int main()
{
	struct student s = { {"xiao ming"}, {18}, {170.25}, {59.6} };
	struct student* ps = &s;
	printf("%s\n", ps->name);//xiao ming
	printf("%d\n", ps->age);
	printf("%.2f\n", ps->height);
	printf("%.2f\n", ps->weight);
	return 0;
}

运行结果:

四、结构体内存对齐

c 复制代码
struct S1
{
	char c1;//1
	char c2;//1
	int n;//4
};

struct S2
{
	char c1;
	int n;
	char c2;
};

int main()
{
	printf("%zu\n", sizeof(struct S1));
	printf("%zu\n", sizeof(struct S2));
	return 0;
}

运行结果:

按照我们的理解:结构体的大小是里面成员大小之和,也就是6个字节,但是结果相差太多。

问题出现在哪里?这说明结构体的成员在内存中不是连续存放。

4.1 偏移量

偏移量是相对于某个基准地址的距离或者偏移值,比如,我们将内存的第一个字节记为基准地址,那么后续每个字节的偏移量为n-1(n为第n个字节)。

偏移量如何计算?

在C语言库中有一个专门用来计算偏移量的函数 --- offsetof。

c 复制代码
#include <stddef.h>

struct stu
{
	char c1;
	int n;
	char c2;
};

int main()
{
	struct stu s;
	int ret = offsetof(struct stu, c1);
	printf("%d\n",ret);
	return 0;
}

结构体变量中c1、n、c2偏移量:


求得偏移量后,可以知道,c1从基准地址处开始存放,n从偏移量4的位置开始存放,c2从偏移量为8的位置开始存放。此时,结构体各成员在内存中的布局如下:

按照上面的内存布局,结果应该是9个字节,还是和结果对不上啊。别着急,结构体内存对齐可不止这么点内容。

4.1 结构体内存对齐规则

1.结构体的第一个成员对齐到相对结构体变量起始位置偏移量为0的地址。
2. 其他成员变量要对齐到对齐数的整数倍地址处。

PS:什么是对齐数?

对齐数 = 编译器默认的一个对齐数与该成员变量大小的较较小值。

VS编译器的默认对齐数是8。
3.结构体总大小为结构体最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的那一个)的整数倍。
4. 如果存在嵌套结构体的情况,嵌套的结构体成员对齐到自己成员中最大对齐数的整数倍处,结构体的整体总大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

回过头再看前面的代码:struct S2的最大对齐数是4,9显然不是4的倍数,还需要3个字节的空间,所以结果是12个字节。

4.2 为什么存在结构体内存对齐

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

假设一个处理器总是从内存中取8个字节,则它读取的时候是从偏移量为8的倍数的地址处开始访问。如果我们能保证将所有的double类型的数据的地址偏移量都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两 个8字节内存块中。

可以用下面两幅图进行演示:

如果数据按照上面这种方式存放,通过一次内访问就能拿到8个字节的内容。

如果按照上面这种方式存放数据,需要访问内存操作需要进行两次,第一次只能拿到3个字节的数据,第二次拿到5个字节的数据,再进行拼接,效率比较低。

总结:结构体内存对齐是一种牺牲空间换取效率的操作(空间换时间)。

那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到?
让占用空间小的成员尽量集中在一起。

4.3 修改默认对齐数

如果我们想让结构体再内存中连续存放,可不可以呢?

答案是可以的,这就需要我们修改编译器的默认对齐数。

修改编译器的对齐数需要用到指令:#pragma pack();

c 复制代码
#pragma pack()

struct stu
{
	char c1;
	int n;
	char c2;
};

int main()
{
	printf("%zd\n",sizeof(struct stu));
}

运行结果:

修改后结构体大小就为6,跟我们想的一样,但是不建议这样操作,因为会让程序运行效率降低。

五、结构体传参

c 复制代码
#include <stdio.h>

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

运行结果:

结构体传参,传值和传地址都是可行的,但是传址明显更好。

原因:传值和传址都需要在内存中开辟空间,但是传值需要的空间明显更大,而地址大小只占4/8个字节。

六、结构体实现位段

什么是位段?
位段是一种特殊的数据结构,用于按位存储和操作数据,能有效节省内存。

位段基于结构体,位段的成员必须是 int、unsigned int或signed int,在C99标准中位段成员的类型也可以选择其他类型(如:char,char类型变量在内存中存放的是它的ASCII码)。

位段的成员名后边有一个冒号和一个数字。

c 复制代码
//位段举例
#include <stdio.h>

struct A
{
	int _a:2;
	int _b:5;
	int _c:10;
	int _d:30;
};//数字表示这个成员要占用的比特位数量

struct B
{
	int _a;
	int _b;
	int _c;
	int _d;
};

int main()
{
	printf("%zu\n", sizeof(struct A));
	printf("%zu\n", sizeof(struct B));
	return 0;
}

运行结果:

设计成位段,结构体在内存中占用空间明显变小。相应的,里面能存放的数据也会变小。

什么时候用位段?

能够明确知道数据占用几个比特位!

将结构体A中成员所占内存空间大小相加得到47 bit,最多需要6个字节空间就够了,为什么编译器要分配8个字节的空间?

说明位段在内存中也不是连续存放,有自己的分配规则。

6.1 位段内存分配

1)位段的成员可以是 int、unsigned int、signed int、char类型数据。
2)位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。 (也就是是说,先申请一块4个字节或1个字节的内存空间)
3)位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

c 复制代码
struct S
{ 
	char a:3;
	char b:4;
	char c:5;
	char d:4;
};

int main()
{
	struct S s = { 0 };
	s.a = 10;//00000000 00000000 00000000 00001010 ------ 010
	s.b = 12;//00000000 00000000 00000000 00001100 ------ 100
	s.c = 3;//000000000 00000000 00000000 00000011 ------ 011
	s.d = 4;//000000000 00000000 00000000 00000100 ------ 100 
	return 0;
}

内存布局:

分析:

char类型每次创建一个字节(8bit),在VS编译器中,一个字节(或int)内部从右向左边存入数据,一个字节(或int)内部的空间存不下,直接浪费掉,所以结构体s需要3个字节大小的空间,内存中以16进制形式存储:62 03 04。

不确定的方面:

(1)一个字节(整形)的内部,到底是从左向右使用还是从右向左使用不确定。

(2)一个字节(整形)内部剩下的空间不能满足下一个成员的时候,是否浪费不确定。

前面代码结果是8个字节原因:

6.2 位段跨平台问题

1.int 位段被当成有符号数还是无符号数是不确定的。
2.位段中最大位的数目不能确定,比如,16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
3.一个字节(整形)的内部,到底是从左向右使用还是从右向左使用不确定。
4.一个字节(整形)内部剩下的空间不能满足下一个成员的时候,是否浪费不确定

6.3 位段的应用

下图是网络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要几个bit位就能描述,这里使用位段,能够实现想要的效果,也节省了空间,这样网络传输的数据报大小也会较小一些,对网络的畅通是有帮助的。

6.4 位段注意事项

位段的几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的。 所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输⼊值,只能是先输入放在变量中,然后赋值给位段的成员。

c 复制代码
//错误示范:
struct A
{
	int _a:2;
	int _b:5;
	int _c:10;
	int _d:30;
};

int main()
{
	struct A sa = { 0 };
	scanf("%d", &sa.b);
	return 0;
}

//正确示范
struct A
{
	int _a:2;
	int _b:5;
	int _c:10;
	int _d:30;
};

int main()
{
	struct A sa = { 0 };
	int b = 0;
	scanf("%d", &b);
	sa.b = b;
	return 0;
}
相关推荐
FFF团团员9091 小时前
树莓派学习笔记4:终端常用指令
笔记·学习
无心水1 小时前
【Python实战进阶】4、Python字典与集合深度解析
开发语言·人工智能·python·python字典·python集合·python实战进阶·python工业化实战进阶
L***一1 小时前
中专毕业生计算机证书选择指南:零基础入门路径(2026届适用)
学习
代码不停2 小时前
Java单链表和哈希表题目练习
java·开发语言·散列表
Dxxyyyy2 小时前
零基础学JAVA--Day37(坦克大战1.0)
java·开发语言
用户69371750013842 小时前
11.Kotlin 类:继承控制的关键 ——final 与 open 修饰符
android·后端·kotlin
Ayanami_Reii2 小时前
基础数学算法-开关问题
数学·算法·高斯消元
用户0273851840262 小时前
【Android】LiveData的使用以及源码浅析
android·程序员
用户69371750013842 小时前
10.Kotlin 类:延迟初始化:lateinit 与 by lazy 的对决
android·后端·kotlin