复合数据类型
5. 结构体与联合体、枚举 、位域
基础数据类型或者是数组,通常不能满足全部的开发需求,因为数据的存储结构并不完全单一,于是就需要使用自定义数据类型了,或者说复合数据类型。
结构体(Struct)
结构体用于将多个相关变量组合成一个单独的实体。
定义结构体与结构体变量
- 定义结构体:使用
struct
关键字可以定义一个新的数据类型。结构体可以包含不同类型的变量。
#include <stdio.h>
// 定义结构体 'struct Student' 是类型, 'Student' 是标签名
struct Student {
char name[50];
int age;
float grade;
};
int main() {
// 定义结构体变量并初始化
struct Student student1 = {"Alice", 20, 88.5};
// 访问结构体成员
printf("Name: %s\n", student1.name);
printf("Age: %d\n", student1.age);
printf("Grade: %.2f\n", student1.grade);
return 0;
}
- 简化结构体类型名:使用
typedef
可以为结构体起一个别名,减少书写。
#include <stdio.h>
typedef struct { // 这里省略了标签名,如果没有typedef,就是一个匿名结构体类型
char name[50];
int age;
float grade;
} Student;
int main() {
Student student1 = {"Alice", 20, 88.5};
printf("Name: %s\n", student1.name);
return 0;
}
嵌套结构体与指向结构体的指针
- 嵌套结构体:一个结构体可以作为另一个结构体的成员,称为嵌套结构体。
#include <stdio.h>
// 嵌套结构体
struct Address {
char city[50];
char state[50];
};
struct Person {
char name[50];
struct Address address; // 嵌套结构体成员
};
int main() {
struct Person person = {"Bob", {"New York", "NY"}};
printf("Name: %s\n", person.name);
printf("City: %s\n", person.address.city);
printf("State: %s\n", person.address.state);
return 0;
}
- 指向结构体的指针:使用结构体指针可以动态访问结构体成员。
#include <stdio.h>
struct Point {
int x;
int y;
};
int main() {
struct Point p = {10, 20};
struct Point *ptr = &p; // 定义指针指向结构体
// 使用 -> 操作符访问成员
printf("x: %d, y: %d\n", ptr->x, ptr->y);
return 0;
}
结构体作为函数参数
- 按值传递:结构体可以作为参数按值传递,但会复制结构体的全部内容。
#include <stdio.h>
struct Rectangle {
int length;
int width;
};
// 按值传递结构体
void printArea(struct Rectangle r) {
printf("Area: %d\n", r.length * r.width);
}
int main() {
struct Rectangle rect = {10, 5};
printArea(rect); // 传递整个结构体
return 0;
}
- 按引用传递:使用指针传递结构体避免拷贝,提升效率。
#include <stdio.h>
struct Rectangle {
int length;
int width;
};
// 按引用传递结构体
void printArea(struct Rectangle *r) {
printf("Area: %d\n", r->length * r->width);
}
int main() {
struct Rectangle rect = {10, 5};
printArea(&rect); // 传递结构体地址
return 0;
}
关于结构体的定义和初始化,小结后面继续说明。
联合体(Union)
联合体是特殊的结构体,其所有成员共享同一块内存。
联合体与结构体的区别
- 定义与访问:
#include <stdio.h>
// 定义联合体
union Data {
int i;
float f;
char str[20];
};
int main() {
union Data data;
data.i = 10;
printf("i: %d\n", data.i);
data.f = 3.14; // 此时覆盖了 data.i 的值
printf("f: %.2f\n", data.f);
return 0;
}
- 区别:
- 结构体: 各成员有独立的内存,大小为所有成员内存之和。
- 联合体: 所有成员共享内存,大小为最大成员的内存大小。
联合体的内存管理与使用场景
- 内存管理:联合体节省内存,其大小等于最大成员的大小。适用于同一时间只使用一个成员的场景。
- 使用场景:
- 类型转换
- 网络协议解析
- 嵌入式开发
枚举(Enum)
枚举是一组相关常量的集合。
定义枚举与使用枚举常量
#include <stdio.h>
// 定义枚举
enum Color {
RED = 0,
GREEN = 1,
BLUE = 2
};
int main() {
enum Color color = GREEN;
printf("Color: %d\n", color); // 输出 1
return 0;
}
与整数的转换
#include <stdio.h>
enum Days {
MON = 1, TUE, WED, THU, FRI, SAT, SUN
};
int main() {
enum Days today = FRI;
printf("Today is day %d\n", today); // 输出 5
int day = 3; // 转换为枚举类型
today = (enum Days)day;
printf("Converted day: %d\n", today);
return 0;
}
位域(Bit-field)
位域允许更紧凑地存储布尔值或小范围的整数。
位域的定义与应用
#include <stdio.h>
struct Flags {
unsigned int isAvailable : 1; // 占 1 位
unsigned int isEnabled : 1;
unsigned int reserved : 6; // 占 6 位
};
int main() {
struct Flags flags = {1, 0, 0};
printf("isAvailable: %u\n", flags.isAvailable); // 输出 1
printf("isEnabled: %u\n", flags.isEnabled); // 输出 0
return 0;
}
位域的存储与对齐问题
- 存储:位域通常以
unsigned int
的大小分配内存。 - 对齐:位域的起始位置可能受编译器对齐规则影响。
示例:
#include <stdio.h>
struct Flags {
unsigned int a : 3;
unsigned int b : 5;
unsigned int c : 8;
};
int main() {
printf("Size of struct: %zu bytes\n", sizeof(struct Flags));
return 0;
}
说明:不同编译器可能输出不同的大小,具体取决于内存对齐。
注意:
- 位域的类型:每个位域成员必须有一个基础数据类型,通常是
int
、unsigned int
、char
、unsigned char
等。位域的长度是相对于这个基础类型来定义的,也可以使用位更长的基础数据类型 如long
、unsigned long
。 - 位域长度:位域的长度是指一个成员占用的比特数。可以指定任意长度的比特数,但必须在该类型的位数范围内, 即 (定义位长之和 <= 最大类型位长)。例如,对于
unsigned int
类型,位域长度可以从 1 位到 32 位不等(在 32 位系统上)。
示例:位域长度和基础类型的关系
示例 1:基础类型为 unsigned int
(32 位系统)
#include <stdio.h>
struct Example {
unsigned int a : 10; // 10 位
unsigned int b : 15; // 15 位
unsigned int c : 7; // 7 位
// 如果没有变量名,则视作填充位
// 总共 10 + 15 + 7 = 32 位
};
int main() {
struct Example ex;
ex.a = 1023; // 最大 10 位数值
ex.b = 32767; // 最大 15 位数值
ex.c = 127; // 最大 7 位数值
printf("a = %u, b = %u, c = %u\n", ex.a, ex.b, ex.c);
return 0;
}
在这个例子中,位域的总长度为 32 位,正好等于 unsigned int
类型在 32 位系统上的位数。每个位域成员的位数不能超过 32 位(即 unsigned int
类型的位数)。
示例 2:基础类型为 unsigned char
(8 位)
cCopy Code#include <stdio.h>
struct Example {
unsigned char a : 5; // 5 位
unsigned char b : 3; // 3 位
// 总共 5 + 3 = 8 位
};
int main() {
struct Example ex;
ex.a = 31; // 最大 5 位数值
ex.b = 7; // 最大 3 位数值
printf("a = %u, b = %u\n", ex.a, ex.b);
return 0;
}
这里,位域总长度为 8 位,正好等于 unsigned char
类型的位数。
注意:虽然指针可以访问和修改位域成员,但不建议进行指针算术运算,如通过偏移量访问位域。因为位域是按位分配内存的,但指针运算通常是按字节来进行的。位域的布局可能并不严格遵循字节对字节的顺序,特别是跨字节边界时,指针运算可能会导致不可预期的结果。
小结
- 结构体: 用于表示复杂数据结构,支持嵌套和指针访问。
- 联合体: 节省内存,适合场景如类型转换和嵌入式开发。
- 枚举: 定义一组常量,便于代码可读性。
- 位域: 紧凑存储标志位或小整数,适合受限存储的场景。
补充:
熟练掌握结构体的定义、初始化、成员访问、传递等操作,是进行高效C开发的基础。
一、结构体的定义
首先,结构体的定义是一个前提。结构体可以用 struct
关键字定义,格式如下:
struct Person {
char name[20];
int age;
float height;
};
实际开发中,使用的自定义结构类型往往比较复杂,好的结构应贴合数据的需求。
二、结构体的初始化
结构体的初始化有多种方式,可以在声明时进行初始化,也可以在程序运行过程中进行动态初始化。
- 静态初始化(在定义时初始化)
当你定义结构体变量时,可以直接给它们初始化值。这是最常用的初始化方式。
方式 1:使用大括号进行初始化
可以直接在结构体变量声明时,使用大括号 {}
来初始化它的成员。
#include <stdio.h>
struct Person {
char name[20];
int age;
float height;
};
int main() {
struct Person person1 = {"Alice", 30, 5.6}; // 静态初始化
printf("Name: %s\n", person1.name);
printf("Age: %d\n", person1.age);
printf("Height: %.2f\n", person1.height);
return 0;
}
这里,person1
被初始化为 name="Alice"
,age=30
,height=5.6
。这种初始化方式必须按照结构体成员的声明顺序来赋值。
方式 2:使用指定成员的初始化(命名初始化)
C99 标准引入了一种方式,你可以通过成员名称来初始化结构体的特定成员,这样可以避免按顺序初始化时出现错误。
#include <stdio.h>
struct Person {
char name[20];
int age;
float height;
};
int main() {
struct Person person1 = {.name = "Bob", .age = 25, .height = 5.9};
printf("Name: %s\n", person1.name);
printf("Age: %d\n", person1.age);
printf("Height: %.2f\n", person1.height);
return 0;
}
在这个例子中,通过指定成员名来初始化 person1
,你可以以任何顺序初始化结构体的成员。
- 部分初始化
如果在初始化时没有为所有成员提供初始值,未初始化的成员将会被默认赋值为零(对于基本类型来说是 0,对于字符数组是空字符 \0
)。
#include <stdio.h>
struct Person {
char name[20];
int age;
float height;
};
int main() {
struct Person person1 = {"Charlie"}; // 只初始化 name,age 和 height 会被置为 0
printf("Name: %s\n", person1.name); // Charlie
printf("Age: %d\n", person1.age); // 0
printf("Height: %.2f\n", person1.height); // 0.00
return 0;
}
此时,name
被初始化为 "Charlie"
,age
和 height
都会被自动初始化为 0。
三、结构体的赋值
结构体变量的赋值有时可能会引起困惑,特别是结构体之间的赋值。我们可以将一个结构体的值赋给另一个同类型的结构体变量。赋值时,结构体成员会逐个赋值。
- 通过
=
操作符赋值
可以直接使用 =
操作符将一个结构体的值赋给另一个结构体。这种赋值会将源结构体的每个成员复制到目标结构体中。
#include <stdio.h>
struct Person {
char name[20];
int age;
float height;
};
int main() {
struct Person person1 = {"Alice", 30, 5.6};
struct Person person2;
// 使用 = 操作符进行赋值
person2 = person1;
// 输出 person2 的内容
printf("Name: %s\n", person2.name);
printf("Age: %d\n", person2.age);
printf("Height: %.2f\n", person2.height);
return 0;
}
在这个例子中,person1
的值被赋给了 person2
,所以 person2
的 name
、age
和 height
都是 person1
的值。
- 结构体成员单独赋值
如果你只想修改结构体中的某个成员,可以直接通过成员名来进行赋值。
#include <stdio.h>
struct Person {
char name[20];
int age;
float height;
};
int main() {
struct Person person1 = {"Alice", 30, 5.6};
// 只修改 person1 的某些成员
person1.age = 31; // 修改 age
person1.height = 5.7; // 修改 height
printf("Name: %s\n", person1.name);
printf("Age: %d\n", person1.age); // 31
printf("Height: %.2f\n", person1.height); // 5.7
return 0;
}
在这个例子中,我们直接修改了 person1
中的 age
和 height
成员。
- 结构体的指针赋值
你也可以通过指针来访问和赋值结构体的内容。此时,你需要使用 ->
操作符来访问结构体的成员。
#include <stdio.h>
struct Person {
char name[20];
int age;
float height;
};
int main() {
struct Person person1 = {"Alice", 30, 5.6};
struct Person *personPtr = &person1;
// 通过指针修改结构体成员
strcpy(personPtr->name, "licx"); // 将字符串 "licx" 复制到 personPtr->name
personPtr->age = 35; // 使用指针修改 age
personPtr->height = 5.9; // 使用指针修改 height
printf("Name: %s\n", person1.name); // licx
printf("Age: %d\n", person1.age); // 35
printf("Height: %.2f\n", person1.height); // 5.9
return 0;
}
四、注意
-
结构体赋值是逐个成员的复制,对于包含指针成员的结构体,赋值操作仅复制指针的地址,导致多个指针指向同一内存区域,这种行为称为浅拷贝。浅拷贝可能导致修改或释放内存时出现问题。深拷贝则会复制指针所指向的内存内容,通常需要手动编写代码实现。
-
结构体成员的初始化顺序:结构体的成员会按照声明的顺序进行初始化和赋值,因此要确保初始化顺序正确。
-
内存对齐:结构体的大小可能会受到内存对齐的影响,因此有时即使结构体成员看似占用较小的内存,实际的内存大小可能会有所不同。