C++ 结构体数组与指针

单个结构体变量只能保存一条记录,比如一名学生的信息。如果我们需要批量保存全班多名学生的信息,或者需要通过内存地址来传递和操作数据,就要用到 “结构体数组” 和 “结构体指针”。

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),就使用箭头运算符(->)。

上一篇:

下一篇:

给站长反馈

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

邮箱:lvyenet@vip.qq.com

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