C 自建数据类型

本章导览 C 语言除了基本数据类型(int、float、char 等),还允许用户自己构造数据类型来描述复杂的现实对象。本章介绍三种构造类型和一种类型重命名机制: 类型 关键字 用途 结构体 struct 将不同类型的数据组合成一个整体 共用体 union 多个成员共享同一段内存 枚举 enum 将变量的取值限定在有限集合内 类型重命名 typedef 为已有类型起一个更简洁的别名 本章涵盖:结构体(定义、变量、数组、指针) → 链表(见 c-linked-list) → 共

Abstract: 本章导览 C 语言除了基本数据类型(intfloatchar 等),还允许用户自己构造数据类型来描述复杂的现实对象。本章介绍三种构造类型和一种类型重命名机制:

类型 关键字 用途
结构体 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 的结构体类型,它包含 numnamesexagescoreaddr 等不同类型的成员

Note: 说明

  1. 结构体类型并非只有一种,可以设计出许多种,例如 struct Teacherstruct Workerstruct Date 等,各自包含不同的成员。
  2. 成员可以属于另一个结构体类型(嵌套结构体)。

嵌套结构体

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 成员本身又是一个结构体,包含 monthdayyear 三个子成员。


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].numstu[i].namestu[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 = 1s[1].b = 51 + 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 函数)
  • 链表的遍历输出(print 函数)

请移步 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.ka.u 共享同一段内存。a.k = 0 会覆盖 a.u 开头的内存区域。由于 a.u.xstruct 的第一个成员,它与 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: 三步法

  1. 先按定义变量的方法写出定义体(如 int a[100];
  2. 将变量名换成新类型名(将 a 换成 Num)→ int Num[100];
  3. 在最前面加 typedeftypedef int Num[100];
  4. 然后就可以用新类型名去定义变量了: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: 常见错误

  1. 类型 vs 变量混淆:只能对变量赋值/运算,不能对类型操作
  2. 整体输入输出:不能 printf("%s", stu)scanf(..., &stu) 整体操作
  3. 共用体覆盖:对共用体一个成员赋值后,其他成员的值不再可靠
  4. 枚举赋值:枚举元素是常量,不能被赋值(sun = 0 是错误的)
  5. typedef vs #definetypedef 是类型别名,#define 是文本替换,指针场景下 #define 会出错
  6. -> 只能用于指针:普通结构体变量只能用 .,不能用 ->

相关笔记