实验4 组合与继承
实验四:
实验任务1
源代码:
1 #pragma once 2 3 #include <vector> 4 #include <array> 5 #include <string> 6 7 class GradeCalc { 8 public: 9 GradeCalc(const std::string &cname); 10 void input(int n); // 录入n个成绩 11 void output() const; // 输出成绩 12 void sort(bool ascending = false); // 排序 (默认降序) 13 int min() const; // 返回最低分(如成绩未录入,返回-1) 14 int max() const; // 返回最高分 (如成绩未录入,返回-1) 15 double average() const; // 返回平均分 (如成绩未录入,返回0.0) 16 void info(); // 输出课程成绩信息 17 18 private: 19 void compute(); // 成绩统计 20 21 private: 22 std::string course_name; // 课程名 23 std::vector<int> grades; // 课程成绩 24 std::array<int, 5> counts; // 保存各分数段人数([0, 60), [60, 70), [70, 80), [80, 90), [90, 100] 25 std::array<double, 5> rates; // 保存各分数段人数占比 26 bool is_dirty; // 脏标记,记录是否成绩信息有变更 27 };
1 #include <algorithm> 2 #include <array> 3 #include <cstdlib> 4 #include <iomanip> 5 #include <iostream> 6 #include <numeric> 7 #include <string> 8 #include <vector> 9 10 #include "GradeCalc.hpp" 11 12 GradeCalc::GradeCalc(const std::string &cname):course_name{cname},is_dirty{true} { 13 counts.fill(0); 14 rates.fill(0); 15 } 16 17 void GradeCalc::input(int n) { 18 if(n < 0) { 19 std::cerr << "无效输入! 人数不能为负数\n"; 20 std::exit(1); 21 } 22 23 grades.reserve(n); 24 25 int grade; 26 27 for(int i = 0; i < n;) { 28 std::cin >> grade; 29 30 if(grade < 0 || grade > 100) { 31 std::cerr << "无效输入! 分数须在[0,100]\n"; 32 continue; 33 } 34 35 grades.push_back(grade); 36 ++i; 37 } 38 39 is_dirty = true; // 设置脏标记:成绩信息有变更 40 } 41 42 void GradeCalc::output() const { 43 for(auto grade: grades) 44 std::cout << grade << ' '; 45 std::cout << std::endl; 46 } 47 48 void GradeCalc::sort(bool ascending) { 49 if(ascending) 50 std::sort(grades.begin(), grades.end()); 51 else 52 std::sort(grades.begin(), grades.end(), std::greater<int>()); 53 } 54 55 int GradeCalc::min() const { 56 if(grades.empty()) 57 return -1; 58 59 auto it = std::min_element(grades.begin(), grades.end()); 60 return *it; 61 } 62 63 int GradeCalc::max() const { 64 if(grades.empty()) 65 return -1; 66 67 auto it = std::max_element(grades.begin(), grades.end()); 68 return *it; 69 } 70 71 double GradeCalc::average() const { 72 if(grades.empty()) 73 return 0.0; 74 75 double avg = std::accumulate(grades.begin(), grades.end(), 0.0)/grades.size(); 76 return avg; 77 } 78 79 void GradeCalc::info() { 80 if(is_dirty) 81 compute(); 82 83 std::cout << "课程名称:\t" << course_name << std::endl; 84 std::cout << "平均分:\t" << std::fixed << std::setprecision(2) << average() << std::endl; 85 std::cout << "最高分:\t" << max() << std::endl; 86 std::cout << "最低分:\t" << min() << std::endl; 87 88 const std::array<std::string, 5> grade_range{"[0, 60) ", 89 "[60, 70)", 90 "[70, 80)", 91 "[80, 90)", 92 "[90, 100]"}; 93 94 for(int i = static_cast<int>(grade_range.size())-1; i >= 0; --i) 95 std::cout << grade_range[i] << "\t: " << counts[i] << "人\t" 96 << std::fixed << std::setprecision(2) << rates[i]*100 << "%\n"; 97 } 98 99 void GradeCalc::compute() { 100 if(grades.empty()) 101 return; 102 103 counts.fill(0); 104 rates.fill(0.0); 105 106 // 统计各分数段人数 107 for(auto grade:grades) { 108 if(grade < 60) 109 ++counts[0]; // [0, 60) 110 else if (grade < 70) 111 ++counts[1]; // [60, 70) 112 else if (grade < 80) 113 ++counts[2]; // [70, 80) 114 else if (grade < 90) 115 ++counts[3]; // [80, 90) 116 else 117 ++counts[4]; // [90, 100] 118 } 119 120 // 统计各分数段比例 121 for(size_t i = 0; i < rates.size(); ++i) 122 rates[i] = counts[i] * 1.0 / grades.size(); 123 124 is_dirty = false; // 更新脏标记 125 }
1 #include <iostream> 2 #include <string> 3 #include "GradeCalc.hpp" 4 5 void test() { 6 GradeCalc c1("OOP"); 7 8 std::cout << "录入成绩:\n"; 9 c1.input(5); 10 11 std::cout << "输出成绩:\n"; 12 c1.output(); 13 14 std::cout << "排序后成绩:\n"; 15 c1.sort(); c1.output(); 16 17 std::cout << "*************成绩统计信息*************\n"; 18 c1.info(); 19 20 } 21 22 int main() { 23 test(); 24 }
运行结果:

问题回答:
- 问题1:组合关系识别
GradeCalc 类声明中,逐行写出所有体现"组合"关系的成员声明,并用一句话说明每个被组合对象的功能。
std::string course_name; 存储课程名称
std::vector<int> grades; 存储成绩
std::array<int, 5> counts; 存储各个分数段
std::array<double, 5> rates; 存储各分数段人数占比
- 问题2:接口暴露理解
如在 test 模块中这样使用,是否合法?如不合法,解释原因。
1 GradeCalc c("OOP"); 2 c.inupt(5); 3 c.push_back(97); // 合法吗?
不合法:一是input拼错了,二是grades是GradeCalc的私有成员,push_back是std::vector的成员函数,外部不能直接访问私有成员,并且GradeCalc没有暴露这个接口。
- 问题3:架构设计分析
- 问题4:功能扩展设计
1 double median() const; 2 3 double GradeCalc::median() const { 4 if (grades.empty()) return 0.0; 5 std::vector<int> temp_grades = grades; 6 std::sort(temp_grades.begin(), temp_grades.end()); 7 int n = temp_grades.size(); 8 if (n % 2 == 1) { 9 return temp_grades[n / 2]; 10 } else { 11 return (temp_grades[n/2 - 1] + temp_grades[n/2]) / 2.0; 12 } 13 } 14 15 cout << "中位数:\t" << std::fixed << std::setprecision(2) << median() << endl;
- 问题5:数据状态管理
GradeCalc 和 compute 中都包含代码: counts.fill(0); rates.fill(0); 。
compute 中能否去掉这两行?如去掉,在哪种使用场景下会引发统计错误?
不能删去:若变更成绩时,由于此时counts和rates未归0,counts和rates在已有数据上累加,导致统计结果错误。
- 问题6:内存管理理解

实验任务2
源代码:
1 #pragma once 2 3 #include <array> 4 #include <string> 5 #include <vector> 6 7 class GradeCalc: private std::vector<int> { 8 public: 9 GradeCalc(const std::string &cname); 10 void input(int n); // 录入n个成绩 11 void output() const; // 输出成绩 12 void sort(bool ascending = false); // 排序 (默认降序) 13 int min() const; // 返回最低分 14 int max() const; // 返回最高分 15 double average() const; // 返回平均分 16 void info(); // 输出成绩统计信息 17 18 private: 19 void compute(); // 计算成绩统计信息 20 21 private: 22 std::string course_name; // 课程名 23 std::array<int, 5> counts; // 保存各分数段人数([0, 60), [60, 70), [70, 80), [80, 90), [90, 100] 24 std::array<double, 5> rates; // 保存各分数段占比 25 bool is_dirty; // 脏标记,记录是否成绩信息有变更 26 };
1 #include <algorithm> 2 #include <array> 3 #include <cstdlib> 4 #include <iomanip> 5 #include <iostream> 6 #include <numeric> 7 #include <string> 8 #include <vector> 9 #include "GradeCalc.hpp" 10 11 12 GradeCalc::GradeCalc(const std::string &cname): course_name{cname}, is_dirty{true}{ 13 counts.fill(0); 14 rates.fill(0); 15 } 16 17 void GradeCalc::input(int n) { 18 if(n < 0) { 19 std::cerr << "无效输入! 人数不能为负数\n"; 20 return; 21 } 22 23 this->reserve(n); 24 25 int grade; 26 27 for(int i = 0; i < n;) { 28 std::cin >> grade; 29 if(grade < 0 || grade > 100) { 30 std::cerr << "无效输入! 分数须在[0,100]\n"; 31 continue; 32 } 33 34 this->push_back(grade); 35 ++i; 36 } 37 38 is_dirty = true; 39 } 40 41 void GradeCalc::output() const { 42 for(auto grade: *this) 43 std::cout << grade << ' '; 44 std::cout << std::endl; 45 } 46 47 void GradeCalc::sort(bool ascending) { 48 if(ascending) 49 std::sort(this->begin(), this->end()); 50 else 51 std::sort(this->begin(), this->end(), std::greater<int>()); 52 } 53 54 int GradeCalc::min() const { 55 if(this->empty()) 56 return -1; 57 58 return *std::min_element(this->begin(), this->end()); 59 } 60 61 int GradeCalc::max() const { 62 if(this->empty()) 63 return -1; 64 65 return *std::max_element(this->begin(), this->end()); 66 } 67 68 double GradeCalc::average() const { 69 if(this->empty()) 70 return 0.0; 71 72 double avg = std::accumulate(this->begin(), this->end(), 0.0) / this->size(); 73 return avg; 74 } 75 76 void GradeCalc::info() { 77 if(is_dirty) 78 compute(); 79 80 std::cout << "课程名称:\t" << course_name << std::endl; 81 std::cout << "平均分:\t" << std::fixed << std::setprecision(2) << average() << std::endl; 82 std::cout << "最高分:\t" << max() << std::endl; 83 std::cout << "最低分:\t" << min() << std::endl; 84 85 const std::array<std::string, 5> grade_range{"[0, 60) ", 86 "[60, 70)", 87 "[70, 80)", 88 "[80, 90)", 89 "[90, 100]"}; 90 91 for(int i = static_cast<int>(grade_range.size())-1; i >= 0; --i) 92 std::cout << grade_range[i] << "\t: " << counts[i] << "人\t" 93 << std::fixed << std::setprecision(2) << rates[i]*100 << "%\n"; 94 } 95 96 void GradeCalc::compute() { 97 if(this->empty()) 98 return; 99 100 counts.fill(0); 101 rates.fill(0); 102 103 // 统计各分数段人数 104 for(int grade: *this) { 105 if(grade < 60) 106 ++counts[0]; // [0, 60) 107 else if (grade < 70) 108 ++counts[1]; // [60, 70) 109 else if (grade < 80) 110 ++counts[2]; // [70, 80) 111 else if (grade < 90) 112 ++counts[3]; // [80, 90) 113 else 114 ++counts[4]; // [90, 100] 115 } 116 117 // 统计各分数段比例 118 for(size_t i = 0; i < rates.size(); ++i) 119 rates[i] = counts[i] * 1.0 / this->size(); 120 121 is_dirty = false; 122 }
1 #include <iostream> 2 #include <string> 3 #include "GradeCalc.hpp" 4 5 void test() { 6 GradeCalc c1("OOP"); 7 8 std::cout << "录入成绩:\n"; 9 c1.input(5); 10 11 std::cout << "输出成绩:\n"; 12 c1.output(); 13 14 std::cout << "排序后成绩:\n"; 15 c1.sort(); c1.output(); 16 17 std::cout << "*************成绩统计信息*************\n"; 18 c1.info(); 19 20 } 21 22 int main() { 23 test(); 24 }
运行结果:

问题回答:
- 问题1:继承关系识别
写出 GradeCalc 类声明体现"继承"关系的完整代码行
1 class GradeCalc: private std::vector<int>
- 问题2:接口暴露理解
当前继承方式下,基类 vector<int> 的接口会自动成为 GradeCalc 的接口吗?
如在 test 模块中这样用,能否编译通过?用一句话解释原因。
1 GradeCalc c("OOP"); 2 c.input(5); 3 c.push_back(97); // 合法吗?
会成为接口;不能编译通过:当前继承方式为私有继承,私有继承隐藏基类std::vector<int>的公有接口,但派生类GradeCalc没有将该接口暴露为自身的公有接口,因此对象c无法直接调用基类的公有成员函数push_back。
- 问题3:数据访问差异
1 // 组合方式 2 for(auto grade: grades) // 通过什么接口访问数据 3 // 略 4 // 继承方式 5 for(int grade: *this) // 通过什么接口访问数据 6 // 略
继承方式的封装性弱 :GradeCalc私有继承std::vector<int>,数据存储直接依赖基类。数据访问接口本质是复用基类vector的迭代器接口(begin()和end()),接口由基类决定,派生类被动依赖,基类接口的变化会直接影响内部访问的逻辑。
组合方式的封装性强:若将std::vector<int>作为GradeCalc的私有成员,数据存储则完全由类内部管控。数据访问接口来自成员对象的迭代器接口,外部无法直接访问存储数据,并且类可自行决定暴露哪些接口,就算后续替换存储容器也会不影响外部使用。
- 问题4:组合 vs. 继承方案选择
实验任务3
源代码:
1 #pragma once 2 3 #include <string> 4 #include <vector> 5 6 enum class GraphType {circle, triangle, rectangle}; 7 8 // Graph类定义 9 class Graph { 10 public: 11 virtual void draw() {} 12 virtual ~Graph() = default; 13 }; 14 15 // Circle类声明 16 class Circle : public Graph { 17 public: 18 void draw(); 19 }; 20 21 // Triangle类声明 22 class Triangle : public Graph { 23 public: 24 void draw(); 25 }; 26 27 // Rectangle类声明 28 class Rectangle : public Graph { 29 public: 30 void draw(); 31 }; 32 33 // Canvas类声明 34 class Canvas { 35 public: 36 void add(const std::string& type); // 根据字符串添加图形 37 void paint() const; // 使用统一接口绘制所有图形 38 ~Canvas(); // 手动释放资源 39 40 private: 41 std::vector<Graph*> graphs; 42 }; 43 44 // 4. 工具函数 45 GraphType str_to_GraphType(const std::string& s); // 字符串转枚举类型 46 Graph* make_graph(const std::string& type); // 创建图形,返回堆对象指针
1 #include <algorithm> 2 #include <cctype> 3 #include <iostream> 4 #include <string> 5 6 #include "Graph.hpp" 7 8 // Circle类实现 9 void Circle::draw() { std::cout << "draw a circle...\n"; } 10 11 // Triangle类实现 12 void Triangle::draw() { std::cout << "draw a triangle...\n"; } 13 14 // Rectangle类实现 15 void Rectangle::draw() { std::cout << "draw a rectangle...\n"; } 16 17 // Canvas类实现 18 void Canvas::add(const std::string& type) { 19 Graph* g = make_graph(type); 20 if (g) 21 graphs.push_back(g); 22 } 23 24 void Canvas::paint() const { 25 for (Graph* g : graphs) 26 g->draw(); 27 } 28 29 Canvas::~Canvas() { 30 for (Graph* g : graphs) 31 delete g; 32 } 33 34 // 工具函数实现 35 // 字符串 → 枚举转换 36 GraphType str_to_GraphType(const std::string& s) { 37 std::string t = s; 38 std::transform(s.begin(), s.end(), t.begin(), 39 [](unsigned char c) { return std::tolower(c);}); 40 41 if (t == "circle") 42 return GraphType::circle; 43 44 if (t == "triangle") 45 return GraphType::triangle; 46 47 if (t == "rectangle") 48 return GraphType::rectangle; 49 50 return GraphType::circle; // 缺省返回 51 } 52 53 // 创建图形,返回堆对象指针 54 Graph* make_graph(const std::string& type) { 55 switch (str_to_GraphType(type)) { 56 case GraphType::circle: return new Circle; 57 case GraphType::triangle: return new Triangle; 58 case GraphType::rectangle: return new Rectangle; 59 default: return nullptr; 60 } 61 }
1 #include <string> 2 #include "Graph.hpp" 3 4 void test() { 5 Canvas canvas; 6 7 canvas.add("circle"); 8 canvas.add("triangle"); 9 canvas.add("rectangle"); 10 canvas.paint(); 11 } 12 13 int main() { 14 test(); 15 }
实验结果:

问题回答:
- 问题1:对象关系识别
(1)写出Graph.hpp中体现"组合"关系的成员声明代码行,并用一句话说明被组合对象的功能。
1 private: std::vector<Graph*> graphs;
功能:存储所有图形对象的指针。
(2)写出Graph.hpp中体现"继承"关系的类声明代码行。
1 class Circle : public Graph; 2 class Triangle : public Graph; 3 class Rectangle : public Graph;
- 问题2:多态机制观察
- 问题3:扩展性思考
1 //在枚举GraphType中添加star 2 enum class GraphType {circle, triangle, rectangle, star}; 3 4 //声明Star类 5 class Star : public Graph { 6 public: 7 void draw(); 8 };
1 //实现Star类的draw()方法: 2 void Star::draw() { std::cout << "draw a star...\n"; } 3 4 //修改str_to_GraphType函数,添加Star对应的字符串转换为枚举: 5 if (t == "star") return GraphType::star; 6 7 //修改make_graph函数的switch语句,添加Star的创建: 8 case GraphType::star: return new Star;
1 //在test( 函数中添加Star的调用: 2 canvas.add("star");
- 问题4:资源管理
利: 支持多态:如vector<Graph*> 存派生类指针,实现 draw() 多态调用
操作无额外开销:直接存、取、释放指针,没有额外的内存开销
弊:容易发生内存泄漏:完全依赖Canvas析构函数的for (Graph* g : graphs) delete g; 释放,若代码被修改则可能导致内存泄漏。
有产生野指针风险:指针内存释放后未置空,会产生野指针。
实验任务4
源代码:
toy.hpp
1 #ifndef TOY_HPP 2 #define TOY_HPP 3 4 #include <string> 5 #include <vector> 6 using namespace std; 7 8 class Toy { 9 protected: 10 string name; 11 string type; 12 string color; 13 public: 14 Toy(string n, string t, string c); 15 virtual ~Toy(); 16 void showBasicInfo() const; 17 virtual void showSpecialAbility() const = 0; 18 }; 19 20 class TalkingToy : public Toy { 21 private: 22 string phrase; 23 public: 24 TalkingToy(string n, string t, string c, string p); 25 void showSpecialAbility() const override; 26 }; 27 28 class SingingToy : public Toy { 29 private: 30 string song; 31 public: 32 SingingToy(string n, string t, string c, string s); 33 void showSpecialAbility() const override; 34 }; 35 36 class DancingToy : public Toy { 37 private: 38 string danceType; 39 public: 40 DancingToy(string n, string t, string c, string d); 41 void showSpecialAbility() const override; 42 }; 43 44 class ToyFactory { 45 private: 46 vector<Toy*> toyList; 47 public: 48 void addToy(Toy* toy); 49 void showAllToys() const; 50 ~ToyFactory(); 51 }; 52 53 #endif // TOY_HPP
toy.cpp
1 #include "toy.hpp" 2 #include <iostream> 3 using namespace std; 4 5 Toy::Toy(string n, string t, string c) : name(n), type(t), color(c) {} 6 7 Toy::~Toy() {} 8 9 void Toy::showBasicInfo() const { 10 cout << "名称:" << name << ",类型:" << type << ",颜色:" << color; 11 } 12 13 TalkingToy::TalkingToy(string n, string t, string c, string p) 14 : Toy(n, t, c), phrase(p) { 15 } 16 17 void TalkingToy::showSpecialAbility() const { 18 cout << ",特异功能:会说:“" << phrase << "”" << endl; 19 } 20 21 SingingToy::SingingToy(string n, string t, string c, string s) 22 : Toy(n, t, c), song(s) { 23 } 24 25 void SingingToy::showSpecialAbility() const { 26 cout << ",特异功能:会唱:《" << song << "》" << endl; 27 } 28 29 DancingToy::DancingToy(string n, string t, string c, string d) 30 : Toy(n, t, c), danceType(d) { 31 } 32 33 void DancingToy::showSpecialAbility() const { 34 cout << ",特异功能:会跳" << danceType << "舞" << endl; 35 } 36 37 void ToyFactory::addToy(Toy* toy) { 38 toyList.push_back(toy); 39 } 40 41 void ToyFactory::showAllToys() const { 42 cout << "===== 玩具工厂 所有玩具信息 =====" << endl; 43 for (size_t i = 0; i < toyList.size(); ++i) { 44 cout << "【玩具" << i + 1 << "】"; 45 toyList[i]->showBasicInfo(); 46 toyList[i]->showSpecialAbility(); 47 cout << "------------------------" << endl; 48 } 49 } 50 51 ToyFactory::~ToyFactory() { 52 for (Toy* toy : toyList) { 53 delete toy; 54 } 55 toyList.clear(); 56 }
demo4.cpp
1 #include "toy.hpp" 2 #include <iostream> 3 using namespace std; 4 5 int main() { 6 ToyFactory myFactory; 7 8 myFactory.addToy(new TalkingToy("Labubu", "毛绒公仔", "深棕色", "你好你好你好")); 9 myFactory.addToy(new SingingToy("千早爱音", "互动毛绒玩偶", "纯白色", "嘻嘻嘻嘻嘻")); 10 myFactory.addToy(new DancingToy("叮咚鸡", "动感毛绒玩偶", "黑白色", "叮咚鸡完大狗叫")); 11 myFactory.addToy(new TalkingToy("小猪佩奇", "萌系毛绒公仔", "粉红色", "你们好呀我是小猪佩奇")); 12 13 myFactory.showAllToys(); 14 15 return 0; 16 }
运行结果:

问题场景描述:
模拟毛绒玩具工厂管理场景:工厂生产TalkingToy、SingingToy、DancingToy等毛绒玩具,需通过 ToyFactory 的 toyList 统一存储所有玩具,展示其 name、type、color等基础信息,并调用各玩具的特异功能(phrase 、song 、danceType ),同时支持新增玩具类型。
类间关系及设计理由:
继承关系:TalkingToy/SingingToy/DancingToy继承Toy,复用name/type/color 及 showBasicInfo,通过纯虚函数 showSpecialAbility 统一接口,同时运用了多态支持功能扩展。
组合关系:ToyFactory 组合 vector<Toy*>,使得工厂可以集中管理玩具,兼容玩具的不同类型,统一实现添加、展示、内存释放等功能。
实验总结
本次实验我通过组合实现成绩计算器、继承复用容器功能、多态实现图形绘制及综合设计玩具工厂四大任务,深入理解了组合 “has-a(包含一个容器)” 的封装优势与继承 “is-a(是一个容器)” 的复用特性,明晰了多态依赖虚函数和基类指针实现统一接口的核心逻辑,不仅掌握了面向对象编程的关键语法,更学会了根据场景选型设计可扩展、易维护的类结构,真正的提升了从问题分析到代码实现的实践能力。
浙公网安备 33010602011771号