一、回顾与本篇目标
上一篇你学会了 C 语言中的函数——如何定义函数、传值调用的本质、数组作为函数参数、递归函数。
到目前为止,我们学的数据类型都是单一的:一个 int 存一个整数,一个 char 存一个字符,一个数组存相同类型的多个数据。但现实世界中的数据往往是复合的:一个学生有姓名(字符串)、年龄(整数)、成绩(浮点数);一本书有书名(字符串)、作者(字符串)、价格(浮点数)。这些数据由不同类型的字段组成,但它们属于同一个实体。
C 语言提供了结构体来解决这个问题。结构体让你把多个不同类型的数据打包成一个整体。如果你学过 JavaScript 或 Python,可以把结构体理解为 C 语言中“对象”的原始形态——它是 C++ 类、Java 类的前身。
本篇的目标:
- 理解结构体的概念——把不同类型的数据组合成一个新类型
- 学会定义结构体、声明结构体变量、初始化结构体
- 掌握访问结构体成员的两种方式:
.和-> - 学会结构体作为函数参数的用法
- 理解
typedef简化类型名
二、为什么需要结构体
假设你要在程序中管理学生信息。没有结构体的话,你只能把每个字段存成独立的变量或数组:
// 没有结构体:用多个数组存储学生信息(字段之间没有关联)
char names[3][20] = {"张三", "李四", "王五"};
int ages[3] = {20, 21, 19};
float scores[3] = {85.5, 92.0, 78.5};
// 打印第一个学生的信息——需要从三个数组分别取数据
printf("姓名:%s,年龄:%d,成绩:%.1f\n", names[0], ages[0], scores[0]);
这种方式有几个问题:
- 字段之间没有关联:
names[0]和ages[0]的关联完全靠你手动保证索引一致。如果不小心搞错了索引,张三的名字可能对上了李四的年龄。 - 代码难以维护:如果要增加一个字段(比如“班级”),需要新建一个数组,修改所有相关代码。
- 传递不方便:要把一个学生的所有信息传给函数,需要传三个参数。
用结构体改写:
// 用结构体:把学生的所有信息打包在一起
struct Student {
char name[20];
int age;
float score;
};
struct Student s1 = {"张三", 20, 85.5};
struct Student s2 = {"李四", 21, 92.0};
// 打印学生信息——通过一个变量访问所有字段
printf("姓名:%s,年龄:%d,成绩:%.1f\n", s1.name, s1.age, s1.score);
一个 s1 变量就包含了学生的所有信息,字段之间有了明确的关联。这就是结构体的核心价值:把相关数据组织在一起,形成一个逻辑整体。
三、定义结构体
结构体定义的语法:
struct 结构体名 {
类型 成员名1;
类型 成员名2;
// ... 可以有任意多个成员
};
注意花括号末尾的分号不能省略。这是 C 语言初学者常见的错误——忘记写分号。
定义一个学生结构体
struct Student {
char name[50]; // 姓名
int age; // 年龄
float score; // 成绩
char gender; // 性别('M' 或 'F')
};
这行代码只是定义了一个新的数据类型,叫 struct Student。它还没有占用任何内存——就像你画了一张设计图纸,但还没盖房子。
声明结构体变量(盖房子)
// 方式一:先定义结构体,再声明变量
struct Student s1;
// 方式二:定义结构体的同时声明变量
struct Student {
char name[50];
int age;
float score;
} s1, s2; // 同时声明两个变量
// 方式三:匿名结构体(只能在这一个地方使用)
struct {
char name[50];
int age;
} temp; // 只能用一次,因为没有结构体名
四、初始化结构体
和数组类似,结构体可以在声明时用 {} 初始化:
// 方式一:按顺序初始化
struct Student s1 = {"张三", 20, 92.5, 'M'};
// 方式二:指定成员名初始化(C99 支持,推荐这种写法)
struct Student s2 = {
.name = "李四",
.age = 21,
.score = 85.0,
.gender = 'F'
};
// 方式三:部分初始化(未指定的成员自动设为 0 或空)
struct Student s3 = {"王五"}; // age=0, score=0.0, gender='\0'
// 方式四:先声明,后赋值
struct Student s4;
s4.age = 19;
s4.score = 78.5;
s4.gender = 'M';
// s4.name = "赵六"; // 错误!不能用 = 给字符数组赋值
strcpy(s4.name, "赵六"); // 正确:用 strcpy 复制字符串
重要:字符数组(char name[50])不能用 = 直接赋值(除了在初始化时)。已经声明的结构体变量,修改字符串成员需要用 strcpy。
五、访问结构体成员
访问结构体成员用点运算符 .:
struct Student s = {"张三", 20, 92.5, 'M'};
printf("姓名:%s\n", s.name);
printf("年龄:%d\n", s.age);
printf("成绩:%.1f\n", s.score);
// 修改成员
s.age = 21;
s.score = 95.0;
如果你之前学过 JavaScript 或 Python,这个 . 语法应该非常熟悉:
// JavaScript
let student = { name: "张三", age: 20 };
console.log(student.name); // 用点号访问
# Python
class Student:
pass
s = Student()
s.name = "张三" # 用点号访问
C 语言结构体的点运算符和它们语法相同,但底层机制完全不同——C 语言中结构体的成员在内存中是连续存放的,点运算符在编译时就被翻译成了“基地址 + 偏移量”的内存访问。
六、结构体在内存中的布局
结构体的成员在内存中按照声明的顺序依次存放。每个成员相对于结构体起始地址的偏移量由编译时计算:
struct Student {
char name[50]; // 偏移 0,占 50 字节
int age; // 偏移 54(需要对齐到 4 的倍数),占 4 字节
float score; // 偏移 60,占 4 字节
char gender; // 偏移 64,占 1 字节
};
// 结构体总大小:68 字节(实际可能因为对齐而更大)
你可以用 sizeof 查看结构体的实际大小:
printf("struct Student 大小:%zu 字节\n", sizeof(struct Student));
实际大小可能比你手动算的大,因为编译器会对成员进行内存对齐——让每个成员的地址是其大小的整数倍,以提高 CPU 读取效率。这和 JavaScript/Python 中对象的存储方式完全不同(JS 对象用哈希表,Python 对象用字典,都不是连续内存)。
七、结构体数组
结构体本身是一个类型,自然可以创建结构体数组:
// 创建包含 3 个学生的数组
struct Student students[3] = {
{"张三", 20, 85.5, 'M'},
{"李四", 21, 92.0, 'F'},
{"王五", 19, 78.5, 'M'}
};
// 遍历学生数组
for (int i = 0; i < 3; i++) {
printf("学生 %d:%s,年龄 %d,成绩 %.1f\n",
i + 1, students[i].name, students[i].age, students[i].score);
}
结构体数组在内存中是连续的——每个结构体紧挨着下一个。这和数组的内存布局原理一致。
八、结构体指针
你可以创建指向结构体的指针,通过指针访问成员:
struct Student s = {"张三", 20, 85.5, 'M'};
struct Student *p = &s; // p 是指向 s 的指针
// 通过指针访问成员:用 -> 运算符(箭头运算符)
printf("姓名:%s\n", p->name); // 等价于 (*p).name
printf("年龄:%d\n", p->age);
printf("成绩:%.1f\n", p->score);
-> 运算符是 C 语言中专为结构体指针设计的。p->name 等价于 (*p).name——先解引用拿到结构体本身,再用点号访问成员。因为这种操作实在太常见了,C 语言提供了 -> 作为简洁写法。
记忆规则:
- 结构体变量用
.访问成员。 - 结构体指针用
->访问成员。
九、结构体作为函数参数
结构体作为函数参数时,同样遵循传值规则:
传结构体本身(传值——复制整个结构体)
// 打印学生信息(只读,用传值没问题)
void print_student(struct Student s) {
printf("姓名:%s,年龄:%d,成绩:%.1f\n", s.name, s.age, s.score);
// 修改 s 不会影响外部的原始结构体
}
int main() {
struct Student s1 = {"张三", 20, 85.5, 'M'};
print_student(s1);
return 0;
}
传结构体本身有一个性能问题:如果结构体很大(比如有几十个字段),复制整个结构体会消耗大量时间和栈空间。
传结构体指针(推荐——只传地址,高效)
// 传递指针:只传 8 字节(地址),不复制整个结构体
void print_student(const struct Student *p) {
printf("姓名:%s,年龄:%d,成绩:%.1f\n", p->name, p->age, p->score);
}
// 修改结构体的函数:必须传指针
void update_score(struct Student *p, float new_score) {
p->score = new_score; // 通过指针修改原始结构体
}
int main() {
struct Student s1 = {"张三", 20, 85.5, 'M'};
print_student(&s1);
update_score(&s1, 95.0);
print_student(&s1);
return 0;
}
最佳实践:
- 如果函数只读取结构体,传指向 const 结构体的指针——
const struct Student *p。 - 如果函数需要修改结构体,传结构体指针——
struct Student *p。 - 几乎不要传结构体本身,除非结构体非常小(比如只有一两个
int成员)。
十、typedef:给类型起别名
每次写 struct Student 有点啰嗦。可以用 typedef 给它起个简短的名字:
// 方式一:先定义结构体,再用 typedef 起别名
struct Student {
char name[50];
int age;
float score;
};
typedef struct Student Student; // 现在 Student 等价于 struct Student
// 方式二:定义结构体的同时起别名(更常用)
typedef struct {
char name[50];
int age;
float score;
} Student; // 现在直接用 Student,不需要写 struct
// 使用
Student s1 = {"张三", 20, 85.5};
Student s2;
有了 typedef,Student 就像一个普通的类型名(如 int、float),不需要再加 struct 关键字。这是 C 语言中非常常见的写法。
十一、综合演示:学生管理系统
下面这段代码综合运用了结构体定义、结构体数组、结构体指针、typedef、以及函数封装:
#include <stdio.h>
#include <string.h>
#define MAX_STUDENTS 100
// 定义学生结构体
typedef struct {
char name[50];
int age;
float score;
} Student;
// 函数原型
int input_students(Student students[], int max_count);
void print_student(const Student *s);
void print_all_students(const Student students[], int count);
float calculate_average(const Student students[], int count);
int find_highest(const Student students[], int count);
int main() {
Student students[MAX_STUDENTS];
int count;
count = input_students(students, MAX_STUDENTS);
if (count == 0) {
printf("没有输入任何学生信息。\n");
return 0;
}
printf("\n===== 学生信息列表 =====\n");
print_all_students(students, count);
float avg = calculate_average(students, count);
printf("\n平均成绩:%.1f\n", avg);
int highest_idx = find_highest(students, count);
printf("成绩最高的学生:");
print_student(&students[highest_idx]);
return 0;
}
// 输入学生信息
int input_students(Student students[], int max_count) {
int count;
printf("请输入学生人数(最多 %d):", max_count);
scanf("%d", &count);
if (count <= 0 || count > max_count) {
return 0;
}
// 清理输入缓冲区中的换行符
getchar();
for (int i = 0; i < count; i++) {
printf("\n--- 学生 %d ---\n", i + 1);
printf("姓名:");
fgets(students[i].name, sizeof(students[i].name), stdin);
// 去掉 fgets 读取的换行符
students[i].name[strcspn(students[i].name, "\n")] = '\0';
printf("年龄:");
scanf("%d", &students[i].age);
printf("成绩:");
scanf("%f", &students[i].score);
// 再次清理输入缓冲区
getchar();
}
return count;
}
// 打印单个学生信息
void print_student(const Student *s) {
printf("%s,年龄 %d,成绩 %.1f\n", s->name, s->age, s->score);
}
// 打印所有学生信息
void print_all_students(const Student students[], int count) {
for (int i = 0; i < count; i++) {
printf("%d. ", i + 1);
print_student(&students[i]);
}
}
// 计算平均成绩
float calculate_average(const Student students[], int count) {
float total = 0.0;
for (int i = 0; i < count; i++) {
total += students[i].score;
}
return total / count;
}
// 查找成绩最高的学生(返回索引)
int find_highest(const Student students[], int count) {
int highest = 0;
for (int i = 1; i < count; i++) {
if (students[i].score > students[highest].score) {
highest = i;
}
}
return highest;
}
代码设计要点:
typedef struct { ... } Student;:定义结构体的同时起别名,后面直接用Student作为类型名。const Student *s:函数只读取学生信息,用const指针明确表示“不会修改”。getchar():清理输入缓冲区。在scanf读取数字后,缓冲区里会残留一个换行符。如果接下来用fgets读字符串,这个残留的换行符会被当作输入吃掉。用getchar()手动把这个换行符读掉。sizeof(students[i].name):获取name数组的大小,传给fgets防止越界。
十二、本篇动手练习
练习 1:图书管理结构体
新建 practice8-1.c,定义一个 Book 结构体,包含书名、作者、价格、页数。创建 3 本书的数组,输出所有书的信息,找出最贵的书。
练习 2:点坐标计算
新建 practice8-2.c,定义一个 Point 结构体(包含 x 和 y 坐标)。写一个函数计算两点之间的距离(公式:√((x1-x2)² + (y1-y2)²)),写另一个函数判断三点是否共线。
练习 3:结构体嵌套
新建 practice8-3.c,定义一个 Date 结构体(年、月、日),然后定义一个 Employee 结构体,包含姓名、入职日期(Date 类型)、工资。创建数组并输出工龄最长的员工。
练习 4:链表节点
新建 practice8-4.c,定义一个 Node 结构体,包含一个整数数据和一个指向自身类型的指针(struct Node *next)。创建三个节点并手动串联成链表,遍历输出。这是数据结构中链表的前置练习。
十三、本篇小结
这一篇你学会了 C 语言中最重要的复合数据类型——结构体:
- 结构体的定义:
struct 结构体名 { 成员列表 };。定义只是创建了一个新类型,不占内存。 - 结构体变量的声明和初始化:
struct Student s = {"张三", 20, 85.5};。可以用指定成员名的方式初始化(.name = "张三")。 - 访问成员:结构体变量用
.,结构体指针用->。p->name等价于(*p).name。 - 结构体作为函数参数:推荐传结构体指针,避免复制整个结构体。只读时加
const修饰。 typedef:给类型起别名,简化结构体类型名的书写。- 内存布局:结构体成员在内存中连续存放,但编译器会进行内存对齐以提升访问效率。
结构体是 C 语言面向对象编程的基石。下一篇,我们学习 C 语言中另一个重要概念——动态内存分配。你会学到 malloc 和 free,理解堆和栈的区别,学会在程序运行时动态地创建和销毁数据。
下一篇预告
下一篇——《动态内存分配——在运行时申请和释放内存》:栈和堆的区别、malloc 申请内存、free 释放内存、calloc 和 realloc、内存泄漏的危险和防范。这是 C 语言中最容易出错的领域,也是区分 C 和高级语言(JS/Python 自动管理内存)的关键差异。
C/C++ 零基础入门,每周更新。












暂无评论内容