结构体(C 语言)

目录

  • 一、什么是结构体?
  • 二、结构体变量的创建和初始化
    • [1. C99 指定初始化](#1. C99 指定初始化)
    • [2. 结构体最好不要使用部分初始化](#2. 结构体最好不要使用部分初始化)
    • [3. 结构体不能自己包含自己](#3. 结构体不能自己包含自己)
    • [4. 使用 typedef 简化结构体类型](#4. 使用 typedef 简化结构体类型)
    • [5. 匿名结构体](#5. 匿名结构体)
  • 三、结构体变量的使用和传参
    • [1. 结构体传参的两种方式](#1. 结构体传参的两种方式)
  • 四、结构体内存对齐
    • [1. 内存对齐的规则](#1. 内存对齐的规则)
    • [2. 内存对齐的练习题](#2. 内存对齐的练习题)
      • [1. 练习1](#1. 练习1)
      • [2. 练习2](#2. 练习2)
      • [3. 练习3](#3. 练习3)
      • [4. 练习4](#4. 练习4)
    • [3. 为什么存在内存对⻬?](#3. 为什么存在内存对⻬?)
  • 五、结构体实现位段
    • [1. 什么是位段](#1. 什么是位段)
    • [2. 位段内存的分配](#2. 位段内存的分配)
    • [3. 位段的跨平台问题](#3. 位段的跨平台问题)
    • [4. 位段的应用(了解)](#4. 位段的应用(了解))
    • [5. 位段使用的注意事项](#5. 位段使用的注意事项)

一、什么是结构体?

在 C 语言中,结构体(struct)是一种用户自定义的数据类型,它可以把不同类型的数据组合在一起。

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

在创建结构体变量之前,需要对该结构体类型进行声明,因为这是我们创建的类型,需要告知编译器。如下是一个简单的学生结构体声明:

c 复制代码
// Stu 结构体声明
struct Stu
{
	char name[20];  // 姓名
	int age;  // 年龄
	double weight;  // 体重
};

上述声明告诉编译器,我们创建了一个 Stu 结构体类型,该结构体有三个成员,字符数组 name,int 变量 age,double 变量 weight。现在就可以像使用 int、double 等类型一样创建变量了,但是创建变量时前面要加上关键字 struct,

c 复制代码
// 创建 Stu 结构体变量
int main()
{
	// 创建 Stu 结构体变量
	struct Stu stu1 = { "李华", 18, 60 };
}

上述代码创建了一个 Stu 结构体变量 stu1,并给它的每个成员都赋了初值。结构体变量初始化采用列表初始化的方式,如果其内还有数组或者其他结构体作为成员,则在内部再次使用列表初始化。

还可以在声明结构体类型的同时创建该结构体变量:

c 复制代码
// Stu 结构体声明
struct Stu
{
	char name[20];  // 姓名
	int age;  // 年龄
	double weight;  // 体重
}stu1 = { "李华", 18, 60.24 }, stu2 = { "张三", 20, 62.4 };

结构体声明在所有函数之外就是全局结构体,在所有函数中均可使用该结构体;如果该结构体声明在特定的函数之内,那么只能在该函数中使用该结构体。

1. C99 指定初始化

从 C99 标准之后,C 语言的结构体可以使用指定初始化。其格式为:

struct 结构体名 变量名 = {.成员名1 = 值1,.成员名2 = 值2,...};

那么上面的 Stu 结构体变量 stu1 的初始化就可以改成下面的代码:

c 复制代码
// 使用指定成员初始化
struct Stu stu1 = { .name = "李华", .age = 18, .weight = 60.2 };

2. 结构体最好不要使用部分初始化

因为结构体使用部分初始化后,剩余的成员不一定会初始化为 0,且没有标准规定。一般取决于使用的环境,作者使用的 VS 2022 部分初始化的剩余成员是被设置为 0 的:

但是最好不要使用部分初始化。

3. 结构体不能自己包含自己

如下代码:

c 复制代码
// 结构体自己包含自己
struct Stu
{
	char name[20];  // 20 字节
	Stu stu1;  // ?字节
}

请问如何计算上述结构体变量的大小?这根本无法计算,只会陷入无线的套娃之中。所以,结构体是不能自己包含自己的,只能包含其他结构体变量,或者包含自己的指针。

4. 使用 typedef 简化结构体类型

前面创建结构体变量的代码中,都需要前缀关键字 struct。如果想要像使用 int 等类型一样创建变量,那么只需要在声明结构体类型的时候使用关键字 typedef 就可以,下面两种形式均可:

(1)声明的同时使用 typedef

c 复制代码
// Stu 结构体声明
typedef struct Stu
{
	char name[20];  // 姓名
	int age;  // 年龄
	double weight;  // 体重
}Stu;

(2)声明之后使用 typedef

c 复制代码
// Stu 结构体声明
struct Stu
{
	char name[20];  // 姓名
	int age;  // 年龄
	double weight;  // 体重
};
typedef struct Stu Stu;

然后,现在就可以像创建普通变量一样,创建 Stu 结构体变量了:

c 复制代码
Stu s1 = { "李华", 18, 60.24 };

5. 匿名结构体

匿名结构体是没有名称的结构体,且只能在声明的时候创建变量:

c 复制代码
// 匿名结构体
struct
{
	int i;
}A;

上述匿名结构体只有 A 这个变量,由于没有名称,所以不能在其他地方再创建该结构体变量。

可以使用关键字 typedef 解决上述问题:

c 复制代码
// 匿名结构体
typedef struct
{
	int i;
}Unknown;

int main()
{
	Unknown A = { 10 };

	return 0;
}

但是如果使用了 typedef 关键字,就失去了使用匿名结构体的意义。

三、结构体变量的使用和传参

我们可以通过成员运算符(.)来访问结构体变量的每个成员,如下代码:

c 复制代码
// Stu 结构体声明
typedef struct Stu
{
	char name[20];
	int age;
	double weight;
}Stu;

int main()
{
	Stu stu1 = { "张三", 18, 60.24 };
	// 打印
	printf("姓名:%s\n", stu1.name);
	printf("年龄:%d\n", stu1.age);
	printf("体重:%f\n", stu1.weight);

	return 0;
}

从上述代码中,我们可以像使用普通变量一样使用结构体的成员,如:struct.age 就是一个 int 变量,可以进行 int 变量的所有操作。那么结构体数组也同样如此:

c 复制代码
// 创建 Stu 结构数组并初始化
Stu stus[3] = {
	{"张三", 18, 60.24},
	{"李四", 19, 61.1 },
	{"王五", 20, 62.3}
};
// 打印
int i;
printf("%-10s%-5s%-10s\n", "姓名", "年龄", "体重");
for (i = 0; i < 3; ++i)
{
	printf("%-10s%-5d%-8.2f\n", stus[i].name, stus[i].age, stus[i].weight);
}

结果如下:

1. 结构体传参的两种方式

结构体和其他类型一样,传参时都是值传递和址传递两种方式。我们通过一个打印结构体信息的函数,来看看结构体的这两种传参方式。

(1)值传递

c 复制代码
// 值传递
void print_Stu(Stu stu)
{
	printf("%s ", stu.name);
	printf("%d ", stu.age);
	printf("%f ", stu.weight);
}

(2)址传递

c 复制代码
// 址传递
void print_pStu(Stu* ps)
{
	printf("%s ", (*ps).name);
	printf("%d ", (*ps).age);
	printf("%f ", (*ps).weight);
}

从上述代码来看,结构体指针的创建和普通变量的指针创建是类似的。上述址传递的时候先对指针解引用拿到该指针所指向的结构体,然后再打印该结构体成员的信息。而 C 语言还提供了一种通过指针间接访问结构体成员的办法:

c 复制代码
// 址传递
void print_pStu(Stu* ps)
{
	printf("%s ", ps->name);
	printf("%d ", ps->age);
	printf("%f ", ps->weight);
}

符号 -> 称为箭头运算符,是专门提供给指针访问结构体成员的运算符。而 ps->name 的实质还是是 (*ps).name,这就和数组中 arr[i] 的实质是 *(arr+i) 一样,箭头运算符和下标运算符都是为了让使用者更加方便和更容易理解。

四、结构体内存对齐

结构体大小的计算和其他类型不一样,它有自己的一套规则。结构体内存对齐后才能计算其所占空间大小。

1. 内存对齐的规则

(1)结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处

(2)其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。对⻬数 = 编译器默认的⼀个对⻬数与该成员变量⼤⼩的较⼩值。VS 中默认的值为 8 ,Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩。

(3)结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。

(4)如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

2. 内存对齐的练习题

1. 练习1

首先 c1 是 S1 的第一个成员,其类型为 char 站一个字节,直接对齐到起始位置,如下:

然后是 S1 的第二个成员 i,其类型为 int 占 4 个字节,VS 下默认对其数为 8,那么成员 i 的对其数为 min{4, 8} = 4。所以,i 需要在 4 的倍数的地方开始存储,

最后是 S1 的第三个成员 c2,其类型为 char,占 1 个字节,那么其对齐数为 min{1, 8} = 1,所以 c2 需要在 1 的倍数的地方开始存储:

存储完结构体 S1 的每个成员之后,需要给整个结构体对其,而整个结构体的对齐数是所有成员中对其数最大的那个,也就是 max{1,4,1} = 4。而目前结构体所占空间为 4 + 4 + 1 = 9,不是 4 的整数倍,所以需要补 3 个字节,达到 12 字节。所以最后结构体 S1 的大小为 12 字节。

下面是在 64 位环境下,程序运行的结果:

2. 练习2

首先,结构体 S2 的第一个成员 c1 对齐到 0 位置,类型为 char,占 一个字节。

其次,结构体 S2 的第二个成员 c2,类型为 char,占一个字节,其对齐数为 min{1, 8} = 1,对齐到 1 位置。

最后,结构体 S2 的第三个成员 i,类型为 int,占 4 个字节,对齐数为 min{4, 8} = 4,对齐到 4 的整数倍,也就是 4 的位置。

最后是结构体对其,目前结构 S2 的大小为 8,需要对齐到整个结构体的最大对齐数,也就是 max{1,4} = 4,所以 S2 的大小为 8。

下面是 64 位环境下,程序运行的结果:

3. 练习3

首先,结构体 S3 的第一个成员 d,类型为 double,占 8 个字节,对齐到 0 位置。

其次,结构体 S3 第二个成员 c,类型为 char,对齐数为 min{1, 8} = 1,对其到 8 位置。

最后结构体 S3 的第三个成员 i,类型为 int,对齐数为 min{4, 8} = 4,对齐到 12 位置。

现在进行结构体对齐,结构体的对齐数为所有成员对齐数的最大数,也就是 max{8,1,4} = 8,而现在结构体的大小为 16,刚好是 8 的整数倍。所以,结构体 S3 的大小为 8。

下面是 64 位环境下程序运行的结果:

4. 练习4

首先,结构体 S4 的第一个成员 c1,类型为 char,占 1 个字节,对齐到 0 位置。

其次,结构体 S4 第二个成员 s3,其类型为结构体 S3,大小为 16,结构体S3 的最大对其数为 8,则成员 s3 的对其数为 min{8,8} = 8,则对其到 8 位置。

最后,结构体第三个成员 d,类型为 double,对齐数为 min{8, 8} = 8,对齐到 24 位置。

现在进行结构体对其,该结构体的对齐数为所有成员对齐数的最大值,即 max{1,8,8} = 8,现在结构体的大小为 32,是 8 的整数倍。所以结构体 S4 的大小为 32。

下面是 64 位环境下程序运行的结果:

3. 为什么存在内存对⻬?

⼤部分的参考资料都是这样说的:

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

    不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

  2. 性能原因:

    数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。

总体来说:结构体的内存对⻬是拿空间来换取时间的做法。

那在设计结构体的时候,我们既要满⾜对⻬,⼜要节省空间,如何做到:让占⽤空间⼩的成员尽量集中在⼀起

如下代码:

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

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

虽然两个结构体的成员相同,但是 S1 占 12 个字节,S2 占 8 个字节。

五、结构体实现位段

1. 什么是位段

位段的声明和结构是类似的,有两个不同:

  1. 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以选择其他类型。
  2. 位段的成员名后边有⼀个冒号和⼀个数字。

比如:

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

在上述代码中,A 就是一个位段。那么如何计算 A 的大小?

2. 位段内存的分配

  1. 位段的成员可以是 int unsigned int signed int 或者是 char 等类型
  2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。
  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;
	s.b = 12;
	s.c = 3;
	s.d = 4;
 
	return 0;
}

首先,在 VS 2022 下通过上述规则,成员 a 和 b共占一个字节,剩下一位不够成员 c 存储,然后开辟一个字节,成员 c 在这个新的字节上存储,而上一个字节剩下的一个位就浪费了。然后新字节剩下的 4 各位不够 3 个位不够 d 存储,又开辟一个字节。所以一共位段 S 一共需要 3 个字节的空间。

有了上面的图,就可以知道在 VS2022 中,位段的成员是从当前字节的低位开始使用的。现在我们来解释一下 main() 函数中的赋值语句是如何进行的:

s.a = 10,且 a 为 char 类型,所以二进制为:00001010,但是 a 只有 3 位的空间,所以只取后 3 位如下:

同理,其他成员也是如此:

然后我们把初始化的 0 补上,算出这三个字节的十六进制:

这里没有大小端的问题哈,大小端是字节的顺序,这里都是 char 类型,最大就是一个字节。

下面是 64 位环境下,程序运行时结构体 s 在内存中的十六进制,与上述计算的十六进制相同:

3. 位段的跨平台问题

  1. int 位段被当成有符号数还是⽆符号数是不确定的。
  2. 位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会出问题。
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
  4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利⽤,这是不确定的。

总结:

跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。

4. 位段的应用(了解)

位段在计算机编程中有很多重要的应用,以下是一些主要的方面:

一、内存优化

在资源受限的系统中,如嵌入式设备,内存空间非常宝贵。位段可以将多个布尔值或小整数紧凑地存储在一个字中,极大地节省了内存空间。例如,在一个嵌入式系统中,用于表示设备状态的多个标志可以组合成一个位段结构。假设我们有一个设备,其状态包括电源状态(开 / 关)、连接状态(已连接 / 未连接)和工作模式(正常 / 节能 / 高性能)。如果不使用位段,每个状态可能需要占用一个字节的内存空间,而使用位段,可以将这三个状态存储在一个字节甚至更少的空间中。

二、协议解析

在网络通信和数据传输中,各种协议通常会定义一组标志位来表示不同的状态或选项。位段可以方便地解析和处理这些标志位。比如在 TCP/IP 协议中,IP 报头中的标志字段就可以用位段来表示和解析。其中包括保留位、不分片标志和更多分片标志等。通过位段,可以快速地检查这些标志位的值,从而确定如何处理数据包。

三、图形编程

在图形处理中,位段可以用于表示像素的属性。例如,在一个图像格式中,每个像素可能包含多个属性,如透明度、颜色通道和特殊效果标志等。使用位段可以将这些属性紧凑地存储在一个数据结构中,方便对图像进行处理和渲染。

四、硬件描述语言

在硬件设计中,硬件描述语言(如 Verilog 和 VHDL)经常使用位段来描述寄存器和信号的位级属性。例如,在设计一个处理器的控制寄存器时,可以使用位段来表示不同的控制信号,如中断使能、流水线控制和缓存策略等。这样可以更清晰地描述硬件的行为,并方便进行综合和仿真。

五、配置文件存储

位段可以用于存储配置文件中的选项和设置。例如,在一个软件的配置文件中,可以使用位段来表示各种功能的启用或禁用状态。这样可以节省文件空间,并方便快速地读取和解析配置信息。

总之,位段在计算机编程中是一种非常有用的技术,可以在多个领域中实现内存优化、高效的数据处理和紧凑的数据存储。它虽然在使用上需要一些小心和注意,但在合适的场景下可以发挥重要的作用。

5. 位段使用的注意事项

在 C 语言中,内存中的地址都是以字节为最小单位划分的。但是在位段中,有时会有几个成员共用一个字节,那么其中不是从该字节起始位置开始存储的成员是没有地址的,因为一个字节内部的 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);//这是错误的 
	 
	 //正确的⽰范 
	 int b = 0;
	 scanf("%d", &b);
	 sa._b = b;
	 return 0;
}
``
相关推荐
Stark、几秒前
【Linux】文件IO--fcntl/lseek/阻塞与非阻塞/文件偏移
linux·运维·服务器·c语言·后端
taoyong0011 分钟前
Java线程核心01-中断线程的理论原理
java·开发语言
一雨方知深秋2 分钟前
智慧商城:封装getters实现动态统计 + 全选反选功能
开发语言·javascript·vue2·foreach·find·every
海威的技术博客4 分钟前
关于JS中的this指向问题
开发语言·javascript·ecmascript
froginwe1130 分钟前
PostgreSQL表达式的类型
开发语言
委婉待续33 分钟前
java抽奖系统(八)
java·开发语言·状态模式
deja vu水中芭蕾35 分钟前
嵌入式C面试
c语言·开发语言
爱码小白35 分钟前
PyQt5 学习方法之悟道
开发语言·qt·学习方法
西猫雷婶1 小时前
python学opencv|读取图像(十六)修改HSV图像HSV值
开发语言·python·opencv
weixin_537590451 小时前
《Java编程入门官方教程》第八章练习答案
java·开发语言·servlet