C++实战:基于STL与面向对象的员工分组系统设计与实现

1. 项目概述与核心价值

最近在带新人,发现很多刚接触C++的朋友,学完了基础语法和面向对象,但一到实际应用场景就有点懵,不知道如何把零散的知识点串起来。正好,之前带团队时做过一个内部管理系统的小模块,核心功能就是“员工分组”。这听起来简单,但麻雀虽小五脏俱全,它完美地融合了面向对象设计、STL容器使用、文件操作和算法应用这几个C++核心技能点。今天我就把这个项目的完整实现思路和源码拿出来,手把手拆解一遍。无论你是想巩固C++基础,还是正在寻找一个能写进简历的练手项目,这篇文章都能给你提供一条清晰的路径。我们不止要实现功能,更要弄懂每一个设计决策背后的“为什么”。

这个项目的目标是:模拟一个公司场景,根据员工的部门、职级等信息,将他们动态地分配到不同的小组中,并支持查询、统计和持久化存储。它避开了复杂的图形界面,专注于控制台下的核心逻辑,让你能更纯粹地理解C++的威力。

2. 项目整体设计与思路拆解

2.1 需求分析与核心类设计

接到“员工分组”这个需求,第一步不是马上写代码,而是先想清楚我们要处理哪些“东西”。显然,核心的“东西”有两个:员工(Employee)小组(Group)。一个员工属于一个小组,一个小组包含多个员工,这是一种典型的“一对多”关系。

基于此,我设计了两个核心类:

  1. Employee类:用于封装员工的所有属性。这里我选择了几个有代表性的字段:员工ID(唯一标识)、姓名、所属部门、职级。ID用整型,其他用std::string。关键在于,我重载了<运算符。为什么?因为后续我们可能会需要按ID排序,或者将员工对象放入std::set或作为std::map的键,这些容器都需要元素是可比较的。重载<运算符是一种标准做法。

  2. Group类:用于管理一个小组。它包含小组ID、小组名称,以及一个存储组内员工的容器。这里就面临第一个设计选择:用什么容器来存员工?std::vectorstd::list还是std::set?我选择了std::vector<Employee>。理由是:对于分组场景,我们更频繁的操作是遍历组内所有员工进行展示或统计,偶尔进行增删。vector在内存中是连续存储的,遍历效率极高(CPU缓存友好),虽然中间插入删除效率不如list,但在小组规模不大(比如几十人)时,这个开销可以接受,且代码更简洁。如果小组规模动辄上千且需要频繁在中间插入,那就要重新评估了。

2.2 管理器的职责与数据结构的选型

有了员工和小组,还需要一个“大脑”来统筹管理所有的员工和小组,处理分组逻辑。这就是GroupManager类(或称EmployeeGroupSystem)的职责。它的设计是整个项目的枢纽。

首先,它需要存储所有的员工和所有的小组。我使用了两个std::vector作为主存储池:std::vector<Employee> allEmployeesstd::vector<Group> allGroups。用vector是因为我们需要随机访问(通过ID查找),并且数量是动态增长的。

但问题来了:我们经常需要根据员工ID快速找到对应的员工对象,或者根据小组ID快速找到小组。如果每次都用for循环遍历vector,时间复杂度是O(n),效率太低。这就是引入索引映射数据结构的时候了。

我使用了两个std::unordered_map

  • std::unordered_map<int, Employee*> empIdMap:键是员工ID,值是该员工对象在allEmployees向量中的地址(指针)。这样,给定一个ID,我能在O(1)的平均时间复杂度内找到员工。
  • std::unordered_map<int, Group*> groupIdMap:同理,用于快速通过小组ID查找小组。

为什么不直接用map而用unordered_map?因为我们的键(ID)是整数,哈希冲突少,unordered_map的查找效率(O(1))通常比map(O(log n))更高,且我们不需要键按顺序排列。这是一个基于性能考量的典型选择。

2.3 分组策略的思考

“分组”的规则是什么?这是业务逻辑的核心。在实际项目中,规则可能非常复杂(如根据技能、项目经验、绩效等)。为了演示,我实现了一个简单的策略:根据部门(Department)进行分组。系统会扫描所有员工,自动为每个出现的部门创建一个小组,并将该部门的员工加入其中。

这个策略看似简单,但实现时需要考虑几个边界情况:

  1. 部门名称可能重复吗?(在我们的设计里,部门名称作为小组名称,所以同名部门就是同一个小组)。
  2. 如果后来新增了一个员工,其部门是全新的,系统需要能动态创建对应的小组。
  3. 如果后来有员工修改了部门,他需要从原小组移除,并加入新部门的小组。

这些情况都要求我们的分组逻辑不能是一次性的,而应该是可维护、可更新的。在代码中,我将其设计为GroupManager的一个方法groupByDepartment(),它可以被多次调用以重新分组。

3. 核心细节解析与实操要点

3.1 Employee类的实现细节

class Employee { public: Employee(int id, const std::string& name, const std::string& department, const std::string& level) : id_(id), name_(name), department_(department), level_(level) {} // Getter 方法:提供对私有成员的只读访问,这是封装性的体现。 int getId() const { return id_; } std::string getName() const { return name_; } std::string getDepartment() const { return department_; } std::string getLevel() const { return level_; } // Setter 方法:用于修改属性,特别是部门,因为员工可能会调岗。 void setDepartment(const std::string& newDept) { department_ = newDept; } void setLevel(const std::string& newLevel) { level_ = newLevel; } // 重载 < 运算符,用于排序和基于红黑树的容器(如std::set, std::map)。 // 这里选择按ID比较,因为ID是唯一的,能产生严格弱序。 bool operator<(const Employee& other) const { return id_ < other.id_; } // 打印员工信息,方便调试和展示。 void display() const { std::cout << "ID: " << id_ << ", Name: " << name_ << ", Dept: " << department_ << ", Level: " << level_ << std::endl; } private: int id_; // 唯一标识,不可修改,故没有setId方法。 std::string name_; std::string department_; std::string level_; };

注意:将成员变量(id_,name_等)设为private,并通过公有的getter/setter方法来访问,这是面向对象封装的基本原则。它保护了对象内部状态不被随意修改,也便于未来在setter方法中加入验证逻辑(例如,检查部门名称是否合法)。

3.2 Group类的实现与成员管理

class Group { public: Group(int id, const std::string& name) : id_(id), name_(name) {} int getId() const { return id_; } std::string getName() const { return name_; } // 添加员工到组内。这里传入Employee对象的引用。 void addMember(const Employee& emp) { members_.push_back(emp); } // 从组内移除员工。这是一个稍复杂的操作。 bool removeMember(int employeeId) { // 使用std::remove_if算法。它并不直接删除,而是把要删除的元素移到容器末尾,并返回新的逻辑终点迭代器。 auto it = std::remove_if(members_.begin(), members_.end(), [employeeId](const Employee& emp) { return emp.getId() == employeeId; }); // 判断是否有元素需要移除 if (it != members_.end()) { members_.erase(it, members_.end()); // 真正删除尾部不需要的元素 return true; } return false; } // 显示组内所有成员 void displayMembers() const { if (members_.empty()) { std::cout << " Group \"" << name_ << "\" is empty." << std::endl; return; } std::cout << " Members of Group \"" << name_ << "\" (ID: " << id_ << "):" << std::endl; for (const auto& member : members_) { std::cout << " "; member.display(); // 复用Employee的display方法 } } // 获取成员数量,用于统计 size_t getMemberCount() const { return members_.size(); } private: int id_; std::string name_; std::vector<Employee> members_; // 存储组内员工的容器 };

实操心得:在removeMember函数中,我使用了std::remove_if算法配合vector::erase。这是从vector中删除满足特定条件元素的标准惯用法(Erase–remove idiom)。直接遍历vector并用erase删除单个元素是低效的,因为erase会导致后续元素向前移动,多次调用会产生O(n²)复杂度。而remove_if是O(n)复杂度,它通过移动元素来整理容器,最后一次性erase,效率高得多。这是STL算法在实际中的经典应用。

3.3 GroupManager:系统的中枢神经

GroupManager类是最复杂的,它负责所有全局状态和逻辑。

class GroupManager { private: std::vector<Employee> allEmployees; std::vector<Group> allGroups; std::unordered_map<int, Employee*> empIdMap; std::unordered_map<int, Group*> groupIdMap; int nextGroupId; // 用于生成唯一的小组ID public: GroupManager() : nextGroupId(1) {} // 1. 员工管理 void addEmployee(int id, const std::string& name, const std::string& dept, const std::string& level) { // 检查ID是否重复 if (empIdMap.find(id) != empIdMap.end()) { std::cerr << "Error: Employee ID " << id << " already exists!" << std::endl; return; } allEmployees.emplace_back(id, name, dept, level); empIdMap[id] = &allEmployees.back(); // 记录指针 std::cout << "Employee added: " << name << std::endl; } Employee* findEmployeeById(int id) { auto it = empIdMap.find(id); return (it != empIdMap.end()) ? it->second : nullptr; } // 2. 小组管理 Group* createGroup(const std::string& groupName) { int newId = nextGroupId++; allGroups.emplace_back(newId, groupName); Group* newGroup = &allGroups.back(); groupIdMap[newId] = newGroup; std::cout << "Group created: ID=" << newId << ", Name=\"" << groupName << "\"" << std::endl; return newGroup; } Group* findGroupById(int id) { auto it = groupIdMap.find(id); return (it != groupIdMap.end()) ? it->second : nullptr; } // 3. 核心分组逻辑:按部门分组 void groupByDepartment() { std::cout << "\n--- Grouping employees by department ---" << std::endl; // 先清空所有现有小组的成员(但不删除小组本身) for (auto& group : allGroups) { // 这里需要一个清空成员的方法,我们可以在Group类中添加 `void clearMembers() { members_.clear(); }` // 为了简化,我们暂时重新创建小组。在实际项目中,更优的做法是更新现有小组。 } // 更简单的实现:基于当前员工,为每个部门创建或找到小组,然后加入。 std::unordered_map<std::string, Group*> deptGroupMap; // 部门名到小组指针的临时映射 for (auto& emp : allEmployees) { std::string dept = emp.getDepartment(); Group* targetGroup = nullptr; // 检查这个部门是否已有对应小组 auto it = deptGroupMap.find(dept); if (it != deptGroupMap.end()) { targetGroup = it->second; } else { // 没有,则创建新小组 targetGroup = createGroup(dept); deptGroupMap[dept] = targetGroup; } // 将员工加入小组 targetGroup->addMember(emp); } // 打印分组结果 displayAllGroups(); } // 4. 数据显示 void displayAllEmployees() { std::cout << "\n--- All Employees (" << allEmployees.size() << ") ---" << std::endl; for (const auto& emp : allEmployees) { emp.display(); } } void displayAllGroups() { std::cout << "\n--- All Groups (" << allGroups.size() << ") ---" << std::endl; for (const auto& group : allGroups) { std::cout << "[Group] ID: " << group.getId() << ", Name: \"" << group.getName() << "\", Member Count: " << group.getMemberCount() << std::endl; group.displayMembers(); } } };

关键点解析

  1. 指针与引用empIdMapgroupIdMap存储的是指针(Employee*,Group*),指向vector中的实际对象。这避免了数据的重复存储,保证了数据一致性。修改vector中的对象,通过指针也能看到变化。
  2. emplace_backvspush_back:在向vector添加对象时,我使用了emplace_back。它可以直接在vector尾部构造对象,省去了先创建临时对象再拷贝或移动的过程,效率更高,代码也更简洁。
  3. 分组逻辑的优化:在groupByDepartment中,我使用了一个临时的std::unordered_map<std::string, Group*>来建立部门名到小组的映射。这样,对于每个员工,我都能在O(1)时间内找到他应该加入的小组,整个分组过程的复杂度是O(n),非常高效。这是“用空间换时间”的典型策略。

4. 实操过程与核心环节实现

4.1 主程序流程与用户交互

为了让项目完整,我们需要一个main函数来驱动整个系统,并模拟用户操作。这里我设计了一个简单的控制台菜单。

#include <iostream> #include <vector> #include <unordered_map> #include <string> #include <algorithm> // for std::remove_if // 将上述 Employee, Group, GroupManager 类的定义放在这里... int main() { GroupManager manager; // 初始化一些测试数据 manager.addEmployee(101, "Alice", "Engineering", "Senior"); manager.addEmployee(102, "Bob", "Engineering", "Junior"); manager.addEmployee(103, "Charlie", "Sales", "Manager"); manager.addEmployee(104, "Diana", "Marketing", "Specialist"); manager.addEmployee(105, "Eve", "Engineering", "Junior"); manager.addEmployee(106, "Frank", "Sales", "Associate"); int choice = 0; do { std::cout << "\n===== Employee Grouping System =====" << std::endl; std::cout << "1. Display All Employees" << std::endl; std::cout << "2. Display All Groups" << std::endl; std::cout << "3. Group Employees by Department" << std::endl; std::cout << "4. Add a New Employee" << std::endl; std::cout << "5. Find Employee by ID" << std::endl; std::cout << "0. Exit" << std::endl; std::cout << "Enter your choice: "; std::cin >> choice; switch (choice) { case 1: manager.displayAllEmployees(); break; case 2: manager.displayAllGroups(); break; case 3: manager.groupByDepartment(); break; case 4: { int id; std::string name, dept, level; std::cout << "Enter Employee ID: "; std::cin >> id; std::cin.ignore(); // 清除输入缓冲区的换行符 std::cout << "Enter Name: "; std::getline(std::cin, name); std::cout << "Enter Department: "; std::getline(std::cin, dept); std::cout << "Enter Level: "; std::getline(std::cin, level); manager.addEmployee(id, name, dept, level); break; } case 5: { int searchId; std::cout << "Enter Employee ID to find: "; std::cin >> searchId; Employee* emp = manager.findEmployeeById(searchId); if (emp) { std::cout << "Employee found: "; emp->display(); } else { std::cout << "Employee with ID " << searchId << " not found." << std::endl; } break; } case 0: std::cout << "Exiting system. Goodbye!" << std::endl; break; default: std::cout << "Invalid choice. Please try again." << std::endl; } } while (choice != 0); return 0; }

4.2 编译与运行示例

将上述所有代码(类定义和main函数)保存到一个文件,例如employee_grouping.cpp。使用C++编译器进行编译。如果你使用g++,命令如下:

g++ -std=c++11 -o employee_grouping employee_grouping.cpp

然后运行生成的可执行文件:

./employee_grouping # Linux/macOS # 或者 employee_grouping.exe # Windows

你会看到一个文本菜单。依次选择选项3(按部门分组),然后选择选项2(显示所有小组),就能看到类似下面的输出:

--- Grouping employees by department --- Group created: ID=1, Name="Engineering" Group created: ID=2, Name="Sales" Group created: ID=3, Name="Marketing" --- All Groups (3) --- [Group] ID: 1, Name="Engineering", Member Count: 3 Members of Group "Engineering" (ID: 1): ID: 101, Name: Alice, Dept: Engineering, Level: Senior ID: 102, Name: Bob, Dept: Engineering, Level: Junior ID: 105, Name: Eve, Dept: Engineering, Level: Junior [Group] ID: 2, Name="Sales", Member Count: 2 Members of Group "Sales" (ID: 2): ID: 103, Name: Charlie, Dept: Sales, Level: Manager ID: 106, Name: Frank, Dept: Sales, Level: Associate [Group] ID: 3, Name="Marketing", Member Count: 1 Members of Group "Marketing" (ID: 3): ID: 104, Name: Diana, Dept: Marketing, Level: Specialist

4.3 功能扩展:数据持久化(文件存储)

一个完整的系统不能只在内存中运行。我们需要将员工和小组数据保存到文件中,下次启动程序时可以加载。这里我们实现简单的文本文件存储。

GroupManager类中添加两个方法:

// 保存数据到文件 void saveToFile(const std::string& filename) { std::ofstream outFile(filename); if (!outFile) { std::cerr << "Error: Cannot open file " << filename << " for writing." << std::endl; return; } // 保存员工数据 outFile << "[Employees]" << std::endl; for (const auto& emp : allEmployees) { outFile << emp.getId() << "," << emp.getName() << "," << emp.getDepartment() << "," << emp.getLevel() << std::endl; } // 保存小组数据(这里简化,只保存小组基本信息,成员关系通过员工部门重建) outFile << "[Groups]" << std::endl; for (const auto& group : allGroups) { outFile << group.getId() << "," << group.getName() << std::endl; } outFile.close(); std::cout << "Data saved to " << filename << std::endl; } // 从文件加载数据 void loadFromFile(const std::string& filename) { std::ifstream inFile(filename); if (!inFile) { std::cerr << "Info: File " << filename << " not found. Starting with empty data." << std::endl; return; } // 清空现有数据 allEmployees.clear(); allGroups.clear(); empIdMap.clear(); groupIdMap.clear(); nextGroupId = 1; std::string line; std::string section; while (std::getline(inFile, line)) { if (line.empty()) continue; if (line[0] == '[') { // 节标题 section = line; continue; } std::istringstream iss(line); std::string token; if (section == "[Employees]") { std::vector<std::string> tokens; while (std::getline(iss, token, ',')) { tokens.push_back(token); } if (tokens.size() >= 4) { int id = std::stoi(tokens[0]); addEmployee(id, tokens[1], tokens[2], tokens[3]); // 复用addEmployee,它会处理ID重复和映射 } } else if (section == "[Groups]") { std::vector<std::string> tokens; while (std::getline(iss, token, ',')) { tokens.push_back(token); } if (tokens.size() >= 2) { // 这里只是加载小组定义,实际成员关系在分组时重建 int id = std::stoi(tokens[0]); // 注意:这里直接创建小组,会与nextGroupId逻辑冲突。更健壮的做法是记录最大的ID。 // 简化处理:我们只加载员工,小组通过分组逻辑自动创建。 // 所以这里可以暂时忽略小组数据的加载,或者用更复杂的逻辑同步nextGroupId。 } } } inFile.close(); std::cout << "Data loaded from " << filename << std::endl; // 加载后,根据员工的部门信息重新分组 groupByDepartment(); }

然后在主菜单中加入保存和加载的选项。这样,一个具备基本CRUD(增删改查)和持久化功能的“员工分组”系统核心就完成了。

5. 常见问题与排查技巧实录

在实际编写和运行这类项目时,你肯定会遇到各种问题。下面是我总结的一些典型坑点和解决思路。

5.1 内存与指针管理问题

问题1:empIdMap中存储的指针失效了?我们的empIdMap存储的是指向allEmployees这个vector中元素的指针。vector在内存中是连续的,当它扩容(push_backemplace_back导致容量不足)时,会在另一块内存分配更大的空间,并把所有元素移动或拷贝过去。这时,原来内存地址上的对象就不复存在了,我们map里存的指针就变成了悬空指针(Dangling Pointer),指向无效内存,后续使用会导致未定义行为(通常是程序崩溃)。

解决方案:这是使用vector存储对象并同时用指针索引时的一个经典陷阱。有几种应对策略:

  1. 使用std::liststd::deque:这些容器在插入时不会使其他元素的引用/指针失效(list完全不失效,deque在首尾插入不失效)。但牺牲了vector的缓存友好性和随机访问性能。
  2. 预分配空间:如果知道大致数据量,可以用allEmployees.reserve(1000)预先分配足够空间,避免中途扩容。
  3. 存储索引而非指针empIdMap不存Employee*,而是存int类型的在vector中的下标(索引)。查找时通过allEmployees[index]访问。下标在vector中间插入或删除时会变,但尾部添加不会导致前面的下标失效。这要求你的业务逻辑中插入删除不频繁,或者有相应的索引更新机制。
  4. 使用std::unique_ptr:将vector<std::unique_ptr<Employee>>,这样对象存储在堆上,vector里存的是指针本身,vector扩容时移动的是指针(很小),对象地址不变。empIdMap也存裸指针或weak_ptr。这是更现代、更安全但稍复杂的方法。

在我们的示例中,由于数据量小且主要在初始化时添加,风险较低。但在生产代码中必须慎重处理。

问题2:如何安全地删除一个员工?删除员工不仅要从allEmployees中移除,还要从empIdMap中移除其索引,并且要从他所在的小组的成员列表中移除。这是一个需要保持数据一致性的“事务性”操作。

bool GroupManager::removeEmployee(int id) { // 1. 在map中查找指针 auto mapIt = empIdMap.find(id); if (mapIt == empIdMap.end()) { return false; // 员工不存在 } Employee* empPtr = mapIt->second; // 2. 找到该员工所在的小组并将其移除(需要遍历所有小组) std::string dept = empPtr->getDepartment(); Group* group = findGroupByDeptName(dept); // 需要实现一个根据部门名找小组的函数 if (group) { group->removeMember(id); } // 3. 从vector中删除员工(这是最棘手的部分,因为会使指针失效) // 先找到该员工在vector中的迭代器 auto vecIt = std::find_if(allEmployees.begin(), allEmployees.end(), [id](const Employee& emp) { return emp.getId() == id; }); if (vecIt != allEmployees.end()) { // 3.1 删除前,必须更新map中所有指向被删除元素之后元素的指针! // 因为vector删除一个元素,后面所有元素都会向前移动,地址都变了。 // 一种方法是:先删除map中的条目,等vector删除并可能扩容后,重新构建整个map。对于小型系统可以接受。 // 另一种更精细的方法:计算被删除元素的位置,然后更新map中所有索引大于该位置的指针。 // 这里演示第一种简单粗暴的方法(效率低但正确): allEmployees.erase(vecIt); // 删除元素,后续元素前移,迭代器失效 // 清空并重建empIdMap empIdMap.clear(); for (auto& emp : allEmployees) { empIdMap[emp.getId()] = &emp; // 重新建立映射 } return true; } return false; }

可以看到,删除操作非常复杂。这反过来说明了我们最初的设计(用指针做映射)在面临修改时带来的挑战。在实际项目中,需要根据最主要的操作(查询、遍历、修改)来权衡数据结构的选择。

5.2 输入输出与异常处理

问题3:用户输入了非数字的ID怎么办?在主程序的switch case 4中,我们直接用std::cin >> id。如果用户输入了abccin会进入错误状态,后续所有输入都会失败。

解决方案:始终验证输入。

int id; while (true) { std::cout << "Enter Employee ID (must be a number): "; if (std::cin >> id) { break; // 输入成功,跳出循环 } else { std::cout << "Invalid input. Please enter a number." << std::endl; std::cin.clear(); // 清除错误状态 std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 丢弃错误输入行 } }

需要包含<limits>头文件来使用std::numeric_limits

问题4:文件加载时,数据格式错误怎么办?loadFromFile函数中,我们假设文件格式完全正确。但如果某一行格式不对(例如,员工数据只有3个字段),std::stoi可能会抛出异常,导致程序崩溃。

解决方案:使用try-catch块包裹转换代码,或者使用更健壮的解析方式(如检查tokens数量,使用std::strtol并检查错误)。

try { int id = std::stoi(tokens[0]); // ... 其他操作 } catch (const std::invalid_argument& e) { std::cerr << "Warning: Invalid number format in line: " << line << std::endl; continue; // 跳过这一行 } catch (const std::out_of_range& e) { std::cerr << "Warning: Number out of range in line: " << line << std::endl; continue; }

5.3 设计模式与代码结构优化

当功能越来越复杂,你会发现GroupManager类变得非常庞大,职责过多(管理员工、管理小组、分组逻辑、文件IO)。这违反了单一职责原则

优化方向:可以考虑引入更多的类来分担职责。

  • EmployeeRepository:专门负责员工的增删改查和持久化。
  • GroupRepository:专门负责小组的增删改查。
  • GroupingStrategy:抽象分组策略(如按部门、按职级、随机等),使用策略模式,让分组算法易于扩展。
  • FilePersistenceService:专门处理文件的读写。

GroupManager则变成协调这些服务的“门面”(Facade Pattern)或“服务层”。这样代码更清晰,也更易于单元测试。

5.4 性能考量与小技巧

  • 查找优化:我们使用了unordered_map做索引,这是正确的。如果键的范围很小且连续(如ID从1到1000),甚至可以用std::vector直接做索引表(vector<Employee*>,下标就是ID),查找速度是O(1)且更稳定(无哈希冲突)。
  • 遍历选择:当需要频繁遍历且顺序不重要时,vector是最快的。如果需要频繁在中间插入删除,考虑listdeque。如果需要快速查找且保持排序,考虑setmap
  • const正确性:仔细思考哪些成员函数应该被声明为const(不修改对象状态),如display()getter方法。这不仅能提高代码可读性,还能让对象在const语境下被使用。
  • 使用智能指针:如果项目规模扩大,考虑使用std::unique_ptr来管理动态分配的对象,可以避免很多内存泄漏的问题。vector可以存储unique_ptr<Employee>

这个“员工分组”项目虽然基础,但像一颗棱镜,折射出C++面向对象设计、STL使用、内存管理、异常安全等多个核心知识点。我建议你在理解上述代码的基础上,自己动手实现一遍,并尝试添加新功能,比如“按职级分组”、“将小组信息保存到独立文件”、“实现一个更高效的员工删除逻辑”等。踩坑和解决问题的过程,才是提升最快的途径。