【C++实战⑪】解锁C++结构体:从基础到实战的进阶之旅
目录
- 一、结构体的定义与使用
- 1.1 结构体的语法结构与成员定义
- 1.2 结构体变量的定义与初始化
- 1.3 结构体成员的访问方式
- 二、结构体数组的实战应用
- 2.1 结构体数组的定义与初始化
- 2.2 结构体数组的遍历与成员操作
- 2.3 结构体数组在批量数据存储中的应用
- 三、结构体指针的实战技巧
- 3.1 结构体指针的定义与初始化
- 3.2 结构体指针访问成员的方法(-> 运算符)
- 3.3 结构体指针作为函数参数的应用
- 四、实战项目:学生信息管理系统(结构体版)
- 4.1 项目需求与功能模块设计
- 4.2 结构体与结构体数组实现信息存储代码
- 4.3 信息添加、查询、修改与删除功能开发
一、结构体的定义与使用
1.1 结构体的语法结构与成员定义
在 C++ 中,结构体是一种用户自定义的数据类型,它允许将不同类型的数据组合在一起,形成一个逻辑上的整体。结构体的定义使用struct关键字,后面跟着结构体的名称和一对花括号,花括号内包含成员变量的声明。其基本语法如下:
struct 结构体名 {数据类型1 成员变量1;数据类型2 成员变量2;// 可以有更多不同类型的成员变量
};
例如,我们定义一个表示学生信息的结构体Student:
struct Student {std::string name; // 学生姓名,使用string类型存储字符串int age; // 学生年龄,使用int类型存储整数double score; // 学生成绩,使用double类型存储浮点数
};
在这个例子中,Student结构体包含了三个成员变量:name用于存储学生的姓名,类型为std::string;age用于存储学生的年龄,类型为int;score用于存储学生的成绩,类型为double。这些成员变量可以是 C++ 中任何合法的数据类型,包括基本数据类型(如int、double、char等)和自定义数据类型(如其他结构体、类等)。通过结构体,我们将学生的相关信息组合在了一起,方便进行管理和操作。
1.2 结构体变量的定义与初始化
定义结构体变量有多种方式,并且可以在定义时进行初始化。
- 先定义结构体类型,再定义变量:
struct Student {std::string name;int age;double score;
};
Student stu1; // 定义一个Student类型的结构体变量stu1
这种方式先声明了Student结构体类型,然后使用该类型定义了变量stu1。此时stu1的成员变量都是未初始化的,其值是不确定的。
- 定义结构体类型的同时定义变量:
struct Student {std::string name;int age;double score;
} stu2; // 定义结构体类型Student的同时,定义变量stu2
这种方式在定义结构体类型的大括号后直接跟上变量名stu2,一步完成了结构体类型的定义和变量的声明。同样,stu2的成员变量此时也是未初始化的。
- 使用匿名结构体定义变量:
struct {std::string name;int age;double score;
} stu3; // 使用匿名结构体定义变量stu3
这种方式没有给结构体命名,直接定义了变量stu3。由于结构体没有名称,所以无法在其他地方再使用这个结构体类型来定义新的变量,它通常用于只需要使用一次该结构体类型的场景。
结构体变量的初始化也有多种方式:
- 完全初始化:使用花括号初始化列表,按成员顺序进行赋值。
Student stu4 = {"Alice", 20, 85.5}; // 完全初始化stu4
这里,花括号中的值按照结构体中成员变量的定义顺序依次赋值给name、age和score。
- 部分初始化:同样使用花括号初始化列表,只给部分成员变量赋值,未赋值的成员变量将使用默认值(对于基本数据类型,数值型默认值为 0,char类型默认值为空字符;对于std::string类型,默认值为空字符串)。
Student stu5 = {"Bob"}; // 部分初始化stu5,age和score使用默认值
这里只给name成员变量赋值为"Bob",age会默认初始化为 0,score会默认初始化为 0.0。
1.3 结构体成员的访问方式
通过点运算符(.)可以访问结构体变量的成员。例如,对于前面定义的stu4结构体变量,我们可以这样访问它的成员:
#include <iostream>
#include <string>struct Student {std::string name;int age;double score;
};int main() {Student stu4 = {"Alice", 20, 85.5};std::cout << "姓名:" << stu4.name << std::endl;std::cout << "年龄:" << stu4.age << std::endl;std::cout << "成绩:" << stu4.score << std::endl;return 0;
}
在上述代码中,stu4.name、stu4.age和stu4.score分别表示访问stu4结构体变量的name、age和score成员,通过std::cout输出这些成员的值。点运算符的左边是结构体变量名,右边是结构体的成员名,通过这种方式可以方便地对结构体成员进行读取和修改操作。例如,我们可以修改stu4的成绩:
stu4.score = 90.0; // 修改stu4的成绩为90.0
这样就将stu4的score成员变量的值从原来的85.5修改为了90.0。
二、结构体数组的实战应用
2.1 结构体数组的定义与初始化
结构体数组是指数组中的每个元素都是一个结构体类型。它在存储大量相关数据时非常有用,比如存储多个学生的信息。定义结构体数组的方式和普通数组类似,只是数组元素的类型是结构体类型。例如,我们继续使用前面定义的Student结构体来定义结构体数组:
struct Student {std::string name;int age;double score;
};
Student students[3]; // 定义一个包含3个Student结构体的数组
这里定义了一个名为students的结构体数组,它可以存储 3 个Student类型的元素,每个元素都包含name、age和score这三个成员变量。
结构体数组的初始化也有多种方式:
- 静态初始化(声明时直接初始化):在定义结构体数组时,使用花括号初始化列表,按顺序为每个数组元素的结构体成员赋值。
Student students[3] = {{"Alice", 20, 85.5},{"Bob", 21, 78.0},{"Charlie", 22, 90.5}
};
这种方式清晰直观,适用于在编写代码时就知道所有数组元素初始值的情况。每个结构体元素的初始化值用花括号括起来,按照结构体中成员变量的定义顺序依次赋值。
- 部分静态初始化:可以只对部分数组元素进行初始化,未初始化的元素将使用结构体成员的默认值。
Student students[3] = {{"David", 19, 88.0}
};
这里只初始化了students数组的第一个元素,第二个和第三个元素的name将默认为空字符串,age默认为 0,score默认为 0.0。
- 动态初始化(运行时初始化):当数组大小在运行时才能确定,或者需要根据用户输入等动态情况进行初始化时,可以使用动态内存分配和循环赋值的方式。
int n;
std::cout << "请输入学生人数: ";
std::cin >> n;
Student* students = new Student[n]; // 使用new动态分配内存
for (int i = 0; i < n; ++i) {std::cout << "请输入第" << i + 1 << "个学生的姓名: ";std::cin >> students[i].name;std::cout << "请输入第" << i + 1 << "个学生的年龄: ";std::cin >> students[i].age;std::cout << "请输入第" << i + 1 << "个学生的成绩: ";std::cin >> students[i].score;
}
// 使用完后记得释放内存
delete[] students;
在这个例子中,首先通过用户输入确定学生人数n,然后使用new运算符在堆上动态分配了一个大小为n的Student结构体数组。接着通过循环,根据用户输入为每个数组元素的结构体成员赋值。最后,使用delete[]释放动态分配的内存,以避免内存泄漏。
2.2 结构体数组的遍历与成员操作
遍历结构体数组通常使用循环,通过数组下标来访问每个元素的结构体成员。以之前初始化的students结构体数组为例,我们可以使用for循环遍历并输出每个学生的信息:
#include <iostream>
#include <string>struct Student {std::string name;int age;double score;
};int main() {Student students[3] = {{"Alice", 20, 85.5},{"Bob", 21, 78.0},{"Charlie", 22, 90.5}};for (int i = 0; i < 3; ++i) {std::cout << "学生 " << i + 1 << " 的信息:" << std::endl;std::cout << "姓名:" << students[i].name << std::endl;std::cout << "年龄:" << students[i].age << std::endl;std::cout << "成绩:" << students[i].score << std::endl;std::cout << "-------------------" << std::endl;}return 0;
}
在上述代码中,for循环从 0 到 2(因为数组下标从 0 开始,而数组大小为 3),通过students[i]访问数组中的每个元素,再使用点运算符(.)访问每个元素的name、age和score成员变量,并将其输出。
除了读取成员变量的值,还可以对成员变量进行修改操作。例如,将第二个学生的成绩提高 5 分:
students[1].score += 5.0; // 将第二个学生的成绩提高5分
这里通过students[1]定位到数组中的第二个元素(数组下标从 0 开始),然后使用点运算符修改其score成员变量的值。
2.3 结构体数组在批量数据存储中的应用
结构体数组在实际编程中常用于存储大量相关的数据。例如,在一个学校管理系统中,需要存储全校学生的信息,使用结构体数组就非常合适。假设学校有 1000 名学生,我们可以定义一个包含 1000 个元素的Student结构体数组来存储他们的姓名、年龄和成绩等信息。
const int studentCount = 1000;
Student students[studentCount];
通过这种方式,我们可以方便地对所有学生的信息进行统一管理。比如,统计所有学生的平均成绩,可以通过遍历结构体数组来实现:
double totalScore = 0.0;
for (int i = 0; i < studentCount; ++i) {totalScore += students[i].score;
}
double averageScore = totalScore / studentCount;
std::cout << "全校学生的平均成绩为:" << averageScore << std::endl;
在这个例子中,首先通过for循环遍历students结构体数组,将每个学生的成绩累加到totalScore变量中。然后,用总成绩除以学生总数,得到平均成绩并输出。
再比如,查找成绩最高的学生,也可以通过遍历结构体数组来比较每个学生的成绩:
int maxIndex = 0;
for (int i = 1; i < studentCount; ++i) {if (students[i].score > students[maxIndex].score) {maxIndex = i;}
}
std::cout << "成绩最高的学生是:" << students[maxIndex].name << ",成绩为:" << students[maxIndex].score << std::endl;
这段代码通过for循环遍历数组,从第二个学生开始(索引为 1),逐个比较每个学生的成绩与当前记录的最高成绩(初始时假设第一个学生成绩最高,索引为 0)。如果发现有学生成绩更高,则更新maxIndex为该学生的索引。最后,输出成绩最高的学生的姓名和成绩。
结构体数组在批量数据存储中的优势在于它能够将相关的数据紧密地组织在一起,并且可以利用数组的特性进行高效的访问和操作,使得代码逻辑更加清晰和易于维护。
三、结构体指针的实战技巧
3.1 结构体指针的定义与初始化
结构体指针是指向结构体变量的指针,它存储的是结构体变量在内存中的地址。通过结构体指针,我们可以更灵活地访问和操作结构体的成员。定义结构体指针的语法与普通指针类似,只是指针指向的类型是结构体类型。以之前定义的Student结构体为例,定义结构体指针的方式如下:
struct Student {std::string name;int age;double score;
};
Student* stuPtr; // 定义一个指向Student结构体的指针stuPtr
这里Student*表示指针指向的类型是Student结构体,stuPtr是指针变量名。需要注意的是,此时stuPtr只是一个指针变量,它并没有指向任何有效的内存地址,即它是一个未初始化的指针。如果直接使用未初始化的指针,会导致程序运行时错误,因此在使用指针之前,必须对其进行初始化。
初始化结构体指针有两种常见的方式:
- 指向已存在的结构体变量:首先定义一个结构体变量,然后将指针指向该变量。
Student stu = {"Alice", 20, 85.5};
stuPtr = &stu; // 将stuPtr指向stu结构体变量
在这个例子中,先定义了一个Student结构体变量stu并进行了初始化,然后使用取地址运算符&获取stu的地址,并将其赋值给stuPtr,这样stuPtr就指向了stu。
- 通过动态内存分配初始化:使用new运算符在堆上分配内存,创建一个新的结构体对象,并让指针指向它。
stuPtr = new Student;
stuPtr->name = "Bob";
stuPtr->age = 21;
stuPtr->score = 90.0;
这里通过new Student在堆上分配了一块内存,用于存储一个Student结构体对象,并返回该对象的地址,将其赋值给stuPtr。然后通过指针访问结构体成员,对其进行赋值操作。使用动态内存分配时,一定要记得在不再需要该内存时,使用delete运算符释放内存,以避免内存泄漏。例如,在上述代码的后续部分,可以使用delete stuPtr;来释放分配的内存。
3.2 结构体指针访问成员的方法(-> 运算符)
当我们有了一个指向结构体变量的指针后,需要一种方式来访问结构体的成员。在 C++ 中,使用箭头运算符->来通过结构体指针访问结构体成员。->运算符的左边是结构体指针,右边是结构体的成员名。例如,对于前面初始化的stuPtr指针:
std::cout << "姓名:" << stuPtr->name << std::endl;
std::cout << "年龄:" << stuPtr->age << std::endl;
std::cout << "成绩:" << stuPtr->score << std::endl;
在这段代码中,stuPtr->name、stuPtr->age和stuPtr->score分别表示通过指针stuPtr访问其所指向的Student结构体对象的name、age和score成员。箭头运算符->实际上是一种语法糖,它等价于先对指针进行解引用(*运算符),再使用点运算符(.)来访问成员。例如,stuPtr->name等价于(*stuPtr).name。下面通过完整的代码示例来展示这两种方式的等价性:
#include <iostream>
#include <string>struct Student {std::string name;int age;double score;
};int main() {Student stu = {"Alice", 20, 85.5};Student* stuPtr = &stu;// 使用箭头运算符访问成员std::cout << "使用箭头运算符访问成员:" << std::endl;std::cout << "姓名:" << stuPtr->name << std::endl;std::cout << "年龄:" << stuPtr->age << std::endl;std::cout << "成绩:" << stuPtr->score << std::endl;// 使用解引用和点运算符访问成员std::cout << "\n使用解引用和点运算符访问成员:" << std::endl;std::cout << "姓名:" << (*stuPtr).name << std::endl;std::cout << "年龄:" << (*stuPtr).age << std::endl;std::cout << "成绩:" << (*stuPtr).score << std::endl;return 0;
}
在上述代码中,先定义了一个Student结构体变量stu和一个指向它的指针stuPtr。然后分别使用箭头运算符和先解引用再使用点运算符的方式来访问结构体成员,并将结果输出。可以看到,两种方式都能正确访问结构体成员,且输出结果相同,但箭头运算符->的写法更加简洁直观,在实际编程中更为常用。
3.3 结构体指针作为函数参数的应用
在函数调用时,将结构体作为参数传递有两种常见方式:值传递和指针传递。当结构体比较大时,值传递会导致大量的数据复制,消耗较多的时间和内存空间,而指针传递则可以避免这个问题。
- 值传递:在值传递方式下,函数接收的是结构体变量的副本,对副本的任何修改都不会影响原始的结构体变量。例如:
#include <iostream>
#include <string>struct Student {std::string name;int age;double score;
};// 函数接收Student结构体值传递
void printStudentByValue(Student s) {s.age = 25; // 修改副本的age成员std::cout << "值传递方式下的学生信息:" << std::endl;std::cout << "姓名:" << s.name << std::endl;std::cout << "年龄:" << s.age << std::endl;std::cout << "成绩:" << s.score << std::endl;
}int main() {Student stu = {"Alice", 20, 85.5};printStudentByValue(stu);std::cout << "\n原始学生信息:" << std::endl;std::cout << "姓名:" << stu.name << std::endl;std::cout << "年龄:" << stu.age << std::endl;std::cout << "成绩:" << stu.score << std::endl;return 0;
}
在上述代码中,printStudentByValue函数接收一个Student结构体变量s,在函数内部修改了s的age成员。但从输出结果可以看出,原始的stu结构体变量的age并没有改变,因为函数操作的是副本。
- 指针传递:当使用结构体指针作为函数参数时,函数接收的是结构体变量的地址,通过这个地址可以直接操作原始的结构体变量。例如:
#include <iostream>
#include <string>struct Student {std::string name;int age;double score;
};// 函数接收Student结构体指针传递
void modifyStudentByPointer(Student* p_stu) {p_stu->age = 25; // 修改原始结构体的age成员std::cout << "指针传递方式下修改后的学生信息:" << std::endl;std::cout << "姓名:" << p_stu->name << std::endl;std::cout << "年龄:" << p_stu->age << std::endl;std::cout << "成绩:" << p_stu->score << std::endl;
}int main() {Student stu = {"Alice", 20, 85.5};modifyStudentByPointer(&stu);std::cout << "\n原始学生信息已被修改为:" << std::endl;std::cout << "姓名:" << stu.name << std::endl;std::cout << "年龄:" << stu.age << std::endl;std::cout << "成绩:" << stu.score << std::endl;return 0;
}
在这个例子中,modifyStudentByPointer函数接收一个Student结构体指针p_stu,在函数内部通过指针修改了原始结构体的age成员。从输出结果可以看到,原始的stu结构体变量的age已经被成功修改。
结构体指针作为函数参数的优势主要体现在以下几个方面:
- 提高效率:对于大型结构体,传递指针比传递整个结构体的副本要快得多,因为指针的大小是固定的(通常为 4 字节或 8 字节,取决于系统架构),而结构体副本的大小可能很大,减少了数据复制的开销。
- 允许函数修改原始数据:在某些情况下,函数需要修改传递进来的结构体变量的值,使用指针传递可以实现这一目的。如果使用值传递,函数只能修改副本,无法影响原始数据。
- 动态内存管理:当结构体对象是通过动态内存分配创建的(使用new运算符),传递指针可以方便地在函数中对该对象进行操作,并且在不需要时可以在函数中释放内存(使用delete运算符)。
结构体指针作为函数参数在需要高效处理大型结构体、需要修改原始数据或涉及动态内存管理的场景中非常有用,它能使程序更加灵活和高效。
四、实战项目:学生信息管理系统(结构体版)
4.1 项目需求与功能模块设计
在实际的学校管理场景中,学生信息管理是一项至关重要的任务。随着学生数量的不断增加,传统的手工记录和管理方式已经无法满足高效、准确的管理需求。因此,我们需要开发一个学生信息管理系统,来帮助学校更好地管理学生信息。
该系统的主要需求如下:
- 学生信息存储:能够存储学生的基本信息,包括姓名、年龄、学号、成绩等。
- 信息添加功能:可以方便地添加新学生的信息到系统中。
- 信息查询功能:根据学号或姓名等关键字,快速查询学生的详细信息。
- 信息修改功能:当学生信息发生变化时,能够及时对已有信息进行修改。
- 信息删除功能:对于毕业或退学等不再在校的学生,能够从系统中删除其信息。
基于以上需求,我们设计了以下功能模块:
- 添加学生信息模块:负责接收用户输入的新学生信息,并将其存储到结构体数组中。在这个模块中,需要对用户输入的信息进行有效性验证,比如学号不能重复,年龄要在合理范围内等。例如,学号通常是唯一标识学生的重要信息,如果输入的学号已经存在于系统中,就需要提示用户重新输入。
- 查询学生信息模块:根据用户输入的查询关键字(如学号或姓名),在结构体数组中查找对应的学生信息,并将其显示出来。在实现查询功能时,可以采用顺序查找或更高效的查找算法(如二分查找,前提是数据已排序)。如果是顺序查找,就需要逐个遍历结构体数组中的元素,比较其学号或姓名是否与查询关键字匹配。
- 修改学生信息模块:根据用户输入的学号,找到对应的学生信息,然后允许用户修改其各项信息,如姓名、年龄、成绩等。在修改信息时,同样要进行输入验证,确保修改后的数据的正确性和合理性。比如成绩不能为负数,年龄不能超过合理范围等。
- 删除学生信息模块:根据用户输入的学号,从结构体数组中删除对应的学生信息。在删除信息时,需要考虑数组元素的重新排列,以确保数组中数据的连续性。例如,可以将待删除元素后面的所有元素依次向前移动一位,覆盖掉待删除元素的位置。
4.2 结构体与结构体数组实现信息存储代码
为了实现学生信息的存储,我们首先定义一个Student结构体,用于表示学生的各项信息:
#include <iostream>
#include <string>struct Student {std::string name; // 学生姓名int age; // 学生年龄std::string id; // 学生学号double score; // 学生成绩
};
接下来,我们定义一个结构体数组来存储多个学生的信息,并实现一些基本的初始化操作。假设我们最多可以存储 100 个学生信息:
const int MAX_STUDENTS = 100;
Student students[MAX_STUDENTS];
int studentCount = 0; // 当前已存储的学生数量
在上述代码中,students是一个Student类型的数组,最多可以容纳 100 个学生信息。studentCount用于记录当前实际存储的学生数量,初始值为 0。当有新学生信息添加到系统中时,studentCount会相应增加。
4.3 信息添加、查询、修改与删除功能开发
- 添加学生信息功能:
void addStudent() {if (studentCount >= MAX_STUDENTS) {std::cout << "系统已满,无法添加更多学生!" << std::endl;return;}Student newStudent;std::cout << "请输入学生姓名:";std::cin >> newStudent.name;std::cout << "请输入学生年龄:";std::cin >> newStudent.age;std::cout << "请输入学生学号:";std::cin >> newStudent.id;// 检查学号是否重复for (int i = 0; i < studentCount; ++i) {if (students[i].id == newStudent.id) {std::cout << "该学号已存在,请重新输入!" << std::endl;return;}}std::cout << "请输入学生成绩:";std::cin >> newStudent.score;students[studentCount] = newStudent;studentCount++;std::cout << "学生信息添加成功!" << std::endl;
}
在addStudent函数中,首先检查系统是否已满,如果已满则提示用户无法添加。然后,接收用户输入的新学生信息,并检查学号是否重复。若学号不重复,则将新学生信息存储到students数组中,并增加studentCount的值,最后提示用户添加成功。
- 查询学生信息功能:
void searchStudent() {std::string searchId;std::cout << "请输入要查询的学生学号:";std::cin >> searchId;for (int i = 0; i < studentCount; ++i) {if (students[i].id == searchId) {std::cout << "姓名:" << students[i].name << std::endl;std::cout << "年龄:" << students[i].age << std::endl;std::cout << "学号:" << students[i].id << std::endl;std::cout << "成绩:" << students[i].score << std::endl;return;}}std::cout << "未找到该学号对应的学生信息!" << std::endl;
}
searchStudent函数接收用户输入的学号,然后在students数组中查找对应的学生信息。如果找到,则输出该学生的详细信息;如果未找到,则提示用户未找到相关信息。
- 修改学生信息功能:
void updateStudent() {std::string updateId;std::cout << "请输入要修改的学生学号:";std::cin >> updateId;for (int i = 0; i < studentCount; ++i) {if (students[i].id == updateId) {std::cout << "请输入新的学生姓名(原姓名:" << students[i].name << "):";std::cin >> students[i].name;std::cout << "请输入新的学生年龄(原年龄:" << students[i].age << "):";std::cin >> students[i].age;std::cout << "请输入新的学生成绩(原成绩:" << students[i].score << "):";std::cin >> students[i].score;std::cout << "学生信息修改成功!" << std::endl;return;}}std::cout << "未找到该学号对应的学生信息,无法修改!" << std::endl;
}
在updateStudent函数中,先接收用户输入的要修改的学生学号,然后在数组中查找该学生。若找到,则提示用户输入新的信息,并进行相应的修改,最后提示修改成功;若未找到,则提示用户无法修改。
- 删除学生信息功能:
void deleteStudent() {std::string deleteId;std::cout << "请输入要删除的学生学号:";std::cin >> deleteId;for (int i = 0; i < studentCount; ++i) {if (students[i].id == deleteId) {for (int j = i; j < studentCount - 1; ++j) {students[j] = students[j + 1];}studentCount--;std::cout << "学生信息删除成功!" << std::endl;return;}}std::cout << "未找到该学号对应的学生信息,无法删除!" << std::endl;
}
deleteStudent函数接收用户输入的要删除的学生学号,在数组中查找该学生。若找到,则将该学生后面的所有元素依次向前移动一位,覆盖掉要删除的学生信息,并减少studentCount的值,最后提示删除成功;若未找到,则提示用户无法删除。
通过以上代码实现,我们完成了一个简单的学生信息管理系统,能够实现学生信息的添加、查询、修改和删除功能,利用结构体和结构体数组有效地管理学生数据。