目录
一、结构体
- 结构体类型的声明
结构:一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
数组:一组相同类型元素的集合。
在C语言中,可以使用结构体(struct)来定义自定义的数据类型。结构体是一种用户自定义的数据类型,它可以包含多个不同类型的成员。
结构体的声明通常放在函数外部、全局变量的前面,或者作为函数的参数或返回值。它的基本语法如下:
struct 结构体名 {
类型 成员1;
类型 成员2;
// ...
};
其中,"结构体名" 是你给这个结构体类型起的名称,可以自定义,"成员1"、"成员2" 等是结构体的成员变量,可以根据需求进行定义,它们可以是任意合法的数据类型。
下面是一个示例,演示如何声明一个名为 "Person" 的结构体类型,包含成员变量姓名(char数组)和年龄(整型):
struct Person {
char name[20];
int age;
};
定义了结构体类型之后,就可以使用这个类型来声明变量,例如:
struct Person p1;
这样就声明了一个名为 "p1" 的结构体变量,可以通过成员运算符.
来访问结构体成员,例如:
strcpy(p1.name, "Alice");
p1.age = 25;
上述代码将给结构体变量 p1 的成员 name 赋值为 "Alice",并将 age 赋值为 25。
除了使用 struct 结构体名
的方式声明结构体变量,还可以使用 typedef 关键字定义一个结构体类型的别名,例如:
typedef struct Person {
char name[20];
int age;
} Person;
这样就可以直接使用 "Person" 来声明结构体变量,而不需要再写 struct
关键字。
C语言中还有一种特殊的结构体声明方式,即匿名结构体(anonymous struct)。匿名结构体是指在声明结构体变量时直接定义结构体类型,而不给结构体类型起一个名称。
匿名结构体可以直接在需要使用的地方进行声明,其基本语法如下:
struct {
类型 成员1;
类型 成员2;
// ...
} 结构体变量;
下面是一个示例,演示如何声明一个匿名结构体类型,并创建一个结构体变量:
struct {
char name[20];
int age;
} person;
strcpy(person.name, "Alice");
person.age = 25;
在上述代码中,我们声明了一个匿名结构体类型,并创建了一个结构体变量 person。然后,通过成员运算符.来访问结构体成员,并给它们赋值。
匿名结构体通常用于一次性的、临时的结构体需求,不需要在其他地方重复使用该结构体类型。它的作用类似于定义一个结构体类型的同时立即创建一个对象。
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
int main() {
p = &x; 虽然成员相同,但仍然是不同的结构体
return 0;
}
需要注意的是,由于匿名结构体没有类型名,所以无法在其他地方再次使用匿名结构体类型。如果需要在多个地方使用相同的结构体类型,仍然推荐使用常规的结构体声明方式。
- 结构体内存对齐
结构体成员不是按照顺序在内存中连续存放的,而是由一定的对齐规则的。
结构体的对齐规则:
- 结构体的第一个成员放在相较于结构体变量起始位置偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数 与 结构体成员自身大小的较小值。 VS中默认的值为8 Linux中没有默认对齐数,对齐数就是成员自身的大小。
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
为什么存在内存对齐?
- 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访 问。
结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到: 让占用空间小的成员尽量集中在一起。
struct S1 {
char c1;
int i;
char c2;
};
struct S2 {
int i;
char c1;
char c2;
};
S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。
在C语言中,可以使用特定的编译指令来修改结构体类型的对齐数。结构体的对齐数是指结构体成员在内存中对齐的要求,它决定了成员在内存中的布局方式。
一般情况下,编译器会根据平台的规范和优化设置自动选择合适的默认对齐数。但有时候我们可能需要手动修改对齐数来满足特定的需求,例如与硬件交互或与其他语言进行交互时。
在C语言中,可以使用
#pragma pack
编译指令来修改结构体类型的对齐数。#pragma pack
指令通常用于告诉编译器按照指定的对齐数对结构体进行对齐。
下面是一个示例,演示了如何使用 #pragma pack
修改结构体类型的对齐数:
#pragma pack(1) // 保存当前对齐设置,并将对齐数设置为1
struct MyStruct {
int a;
char b;
double c;
};
#pragma pack() // 恢复之前保存的对齐设置
int main() {
printf("Size of struct: %zu
", sizeof(struct MyStruct));
return 0;
}
在上述代码中,我们使用 #pragma pack(1)
设置对齐数为1,然后定义了一个结构体 MyStruct
,包含了不同类型的成员。最后使用 #pragma pack()
恢复之前的对齐设置。
在这个示例中,我们设置对齐数为1,这意味着结构体成员按照字节对齐,没有额外空隙。通过 sizeof
操作符,我们可以打印出结构体的大小。
需要注意的是,修改结构体的对齐数可能导致一些性能上的损失,并且在与其他代码或库进行交互时可能会产生兼容性问题。因此,在修改对齐数之前,请确保理解其影响并仔细考虑。
二、结构体实现位段(位段的填充&可移植性)
- 位段的定义
结构体类型中的位段(bit-field)是一种优化存储空间的技术,在C语言中经常使用。它允许我们在结构体成员中指定所需的位数,以便更有效地利用内存。
位段的定义方式为:在结构体成员后面加上冒号(:)和位数。例如:
struct example {
unsigned int a : 4; // 声明一个占4位的无符号整数
unsigned int b : 8; // 声明一个占8位的无符号整数
};
上述代码定义了一个名为example的结构体,其中包含了两个位段成员a和b。a占据4位,b占据8位。
通过使用位段,我们可以按需使用非常小的位数来表示数据,从而减少内存的使用。但是,需要注意的是,位段的使用可能会导致可移植性问题,因为位段的分配和布局在不同的编译器中可能会有所不同。另外,位段只能用于整型(包括有符号和无符号整型)成员。
- 位段的内存分配
- 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
位段的内存分配是按照位(bit)来进行的,而不是按照字节(byte)。具体的内存分配规则可以根据编译器的实现而有所不同,但通常遵循以下原则:
位段的大小不能超过其基本类型的大小。例如,在一个无符号整数(unsigned int)中声明一个占8位的位段是合法的,但声明一个占9位的位段是不合法的。
位段成员之间可能会进行对齐。编译器可能会在位段成员之间添加一些填充位,以确保每个位段成员在正确的边界上对齐。这样可以提高访问位段成员的效率。
如果结构体的所有位段成员的大小之和小于基本类型的大小,则结构体的大小将等于基本类型的大小。例如,如果一个结构体包含两个占4位的位段,则结构体的大小通常为4字节(32位)或8字节(64位)。
需要注意的是,位段的内存分配不是跨字节的,而是在一个字节内根据位的位置进行分配。每个位段成员将按照声明时指定的位数在相应的内存块内进行分配。
- 位段的应用
位段可以在结构体中应用于各种场景,例如表示IP地址是其中之一。
在网络编程中,IP地址通常使用32位整数(IPv4)或128位整数(IPv6)来表示。但是,使用位段可以更有效地存储和操作IP地址。
以下是一个使用位段表示IPv4地址的示例:
struct IPv4Address {
unsigned int a : 8; // 第一个字节
unsigned int b : 8; // 第二个字节
unsigned int c : 8; // 第三个字节
unsigned int d : 8; // 第四个字节
};
在上述示例中,我们定义了一个名为IPv4Address的结构体,它将IPv4地址分成四个字节,并在每个字节中使用8位的位段来表示。
通过使用位段,我们可以直接访问和操作IP地址的各个字节,而无需进行手动的位运算或字节偏移计算。
例如,可以通过以下方式创建和使用IPv4地址的位段结构体:
struct IPv4Address ip;
ip.a = 192;
ip.b = 168;
ip.c = 0;
ip.d = 1;
printf("IP Address: %d.%d.%d.%d
", ip.a, ip.b, ip.c, ip.d);
这样,我们就可以更方便地操作和传递IPv4地址,而无需手动拆分和合并字节。
请注意,上述示例仅针对IPv4地址的示范,并且可能需要进行字节序转换(如大端和小端)。在实际网络编程中,请根据需求选择适当的数据类型和处理方式来表示和操作IP地址。
三、枚举
- 枚举类型的定义
在C语言中,枚举(Enum)是一种用于定义命名常量集合的数据类型。它允许我们为一组相关的常量分配有意义的名称,并赋予其整数值。
枚举类型的定义通常包含以下几个步骤:
- 使用关键字
enum
开始定义。 - 给枚举类型一个名称,例如
Day
。 - 在一对花括号
{}
内,列出枚举成员,并用逗号,
分隔每个成员。 - 每个枚举成员可以有一个可选的赋值表达式,指定相应成员的整数值。如果不指定,则默认从0开始递增。
下面是一个具体的枚举类型定义示例:
enum Day {
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
};
在上述示例中,我们声明了一个名为Day
的枚举类型,其中包含了一周的所有天。每个成员都被赋予一个默认的整数值,从0开始递增,分别对应着MONDAY
到SUNDAY
。
需要注意的是,在C语言中,枚举类型的成员在其所在的作用域中是唯一的。这意味着枚举成员的名称不能重复,且不能与其他变量或常量的名称冲突。
四、联合(共用体)
- 联合类型的定义
C语言中的联合(Union),也被称为共用体,是一种特殊的数据结构,允许在相同的内存位置存储不同类型的数据。它类似于结构体(struct),但所有成员共享同一块内存空间。
定义一个联合使用union
关键字,其语法如下:
union UnionName {
member1_type member1;
member2_type member2;
// 其他成员...
};
其中,UnionName
是联合的名称,member1_type
、member2_type
等是成员的数据类型。
联合的大小取决于其最大的成员大小。当我们给联合赋值或修改其中一个成员时,其他成员的值会被更新或者受影响。
以下是一个简单的示例来说明联合的用法:
#include <stdio.h>
union Data {
int i;
float f;
char str[20];
};
int main() {
union Data data;
printf("Memory size occupied by data : %d\n", sizeof(data));//20
data.i = 10;
printf("data.i : %d\n", data.i);//10
data.f = 220.5;
printf("data.f : %f\n", data.f);//220.500000
strcpy(data.str, "C Programming");
printf("data.str : %s\n", data.str);//C Programming
return 0;
}
在上述示例中,我们定义了一个名为Data
的联合,它具有整数(int
)、浮点数(float
)和字符串(char
数组)类型的成员。我们可以通过访问同一个内存位置的不同成员来存储和修改不同类型的数据。
注意,联合的使用需要谨慎,因为在不正确地访问或使用成员时可能会导致数据不一致或未定义的行为。
- 联合的特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联 合至少得有能力保存最大的那个成员)。
- 联合大小的计算
- 联合的大小至少是最大成员的大小。
- 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
联合的大小只考虑了最大成员的大小,并不会累加其他成员。这是因为联合的所有成员共享同一块内存空间,同一时间只能存储其中的一个成员。
此外,对于包含比较复杂的成员类型,如结构体或者嵌套的联合,联合的大小是由其最大成员及其内部成员的大小一起决定的。