目录
源代码:
代码详解:
第1步:搭建基础框架和数据结构
目标:定义数据结构和全局容器
练习任务:
第2步:实现学生管理功能(使用map)
目标:添加学生和显示学生列表
练习任务:
第3步:实现课程管理功能(使用vector)
目标:添加课程和显示课程列表
练习任务:
第4步:实现选课功能(使用list)
目标:学生选课和退课功能
练习任务:
主函数:
多说一点(重点代码解释):
一.list> enrollments;代码详解
1. 分解理解
list
pair
list>
2. 在项目中的具体用途
3. 为什么选择这种设计?
对比 pair vs tuple
适用场景
二.为什么明明有全局容器了,每个模块开始都定义变量?
1. 为什么定义这些变量?
2. students.emplace(id, Student{id, name, year}) 是什么意思?
(1) students 是什么?
(2) emplace 方法
(3) Student{id, name, year}
合起来的作用:
三,在课程管理里面,为什么vector不能用迭代器查找?
为什么 vector 的检查方式和其他容器不同?
1. vector 需要手动遍历检查的原因
2. 其他容器(map/set)如何检查重复?
(1)map(检查学生ID是否重复)
(2)set(检查课程类别是否重复)
四.clearInputBuffer的辅助函数的作用
函数定义与作用
函数内部实现
1. cin.clear()
2. cin.ignore(numeric_limits::max(), '\n')
为什么需要清空输入缓冲区?
示例场景
五.什么时候用emplace什么时候用emplace_back?
核心区别
详细解释
1. emplace_back
2. emplace
何时选择哪个?
注意事项
总结
六.list,map,vector,set他们都是用什么函数删除?
各容器的删除方法
详细示例
1. std::list 的删除
2. std::map 的删除
3. std::vector 的删除
4. std::set 的删除
常见误区:remove 与 erase 的区别
总结
七.为什么不能使用范围 for 循环删除元素 (重点)
错误原因:迭代器失效
正确代码(使用显式迭代器)
为什么范围 for 循环会出错?
//头文件
#include
#include
#include
//头文件
#include
#include
#include
理解每个数据结构的定义
观察四种容器的声明方式
编译运行确保基础框架正常工作
//5.1学生管理
//5.1.1添加学生信息
void addStudent(){
//临时定义变量
int id, year;
string name;
//1.输入学生id
cout << "输入学生ID: ";
cin >> id;
if(students.count(id)>0){
cout<<"学生ID"<>year;
//4.添加新学生
students.emplace(id,Student{id,name,year});
cout << "添加学生: " << name << " (ID: " << id << ")" << endl;
}
//5.1.2显示学生函数
void displayStudents(){
//1.检查是否为空
if(students.empty()){
cout<<"当前没有学生"<> choice;
switch(choice) {
case 1:
addStudent();
break;
case 2:
displayStudents();
break;
case 0:
return;
default:
cout << "无效选择!\n";
}
} while (true);
}
// 在main函数中添加调用
int main() {
// ...之前代码...
do {
displayMainMenu();
cin >> choice;
switch(choice) {
case 1:
studentMenu();
break;
// 其他case将在后续步骤添加
}
} while (choice != 0);
// ...之后代码...
}
添加至少3名学生
尝试添加重复ID的学生
观察map如何自动按键排序
理解students.find(id)
的用法
//5.2课程管理
//5.2.1添加课程
void addCourse(){
//临时定义变量
int code,credit;
string title,category;
//1.输入课程代码
cout<<"输入课程代码:";
cin>>code;
//vector数组要手动遍历检查
for(const auto& it:courses){
if(it.code==code){
cout << "课程代码 " << code << " 已存在!" << endl;
return;
}
}
//2.输入其他的课程信息
cout << "输入课程名称: ";
clearInputBuffer();
getline(cin, title);
cout << "输入课程学分: ";
cin >> credit;
cout << "输入课程类别: ";
clearInputBuffer();
getline(cin,category);
//3.添加课程
courses.emplace_back(Course{code,title,credit,category});
//添加课程类别到set
categories.emplace(category);
cout << "添加课程: " << title << " (代码: " << code << ")" << endl;
}
//5.2.2显示课程列表函数
void displayCourses(){
//1.检查是否为空
if(courses.empty()){
cout<<"暂时没有课程"<> choice;
switch(choice) {
case 1:
addCourse();
break;
case 2:
displayCourses();
break;
case 0:
return;
default:
cout << "无效选择!\n";
}
} while (true);
}
// 在main函数中添加调用
int main() {
// ...之前代码...
switch(choice) {
case 1:
studentMenu();
break;
case 2:
courseMenu();
break;
// 其他case将在后续步骤添加
}
// ...之后代码...
}
添加至少4门不同类别的课程
观察vector如何保持插入顺序
理解categories.insert(category)
如何自动去重
尝试添加重复课程代码的课程
//5.3选课管理
//5.3.1学生选课函数
void enrollCourse(){
//定义临时变量
int studentId,courseCode;
//1.输入学生id
cout<<"输入学生id:";
cin>>studentId;
//检查是否存在
if(students.count(studentId)==0){
cout<<"当前学生id不存在";
return;
}
//2.输入课程code
cout<<"输入课程代码:";
cin>>courseCode;
//检查是否存在
bool find=false;
string courseTitle;
for(auto& it:courses){
if(it.code==courseCode){
find=true;
//找到当前课程名
courseTitle = it.title;
break;
}
}
if(find==false){
cout<<"当前课程代码不存在";
return;
}
//3.检查是否已选该课程
for(auto& enrollment:enrollments){
if(enrollment.first==studentId && enrollment.second==courseCode){
cout << "该学生已选修此课程!" << endl;
return;
}
}
//4.添加选课记录
enrollments.emplace_back(studentId,courseCode);
cout << students[studentId].name << " 选修了 " <>studentId;
//2.输入课程code
cout<<"输入课程代码:";
cin>>courseCode;
//3.查找选课记录
auto it = enrollments.begin();// 1. 初始化迭代器指向容器起始位置
while (it != enrollments.end()) {// 2. 循环条件:未到达容器末尾
if (it->first == studentId && it->second == courseCode) {// 3. 找到匹配元素
//查找当前学生名
string studentName = students[studentId].name;
//查找课程名
string courseTitle;
for (const auto& course : courses) {
if (course.code == courseCode) {
courseTitle = course.title;
break;
}
}
//删除选课记录
enrollments.erase(it); // 关键:更新迭代器
cout << studentName << " 退选了 " << courseTitle << endl;
return;
}
++it;
}
}
//5.3.3课管理菜单
void enrollmentMenu() {
int choice;
do {
cout << "\n===== 选课管理 =====\n";
cout << "1. 学生选课\n";
cout << "2. 学生退课\n";
cout << "0. 返回主菜单\n";
cout << "请选择: ";
cin >> choice;
switch(choice) {
case 1:
enrollCourse();
break;
case 2:
dropCourse();
break;
case 0:
return;
default:
cout << "无效选择!\n";
}
} while (true);
}
// 在main函数中添加调用
int main() {
// ...之前代码...
switch(choice) {
case 1:
studentMenu();
break;
case 2:
courseMenu();
break;
case 3:
enrollmentMenu();
break;
// 其他case将在后续步骤添加
}
// ...之后代码...
}
让不同学生选择不同课程
尝试重复选同一门课程
实现退课功能
观察list如何保持插入顺序
理解list的遍历和删除操作
//6.主函数
int main(){
int choice;
do{
displayMainMenu();
cin>>choice;
switch (choice) {
case 1:
studentMenu();
break;
case 2:
courseMenu();
break;
case 3:
enrollmentMenu();
break;
default:
//TODO
break;
}
}while(choice!=0);
cout << "感谢使用,再见!\n";
return 0;
}
一.list> enrollments;代码详解
这行代码定义了一个名为 enrollments
的变量,它的类型是 list
。让我为你详细解释它的各个部分:
list
这是C++标准模板库(STL)中的双向链表容器
特点:可以高效地在任意位置插入和删除元素
在这个项目中用于存储选课记录,因为选课记录可能频繁增删
pair
这是STL中的模板类,表示一个包含两个元素的对(pair)
这里的两个元素都是int
类型
第一个int
将存储学生ID
第二个int
将存储课程代码
list>
组合起来表示:一个链表,其中每个节点存储一个pair
相当于一个可以动态增长的"对"的列表
在选课系统中,enrollments
用于记录哪些学生选了哪些课程:
// 表示学生ID 1001 选了课程代码 101
enrollments.push_back(make_pair(1001, 101));
// 也可以使用emplace_back直接构造
enrollments.emplace_back(1002, 102); // 学生1002选课102
高效增删:选课和退课操作频繁,list
的插入删除都是O(1)时间复杂度
简单关联:只需要记录学生ID和课程代码的对应关系
扩展性:可以轻松改为list
如果未来需要添加更多信息(如选课时间)
pair
vs tuple
相似点:都能存储固定数量的不同类型元素
不同点:
pair
只能存两个元素,tuple
可以存任意数量
pair
可以直接用 .first
和 .second
访问,tuple
需要用 get
pair
更简洁,tuple
更灵活
如果需要存储超过两个相关联的数据,tuple
更合适
如果只有两个数据,pair
更直观
int id, year;
string name;
这些变量用于临时存储用户输入的学生信息:
id
:学生的唯一标识(整数类型)
year
:学生的年级(整数类型)
name
:学生的姓名(字符串类型)
这些变量会在后续步骤中通过 cin
和 getline
接收用户输入,然后用于构造一个新的 Student
对象。
students.emplace(id, Student{id, name, year})
是什么意思?这行代码的作用是向 students
容器中插入一个新的学生对象。具体解析如下:
students
是什么?students
是一个全局的 map
容器。
它以学生 id
为键(Key),对应的 Student
对象为值(Value)。
emplace
方法emplace
是 map
的成员函数,用于直接构造并插入一个键值对。
它避免了临时对象的拷贝,效率更高(类似就地构造)。
Student{id, name, year}
这是用 id
、name
、year
构造一个 Student
对象的简洁写法(假设 Student
是一个结构体或类,且有对应的构造函数)。
等价于 Student(id, name, year)
(如果构造函数是显式定义的)。
将 id
作为键,Student{id, name, year}
作为值,插入到 students
这个 map
中。
vector
的检查方式和其他容器不同?容器类型 | 检查重复的方式 | 时间复杂度 | 适用场景 |
---|---|---|---|
vector |
遍历所有元素,逐个比较 | O(n) | 元素较少,或需要顺序存储 |
map / set |
直接 count() 或 find() |
O(log n) | 需要快速查找/去重 |
unordered_map / unordered_set |
直接 count() 或 find() |
O(1)(平均) | 需要极快查找,不关心顺序 |
vector
需要手动遍历检查的原因vector
是一个 动态数组,它 不自动维护元素的唯一性,所以必须手动遍历检查:
for (const auto& course : courses) {
if (course.code == code) { // 逐个比较
cout << "课程代码 " << code << " 已存在!" << endl;
return;
}
}
缺点:如果 courses
很大,遍历会很慢(O(n))。
优点:vector
内存连续,遍历速度快(缓存友好),适合少量数据或需要顺序访问的场景。
map
/set
)如何检查重复?map
(检查学生ID是否重复)if (students.count(id) > 0) { // O(log n)
cout << "学生ID " << id << " 已存在!" << endl;
return;
}
map
是基于 红黑树 的,count()
或 find()
是 O(log n) 的。
不需要遍历,直接通过键(id
)查找。
set
(检查课程类别是否重复)if (categories.count(category) > 0) { // O(log n)
cout << "课程类别 " << category << " 已存在!" << endl;
return;
}
set
也是基于红黑树,count()
是 O(log n) 的。
同样list也和vector差不多
四.clearInputBuffer
的辅助函数的作用其作用是清空标准输入缓冲区,避免残留字符对后续输入操作产生影响。下面对该函数进行详细解释:
clearInputBuffer()
是一个无返回值(void
)的函数,它不接受任何参数。该函数的主要功能是清除输入缓冲区中可能存在的残留字符,确保后续的输入操作能够正常进行。
函数体由两条语句构成:
cin.clear();
cin.ignore(numeric_limits::max(), '\n');
cin.clear()
cin
)的错误标志。cin.clear()
来重置错误标志,让输入流恢复正常工作。cin.clear()
仅仅是重置错误标志,它并不会清除输入缓冲区中的数据。cin.ignore(numeric_limits::max(), '\n')
numeric_limits::max()
:这是输入流能够处理的最大字符数,表示最多可以忽略的字符数量。numeric_limits
是 C++ 标准库中的一个模板类,用于获取各种数据类型的极限值。'\n'
:终止字符,表示遇到换行符时就停止忽略操作。在 C++ 中,输入操作(如cin >> variable
)通常不会读取换行符(\n
),换行符会残留在输入缓冲区中。如果后续的输入操作期望读取字符或字符串,这个残留的换行符就可能被读取,从而导致程序行为不符合预期。
例如,在读取一个整数后紧接着读取字符串时,如果不清除输入缓冲区,残留的换行符会被当作字符串的内容,导致读取的字符串为空。
假设有如下代码:
int num;
string name;
cout << "请输入一个数字:";
cin >> num; // 用户输入 123 并按下回车键
cout << "请输入您的姓名:";
getline(cin, name); // 这里会直接读取到残留的换行符,导致name为空
如果在getline
之前调用clearInputBuffer()
,就能避免上述问题:
int num;
string name;
cout << "请输入一个数字:";
cin >> num;
clearInputBuffer(); // 清空输入缓冲区
cout << "请输入您的姓名:";
getline(cin, name); // 现在可以正常读取用户输入的姓名
在 C++ 中,emplace
和 emplace_back
都是用于在容器中构造元素的函数,但它们的使用场景和适用容器有所不同。以下是详细对比:
函数 | 适用容器 | 作用 |
---|---|---|
emplace |
关联容器(map 、set 、unordered_map 、unordered_set )序列容器( list 、forward_list 、deque ) |
在容器的指定位置构造元素(需要迭代器指定位置)。 |
emplace_back |
序列容器(vector 、deque 、list ) |
在容器的尾部构造元素(无需指定位置)。 |
emplace_back
std::vector> vec;
// 使用 push_back(需要先创建临时对象)
vec.push_back({1, "Alice"}); // 隐式创建临时 pair,再拷贝到容器
// 使用 emplace_back(直接构造)
vec.emplace_back(1, "Alice"); // 直接在容器尾部构造 pair 对象
emplace
map
、set
):插入元素时自动排序或去重,无需指定位置。list
):在指定位置插入元素。// 关联容器示例
std::map myMap;
myMap.emplace(1, "Alice"); // 直接构造键值对,等价于 insert({1, "Alice"})
// 序列容器示例(在指定位置插入)
std::list myList = {1, 3};
auto it = myList.begin();
std::advance(it, 1); // 指向位置 1(第二个元素)
myList.emplace(it, 2); // 在位置 1 插入元素 2,结果:[1, 2, 3]
场景 | 选择 | 示例代码 |
---|---|---|
向 vector /deque 尾部添加元素 |
emplace_back |
vec.emplace_back(1, "Alice"); |
向 map /set 插入元素 |
emplace |
myMap.emplace(1, "Alice"); |
在 list 的指定位置插入元素 |
emplace |
myList.emplace(it, 2); (it 是迭代器) |
在 vector 的中间插入元素 |
不推荐(效率低) | 优先使用 push_back /emplace_back ,避免在中间插入(会导致元素移动) |
参数传递:
emplace_back
:参数是构造对象所需的参数(如 emplace_back(arg1, arg2)
)。emplace
:对于 map
/set
,参数通常是键值对的构造参数(如 emplace(key, value)
)。效率对比:
int
、string
),push_back
和 emplace_back
的性能差异可忽略不计。兼容性:
emplace_back
是 C++11 引入的,旧代码可能使用 push_back
。emplace_back
替代 push_back
,除非需要兼容旧版本。emplace_back
(针对 vector
、deque
、list
)。emplace
(针对 map
、set
等)。emplace
(针对 list
、deque
等支持任意位置插入的容器)。在 C++ 中,list
、map
、vector
、set
删除元素的函数各不相同,且 remove
不是它们的成员函数(除了 std::list
)。以下是详细对比:
容器类型 | 删除元素的正确方法 |
---|---|
std::list |
1. 按值删除:list.remove(value) (直接删除所有等于 value 的元素)2. 按位置删除: list.erase(iterator) 或 list.erase(first, last) |
std::map |
1. 按键删除:map.erase(key) 2. 按位置删除: map.erase(iterator) |
std::vector |
1. 按位置删除:vector.erase(iterator) 或 vector.erase(first, last) 2. 删除尾部元素: vector.pop_back() |
std::set |
1. 按值删除:set.erase(value) 2. 按位置删除: set.erase(iterator) |
std::list
的删除#include
#include
int main() {
std::list myList = {1, 2, 3, 2, 4};
// 按值删除(删除所有等于2的元素)
myList.remove(2); // 结果:[1, 3, 4]
// 按位置删除(删除第一个元素)
auto it = myList.begin();
myList.erase(it); // 结果:[3, 4]
return 0;
}
std::map
的删除#include
std::vector
的删除#include
#include
int main() {
std::vector myVector = {1, 2, 3, 4};
// 按位置删除(删除第二个元素)
auto it = myVector.begin() + 1;
myVector.erase(it); // 结果:[1, 3, 4]
// 删除尾部元素
myVector.pop_back(); // 结果:[1, 3]
return 0;
}
std::set
的删除#include
#include
int main() {
std::set mySet = {1, 2, 3, 4};
// 按值删除
mySet.erase(3); // 结果:{1, 2, 4}
// 按位置删除(删除第一个元素)
auto it = mySet.begin();
mySet.erase(it); // 结果:{2, 4}
return 0;
}
remove
与 erase
的区别remove
是算法,不是成员函数:
std::remove
是标准库算法(定义在
中),适用于 所有容器(但需配合 erase
使用)。vector
中删除值为 value
的元素: #include
myVector.erase(std::remove(myVector.begin(), myVector.end(), value), myVector.end());
remove
将不需要的元素移到末尾,返回新的逻辑末尾迭代器,再用 erase
删除这些元素。std::list
的 remove
是成员函数:
list.remove(value)
直接删除所有等于 value
的元素,无需配合 erase
。list
无需移动元素。容器 | 删除特定值的元素 | 删除特定位置的元素 |
---|---|---|
list |
list.remove(value) |
list.erase(iterator) |
map |
map.erase(key) |
map.erase(iterator) |
vector |
vector.erase(remove(...)) |
vector.erase(iterator) |
set |
set.erase(value) |
set.erase(iterator) |
关键记忆点:
map
/set
):通过键或迭代器删除。vector
/list
):通过迭代器删除,list
可直接按值删除。在 C++ 中,删除容器元素会使指向该元素的迭代器失效。你的代码使用了范围 for 循环(for(auto& it:enrollments)
),这种循环的底层实现依赖于一个隐式的迭代器,当你调用 enrollments.erase(it)
后:
it
不再指向有效元素。你提供的第一段代码是正确的,因为它使用 显式迭代器 并在删除后正确处理迭代器:
auto it = enrollments.begin();
while (it != enrollments.end()) {
if (it->first == studentId && it->second == courseCode) {
// ... 查找学生名和课程名 ...
it = enrollments.erase(it); // 删除当前元素,并获取指向下一个元素的迭代器
cout << studentName << " 退选了 " << courseTitle << endl;
return;
}
++it; // 未删除时,正常推进迭代器
}
关键点:erase(it)
返回指向下一个元素的迭代器,必须将其赋值给 it
。
范围 for 循环绝对不能用于删除元素,因为它的迭代器机制不支持在循环中修改容器结构。必须使用显式迭代器,并在删除元素后更新迭代器位置。
对比错误代码:
for(auto& it:enrollments){ // 范围for循环的隐式迭代器
if(...) {
enrollments.erase(it); // 删除元素后,隐式迭代器失效
// 范围for循环无法正确推进迭代器,导致崩溃
}
}
++it
)。注:该代码是本人自己所写,可能不够好,不够简便,欢迎大家指出我的不足之处。如果遇见看不懂的地方,可以在评论区打出来,进行讨论,或者联系我。上述内容全是我自己理解的,如果你有别的想法,或者认为我的理解不对,欢迎指出!!!如果可以,可以点一个免费的赞支持一下吗?谢谢各位彦祖亦菲!!!!!