单个结构体变量只能保存一条记录,比如一名学生的信息。如果我们需要批量保存全班多名学生的信息,或者需要通过内存地址来传递和操作数据,就要用到 “结构体数组” 和 “结构体指针”。
C++ 结构体数组
在 C++ 中,结构体数组本质上就是一个数组,只不过数组里的每一个元素不再是普通的整数或小数,而是一个完整的结构体。它与单个结构体变量之间的关系,就跟普通数组与普通变量之间的关系一样,小伙伴们可以对比理解一下。
语法:
结构体名 数组名[数组长度] = {
{成员值1, 成员值2, ...},
{成员值1, 成员值2, ...}
};说明:
初始化结构体数组时,外层花括号代表整个数组,里面的每一组内层花括号对应一个结构体元素。如果我们想读写数组中某个元素的成员,可以先通过 数组名[下标] 定位到具体的结构体元素,再配合点运算符(.)访问成员,比如 stuArr[0].id。
示例 1:定义并使用结构体数组
#include <iostream>
#include <string>
struct Student {
int id;
std::string name;
double score;
};
int main() {
Student stuArr[3] = {
{1001, "Jack", 92.5},
{1002, "Lucy", 95.0},
{1003, "Tony", 88.0}
};
// 修改第 2 个元素的成绩
stuArr[1].score = 98.0;
std::cout << stuArr[0].name << " 的学号:" << stuArr[0].id << '\n';
std::cout << stuArr[1].name << " 的成绩:" << stuArr[1].score << '\n';
return 0;
}运行结果如下。
Jack 的学号:1001
Lucy 的成绩:98分析:
Student stuArr[3] = { ... }; 表示定义了一个包含 3 个元素的结构体数组 stuArr。由于数组下标从 0 开始,stuArr[0] 代表第 1 名学生,通过 stuArr[0].id 就能取出学号 1001。后面的 stuArr[1].score = 98.0; 则会将第 2 名学生的成绩修改为 98.0。
初始化结构体数组时,内层花括号在语法上也可以省略,并且当给出了初始值列表时,方括号里的数组长度也可以省略。对于这个例子来说,下面 2 种方式是等价的。
// 方式 1(推荐)
Student stuArr[3] = {
{1001, "Jack", 92.5},
{1002, "Lucy", 95.0},
{1003, "Tony", 88.0}
};
// 方式 2
Student stuArr[3] = {
1001, "Jack", 92.5,
1002, "Lucy", 95.0,
1003, "Tony", 88.0
};其中,方式 1 用内层花括号把每个元素的数据清晰隔开。而方式 2 把所有值平铺在一起,一旦成员变多就很容易看错位,因此在实际开发中我们更推荐使用方式 1。如果省略长度写成 Student stuArr[] = { ... };,编译器也会根据初始值的组数自动推算出长度为 3。
当我们需要把结构体数组中的所有元素依次取出来处理时,可以使用范围 for 循环或普通下标循环进行遍历。
示例 2:遍历结构体数组
#include <iostream>
#include <string>
struct Student {
int id;
std::string name;
double score;
};
int main() {
Student stuArr[3] = {
{1001, "Jack", 92.5},
{1002, "Lucy", 95.0},
{1003, "Tony", 88.0}
};
for (const Student& stu : stuArr) {
std::cout << stu.id << " " << stu.name << " " << stu.score << '\n';
}
return 0;
}运行结果如下。
1001 Jack 92.5
1002 Lucy 95
1003 Tony 88分析:
for (const Student& stu : stuArr) 会依次访问数组 stuArr 中的每一个结构体元素。这里我们把循环变量声明为只读引用 const Student&,可以直接读取原数组里的元素,省去了每次循环复制整个结构体的开销。
如果我们需要在遍历过程中直接修改数组里的成员(如统一加分),把 const 去掉写成 for (Student& stu : stuArr) 即可。要是遍历时还需要用到元素下标,也可以使用普通的 for 循环,配合 stuArr[i].name 按下标逐个访问。
提示 普通结构体数组的长度在定义时就固定了。在实际开发中,如果事先不确定有多少条记录或需要随时增删元素,我们通常会结合前面学过的 vector 容器写成 std::vector<Student>,用起来更加灵活。
C++ 结构体指针
和基本数据类型一样,结构体变量在内存中也占有一块空间,我们可以定义指针来保存它的内存地址。在 C++ 中,结构体指针主要分为两种:一种是 “指向单个结构体变量的指针”,另一种是 “指向结构体数组的指针”。
1. 指向结构体变量的指针
定义指向单个结构体变量的指针时,我们先使用取地址运算符(&)获取结构体变量的地址,再通过箭头运算符(->)直接读写里面的成员。
语法:
结构体名* 指针变量名 = &结构体变量名;
指针变量名->成员名;说明:
箭头运算符 -> 由减号 - 和大于号 > 紧贴组成,两个符号中间不能留空格。
示例 3:通过结构体指针读写成员
#include <iostream>
#include <string>
struct Student {
int id;
std::string name;
double score;
};
int main() {
Student stu = {1001, "Jack", 92.5};
Student* p = &stu;
std::cout << "学号:" << p->id << '\n';
std::cout << "姓名:" << p->name << '\n';
p->score = 96.0; // 通过指针修改成员
std::cout << "修改后成绩:" << stu.score << '\n';
return 0;
}运行结果如下。
学号:1001
姓名:Jack
修改后成绩:96分析:
在这个例子中,Student* p = &stu; 定义了一个指向 stu 的结构体指针 p。通过 p->id 和 p->name 可以直接读取成员的值,执行 p->score = 96.0; 则会顺着内存地址直接修改原变量 stu 的成绩。
此外,由于 *p 解引用后代表的就是结构体变量 stu 本身,我们也可以写成 (*p).name。不过需要特别注意,(*p) 外面的小括号不能省略,因为点运算符(.)的优先级高于解引用运算符(*)。
// 正确方式
std::cout << (*p).name << '\n';
// 错误方式
std::cout << *p.name << '\n';如果漏掉小括号写成 *p.name,编译器会先计算 p.name。但 p 是指针而不是结构体变量,不能直接使用点运算符,编译时就会直接报错。为了省去写括号的麻烦,平时通过指针访问成员时统一写成 p->name 就可以了。
在同一个函数里操作本地变量 stu 时,自然是直接写 stu.score 最简单。但当我们编写接收结构体指针的函数时,函数内部手头只有指针形参,这时 -> 就会派上用场。
示例 4:结构体指针作为函数参数
#include <iostream>
#include <string>
struct Student {
int id;
std::string name;
double score;
};
// 接收结构体指针,为成绩加分
void addBonus(Student* p, double bonus) {
if (p != nullptr) {
p->score += bonus;
}
}
int main() {
Student stu = {1001, "Jack", 90.0};
addBonus(&stu, 5.5);
std::cout << stu.name << " 加分后成绩:" << stu.score << '\n';
return 0;
}运行结果如下。
Jack 加分后成绩:95.5分析:
当我们调用 addBonus(&stu, 5.5) 时,只把 stu 的内存地址传了进去,并没有复制整个结构体。函数内部先通过 p != nullptr 确认传入的不是空指针,再通过 p->score += bonus; 直接更新外部 stu 的成绩。
那什么时候使用引用传参,什么时候使用指针传参呢?在实际开发中,如果参数必须传入有效对象,我们优先使用上一节讲的引用传参(Student&)。只有当参数允许传入 nullptr 表示 “暂无数据”,或者需要对接 C 语言接口时,才使用结构体指针传参。
2. 指向结构体数组的指针
结构体指针不仅可以指向单个结构体变量,还可以指向一个结构体数组。当我们将结构体数组名赋给指针后,指针默认指向数组的第 1 个结构体元素。
示例 5:指向结构体数组的指针
#include <iostream>
#include <string>
struct Student {
int id;
std::string name;
double score;
};
int main() {
Student stuArr[3] = {
{1001, "Jack", 92.5},
{1002, "Lucy", 95.0},
{1003, "Tony", 88.0}
};
Student* p = stuArr;
std::cout << "第 1 名学号:" << p[0].id << '\n';
std::cout << "第 2 名学号:" << (p + 1)->id << '\n';
std::cout << "第 3 名学号:" << (*(p + 2)).id << '\n';
return 0;
}运行结果如下。
第 1 名学号:1001
第 2 名学号:1002
第 3 名学号:1003分析:
Student* p = stuArr; 表示将结构体数组名 stuArr 赋给了指针 p。指向单个结构体变量时需要加 & 取地址,而数组名 stuArr 本身就代表首元素的地址 &stuArr[0],因此前面不需要加 &。
当指针 p 指向结构体数组后,p + i 会向后移动 i 个 Student 元素的步长,指向下标为 i 的结构体元素。因此,通过指针访问第 i 个元素的成员有以下 3 种等价方式:
// 方式 1(推荐)
p[i].id
// 方式 2
(p + i)->id
// 方式 3
(*(p + i)).id这 3 种写法的执行过程与选型建议如下:
- 方式 1:
p[i]会先定位到下标为i的结构体元素,再通过点运算符访问成员。这种写法和普通数组一致,最直观好懂,在实际开发中推荐小伙伴们优先使用。 - 方式 2:先通过
(p + i)算出第i个元素的地址,再通过->访问成员。由于->的优先级高于+,这里的圆括号不能省略。 - 方式 3:阅读时可以从里往外剥开看。内层
(p + i)先算出第i个元素的地址,外层*(p + i)解引用拿到结构体元素本身,最外层再套一对括号防止.抢先结合。这种写法括号嵌套较多,大家在别人代码里能认出来即可。
注意 区分点运算符(.)和箭头运算符(->),只需要看符号左边是什么类型。如果左边是具体的结构体变量或数组元素(如 stu、stuArr[i]、p[i]),就使用点运算符(.)。如果左边是保存地址的结构体指针(如 p、p + i),就使用箭头运算符(->)。
