【C语言-自定义数据类型】1. 结构体——把不同类型的数据打包在一起

开发技术 作者: 精灵星空 SKY315.COM 更新于: 浏览: 3
码字发文不易,转载请注明出处【精灵星空 SKY315.COM】

前面咱们把C语言自带的基本类型和派生类型都学完了。但你现在想想,如果要描述一个“学生”,你有啥办法?

学生的信息有:学号(int)、姓名(字符串)、成绩(float)。按咱们以前学的,你得分开定义三个变量:

int id;
char name[20];
float score;

一个学生还行,但你要是存一个班50个学生呢?定义150个变量?那代码直接炸了。

更要命的是,这三个变量在逻辑上是一伙的,都是描述同一个学生的。但在内存里,它们各待各的,你完全看不出来它们之间有什么关系。你传给函数的时候,得把三个变量都传一遍,麻烦得要死。

结构体就是来解决这个问题的。 它让你把不同类型的数据打包成一个整体,给这个整体起个名字。以后你就可以把“一个学生”当成一个变量来用了。

说白了,结构体就是把一堆零散的数据用绳子捆在一起,打成一个包裹。你想寄快递,直接把整个包裹递过去就行,不用把里面的东西一件一件掏出来分开寄。

结构体长啥样?

定义一个结构体类型,格式是这样的:

struct 结构体名 {
    成员类型 成员名;
    成员类型 成员名;
    ...
};

比如描述一个学生:

struct Student {
    int id;           // 学号
    char name[20];    // 姓名
    float score;      // 成绩
};

这行代码的意思:我定义了一个叫Student的结构体类型,它里面包含三个成员——一个int(id)、一个字符数组(name)、一个float(score)。

注意:这只是定义了一个类型,还没有分配内存。 就像你画了一张图纸,还没有按照图纸盖房子。图纸画好了,你以后就可以照着它盖无数个房子。

定义结构体变量

有了类型,你就可以用它来定义变量了:

struct Student s1;   // 定义了一个Student类型的变量s1

现在s1就是一个“学生”了,它里面包含了id、name、score三个成员。

你也可以在定义类型的同时直接定义变量:

struct Student {
    int id;
    char name[20];
    float score;
} s1, s2;   // 顺便定义了s1和s2两个变量

但一般不建议这么写,因为类型定义和变量定义混在一起,看着乱。分开写更清晰。

访问结构体成员——用点号

定义了变量s1,怎么给里面的成员赋值?

用.(点号)运算符:

s1.id = 101;              // 给学号赋值
strcpy(s1.name, "张三");   // 给姓名赋值,字符数组要用strcpy
s1.score = 85.5;          // 给成绩赋值

printf("学号:%d\n", s1.id);
printf("姓名:%s\n", s1.name);
printf("成绩:%.1f\n", s1.score);

注意: name是字符数组,你不能写s1.name = "张三",数组名是常量地址,不能赋值。必须用strcpy函数把字符串拷贝进去。

结构体初始化

跟数组一样,结构体也可以在定义的时候直接初始化:

struct Student s1 = {101, "张三", 85.5};

顺序就是成员定义的顺序:第一个值给id,第二个给name,第三个给score。

也可以指定成员来初始化(C99之后支持):

struct Student s1 = {.id = 101, .name = "张三", .score = 85.5};

这种方式更清晰,不容易搞错顺序。

结构体数组——一次存一堆

你想存一个班的学生,那就定义结构体数组:

struct Student class[50];

这就相当于50个Student打包排在一起。访问第3个学生的姓名,就写:

class[2].name

遍历结构体数组:

for (int i = 0; i < 50; i++) {
    printf("%d %s %.1f\n", class[i].id, class[i].name, class[i].score);
}

完整例子:

#include <stdio.h>
#include <string.h>

struct Student {
    int id;
    char name[20];
    float score;
};

int main() {
    struct Student class[3] = {
        {101, "张三", 85.5},
        {102, "李四", 92.0},
        {103, "王五", 78.5}
    };

    for (int i = 0; i < 3; i++) {
        printf("%d %s %.1f\n", class[i].id, class[i].name, class[i].score);
    }

    return 0;
}

结构体指针——用箭头访问成员

结构体的指针跟普通指针一样,存的是结构体变量的地址:

struct Student s1 = {101, "张三", 85.5};
struct Student *p = &s1;

通过指针访问成员,不能用点号,得用箭头->:

printf("%d\n", p->id);       // 等价于 (*p).id
printf("%s\n", p->name);     // 等价于 (*p).name
printf("%.1f\n", p->score);  // 等价于 (*p).score

p->id读作“p指向的结构体的id成员”。它跟(*p).id完全等价,但写起来更简洁。

为啥要有箭头? 因为*p.id会被编译器理解成*(p.id)——先取p的id成员(但p是指针,没有id成员),语法错误。所以必须加括号写成(*p).id。为了省事,C语言就发明了->运算符。

结构体作为函数参数

结构体可以像普通变量一样传给函数:

void print_student(struct Student s) {
    printf("%d %s %.1f\n", s.id, s.name, s.score);
}

int main() {
    struct Student s1 = {101, "张三", 85.5};
    print_student(s1);   // 传的是整个结构体的副本
    return 0;
}

注意:结构体传参是值传递,函数收到的是整个结构体的拷贝。 如果结构体很大(比如里面有个大数组),拷贝的开销会很大。

所以更常见的做法是传结构体的指针:

void print_student(struct Student *p) {
    printf("%d %s %.1f\n", p->id, p->name, p->score);
}

int main() {
    struct Student s1 = {101, "张三", 85.5};
    print_student(&s1);   // 传地址,效率高
    return 0;
}

传指针只传一个地址(8字节),不管结构体多大,开销都一样。而且通过指针还能修改原结构体的内容。

结构体嵌套——结构体里面套结构体

结构体的成员可以是另一个结构体。比如学生有出生日期,日期本身也可以是个结构体:

struct Date {
    int year;
    int month;
    int day;
};

struct Student {
    int id;
    char name[20];
    struct Date birthday;   // 嵌套一个Date结构体
    float score;
};

访问嵌套成员要用两层点号:

struct Student s1;
s1.birthday.year = 2005;
s1.birthday.month = 3;
s1.birthday.day = 15;

如果是指针,就用箭头加点号:

struct Student *p = &s1;
p->birthday.year = 2005;

一个完整的例子:学生成绩管理

把今天学的全串起来,写一个简单的小程序:输入三个学生的信息,打印出来,再找出成绩最高的那个。

#include <stdio.h>
#include <string.h>

struct Student {
    int id;
    char name[20];
    float score;
};

int main() {
    struct Student class[3];

    // 输入
    for (int i = 0; i < 3; i++) {
        printf("请输入第%d个学生的学号、姓名、成绩:\n", i + 1);
        scanf("%d %s %f", &class[i].id, class[i].name, &class[i].score);
    }

    // 输出
    printf("\n所有学生信息:\n");
    for (int i = 0; i < 3; i++) {
        printf("%d %s %.1f\n", class[i].id, class[i].name, class[i].score);
    }

    // 找最高分
    int maxIndex = 0;
    for (int i = 1; i < 3; i++) {
        if (class[i].score > class[maxIndex].score) {
            maxIndex = i;
        }
    }
    printf("\n最高分学生:%s,成绩:%.1f\n", class[maxIndex].name, class[maxIndex].score);

    return 0;
}

这个例子虽然简单,但把结构体定义、结构体数组、成员访问、遍历、比较全用上了。你把它敲一遍,结构体的基本用法就掌握了。

几个常见的坑

坑1:忘了写struct关键字

Student s1;          // 错误!类型名是struct Student
struct Student s1;   // 正确

用typedef可以省掉struct,但不用typedef的话必须写全。

坑2:字符数组不能用=赋值

s1.name = "张三";          // 错误!数组名不能赋值
strcpy(s1.name, "张三");   // 正确

坑3:结构体不能直接比较相等

if (s1 == s2)   // 错误!结构体不能直接用==比较

要比较两个结构体,得逐个成员比较。

坑4:用指针访问成员忘了用->

p.id = 101;    // 错误!p是指针,不能用点号
p->id = 101;   // 正确

坑5:结构体传参效率问题

void func(struct Student s) { ... }   // 值传递,拷贝整个结构体
void func(struct Student *p) { ... }  // 指针传递,只拷贝地址

结构体大的时候,用指针传参。

总结一下

结构体就一句话:把不同类型的数据打包在一起,当成一个整体来用。

核心操作:

  • 定义:struct 名字 { 成员列表 };

  • 定义变量:struct 名字 变量名;

  • 访问成员:变量用.,指针用->

  • 初始化:struct 名字 变量 = {值1, 值2, ...};

  • 数组:struct 名字 数组名[大小];

  • 函数参数:传值(拷贝)或传指针(高效)

结构体是C语言里最常用的自定义类型,后面学链表、学文件操作、学各种数据结构,全都离不开它。你把这节课的内容吃透了,以后写复杂程序就有了基础。

回去自己动手,写一个“商品”结构体(编号、名称、价格、库存),定义几个商品,写函数打印商品信息、找最贵的商品、算总库存价值。练熟了,结构体就是你的了。下节课咱们讲共用体——那个跟结构体长得像但脾气完全不同的东西。