【C 语言】结构体:把一组相关数据装进同一个对象

一个学生通常有姓名、年龄、性别和学号。这些数据的类型并不相同:姓名是字符数组,年龄是整数。如果把它们拆成 name、age、sex、id 四个变量,数据一多,很快就会忘记哪些变量属于同一个人。

结构体把一组有关系、类型可以不同的数据放进同一个对象。本文按课件的主线讲清楚声明和初始化、成员访问、结构体指针、函数传参。每个完整示例都用 C11 编译运行,并给出输出。

1. 先把“学生”描述出来

结构体声明的基本形状是 struct 标签名 { 成员声明; };

例如,学生可以定义为 typedef struct Stu { char name[20]; int age; char sex[5]; char id[20]; } Stu;

最后的分号不能漏。struct Stu 是结构体类型的原名,Stu 是通过 typedef 起的别名。之后写 Stu 就可以声明变量,不必每次都写 struct Stu。

在这里插入图片描述

现在创建一个学生变量,并在定义时初始化:

#include <stdio.h>

typedef struct Stu {
    char name[20];
    int age;
    char sex[5];
    char id[20];
} Stu;

int main(void) {
    Stu s = {"Li Hua", 20, "M", "C2024001"};
    printf("name=%s\nage=%d\nsex=%s\nid=%s\n", s.name, s.age, s.sex, s.id);
    return 0;
}

运行结果:

name=Li Hua
age=20
sex=M
id=C2024001

初始化列表会按成员声明顺序对应:第一个字符串给 name,第二个值给 age,依次类推。顺序写错,编译器不一定能替你发现,读代码时要对照成员声明。

2. 声明变量和嵌套初始化

有了结构体类型后,可以在声明类型时创建变量,也可以稍后单独创建。例如 struct Point { int x; int y; } p1; 会在声明类型的同时创建 p1struct Point p2; 则单独创建 p2。结构体成员还可以是另一个结构体,甚至可以是指向自身的指针。这是链表结点的雏形:

#include <stdio.h>

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

typedef struct Node {
    int data;
    Point point;
    struct Node *next;
} Node;

int main(void) {
    Node n = {10, {4, 5}, NULL};
    printf("data=%d\npoint=(%d, %d)\nnext=%s\n",
           n.data, n.point.x, n.point.y,
           n.next == NULL ? "NULL" : "not NULL");
    return 0;
}

运行结果:

data=10
point=(4, 5)
next=NULL

{10, {4, 5}, NULL} 的第二项对应 point,里面的 {4, 5} 再对应 Point 的 x 和 y。next 暂时没有指向下一个结点,所以初始化为 NULL。

3. 用点操作符访问结构体变量的成员

当手里有的是结构体变量,使用点操作符(.)。例如在前面的完整程序中,n.data 读取整数,n.point.x 则先找到内层的 point,再取它的 x。

s.age 这样的成员表达式既可以读,也可以写。字符数组却不能在定义后用 = 整体赋值;修改字符串时可以使用 strcpy,但要确保数组空间足够。

4. 有结构体指针时,用箭头操作符

函数经常接收结构体指针,而不是整个结构体对象:

#include <stdio.h>

typedef struct Stu {
    char name[20];
    int age;
} Stu;

static void print_by_pointer(const Stu *ps) {
    printf("dot:   name=%s age=%d\n", (*ps).name, (*ps).age);
    printf("arrow: name=%s age=%d\n", ps->name, ps->age);
}

int main(void) {
    Stu s = {"Zhang San", 20};
    print_by_pointer(&s);
    return 0;
}

运行结果:

dot:   name=Zhang San age=20
arrow: name=Zhang San age=20

(*ps).age 和 ps->age 等价。前者先解引用指针,再用点操作符取成员;后者是 C 语言提供的简写。括号不能省:*ps.age 会被理解成 *(ps.age),指针 ps 本身并没有 age 成员。

5. 结构体传参:复制一份,还是传地址?

C 语言的普通参数传递是值传递。把结构体直接传给函数,函数会得到一份副本:

#include <stdio.h>

typedef struct Score {
    int chinese;
    int math;
} Score;

static void add_bonus_copy(Score score) {
    score.math += 10;
    printf("inside copy:   math=%d\n", score.math);
}

static void add_bonus_in_place(Score *pscore) {
    pscore->math += 10;
    printf("inside pointer: math=%d\n", pscore->math);
}

int main(void) {
    Score s = {88, 75};
    add_bonus_copy(s);
    printf("after copy:     math=%d\n", s.math);
    add_bonus_in_place(&s);
    printf("after pointer:  math=%d\n", s.math);
    return 0;
}

运行结果:

inside copy:   math=85
after copy:     math=75
inside pointer: math=85
after pointer:  math=85

第一次调用改的是副本,所以 main 里的 s.math 仍然是 75。第二次传入 &s,函数通过 pscore->math 改到了原对象。

如果结构体很大,按值传参还会复制更多数据。复制可能有开销,因此大结构体只需读取或修改原对象时,通常传指针更合适;小结构体需要独立副本时,按值传递也完全合理。只读函数可以加上 const,让编译器帮我们拦住误修改。

6. 把结构体放进数组

结构体可以组成数组。下面保存三名学生,打印名单,再找出分数最高的人:

#include <stdio.h>

typedef struct Student {
    char name[20];
    int age;
    int score;
} Student;

static void print_student(const Student *student) {
    printf("%-8s age=%d score=%d\n",
           student->name, student->age, student->score);
}

static const Student *best_student(const Student students[], size_t count) {
    const Student *best = &students[0];
    for (size_t i = 1; i < count; i++) {
        if (students[i].score > best->score) {
            best = &students[i];
        }
    }
    return best;
}

int main(void) {
    Student class1[] = {
        {"Li Hua", 20, 86},
        {"Wang Wei", 19, 92},
        {"Chen Yu", 20, 88}
    };
    size_t count = sizeof class1 / sizeof class1[0];

    for (size_t i = 0; i < count; i++) {
        print_student(&class1[i]);
    }
    printf("best: %s\n", best_student(class1, count)->name);
    return 0;
}

运行结果:

Li Hua   age=20 score=86
Wang Wei age=19 score=92
Chen Yu  age=20 score=88
best: Wang Wei

&class1[i] 是第 i 个结构体的地址,所以 print_student 用箭头操作符访问成员。sizeof class1 / sizeof class1[0] 只在 class1 仍是数组的地方计算长度;数组传给函数后会转换成指针,函数本身不知道元素个数,所以要把 count 一起传进去。best_student 假设 count > 0,调用时需满足这个条件。

7. 容易踩的几个坑

结构体声明末尾要有分号。手里是结构体变量时用点操作符,手里是结构体指针时用箭头操作符。要让函数修改调用者的对象,就传地址;按值传参改的是副本。

还有一个常见错误是给字符数组直接赋字符串:s.name = "Li Hua" 会在编译时失败,因为数组不能整体赋值。可以在定义变量时初始化,或在容量足够时使用 string.h 提供的 strcpy(s.name, "Li Hua")。目标数组必须放得下字符串以及末尾的 \0

结构体的语法不算多。先判断手里是对象还是指针,再选点操作符或箭头操作符;传参时先想清楚函数要不要修改原对象,以及复制整个对象值不值得。

练习

给上面的 Student 增加 char id[20] 成员。写一个按学号查找学生的函数,找不到时返回 NULL;再写一个加分函数,让所有学生的分数提高 5 分,但最高不超过 100。测试时别忘了“学号不存在”和“原本是 98 分”这两种情况。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

周杰偷奶茶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值