6C程序设计语言(第2版)

结构体

自定义数据类型

结构体联合体位域结构体指针

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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 定义结构体类型

c
struct Point {
    int x;
    int y;
};

struct Student {
    char name[50];
    int age;
    float score;
    char grade;
};

struct 关键字定义了一个新的复合类型。花括号内是成员列表,每个成员有自己的类型和名字。

2.1.2 创建结构体变量

c
// 方式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 访问结构体成员

c
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指定初始化器

c
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 结构体作为参数

c
// 值传递:复制整个结构体(可能很慢)
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 指针:

c
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 结构体作为返回值

c
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),使每个成员的起始地址满足对齐要求:

c
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)
  • 结构体的总大小必须是最大对齐值的整数倍
  • 成员的排列顺序影响总大小——将相同类型的成员放在一起可以减少填充
c
// 优化内存布局
struct Optimized {
    int b;       // 4字节
    char a, c;   // 1+1字节 + 2字节填充
};
// sizeof = 8,比 struct A 的12字节更紧凑

2.3.2 查看内存布局

c
#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 结构体数组

c
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 结构体自引用

结构体可以包含指向自身的指针成员——这是链表、树等数据结构的基础:

c
struct Node {
    int data;
    struct Node *next;    // 指向同类型的指针
};

// 二叉树
struct TreeNode {
    int data;
    struct TreeNode *left;
    struct TreeNode *right;
};

注意:结构体不能包含自身类型的成员(因为大小不确定),但可以包含指向自身类型的指针。

2.6 联合体(union)

联合体让多个不同类型的成员共享同一块内存:

c
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(所有成员共享同一块内存)

联合体的典型应用:

c
// 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)

位域允许你精确指定结构体成员占用的位数:

c
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 为现有类型创建别名:

c
// 基本用法
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 案例一:通讯录管理

c
#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 案例二:使用联合体实现多态

c
#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: Hello

3.3 案例三:位域实现权限系统

c
#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三种类型的变体数组,并实现排序功能。

实现一个日期结构体:包含年、月、日,提供日期比较、日期加减天数、计算两个日期之间的天数差等功能。

6/8结构体
0%