C++ 结构体

在现实生活中,一个事物往往包含多种不同的属性。比如我们要记录一名学生的信息,学号和年龄是整数,姓名是字符串,成绩则是小数。

前面学过的数组只能存放同一种类型的数据,没法把这些属性装在一起。如果拆成四个零散的变量,一旦学生多起来就很容易搞混。这时我们就可以使用 “结构体(struct)”,把属于同一个事物的多项数据打包成一个整体。

C++ 结构体的定义

在 C++ 中,我们可以使用 struct 关键字来定义结构体类型。其中,struct 是英文 “structure(结构)” 的缩写,花括号里面包含的每一项数据称为结构体的 “成员”。

语法:

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

说明:

结构体类型名由我们自己命名,习惯上首字母大写(如 Student、Book),方便一眼看出这是一个自定义类型。结构体里的每一个成员,既可以是 int、double 等基本类型,也可以是 std::string、数组、指针,甚至是另一个结构体。

定义好结构体类型后,注意右花括号 } 后面的英文分号(;)不能漏掉。接着我们就可以使用这个类型来定义结构体变量,并通过点运算符(.)访问里面的具体成员,格式为 结构体变量名.成员名。

示例 1:定义并使用结构体

#include <iostream>
#include <string>

// 定义结构体类型
struct Student {
    int id;              // 学号
    std::string name;    // 姓名
    int age;             // 年龄
    double score;        // 成绩
};

int main() {
    Student stu = {1001, "Jack", 18, 92.5};

    std::cout << "学号:" << stu.id << '\n';
    std::cout << "姓名:" << stu.name << '\n';
    std::cout << "年龄:" << stu.age << '\n';
    std::cout << "成绩:" << stu.score << '\n';
    return 0;
}

运行结果如下。

学号:1001
姓名:Jack
年龄:18
成绩:92.5

分析:

在这个例子中,我们先使用 struct Student { ... }; 定义了一个名为 Student 的结构体类型。结构体类型通常定义在 main() 函数外面,方便当前文件中的所有函数共用。

这里小伙伴们要分清楚:Student 是一个 “类型名”,而不是 “变量名”。你可以对照 int a = 10; 来理解,其中 int 是类型名,a 是变量名,这里的 Student 就相当于我们自己定义出来的类型名。

接着在 main() 函数中,Student stu = {1001, "Jack", 18, 92.5}; 表示定义了一个结构体变量 stu,花括号里的初始值会按成员声明的先后顺序依次赋给各个成员。初始化时花括号前面的等号 = 也可以省略,写成 Student stu{1001, "Jack", 18, 92.5}; 效果完全一样。

提示 如果你之前学过 C 语言,可能会记得在 C 语言中定义结构体变量时,前面必须带上 struct 关键字写成 struct Student stu;。而在 C++ 中,我们直接写 Student stu; 就可以了,不需要再重复写 struct。

结构体变量的初始化与默认值

在函数内部定义结构体变量时,如果我们只写 Student stu; 而不做初始化,std::string 类型的 name 会自动变成空字符串 ""。但 int、double 等基本类型的成员并不会自动清零,里面装的会是随机垃圾值。

为了避免读到垃圾值,定义空结构体变量时可以加上一对空花括号写成 Student stu{};,让所有数值成员自动归零。除此之外,C++ 还支持在定义 struct 类型时直接为各个成员指定默认初始值。

示例 2:指定成员的默认初始值

#include <iostream>
#include <string>

struct Student {
    int id = 1000;                              // 默认学号
    std::string name = "未填写";                // 默认姓名
    int age = 18;                               // 默认年龄
    double score = 0.0;                         // 默认成绩
};

int main() {
    Student stu1{};                             // 使用默认值
    Student stu2 = {1001, "Jack", 20, 95.5};    // 覆盖默认值

    std::cout << stu1.id << " " << stu1.name << " " << stu1.score << '\n';
    std::cout << stu2.id << " " << stu2.name << " " << stu2.score << '\n';
    return 0;
}

运行结果如下。

1000 未填写 0
1001 Jack 95.5

分析:

定义 stu1{} 时没有传入具体数据,它的各个成员就会直接使用 struct Student 中预设的默认值。而定义 stu2 时提供了初始值列表,就会按顺序覆盖掉原有的默认值。

如果花括号里只给出了前面部分成员的初值(如 Student stu3 = {1002, "Lucy"};),后面未指定的成员同样会自动采用默认值。要是结构体定义里也没写默认值,剩余的数值成员则会自动补 0。平时定义结构体时,给数值成员加上默认初始值是一个很好的防坑习惯。

读写成员与整体赋值

除了在创建结构体变量时一次性填好初始值,我们也可以先创建一个空的结构体变量,再通过点运算符(.)逐项赋值,或者随时修改某一个成员的值。另外,同一种类型的两个结构体变量之间还可以直接使用 = 整体赋值。

语法:

结构体变量名.成员名 = 新值;

说明:

修改某个成员的值时,只会更新当前结构体变量对应的那一项成员,不会影响其他成员。

示例 3:修改成员与结构体整体赋值

#include <iostream>
#include <string>

struct Student {
    int id = 0;
    std::string name;
    double score = 0.0;
};

int main() {
    Student stu1{};
    stu1.id = 1001;
    stu1.name = "Jack";
    stu1.score = 92.5;

    Student stu2 = stu1;    // 结构体变量整体赋值
    stu2.name = "Lucy";     // 修改单个成员的值
    stu2.score = 96.0;

    std::cout << stu1.id << " " << stu1.name << " " << stu1.score << '\n';
    std::cout << stu2.id << " " << stu2.name << " " << stu2.score << '\n';
    return 0;
}

运行结果如下。

1001 Jack 92.5
1001 Lucy 96

分析:

上面代码中,我们先创建了空结构体变量 stu1{} 并逐项赋值,接着通过 Student stu2 = stu1; 把所有成员一次性复制给了 stu2。这里屏幕把 96.0 打印成 96,是因为 std::cout 输出浮点数时默认会省略小数点末尾多余的 0。

前面学习原生数组时,两个数组不能直接使用 = 整体赋值,而同类型的两个结构体变量可以直接使用 = 赋值。复制完成后两个变量彼此独立,因此后面修改 stu2 的姓名和成绩并不会影响 stu1。

不过需要注意,结构体虽然默认支持 = 整体赋值,但默认不支持直接使用 == 或 != 判断两个结构体是否相等。如果我们需要比较两个结构体的数据,应该分别比较它们对应的成员。

// 正确方式
if (stu1.id == stu2.id && stu1.name == stu2.name) {
    // 逐个比较关键成员
}

// 错误方式
if (stu1 == stu2) {
    // 编译报错
}

注意 使用点运算符读写成员时,点号左边必须是具体的结构体变量名(如 stu1.name),不能直接拿结构体类型名写成 Student.name,否则会直接编译报错。

C++ 结构体的嵌套

结构体的成员不仅可以是基本数据类型,还可以是另一个已经定义好的结构体类型。比如我们要在 Student 结构体中记录学生的出生日期,就可以先定义一个表示日期的 Date 结构体,再将它作为 Student 的成员。

示例 4:结构体嵌套结构体

#include <iostream>
#include <string>

// 先定义日期结构体
struct Date {
    int year;
    int month;
    int day;
};

// 在学生结构体中嵌套 Date 结构体
struct Student {
    int id;
    std::string name;
    Date birthday;    // 成员本身是 Date 结构体
};

int main() {
    Student stu = {1001, "Jack", {2008, 5, 20}};

    std::cout << "姓名:" << stu.name << '\n';
    std::cout << "出生年份:" << stu.birthday.year << '\n';
    std::cout << "出生月份:" << stu.birthday.month << '\n';
    std::cout << "出生日期:" << stu.birthday.day << '\n';
    return 0;
}

运行结果如下。

姓名:Jack
出生年份:2008
出生月份:5
出生日期:20

分析:

内层花括号 {2008, 5, 20} 用来初始化嵌套的 birthday 成员。访问子成员时,我们连续使用两个点运算符写成 stu.birthday.year,表示先找到 stu 里的 birthday,再取出里面的 year。

需要注意的是,被嵌套的 struct Date 必须定义在 struct Student 的前面。因为 C++ 编译器是从上往下依次读取代码的,只有先认识了 Date 类型,后面才能在 Student 里面使用它声明成员。

C++ 结构体与函数

在实际开发中,结构体经常与函数配合使用:我们既可以把结构体作为参数传给函数处理,也可以让函数直接返回一个组装好的结构体。

1. 结构体作为函数参数

如果直接按值传递结构体(如 void printStudent(Student s)),每次调用函数都会把结构体里的所有成员完整复制一份。当成员较多时不仅浪费内存和时间,而且在函数内部修改形参也不会影响外面的原变量。

因此,传递结构体参数时我们通常优先使用引用传递,具体遵循以下 2 条规则:

  • 只读不改时使用 const 引用:把形参声明为 const Student&,既省去了复制整个结构体的开销,又能防止我们在函数内部不小心改坏原数据。
  • 需要修改时使用普通引用:把形参声明为 Student&,在函数内部修改形参成员时,外部传入的原结构体变量也会同步更新。

示例 5:结构体作为函数参数

#include <iostream>
#include <string>

struct Student {
    int id;
    std::string name;
    double score;
};

// 普通引用传参:修改原结构体
void updateScore(Student& s, double newScore) {
    s.score = newScore;
}

// const 引用传参:只读打印
void printStudent(const Student& s) {
    std::cout << s.id << " " << s.name << " " << s.score << '\n';
}

int main() {
    Student stu = {1001, "Jack", 92.5};

    updateScore(stu, 98.0);
    printStudent(stu);
    return 0;
}

运行结果如下。

1001 Jack 98

分析:

updateScore() 函数的形参类型是普通引用 Student& s。当我们调用 updateScore(stu, 98.0) 时,形参 s 直接绑定到实参 stu 上,修改 s.score 就会同步把外面 stu.score 的值更新为 98.0。

而 printStudent() 函数只负责读取并打印数据,将形参写成 const Student& s 最稳妥。初学者在编写修改结构体的函数时,最容易犯的错误就是漏写了引用符号 &。

// 正确方式
void updateScore(Student& s, double newScore) {
    s.score = newScore;
}

// 错误方式
void updateScore(Student s, double newScore) {
    s.score = newScore;
}

如果漏掉 & 写成 Student s,就会退化为值传递。此时函数内部修改的只是临时复制出来的副本,函数执行结束后,外面 stu.score 的值依然是原来的 92.5,这一点小伙伴们一定要留意。

2. 结构体作为函数返回值

在 C++ 中,普通函数一次只能 return 一个结果,而且原生数组也不能直接按值返回。如果我们希望函数一次性返回多项关联数据,把它们打包成结构体再整体返回就非常方便。

示例 6:函数返回结构体

#include <iostream>
#include <string>

struct Student {
    int id;
    std::string name;
    double score;
};

// 创建并返回 Student 结构体
Student createStudent(int id, const std::string& name, double score) {
    Student s = {id, name, score};
    return s;
}

int main() {
    Student stu = createStudent(1002, "Lucy", 95.5);

    std::cout << stu.id << " " << stu.name << " " << stu.score << '\n';
    return 0;
}

运行结果如下。

1002 Lucy 95.5

分析:

在这个例子中,createStudent() 函数的返回值类型是 Student。它在内部把传入的数据组装成结构体变量 s,再通过 return s; 整体返回给 stu。由于函数头已经指明了返回 Student 类型,函数体内部也可以省去临时变量 s,直接简写为 return {id, name, score};。

常见问题

1. C++ 中的结构体(struct)和类(class)有什么区别?

我们在后面学习类与对象时会看到,C++ 中的 struct 里面不仅可以放数据成员,其实也能定义函数,它和 class 在底层能力上几乎一样。两者最主要的区别在于默认访问权限:struct 的成员默认公开(public),外部可以直接访问。而 class 的成员默认私有(private),默认只能在内部访问。

在实际开发中,大家通常按用途来做分工:如果只是单纯把几项相关数据打包在一起,优先使用 struct。如果需要封装复杂的业务逻辑并严格控制数据的读写权限,则使用 class。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号