结构体与联合体
结构体和联合体是 C 语言中用于创建自定义数据类型的关键特性。结构体可以将多个不同类型的变量组合成一个整体,联合体则允许多个变量共享同一块内存空间。
结构体基础
结构体的定义
结构体使用 struct 关键字定义,可以将不同类型的数据组合在一起:
struct Person {
char name[50];
int age;
float height;
};
这个结构体定义了一个 Person 类型,包含三个成员:姓名、年龄和身高。
结构体变量的声明与初始化
struct Person person1; // 声明结构体变量
struct Person person2 = {"张三", 25, 1.75f}; // 声明并初始化
struct Person person3 = {
.name = "李四", // C99 指定初始化器
.age = 30,
.height = 1.80f
};
struct Person person4 = {
.age = 28 // 部分初始化,其他成员为 0
};
访问结构体成员
使用点运算符 . 访问结构体成员:
struct Person person;
strcpy(person.name, "王五");
person.age = 35;
person.height = 1.70f;
printf("姓名: %s\n", person.name);
printf("年龄: %d\n", person.age);
printf("身高: %.2f\n", person.height);
结构体的简化定义
使用 typedef 可以简化结构体类型名:
typedef struct {
char name[50];
int age;
float height;
} Person;
Person person = {"赵六", 40, 1.85f}; // 不需要 struct 关键字
也可以同时使用标签和 typedef:
typedef struct Person {
char name[50];
int age;
} Person;
struct Person p1; // 使用 struct 标签
Person p2; // 使用 typedef 名称
结构体与指针
指向结构体的指针
Person person = {"张三", 25, 1.75f};
Person* ptr = &person;
// 使用箭头运算符访问成员
printf("姓名: %s\n", ptr->name);
printf("年龄: %d\n", ptr->age);
// 等价于使用解引用和点运算符
printf("姓名: %s\n", (*ptr).name);
ptr->member 等价于 (*ptr).member,箭头运算符更简洁。
结构体指针作为函数参数
传递结构体指针可以避免复制整个结构体:
void print_person(const Person* p) {
printf("姓名: %s, 年龄: %d, 身高: %.2f\n",
p->name, p->age, p->height);
}
void update_age(Person* p, int new_age) {
p->age = new_age;
}
int main(void) {
Person person = {"张三", 25, 1.75f};
print_person(&person);
update_age(&person, 26);
return 0;
}
结构体数组
声明与初始化
Person people[3] = {
{"张三", 25, 1.75f},
{"李四", 30, 1.80f},
{"王五", 35, 1.70f}
};
Person group[5] = {0}; // 初始化所有成员为 0
遍历结构体数组
Person people[] = {
{"张三", 25, 1.75f},
{"李四", 30, 1.80f},
{"王五", 35, 1.70f}
};
int count = sizeof(people) / sizeof(people[0]);
for (int i = 0; i < count; i++) {
printf("%s: %d岁\n", people[i].name, people[i].age);
}
// 使用指针遍历
for (Person* p = people; p < people + count; p++) {
printf("%s: %d岁\n", p->name, p->age);
}
嵌套结构体
结构体可以包含其他结构体作为成员:
typedef struct {
int year;
int month;
int day;
} Date;
typedef struct {
char name[50];
Date birthday; // 嵌套结构体
float height;
} Student;
Student stu = {
.name = "张三",
.birthday = {2000, 5, 15},
.height = 1.75f
};
printf("出生日期: %d-%d-%d\n",
stu.birthday.year,
stu.birthday.month,
stu.birthday.day);
// 指针访问嵌套成员
Student* ptr = &stu;
printf("出生年份: %d\n", ptr->birthday.year);
结构体与函数
结构体作为参数
void print_student(Student s) {
printf("姓名: %s\n", s.name);
printf("生日: %d-%d-%d\n", s.birthday.year, s.birthday.month, s.birthday.day);
}
这种方式会复制整个结构体,对于大型结构体效率较低。
返回结构体
Student create_student(const char* name, int year, int month, int day) {
Student s;
strncpy(s.name, name, sizeof(s.name) - 1);
s.name[sizeof(s.name) - 1] = '\0';
s.birthday.year = year;
s.birthday.month = month;
s.birthday.day = day;
return s;
}
Student stu = create_student("李四", 2001, 6, 20);
结构体的大小与内存对齐
sizeof 运算符
typedef struct {
char a; // 1 字节
int b; // 4 字节
char c; // 1 字节
} Example;
printf("大小: %zu\n", sizeof(Example)); // 可能是 12,而不是 6
内存对齐
编译器为了提高访问效率,会对结构体成员进行对齐:
假设 int 对齐要求为 4 字节:
偏移量: 0 1 2 3 4 5 6 7 8 9 10 11
成员: [a] [填充...........] [b..........] [c] [填充...]
控制对齐
调整成员顺序可以减少填充:
typedef struct {
int b; // 4 字节
char a; // 1 字节
char c; // 1 字节
} Optimized; // 大小可能是 8
typedef struct {
char a; // 1 字节
char c; // 1 字节
int b; // 4 字节
} Optimized2; // 大小可能是 8
使用 #pragma pack 可以改变对齐方式:
#pragma pack(push, 1) // 1 字节对齐
typedef struct {
char a; // 1 字节
int b; // 4 字节
char c; // 1 字节
} Packed; // 大小为 6
#pragma pack(pop)
offsetof 宏
获取成员在结构体中的偏移量:
#include <stddef.h>
typedef struct {
char a;
int b;
char c;
} Example;
size_t offset_b = offsetof(Example, b); // b 的偏移量
位域
位域允许按位指定结构体成员的宽度,节省内存空间:
typedef struct {
unsigned int is_valid: 1; // 1 位
unsigned int is_active: 1; // 1 位
unsigned int priority: 3; // 3 位,可表示 0-7
unsigned int count: 4; // 4 位,可表示 0-15
} Flags; // 总共 9 位,可能占用 2 或 4 字节
Flags flags = {1, 0, 5, 12};
printf("valid: %u\n", flags.is_valid); // 1
printf("priority: %u\n", flags.priority); // 5
位域的注意事项:
- 位域成员必须是整数类型
- 不能对位域成员取地址
- 位域的布局依赖于编译器实现
- 跨平台代码应避免使用位域
联合体
联合体的定义
联合体使用 union 关键字定义,所有成员共享同一块内存:
union Data {
int i;
float f;
char str[20];
};
联合体的大小
联合体的大小等于最大成员的大小:
union Data data;
printf("联合体大小: %zu\n", sizeof(union Data)); // 20(str 最大)
printf("int 大小: %zu\n", sizeof(data.i)); // 4
printf("float 大小: %zu\n", sizeof(data.f)); // 4
printf("str 大小: %zu\n", sizeof(data.str)); // 20
联合体的使用
同一时刻只能使用一个成员:
union Data data;
data.i = 10;
printf("data.i = %d\n", data.i); // 10
data.f = 3.14f;
printf("data.f = %f\n", data.f); // 3.14
printf("data.i = %d\n", data.i); // 垃圾值(内存被覆盖)
联合体的应用
表示多种可能的数据类型:
typedef enum { INT_VAL, FLOAT_VAL, STR_VAL } DataType;
typedef struct {
DataType type;
union {
int int_val;
float float_val;
char str_val[20];
} value;
} Variant;
void print_variant(const Variant* v) {
switch (v->type) {
case INT_VAL:
printf("整数: %d\n", v->value.int_val);
break;
case FLOAT_VAL:
printf("浮点数: %f\n", v->value.float_val);
break;
case STR_VAL:
printf("字符串: %s\n", v->value.str_val);
break;
}
}
int main(void) {
Variant v1 = {INT_VAL, .value.int_val = 42};
Variant v2 = {FLOAT_VAL, .value.float_val = 3.14f};
Variant v3 = {STR_VAL, .value.str_val = "Hello"};
print_variant(&v1);
print_variant(&v2);
print_variant(&v3);
return 0;
}
IP 地址表示
联合体常用于网络编程中表示 IP 地址:
#include <stdint.h>
typedef union {
uint32_t addr; // 32 位整数表示
uint8_t bytes[4]; // 4 字节数组表示
} IPAddress;
IPAddress ip;
ip.addr = 0x0A000001; // 10.0.0.1
printf("IP: %d.%d.%d.%d\n",
ip.bytes[3], ip.bytes[2], ip.bytes[1], ip.bytes[0]);
枚举
枚举用于定义一组命名的整数常量:
enum Weekday {
SUNDAY, // 默认为 0
MONDAY, // 1
TUESDAY, // 2
WEDNESDAY, // 3
THURSDAY, // 4
FRIDAY, // 5
SATURDAY // 6
};
enum Weekday today = WEDNESDAY;
printf("今天是星期%d\n", today + 1); // 星期4
指定枚举值
enum Month {
JANUARY = 1,
FEBRUARY = 2,
MARCH = 3,
APRIL = 4,
MAY = 5,
JUNE = 6,
JULY = 7,
AUGUST = 8,
SEPTEMBER = 9,
OCTOBER = 10,
NOVEMBER = 11,
DECEMBER = 12
};
enum Status {
OK = 200,
NOT_FOUND = 404,
SERVER_ERROR = 500
};
枚举与 typedef
typedef enum {
RED,
GREEN,
BLUE
} Color;
Color favorite = GREEN;
自引用结构体
结构体可以包含指向自身类型的指针,用于构建链表、树等数据结构:
链表节点
typedef struct Node {
int data;
struct Node* next; // 指向自身类型的指针
} Node;
Node* create_node(int data) {
Node* node = malloc(sizeof(Node));
if (node != NULL) {
node->data = data;
node->next = NULL;
}
return node;
}
void append(Node** head, int data) {
Node* new_node = create_node(data);
if (*head == NULL) {
*head = new_node;
return;
}
Node* current = *head;
while (current->next != NULL) {
current = current->next;
}
current->next = new_node;
}
void print_list(const Node* head) {
const Node* current = head;
while (current != NULL) {
printf("%d -> ", current->data);
current = current->next;
}
printf("NULL\n");
}
void free_list(Node* head) {
Node* current = head;
while (current != NULL) {
Node* next = current->next;
free(current);
current = next;
}
}
二叉树节点
typedef struct TreeNode {
int data;
struct TreeNode* left;
struct TreeNode* right;
} TreeNode;
TreeNode* create_tree_node(int data) {
TreeNode* node = malloc(sizeof(TreeNode));
if (node != NULL) {
node->data = data;
node->left = NULL;
node->right = NULL;
}
return node;
}
void inorder_traversal(const TreeNode* root) {
if (root == NULL) return;
inorder_traversal(root->left);
printf("%d ", root->data);
inorder_traversal(root->right);
}
结构体比较
结构体不能直接用 == 比较,需要逐个成员比较:
bool person_equal(const Person* a, const Person* b) {
return strcmp(a->name, b->name) == 0 &&
a->age == b->age &&
a->height == b->height;
}
可以使用 memcmp 比较内存,但要注意填充字节的问题:
// 可能不可靠,因为填充字节可能不一致
if (memcmp(&p1, &p2, sizeof(Person)) == 0) {
// ...
}
复合字面量
C99 支持复合字面量,可以在表达式中直接创建结构体:
typedef struct {
int x;
int y;
} Point;
void print_point(Point p) {
printf("(%d, %d)\n", p.x, p.y);
}
int main(void) {
print_point((Point){10, 20}); // 复合字面量
Point* ptr = &(Point){5, 5}; // 指向复合字面量的指针
print_point(*ptr);
return 0;
}
小结
本章介绍了 C 语言的结构体与联合体:
- 结构体:定义、声明、初始化、成员访问
- 结构体指针:箭头运算符、指针参数
- 结构体数组:声明、初始化、遍历
- 嵌套结构体:结构体包含结构体
- 内存对齐:理解结构体大小和填充
- 位域:按位分配成员空间
- 联合体:共享内存的多类型数据
- 枚举:命名常量
- 自引用结构体:链表、树的实现
- 复合字面量:表达式中的结构体
下一章将学习 内存管理,深入了解动态内存分配。