第6章 结构体
自定义数据类型
一、导读
1.1 本章学习目标
第6章将C语言从"基本数据类型的集合"提升为"自定义数据结构的工具"。结构体(struct)允许你将不同类型的数据组合在一起,形成一个有机的整体——就像现实世界中的对象一样,一个学生有名字(字符串)、年龄(整数)、成绩(浮点数)。联合体(union)让不同的数据类型共享同一块内存,位域(bit-field)则允许你精确控制每个位的使用。
本章的核心学习目标包括:
- 掌握结构体的定义和使用:如何定义结构体类型、创建结构体变量、访问成员。
- 理解 `.` 和 `->` 运算符的区别:直接访问和通过指针访问结构体成员。
- 掌握结构体与函数的关系:结构体作为参数传递、作为返回值。
- 理解结构体的内存布局:内存对齐的原理和影响。
- 掌握联合体和位域:共享内存的数据类型和按位存储。
- 学会使用 `typedef`:为类型创建别名,简化代码。
1.2 为什么本章如此重要
结构体是C语言构建复杂数据结构的基石。从简单的坐标点(x, y)到复杂的链表节点、哈希表条目、网络数据包,所有复杂的数据组织都依赖结构体。
K&R 在第六章展示了一种优雅的设计哲学:通过组合基本类型来创建新的类型。这种组合式的类型构建方式,是面向对象编程中"类"的前身。事实上,C++的结构体就是从这里演化而来的。
理解结构体的内存布局(特别是对齐规则)对于系统编程至关重要。它影响数据的存储效率、网络传输的兼容性、以及与硬件寄存器的映射方式。
1.3 核心问题引导
在阅读本章时,请带着以下问题思考:
结构体和数组有什么本质区别?为什么不能只用数组?
. 和 -> 运算符分别在什么情况下使用?它们可以互相转换吗?
结构体作为函数参数时,是值传递还是引用传递?有什么性能影响?
为什么 sizeof(struct S) 可能大于所有成员 sizeof 之和?内存对齐是怎么回事?
联合体有什么实际用途?它和结构体的内存布局有什么不同?
typedef 真的只是"起别名"吗?它对代码的可移植性有什么帮助?
二、核心概念详解
2.1 结构体的定义与使用
2.1.1 定义结构体类型
struct Point {
int x;
int y;
};
struct Student {
char name[50];
int age;
float score;
char grade;
};struct 关键字定义了一个新的复合类型。花括号内是成员列表,每个成员有自己的类型和名字。
2.1.2 创建结构体变量
// 方式1:先定义类型,再创建变量
struct Point p1;
struct Point p2 = {3, 4}; // 初始化
// 方式2:定义类型的同时创建变量
struct Rect {
int x, y, width, height;
} rect1, rect2 = {0, 0, 100, 200};
// 方式3:匿名结构体(只能创建声明时的变量)
struct {
int r, g, b;
} color1 = {255, 128, 0};
// 方式4:动态分配
struct Point *p3 = (struct Point *)malloc(sizeof(struct Point));
p3->x = 10;
p3->y = 20;2.1.3 访问结构体成员
struct Student s;
// 使用 . 运算符(直接访问)
s.age = 20;
s.score = 95.5;
strcpy(s.name, "Alice");
// 使用 -> 运算符(通过指针访问)
struct Student *ps = &s;
ps->age = 21; // 等价于 (*ps).age = 21
ps->score = 96.0;. 运算符用于结构体变量,-> 运算符用于结构体指针。-> 是 (*ptr).member 的简写。
2.1.4 C99指定初始化器
struct Point p = {.x = 10, .y = 20};
struct Student s = {
.name = "Bob",
.age = 22,
.score = 88.5
};
// 未指定的成员自动初始化为0/NULL
struct Student s2 = {.age = 25}; // name全为0,score=0,grade='\0'2.2 结构体与函数
2.2.1 结构体作为参数
// 值传递:复制整个结构体(可能很慢)
float distance(struct Point a, struct Point b) {
int dx = a.x - b.x;
int dy = a.y - b.y;
return sqrt(dx * dx + dy * dy);
}
// 指针传递:只传递地址(高效)
void move_point(struct Point *p, int dx, int dy) {
p->x += dx;
p->y += dy;
}经验法则:小的结构体(1-2个成员)可以值传递;大的结构体应该通过指针传递。如果不需要修改结构体的内容,使用 const 指针:
void print_student(const struct Student *s) {
printf("Name: %s, Age: %d, Score: %.1f\n",
s->name, s->age, s->score);
// s->age = 25; // 错误!const指针不能修改成员
}2.2.2 结构体作为返回值
struct Point make_point(int x, int y) {
struct Point p = {x, y};
return p;
}
struct Point p = make_point(3, 4);2.3 结构体的内存布局
2.3.1 内存对齐
编译器会在结构体成员之间插入填充字节(padding),使每个成员的起始地址满足对齐要求:
struct A {
char a; // 1字节 + 3字节填充
int b; // 4字节
char c; // 1字节 + 3字节填充
};
// sizeof(struct A) = 12,不是 1+4+1=6
struct B {
char a; // 1字节
char c; // 1字节
int b; // 4字节(2字节填充后对齐)
};
// sizeof(struct B) = 8,成员顺序影响总大小对齐规则:
- 每个成员的起始地址必须是其对齐值的整数倍(通常等于
sizeof) - 结构体的总大小必须是最大对齐值的整数倍
- 成员的排列顺序影响总大小——将相同类型的成员放在一起可以减少填充
// 优化内存布局
struct Optimized {
int b; // 4字节
char a, c; // 1+1字节 + 2字节填充
};
// sizeof = 8,比 struct A 的12字节更紧凑2.3.2 查看内存布局
#include <stdio.h>
#include <stddef.h>
struct Student {
char name[50];
int age;
float score;
char grade;
};
int main(void) {
printf("sizeof(struct Student) = %lu\n", sizeof(struct Student));
printf("offset of name: %lu\n", offsetof(struct Student, name));
printf("offset of age: %lu\n", offsetof(struct Student, age));
printf("offset of score: %lu\n", offsetof(struct Student, score));
printf("offset of grade: %lu\n", offsetof(struct Student, grade));
return 0;
}2.4 结构体数组
struct Point points[3] = {
{0, 0},
{10, 20},
{30, 40}
};
for (int i = 0; i < 3; i++) {
printf("Point %d: (%d, %d)\n", i, points[i].x, points[i].y);
}
// 指针数组 vs 数组指针
struct Point *arr1[3]; // 3个指向Point的指针
struct Point (*arr2)[3]; // 指向包含3个Point的数组的指针2.5 结构体自引用
结构体可以包含指向自身的指针成员——这是链表、树等数据结构的基础:
struct Node {
int data;
struct Node *next; // 指向同类型的指针
};
// 二叉树
struct TreeNode {
int data;
struct TreeNode *left;
struct TreeNode *right;
};注意:结构体不能包含自身类型的成员(因为大小不确定),但可以包含指向自身类型的指针。
2.6 联合体(union)
联合体让多个不同类型的成员共享同一块内存:
union Data {
int i;
float f;
char str[20];
};
union Data data;
data.i = 10; // 现在int成员有效
data.f = 3.14f; // 现在float成员有效,int成员的值被覆盖
strcpy(data.str, "hello"); // 现在str成员有效,之前的值被覆盖
// sizeof(union Data) = max(sizeof(int), sizeof(float), sizeof(char[20]))
// = 20(所有成员共享同一块内存)联合体的典型应用:
// 1. 节省内存(当多个字段不会同时使用时)
struct Variant {
enum { INT, FLOAT, STRING } type;
union {
int i;
float f;
char str[64];
} data;
};
// 2. 类型双关(type punning)——查看数据的位表示
float f = 3.14f;
unsigned int *bits = (unsigned int *)&f;
printf("Float bits: 0x%08X\n", *bits);
// 3. 硬件寄存器映射
union Register {
unsigned int all;
struct {
unsigned int bit0 : 1;
unsigned int bit1 : 1;
unsigned int reserved : 6;
unsigned int value : 8;
} fields;
};2.7 位域(bit-field)
位域允许你精确指定结构体成员占用的位数:
struct Flags {
unsigned int read : 1; // 1位
unsigned int write : 1; // 1位
unsigned int execute : 1; // 1位
unsigned int owner : 3; // 3位(可表示0-7)
unsigned int group : 3; // 3位
unsigned int other : 3; // 3位
};
// 总共12位,通常存储在一个unsigned int中
struct Flags perm = {1, 1, 0, 7, 5, 4}; // rwxr-xr--位域的限制:
- 只能用于
int、unsigned int、signed int - 不能使用
&取位域的地址 - 位域的布局依赖于编译器和平台
- 不适合需要跨平台兼容的数据格式
2.8 typedef
typedef 为现有类型创建别名:
// 基本用法
typedef unsigned long size_t;
typedef struct Point {
int x;
int y;
} Point;
// 使用typedef后,可以省略struct关键字
Point p = {3, 4}; // 等价于 struct Point p = {3, 4};
// 常见模式:typedef与结构体结合
typedef struct {
float x, y, z;
} Vector3;
// 函数指针的typedef
typedef int (*CompareFunc)(const void *, const void *);
CompareFunc cmp = strcmp;`typedef` 的好处:
- 简化复杂类型的声明
- 提高代码可读性(
size_t比unsigned long更清晰) - 增强可移植性(底层类型可以随平台变化,但别名不变)
三、实战案例
3.1 案例一:通讯录管理
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAX_CONTACTS 100
#define MAX_NAME 50
#define MAX_PHONE 20
typedef struct {
char name[MAX_NAME];
char phone[MAX_PHONE];
char email[MAX_NAME];
} Contact;
typedef struct {
Contact entries[MAX_CONTACTS];
int count;
} AddressBook;
void init_book(AddressBook *book) {
book->count = 0;
}
int add_contact(AddressBook *book, const char *name,
const char *phone, const char *email) {
if (book->count >= MAX_CONTACTS) return -1;
Contact *c = &book->entries[book->count++];
strncpy(c->name, name, MAX_NAME - 1);
strncpy(c->phone, phone, MAX_PHONE - 1);
strncpy(c->email, email, MAX_NAME - 1);
return 0;
}
Contact *find_contact(AddressBook *book, const char *name) {
for (int i = 0; i < book->count; i++) {
if (strcmp(book->entries[i].name, name) == 0) {
return &book->entries[i];
}
}
return NULL;
}
void print_book(const AddressBook *book) {
printf("%-20s %-15s %-30s\n", "Name", "Phone", "Email");
printf("--------------------------------------------------------------\n");
for (int i = 0; i < book->count; i++) {
const Contact *c = &book->entries[i];
printf("%-20s %-15s %-30s\n", c->name, c->phone, c->email);
}
printf("Total: %d contacts\n", book->count);
}
int main(void) {
AddressBook book;
init_book(&book);
add_contact(&book, "Alice", "138-0001", "alice@example.com");
add_contact(&book, "Bob", "139-0002", "bob@example.com");
add_contact(&book, "Charlie", "137-0003", "charlie@example.com");
print_book(&book);
Contact *found = find_contact(&book, "Bob");
if (found) {
printf("\nFound: %s, Phone: %s\n", found->name, found->phone);
}
return 0;
}3.2 案例二:使用联合体实现多态
#include <stdio.h>
#include <string.h>
typedef enum {
TYPE_INT,
TYPE_FLOAT,
TYPE_STRING
} ValueType;
typedef struct {
ValueType type;
union {
int int_val;
float float_val;
char str_val[64];
} data;
} Value;
void print_value(const Value *v) {
switch (v->type) {
case TYPE_INT:
printf("int: %d\n", v->data.int_val);
break;
case TYPE_FLOAT:
printf("float: %.2f\n", v->data.float_val);
break;
case TYPE_STRING:
printf("string: %s\n", v->data.str_val);
break;
}
}
Value make_int(int v) {
Value val = {.type = TYPE_INT};
val.data.int_val = v;
return val;
}
Value make_float(float v) {
Value val = {.type = TYPE_FLOAT};
val.data.float_val = v;
return val;
}
Value make_string(const char *v) {
Value val = {.type = TYPE_STRING};
strncpy(val.data.str_val, v, sizeof(val.data.str_val) - 1);
return val;
}
int main(void) {
Value values[] = {
make_int(42),
make_float(3.14),
make_string("Hello")
};
for (int i = 0; i < 3; i++) {
print_value(&values[i]);
}
return 0;
}运行结果:
int: 42
float: 3.14
string: Hello3.3 案例三:位域实现权限系统
#include <stdio.h>
typedef struct {
unsigned int read : 1;
unsigned int write : 1;
unsigned int execute : 1;
} Permission;
typedef struct {
Permission owner;
Permission group;
Permission other;
} FileMode;
FileMode make_mode(int or, int ow, int ox,
int gr, int gw, int gx,
int otr, int otw, int otx) {
FileMode m;
m.owner.read = or; m.owner.write = ow; m.owner.execute = ox;
m.group.read = gr; m.group.write = gw; m.group.execute = gx;
m.other.read = otr; m.other.write = otw; m.other.execute = otx;
return m;
}
void print_perm(const char *label, Permission p) {
printf("%s: %c%c%c\n", label,
p.read ? 'r' : '-',
p.write ? 'w' : '-',
p.execute ? 'x' : '-');
}
void print_mode(FileMode m) {
print_perm("Owner", m.owner);
print_perm("Group", m.group);
print_perm("Other", m.other);
}
int main(void) {
// rwxr-xr-- (754)
FileMode mode = make_mode(1,1,1, 1,0,1, 1,0,0);
print_mode(mode);
return 0;
}运行结果:
Owner: rwx
Group: r-x
Other: r--四、本章小结
第6章系统讲解了C语言的自定义数据类型:
结构体:将不同类型的数据组合在一起。使用 . 运算符直接访问成员,-> 运算符通过指针访问。结构体可以值传递或指针传递给函数。
内存对齐:编译器在结构体成员之间插入填充字节以满足对齐要求。成员的排列顺序影响结构体的总大小。
自引用结构体:结构体可以包含指向自身类型的指针,这是链表、树等动态数据结构的基础。
联合体:多个成员共享同一块内存,大小等于最大成员的大小。适用于类型双关、变体类型、节省内存等场景。
位域:精确控制结构体成员占用的位数,适用于权限标志、硬件寄存器映射等场景。
`typedef`:为类型创建别名,简化复杂类型的声明,提高代码可读性和可移植性。
结构体是C语言构建复杂数据结构的基石。掌握结构体,你就能在C语言中实现几乎任何数据结构——链表、栈、队列、树、图、哈希表。下一章将学习C语言的输入输出机制。
五、思考与练习
思考题
为什么结构体的大小不等于所有成员大小之和?如何计算结构体的实际大小?
结构体值传递和指针传递各有什么优缺点?
联合体和结构体在内存布局上有什么本质区别?
为什么不能对位域使用 & 运算符取地址?
typedef struct { ... } Name; 和 struct Name { ... }; 有什么区别?
编程练习
实现一个栈数据结构:使用结构体封装数组、栈顶指针和容量,提供 push、pop、peek、is_empty、is_full 操作。
实现一个简单的哈希表:使用结构体数组作为桶,支持字符串键的插入、查找和删除。
编写一个程序:使用联合体实现一个可以存储int、float、string三种类型的变体数组,并实现排序功能。
实现一个日期结构体:包含年、月、日,提供日期比较、日期加减天数、计算两个日期之间的天数差等功能。