跳到主要内容

结构体与联合体

结构体和联合体是 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 语言的结构体与联合体:

  • 结构体:定义、声明、初始化、成员访问
  • 结构体指针:箭头运算符、指针参数
  • 结构体数组:声明、初始化、遍历
  • 嵌套结构体:结构体包含结构体
  • 内存对齐:理解结构体大小和填充
  • 位域:按位分配成员空间
  • 联合体:共享内存的多类型数据
  • 枚举:命名常量
  • 自引用结构体:链表、树的实现
  • 复合字面量:表达式中的结构体

下一章将学习 内存管理,深入了解动态内存分配。