C 自建数据类型
本章导览 C 语言除了基本数据类型(int、float、char 等),还允许用户自己构造数据类型来描述复杂的现实对象。本章介绍三种构造类型和一种类型重命名机制: 类型 关键字 用途 结构体 struct 将不同类型的数据组合成一个整体 共用体 union 多个成员共享同一段内存 枚举 enum 将变量的取值限定在有限集合内 类型重命名 typedef 为已有类型起一个更简洁的别名 本章涵盖:结构体(定义、变量、数组、指针) → 链表(见 c-linked-list) → 共
Abstract: 本章导览 C 语言除了基本数据类型(
int、float、char等),还允许用户自己构造数据类型来描述复杂的现实对象。本章介绍三种构造类型和一种类型重命名机制:
类型 关键字 用途 结构体 struct将不同类型的数据组合成一个整体 共用体 union多个成员共享同一段内存 枚举 enum将变量的取值限定在有限集合内 类型重命名 typedef为已有类型起一个更简洁的别名 本章涵盖:结构体(定义、变量、数组、指针) → 链表(见 c-linked-list) → 共用体 → 枚举 → typedef
延伸应用:结构体常与 c-file-operations 结合,用于
fread/fwrite的整块读写。
1 定义和使用结构体变量
1.1 自己建立结构体类型
当需要用一个整体来描述包含多种不同类型数据的对象时(如一个学生有学号、姓名、性别、年龄、成绩、地址),就需要用到结构体。
声明结构体类型的一般形式
struct 结构体名 {
类型名 成员名;
类型名 成员名;
...
};
示例
struct Student {
int num; // 学号
char name[20]; // 姓名
char sex; // 性别
int age; // 年龄
float score; // 成绩
char addr[30]; // 地址
};
这就定义了一个名为 struct Student 的结构体类型,它包含 num、name、sex、age、score、addr 等不同类型的成员。
Note: 说明
- 结构体类型并非只有一种,可以设计出许多种,例如
struct Teacher、struct Worker、struct Date等,各自包含不同的成员。- 成员可以属于另一个结构体类型(嵌套结构体)。
嵌套结构体
struct Date {
int month;
int day;
int year;
};
struct Stu {
int num;
char name[20];
char sex;
int age;
struct Date birthday; // 成员是另一个结构体
char addr[30];
};
此时 birthday 成员本身又是一个结构体,包含 month、day、year 三个子成员。
1.2 定义结构体类型变量
Important: 类型 ≠ 变量 声明结构体类型只是建立了一个“模型”,并没有定义变量,系统不会分配存储单元。就像设计好了图纸,但并未建成具体的房屋。必须定义变量才能存放具体数据。
方法一:先声明类型,再定义变量(推荐)
struct Student {
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};
// 用类型名定义变量
struct Student student1, student2;
方法二:声明类型的同时定义变量
struct Student {
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
} student1, student2; // 声明的同时定义变量
方法三:不指定类型名,直接定义变量
struct {
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
} student1, student2; // 匿名结构体,只能在此处定义变量
Caution: 注意事项
- 结构体类型与结构体变量是不同的概念。只能对变量赋值、存取或运算,不能对类型做这些操作。
- 结构体类型中的成员名可以与程序中的普通变量名相同,但二者不代表同一对象。
- 对结构体变量中的成员(“域”),可以单独使用,它的作用与地位相当于普通变量。
1.3 结构体变量的初始化和引用
初始化
定义结构体变量时,可以用 { } 对各成员依次赋初值:
#include <stdio.h>
int main()
{
struct Student {
long int num;
char name[20];
char sex;
char addr[20];
} a = {10101, "Li Lin", 'M', "123 Beijing Road"};
printf("NO.:%ld\nname:%s\nsex:%c\naddress:%s\n",
a.num, a.name, a.sex, a.addr);
return 0;
}
引用成员:点运算符 .
通过 变量名.成员名 来访问结构体变量的各成员:
a.num = 10010; // 正确:访问成员 num
printf("%s\n", a.name); // 正确:输出成员 name
Danger: 常见错误
printf("%s\n", a); // 错误!不能整体输出结构体变量 scanf("%ld,%s,%c,%s\n", &a); // 错误!不能整体输入结构体变量必须逐个成员进行输入输出。
结构体变量之间的赋值
同类型的结构体变量之间可以整体赋值:
struct Student b;
b = a; // 正确:将 a 的全部成员值复制给 b
b.num++; // 正确:对 b 的成员单独操作
取成员地址
scanf("%ld", &a.num); // 正确:取成员 num 的地址
printf("%o", &a); // 正确:输出变量 a 的起始地址
嵌套结构体的成员访问
对于嵌套结构体,用连续的点运算符逐层访问:
struct Stu a, b;
a.birthday.month = 12; // 正确:访问嵌套成员
a.age = 10;
b.age = 9;
int sum = a.age + b.age; // 正确:成员参与运算
2 使用结构体数组
2.1 定义结构体数组
当需要处理多个同类型的结构体数据时,使用结构体数组。
struct Student {
int num;
char name[20];
float score;
};
// 定义并初始化结构体数组
struct Student stu[5] = {
{10101, "Zhang", 78},
{10103, "Wang", 98.5},
{10106, "Li", 86},
{10108, "Ling", 73.5},
{10110, "Fun", 100}
};
每个数组元素 stu[i] 都是一个完整的 struct Student 变量,可以通过 stu[i].num、stu[i].name、stu[i].score 访问各成员。
2.2 结构体数组的应用举例
例 4:按成绩排序输出学生信息
题目:有 n 个学生的信息(学号、姓名、成绩),按照成绩从高到低排序输出。
解题思路:用结构体数组存放 n 个学生信息,采用选择排序法对各元素排序(比较的是成绩 score)。
#include <stdio.h>
struct Student {
int num;
char name[20];
float score;
};
int main()
{
struct Student stu[5] = {
{10101, "Zhang", 78},
{10103, "Wang", 98.5},
{10106, "Li", 86},
{10108, "Ling", 73.5},
{10110, "Fun", 100}
};
struct Student temp;
const int n = 5;
int i, j, k;
// 选择排序:按成绩从高到低
for (i = 0; i < n - 1; i++) {
k = i;
for (j = i + 1; j < n; j++)
if (stu[j].score > stu[k].score)
k = j;
temp = stu[k]; // 整体交换结构体变量
stu[k] = stu[i];
stu[i] = temp;
}
printf("The order is:\n");
for (i = 0; i < n; i++)
printf("%6d %8s %6.2f\n",
stu[i].num, stu[i].name, stu[i].score);
printf("\n");
return 0;
}
Tip: 要点
- 排序时比较的是
stu[j].score,但交换的是整个结构体变量(temp = stu[k]),所有成员跟着一起移动。- 同类型的结构体变量可以直接用
=整体赋值。
读程序练习
#include <stdio.h>
struct abc { int a, b, c; };
int main()
{
struct abc s[2] = {{1, 2, 3}, {4, 5, 6}};
int t;
t = s[0].a + s[1].b;
printf("%d\n", t);
}
Question: 点击查看答案 输出:
6
s[0].a = 1,s[1].b = 5,1 + 5 = 6。
3 结构体指针
3.1 指向结构体变量的指针
指向结构体对象的指针变量可以指向结构体变量,也可以指向结构体数组中的元素。指针变量的基类型必须与结构体变量的类型相同。
struct Student *pt; // pt 是指向 struct Student 类型的指针
示例
#include <stdio.h>
#include <string.h>
int main()
{
struct Student {
long num;
char name[20];
char sex;
float score;
};
struct Student stu_1;
struct Student *p;
p = &stu_1; // p 指向 stu_1
stu_1.num = 10101;
strcpy(stu_1.name, "Li Lin");
stu_1.sex = 'M';
stu_1.score = 85;
// 以下三种访问方式完全等价
printf("No.:%ld\n", stu_1.num); // 方式①:变量名.成员名
printf("No.:%ld\n", (*p).num); // 方式②:(*指针).成员名
printf("No.:%ld\n", p->num); // 方式③:指针->成员名
return 0;
}
三种等价的访问方式
如果 p 指向结构体变量 stu,则以下三种方式完全等价:
| 方式 | 写法 | 说明 |
|---|---|---|
| ① | stu.成员名 |
直接通过变量名访问 |
| ② | (*p).成员名 |
先解引用指针,再用点运算符 |
| ③ | p->成员名 |
箭头运算符(最常用) |
Tip:
->运算符p->num是(*p).num的语法糖,更简洁、更常用。只有指针才能用->运算符,普通变量只能用.。
3.2 指向结构体数组的指针
例 6:用指针输出结构体数组
当指针指向结构体数组时,p++ 会让指针移动到下一个数组元素(而不是下一个字节),这与普通数组指针的行为一致。
struct Student stu[3] = {
{10101, "Zhang", 78},
{10103, "Wang", 98.5},
{10106, "Li", 86}
};
struct Student *p;
for (p = stu; p < stu + 3; p++)
printf("%d %s %f\n", p->num, p->name, p->score);
3.3 用结构体变量和指针作函数参数
将结构体数据传递给函数有 3 种方法:
| 方法 | 说明 | 特点 |
|---|---|---|
| 传递成员值 | func(stu.num, stu.name) |
逐个传递,灵活但繁琐 |
| 传递结构体变量 | func(stu) |
整体复制,简洁但开销大 |
| 传递结构体指针 | func(&stu) / func(p) |
推荐——传地址,高效 |
Tip: 推荐使用指针传递 传递结构体变量时会复制整个结构体(值传递),如果结构体很大,开销高。传递指针只需要复制一个地址值(4/8 字节),效率更高,且可以在函数内部修改原始数据。
4 用指针处理链表
Info: 详细内容 链表是本章的重点应用,已在 c-linked-list 中详细讲解,包括:
- 链表的概念与结构
- 静态链表的建立
- 动态链表的创建(
creat函数)- 链表的遍历输出(
请移步 c-linked-list 查看完整内容。
核心要点回顾
- 链表是动态数据结构,各结点地址不连续,通过指针相连。
- 每个结点包含数据域和指针域(
next指向下一个结点)。 - 结点的结构体中必须含有一个指向自身类型的指针成员。
struct Student {
int num;
float score;
struct Student *next; // 自引用指针
};
5 共用体类型
5.1 什么是共用体类型
有时想用同一段内存单元存放不同类型的变量。使几个不同的变量共享同一段内存的结构,称为共用体(Union)类型。
地址: 1000 1001 1002 1003
┌─────────────────────┐
int i │ 整型变量 i │ ← 4 字节
├─────────────────────┤
char ch│ch│ │ ← 1 字节
├─────────────────────┤
float f│ 实型变量 f │ ← 4 字节
└─────────────────────┘
三个成员共用同一段内存(起始地址相同)
定义共用体类型
union 共用体名 {
成员表列
};
示例
// 方式一:先声明类型,再定义变量
union Data {
int i;
char ch;
float f;
};
union Data a, b, c;
// 方式二:声明的同时定义变量
union Data {
int i;
char ch;
float f;
} a, b, c;
Important: 共用体 vs 结构体 的内存区别
特性 结构体 struct共用体 union内存大小 各成员长度之和(可能有对齐填充) 等于最长成员的长度 成员关系 各成员各有自己的内存单元 所有成员共享同一段内存 同时使用 所有成员可以同时存储有效数据 同一时刻只有一个成员有效
5.2 引用共用体变量的方式
Caution: 只能引用成员,不能引用变量整体 不能对共用体变量名直接赋值或引用,只能引用其中的成员。
a.i = 97; // 正确:给 int 成员赋值
a.ch = 'A'; // 正确:给 char 成员赋值
a.f = 3.14; // 正确:给 float 成员赋值
printf("%d", a); // 错误!不能引用共用体变量整体
a = 1; // 错误!不能对共用体变量名赋值
5.3 共用体类型数据的特点
Warning: 使用共用体的注意事项
(1) 同一时刻只能存放一个成员
虽然可以向不同成员赋值,但每次新赋值会覆盖旧数据(因为它们占用同一段内存)。
a.i = 97;
a.ch = 'A'; // 此时 a.i 的值已被覆盖
a.f = 3.14; // 此时 a.ch 和 a.i 的值都不再有意义
(2) 初始化只能有一个值
union Data a = {16}; // 正确:初始化第一个成员
union Data a = {16, 'A', 1.0}; // 错误!只能有一个值
(3) 起作用的成员是最后一次被赋值的成员
共用体变量中“当前有效”的是最后一次被赋值的那个成员。
(4) 各成员地址相同
共用体变量的地址和它的各成员的地址都是同一地址。
// 以下三个地址值相同
printf("%p %p %p\n", &a, &a.i, &a.ch);
(5) 不能对变量名赋值或引用
a = 1; // 错误
printf("%d", a); // 错误
(6) 共用体与结构体可以互相嵌套
共用体类型可以出现在结构体定义中,结构体也可以出现在共用体定义中,数组也可以作为共用体的成员。
(7) 函数参数
可以使用指向共用体变量的指针作函数参数(C99 也允许直接传递共用体变量)。
读程序练习
#include <stdio.h>
union myun {
struct {
int x, y, z;
} u;
int k;
} a;
void main()
{
a.u.x = 4;
a.u.y = 5;
a.u.z = 6;
a.k = 0;
printf("%d\n", a.u.x);
}
Question: 点击查看答案 输出:
0分析:
a.k和a.u共享同一段内存。a.k = 0会覆盖a.u开头的内存区域。由于a.u.x是struct的第一个成员,它与a.k起始地址相同,所以a.k = 0实际上把a.u.x的值也改成了0。内存布局: | x(int) | y(int) | z(int) | | k(int) | | | ← k 只覆盖前 4 字节 ↑ a.k=0 → a.u.x 也变为 0
6 使用枚举类型
什么是枚举类型
如果一个变量只有几种可能的值,可以定义为枚举类型(Enumeration)。所谓“枚举”就是把所有可能的值一一列举出来,变量的取值只限于列举的范围内。
声明枚举类型
enum 枚举类型名 { 枚举元素1, 枚举元素2, ... };
示例
// 声明枚举类型
enum Weekday { sun, mon, tue, wed, thu, fri, sat };
// 定义枚举变量
enum Weekday workday, weekend;
// 赋值(只能赋枚举元素的值)
workday = mon; // 正确
weekend = sun; // 正确
// weekday = monday; // 错误!monday 不是枚举元素
枚举类型的特点
(1) 枚举元素是常量,不能赋值
sun = 0; // 错误!枚举元素是常量,不能修改
mon = 1; // 错误!
(2) 枚举元素有对应的整数值
C 编译器按定义顺序,默认从 0 开始依次赋值:
enum Weekday { sun, mon, tue, wed, thu, fri, sat };
// 0 1 2 3 4 5 6
因此:
workday = mon;
// 相当于 workday = 1;
(3) 可以手动指定枚举值
enum Weekday { sun = 7, mon = 1, tue, wed, thu, fri, sat };
// 7 1 2 3 4 5 6
// 从 mon=1 开始,后续自动 +1
(4) 枚举元素可用于判断和比较
比较规则按照对应的整数值进行:
if (workday == mon) ... // 判断是否为星期一
if (workday > sun) ... // 比较大小
// mon > sun(因为 1 > 0)
// sat > fri(因为 6 > 5)
读程序练习
#include <stdio.h>
enum coin { cent, nickel, dime, quarter, half, dollar };
// 0 1 2 3 4 5
void main()
{
char *name[] = {"cent", "nickel", "dime",
"quarter", "half", "dollar"};
enum coin money1, money2;
money1 = dime; // money1 = 2
money2 = dollar; // money2 = 5
printf("%d %d\n", money1, money2);
printf("%s %s\n", name[money1], name[money2]);
}
Question: 点击查看答案 输出:
2 5 dime dollar
dime的枚举值为 2,dollar为 5。name[2]是"dime",name[5]是"dollar"。
7 用 typedef 声明新类型名
基本用法
typedef 的作用是为已经存在的类型指定一个新的名字(别名/同义词),使代码更简洁、更易读。
Important: typedef 不创造新类型
typedef只是给已有类型起了个别名,并没有创造新的类型。
1. 简单类型别名
typedef int Integer;
typedef float Real;
// 以下两行完全等价
int i, j;
Integer i, j;
// 以下两行完全等价
float a, b;
Real a, b;
2. 为结构体定义别名
typedef struct {
int month;
int day;
int year;
} Date;
Date birthday; // 等价于 struct { int month; int day; int year; } birthday;
Date *p; // 指向 Date 类型的指针
3. 为数组类型定义别名
typedef int Num[100];
Num a; // 等价于 int a[100];
4. 为指针类型定义别名
typedef char *String;
String p, s[10]; // p 是 char* 指针,s 是包含 10 个 char* 的数组
5. 为函数指针定义别名
typedef int (*Pointer)();
Pointer p1, p2; // p1, p2 都是指向"返回 int、无参数的函数"的指针
typedef 的声明方法(通用步骤)
Tip: 三步法
- 先按定义变量的方法写出定义体(如
int a[100];)- 将变量名换成新类型名(将
a换成Num)→int Num[100];- 在最前面加
typedef→typedef int Num[100];- 然后就可以用新类型名去定义变量了:
Num a;
示例推导
| 步骤 | 数组类型 | 指针类型 |
|---|---|---|
| ① 定义变量 | int a[100]; |
char *p; |
| ② 换成类型名 | int Num[100]; |
char *String; |
| ③ 加 typedef | typedef int Num[100]; |
typedef char *String; |
| ④ 使用 | Num a; → int a[100] |
String p; → char *p |
typedef 的补充说明
(1) typedef 定义的是同义词
typedef int Num[100];
Num a; // Num 是 int[100] 的同义词
typedef int (*Pointer)();
Pointer p1; // Pointer 是 int(*)() 的同义词
(2) typedef 没有创造新类型
只是给已有类型起了别名。
(3) typedef 可用于各种类型
数组类型、指针类型、结构体类型、共用体类型、枚举类型都可以用 typedef 简化。
(4) typedef 与 #define 的区别
| 特性 | typedef |
#define |
|---|---|---|
| 处理时机 | 编译时处理 | 预处理时文本替换 |
| 作用域 | 遵循 C 语言作用域规则 | 从定义处到文件末尾 |
| 类型安全 | 是真正的类型别名 | 纯文本替换,可能出错 |
// 区别示例
typedef char *String;
String a, b; // a 和 b 都是 char* 指针 ✓
#define String char *
String a, b; // 展开为 char *a, b; → a 是指针,b 是 char! ✗
(5) 实用建议
- 当不同源文件中用到同一类型数据时,可以把所有
typedef声明放在一个头文件中,然后用#include包含。 - 使用
typedef有利于程序的通用与移植。程序依赖硬件特性时,用typedef类型便于在不同平台间切换。
本章总结
三种构造数据类型
| 类型 | 关键字 | 内存分配 | 核心特点 |
|---|---|---|---|
| 结构体 | struct |
各成员分别占用各自的内存 | 组合不同类型的数据为一个整体 |
| 共用体 | union |
所有成员共享同一段内存 | 同一时刻只有一个成员有效 |
| 枚举 | enum |
本质是 int 常量 |
限定变量的取值范围 |
typedef 类型重命名
- 不创造新类型,只是起别名
- 三步法:定义变量 → 换类型名 → 加
typedef - 比
#define更安全(编译器理解类型含义)
结构体操作速查
| 操作 | 语法 | 示例 |
|---|---|---|
| 定义类型 | struct 名 { ... }; |
struct Student { int num; ... }; |
| 定义变量 | struct 名 变量; |
struct Student stu; |
| 初始化 | = { 值1, 值2, ... } |
= {10101, "Li", 90}; |
| 访问成员 | 变量.成员 |
stu.num |
| 指针访问 | 指针->成员 |
p->num(等价 (*p).num) |
| 整体赋值 | 变量1 = 变量2 |
b = a; |
| 结构体数组 | struct 名 数组[N] |
struct Student stu[5]; |
| 取成员地址 | &变量.成员 |
&stu.num |
易错点汇总
Danger: 常见错误
- 类型 vs 变量混淆:只能对变量赋值/运算,不能对类型操作
- 整体输入输出:不能
printf("%s", stu)或scanf(..., &stu)整体操作- 共用体覆盖:对共用体一个成员赋值后,其他成员的值不再可靠
- 枚举赋值:枚举元素是常量,不能被赋值(
sun = 0是错误的)typedefvs#define:typedef是类型别名,#define是文本替换,指针场景下#define会出错->只能用于指针:普通结构体变量只能用.,不能用->