实验4
一、实验任务1
源代码GradeCalc.hpp
1 #pragma once 2 #include<vector> 3 #include<array> 4 #include<string> 5 class GradeCalc{ 6 public: 7 GradeCalc(const std::string &cname); 8 void input(int n); 9 void output() const; 10 void sort(bool ascending=false); 11 int min() const; 12 int max() const; 13 double average() const; 14 void info(); 15 private: 16 void compute(); 17 private: 18 std::string course_name; 19 std::vector<int> grades; 20 std::array<int,5> counts; 21 std::array<double,5> rates; 22 bool is_dirty; 23 };
源代码GradeCalc.cpp
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 GradeCalc::GradeCalc(const std::string &cname):course_name{cname},is_dirty{true}{ 11 counts.fill(0); 12 rates.fill(0); 13 } 14 void GradeCalc::input(int n){ 15 if(n<0){ 16 std::cerr<<"无效输入!人数不能为负数\n"; 17 std::exit(1); 18 } 19 grades.reserve(n); 20 int grade; 21 for(int i=0;i<n;){ 22 std::cin>>grade; 23 if(grade<0||grade>100){ 24 std::cerr<<"无效输入!分数须在[0,100]\n"; 25 continue; 26 } 27 grades.push_back(grade); 28 ++i; 29 } 30 is_dirty=true; 31 } 32 void GradeCalc::output() const{ 33 for(auto grade:grades) 34 std::cout<<grade<<' '; 35 std::cout<<std::endl; 36 } 37 void GradeCalc::sort(bool ascending){ 38 if(ascending) 39 std::sort(grades.begin(),grades.end()); 40 else 41 std::sort(grades.begin(),grades.end(),std::greater<int>()); 42 } 43 int GradeCalc::min() const{ 44 if(grades.empty()) 45 return -1; 46 auto it=std::min_element(grades.begin(),grades.end()); 47 return *it; 48 } 49 int GradeCalc::max() const{ 50 if(grades.empty()) 51 return -1; 52 auto it=std::max_element(grades.begin(),grades.end()); 53 return *it; 54 } 55 double GradeCalc::average() const{ 56 if(grades.empty()) 57 return 0.0; 58 double avg=std::accumulate(grades.begin(),grades.end(),0.0)/grades.size(); 59 return avg; 60 } 61 void GradeCalc::info(){ 62 if(is_dirty) 63 compute(); 64 std::cout<<"课程名称:\t"<<course_name<<std::endl; 65 std::cout<<"平均分:\t"<<std::fixed<<std::setprecision(2)<<average()<<std::endl; 66 std::cout<<"最高分:\t"<<max()<<std::endl; 67 std::cout<<"最低分:\t"<<min()<<std::endl; 68 const std::array<std::string,5> grade_range{"[0,60)","[60,70)","[70,80)","[80,90)","[90,100]"}; 69 for(int i=grade_range.size()-1;i>=0;i--) 70 std::cout<<grade_range[i]<<"\t: "<<counts[i]<<"人\t" 71 <<std::fixed<<std::setprecision(2)<<rates[i]*100<<"%\n"; 72 } 73 void GradeCalc::compute(){ 74 if(grades.empty()) 75 return; 76 counts.fill(0); 77 rates.fill(0.0); 78 for(auto grade:grades){ 79 if(grade<60) 80 ++counts[0]; 81 else if(grade<70) 82 ++counts[1]; 83 else if(grade<80) 84 ++counts[2]; 85 else if(grade<90) 86 ++counts[3]; 87 else 88 ++counts[4]; 89 } 90 for(int i=0;i<rates.size();i++) 91 rates[i]=counts[i]*1.0/grades.size(); 92 is_dirty=false; 93 }
源代码task1.cpp
1 #include<iostream> 2 #include<string> 3 #include"GradeCalc.hpp" 4 void test(){ 5 GradeCalc c1("OOP"); 6 std::cout<<"录入成绩:\n"; 7 c1.input(5); 8 std::cout<<"输出成绩:\n"; 9 c1.output(); 10 std::cout<<"排序后成绩:\n"; 11 c1.sort();c1.output(); 12 std::cout<<"*************成绩统计信息*************\n"; 13 c1.info(); 14 } 15 int main(){ 16 test(); 17 }
运行成果展示:

问题1:组合关系识别
GradeCalc类声明中,逐行写出所有体现"组合"关系的成员声明,并用一句话说明每个被组合对象的功能。
答:1.std::string course_name;记录课程的名称
2.std::vector<int> grades;记录下每个人的成绩
3.std::array<int,5> counts;记录5个分数段各自的人数
4.std::array<double,5> rates;记录5个分数段各自的人数占比
问题2:接口暴露理解
如在test模块中这样使用,是否合法?如不合法,解释原因。

答:不合法。因为push_back是标准库函数,它可以在类内被vector成员直接调用。但在类的声明中std::vector<int> grades是私有类型的,所以不能在类外部的test模块中直接访问。
问题3:架构设计分析
当前设计方案中,compute在info模块中调用:
(1)连续打印3次统计信息,compute会被调用几次?标记is_dirty起到什么作用?
答:compute会被调用1次。标记is_dirty可以用来判断整体的打印信息有没有被修改过,如果每次打印的信息相同,就不用重复调用compute进行统计,提高效率。
(2)如新增update_grade(index,new_grade),这种设计需要更改compute调用位置吗?简洁说明理由。
答:需要更改。因为新增的接口会改变打印的信息(这时is_dirty标记也会变成true),所以需要调用compute重新统计各项数据。
问题4:功能扩展设计
要增加"中位数"统计,不新增数据成员怎么做?在哪个函数里加?写出伪代码。
答: 可以直接在info函数中加,与平均分之类的一起统计。因为中位数是一组数据中大小在最中间的(奇数情况下是最中间,偶数情况下是取中间两个数的平均值),正常情况需要先对一组成绩数据排序,在根据奇数还是偶数来取中间值,而在test函数中,先对成绩排序后才调用info统计信息,所以可以省去排序的步骤。


问题5:数据状态管理
GradeCalc和compute中都包含代码:counts.fill(0);rates.fill(0);。compute中能否去掉这两行?如去掉,在哪种使用场景下会引发统计错误?
答: 不能去掉。因为当is_false标记为true时会再次调用compute进行数据统计,如果不把counts和rates都归零,再次统计时会在原基础上继续加,导致错误。
问题6:内存管理理解
input 模块中代码 grades.reserve(n); 如果去掉:
(1)对程序功能有影响吗?(去掉重新编译、运行,观察功能是否受影响)
答:没有影响。
(2)对性能有影响吗?如有影响,用一句话陈述具体影响。
答:有影响。reserve函数用来预留容器vector中的元素数量(通常在需要插入大量元素时使用),使用reserve可以避免插入一次新数据就要重新分配内存和复制元素,所以去掉后会降低性能。
二、实验任务2
源代码GradeCalc.hpp
1 #pragma once 2 #include<vector> 3 #include<array> 4 #include<string> 5 class GradeCalc:private std::vector<int>{ 6 public: 7 GradeCalc(const std::string &cname); 8 void input(int n); 9 void output() const; 10 void sort(bool ascending=false); 11 int min() const; 12 int max() const; 13 double average() const; 14 void info(); 15 private: 16 void compute(); 17 private: 18 std::string course_name; 19 std::array<int,5> counts; 20 std::array<double,5> rates; 21 bool is_dirty; 22 };
源代码GradeCalc.cpp
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 GradeCalc::GradeCalc(const std::string &cname):course_name{cname},is_dirty{true}{ 11 counts.fill(0); 12 rates.fill(0); 13 } 14 void GradeCalc::input(int n){ 15 if(n<0){ 16 std::cerr<<"无效输入!人数不能为负数\n"; 17 return; 18 } 19 this->reserve(n); 20 int grade; 21 for(int i=0;i<n;){ 22 std::cin>>grade; 23 if(grade<0||grade>100){ 24 std::cerr<<"无效输入!分数须在[0,100]\n"; 25 continue; 26 } 27 this->push_back(grade); 28 ++i; 29 } 30 is_dirty=true; 31 } 32 void GradeCalc::output() const{ 33 for(auto grade:*this) 34 std::cout<<grade<<' '; 35 std::cout<<std::endl; 36 } 37 void GradeCalc::sort(bool ascending){ 38 if(ascending) 39 std::sort(this->begin(),this->end()); 40 else 41 std::sort(this->begin(),this->end(),std::greater<int>()); 42 } 43 int GradeCalc::min() const{ 44 if(this->empty()) 45 return -1; 46 return *std::min_element(this->begin(),this->end()); 47 } 48 int GradeCalc::max() const{ 49 if(this->empty()) 50 return -1; 51 return *std::max_element(this->begin(),this->end()); 52 } 53 double GradeCalc::average() const{ 54 if(this->empty()) 55 return 0.0; 56 double avg=std::accumulate(this->begin(),this->end(),0.0)/this->size(); 57 return avg; 58 } 59 void GradeCalc::info(){ 60 if(is_dirty) 61 compute(); 62 std::cout<<"课程名称:\t"<<course_name<<std::endl; 63 std::cout<<"平均分:\t"<<std::fixed<<std::setprecision(2)<<average()<<std::endl; 64 std::cout<<"最高分:\t"<<max()<<std::endl; 65 std::cout<<"最低分:\t"<<min()<<std::endl; 66 const std::array<std::string,5> grade_range{"[0,60)", 67 "[60,70)","[70,80)","[80,90)","[90,100]"}; 68 for(int i=grade_range.size()-1;i>=0;i--) 69 std::cout<<grade_range[i]<<"\t: "<<counts[i]<<"人\t" 70 <<std::fixed<<std::setprecision(2)<<rates[i]*100<<"%\n"; 71 } 72 void GradeCalc::compute(){ 73 if(this->empty()) 74 return; 75 counts.fill(0); 76 rates.fill(0.0); 77 for(int grade:*this){ 78 if(grade<60) 79 ++counts[0]; 80 else if(grade<70) 81 ++counts[1]; 82 else if(grade<80) 83 ++counts[2]; 84 else if(grade<90) 85 ++counts[3]; 86 else 87 ++counts[4]; 88 } 89 for(int i=0;i<rates.size();i++) 90 rates[i]=counts[i]*1.0/this->size(); 91 is_dirty=false; 92 }
源代码task2.cpp
1 #include<iostream> 2 #include<string> 3 #include"GradeCalc.hpp" 4 void test(){ 5 GradeCalc c1("OOP"); 6 std::cout<<"录入成绩:\n"; 7 c1.input(5); 8 std::cout<<"输出成绩:\n"; 9 c1.output(); 10 std::cout<<"排序后成绩:\n"; 11 c1.sort();c1.output(); 12 std::cout<<"*************成绩统计信息*************\n"; 13 c1.info(); 14 } 15 int main(){ 16 test(); 17 }
运行成果展示:

问题1:继承关系识别
写出GradeCalc类声明体现"继承"关系的完整代码行。
答:class GradeCalc:private std::vector<int>
问题2:接口暴露理解
当前继承方式下,基类vector的接口会自动成为GradeCalc的接口吗?
如在test模块中这样用,能否编译通过?用一句话解释原因。

答:基类vector的接口会自动成为GradeCalc的接口,但在test模块中编译不能通过。因为是私有继承,所以基类的公有和保护成员在派生类中变为私有,只能在派生类内部访问。
问题3:数据访问差异
对比继承方式与组合方式内部实现数据访问的一行典型代码。说明两种方式下的封装差异带来的数据访问接口差异。

答:组合方式是在内部通过组合类中的vector成员对象grades的接口进行访问,继承方式是在内部用this指针直接通过使用基类的接口来进行访问。在组合中,封装性更强一些,组合类只能通过特定接口访问,看不到内部细节;在继承中,当公有继承时,内部、外部可访问基类接口,当保护和私有继承时,只能在内部对基类接口进行访问,继承可以访问基类内部成员。
问题4:组合 vs. 继承方案选择
你认为组合方案和继承方案,哪个更适合成绩计算这个问题场景?简洁陈述你的结论和理由。
答:我觉得组合方案更适合。1.本身vector容器像一个成绩条,而GradeCalc成绩计算器是具有一个成绩条,然后对这些成绩进行处理,符合has-a的关系。2.在组合方式中,虽然不能直接使用this指针进行调用,但我觉得使用grades.begin()反而看起来更加直观,表示是成绩。3.组合的封装性更好,看不到内部的成绩细节,但有接口可以使用,当std::vector<int> grades设为私有时,外部无法直接访问成绩,更安全。
三、实验任务3
源代码Graph.hpp
1 #pragma once 2 #include<string> 3 #include<vector> 4 enum class GraphType{circle,triangle,rectangle}; 5 class Graph{ 6 public: 7 virtual void draw(){} 8 virtual ~Graph()=default; 9 }; 10 class Circle:public Graph{ 11 public: 12 void draw(); 13 }; 14 class Triangle:public Graph{ 15 public: 16 void draw(); 17 }; 18 class Rectangle:public Graph{ 19 public: 20 void draw(); 21 }; 22 class Canvas{ 23 public: 24 void add(const std::string& type); 25 void paint() const; 26 ~Canvas(); 27 private: 28 std::vector<Graph*> graphs; 29 }; 30 GraphType str_to_GraphType(const std::string& s); 31 Graph* make_graph(const std::string& type);
源代码Graph.cpp
1 #include<algorithm> 2 #include<cctype> 3 #include<iostream> 4 #include<string> 5 #include"Graph.hpp" 6 void Circle::draw(){ 7 std::cout<<"draw a circle...\n"; 8 } 9 void Triangle::draw(){ 10 std::cout<<"draw a triangle...\n"; 11 } 12 void Rectangle::draw(){ 13 std::cout<<"draw a rectangle...\n"; 14 } 15 void Canvas::add(const std::string& type){ 16 Graph* g=make_graph(type); 17 if(g) 18 graphs.push_back(g); 19 } 20 void Canvas::paint() const{ 21 for(Graph* g:graphs) 22 g->draw(); 23 } 24 Canvas::~Canvas(){ 25 for(Graph* g:graphs) 26 delete g; 27 } 28 GraphType str_to_GraphType(const std::string& s){ 29 std::string t=s; 30 std::transform(s.begin(),s.end(),t.begin(), 31 [](unsigned char c){return std::tolower(c);}); 32 if(t=="circle") 33 return GraphType::circle; 34 if(t=="triangle") 35 return GraphType::triangle; 36 if(t=="rectangle") 37 return GraphType::rectangle; 38 return GraphType::circle; 39 } 40 Graph* make_graph(const std::string& type){ 41 switch(str_to_GraphType(type)){ 42 case GraphType::circle: return new Circle; 43 case GraphType::triangle: return new Triangle; 44 case GraphType::rectangle: return new Rectangle; 45 default: return nullptr; 46 } 47 }
源代码task3.cpp
1 #include<string> 2 #include"Graph.hpp" 3 void test(){ 4 Canvas canvas; 5 canvas.add("circle"); 6 canvas.add("triangle"); 7 canvas.add("rectangle"); 8 canvas.paint(); 9 } 10 int main(){ 11 test(); 12 }
运行成果展示:

问题1:对象关系识别
(1)写出Graph.hpp中体现"组合"关系的成员声明代码行,并用一句话说明被组合对象的功能。
答:std::vector<Graph*> graphs; 被组合对象是Graph*类型,代表着指向不同图形类型的指针,Canvas类中用来表示在画布上画不同的图形。
(2)写出Graph.hpp中体现"继承"关系的类声明代码行。
答:class Circle:public Graph/class Triangle:public Graph/class Rectangle:public Graph
问题2:多态机制观察
(1) Graph中的draw若未声明成虚函数,Canvas::paint()中g->draw()运行结果会有何不同?
答:虚函数是在积累中声明为virtual并在派生类中可以被重新定义的成员函数,也是实现多态的关键。如果draw没有声明成虚函数,基类中的同名函数draw会被隐藏,Canvas中就会无法使用g->draw(),导致编译能成功但没有输出结果。

(2)若Canvas类std::vector<Graph*>改成std::vector<Graph>,会出现什么问题?
答:这样改之后,vector容器中的数据从指针类型到基类对象,Canvas类中没有办法存储各个派生类对应的图形指针,与后续在成员函数中定义的Graph* g也不匹配,出现报错问题。同时只能调用基类中的函数,无法进行绘制操作。

(3)若~Graph()未声明成虚函数,会带来什么问题?
答:把基类的析构函数声明为虚函数可以在基类指针指向派生类对象时,用基类指针删除派生类对象,防止在析构时只析构基类而不析构派生类。所以当~Graph()未声明成虚函数时,由于本题中是用基类指针操作派生类对象,所以没有析构派生类,出现内存泄漏。
问题3:扩展性思考
若要新增星形 Star ,需在哪些文件做哪些改动?逐一列出。
答:1.在Graph.hpp中,增加Star类的声明,注意要公有继承Graph类,同时枚举类型也要加上star。
2.在Graph.hpp中,增加Star类内函数draw的定义,输出draw a star...;在str_to_GraphType函数定义中加上对star的枚举转换;在make_graph函数中增加可以返回star图形指针。
3.在task4.cpp中新增canvas.add("star"),就可以实现星形的绘制。
问题4:资源管理
观察make_graph函数和Canvas析构函数:
(1)make_graph返回的对象在什么地方被释放?
答:make_graph返回的对象在Canvas调用析构函数时被释放。
(2)使用原始指针管理内存有何利弊?
答:利:原始指针写起来比智能指针更加简单,且没有智能指针的额外开销,节省内存;而且原始指针可以用于更底层的内存管理(直接操控内存硬件资源),比较灵活。
弊:原始指针需要自己写new/delete,手动管理内存,容易出现因未及时delete而内存泄漏的问题。
四、实验任务4
源代码Toy.hpp
1 #pragma once 2 #include<string> 3 #include<vector> 4 enum class ToyName{Judy,Nick,Gary,Fuzzby};//疯狂动物城2中的朱迪、尼克、盖瑞、狸宝 5 class Toy{ 6 public: 7 Toy(const std::string &name_,const std::string &type_,const std::string &size_):name{name_},type{type_},size{size_}{} 8 virtual ~Toy()=default; 9 std::string getname() const; 10 std::string gettype() const; 11 std::string getsize() const; 12 virtual void greet() const{} 13 virtual void action() const{} 14 private: 15 std::string name; 16 std::string type; 17 std::string size; 18 }; 19 class Judy:public Toy{ 20 public: 21 Judy(); 22 void greet() const; 23 void action() const; 24 }; 25 class Nick:public Toy{ 26 public: 27 Nick(); 28 void greet() const; 29 void action() const; 30 }; 31 class Gary:public Toy{ 32 public: 33 Gary(); 34 void greet() const; 35 void action() const; 36 }; 37 class Fuzzby:public Toy{ 38 public: 39 Fuzzby(); 40 void greet() const; 41 void action() const; 42 }; 43 class ToyFactory{ 44 public: 45 void add(const std::string &name); 46 void display() const; 47 ~ToyFactory(); 48 private: 49 std::vector<Toy*> toys; 50 }; 51 ToyName str_to_ToyName(const std::string &s); 52 Toy* make_toy(const std::string &name);
源代码Toy.cpp
1 #include<string> 2 #include<vector> 3 #include<iostream> 4 #include<algorithm> 5 #include<cctype> 6 #include"Toy.hpp" 7 std::string Toy::getname() const{ 8 return name; 9 } 10 std::string Toy::gettype() const{ 11 return type; 12 } 13 std::string Toy::getsize() const{ 14 return size; 15 } 16 Judy::Judy():Toy("Judy(朱迪)","毛绒兔子","大号"){} 17 void Judy::greet() const{ 18 std::cout<<"你好,我是朱迪警官!欢迎来到动物城\n"; 19 } 20 void Judy::action() const{ 21 std::cout<<"跳跃,并做出举手动作\n"; 22 } 23 Nick::Nick():Toy("Nick(尼克)","毛绒狐狸","大号"){} 24 void Nick::greet() const{ 25 std::cout<<"你好,我是尼克狐尼克!这叫智取,宝贝\n"; 26 } 27 void Nick::action() const{ 28 std::cout<<"歪头,并做出眨眼动作\n"; 29 } 30 Gary::Gary():Toy("Gary(盖瑞)","橡胶小蛇","中号"){} 31 void Gary::greet() const{ 32 std::cout<<"你好,我是盖瑞!嗯,盖瑞一条蛇\n"; 33 } 34 void Gary::action() const{ 35 std::cout<<"呈S型在平面上移动\n"; 36 } 37 Fuzzby::Fuzzby():Toy("Fuzzby(狸宝)","毛绒河狸","中号"){} 38 void Fuzzby::greet() const{ 39 std::cout<<"你好,我是弗兹比!我一定知无不言\n"; 40 } 41 void Fuzzby::action() const{ 42 std::cout<<"做出啃咬手中木棍的动作\n"; 43 } 44 void ToyFactory::add(const std::string &name){ 45 Toy* t=make_toy(name); 46 if(t) 47 toys.push_back(t); 48 } 49 void ToyFactory::display() const{ 50 if(toys.empty()){ 51 std::cout<<"非常抱歉,本玩具工厂暂时缺货中......"; 52 return; 53 } 54 std::cout<<"************欢迎来到疯狂动物城玩具工厂************\n\n"; 55 for(Toy* t:toys){ 56 std::cout<<"|名称|:"<<t->getname() 57 <<" |类型|:"<<t->gettype() 58 <<" |型号|:"<<t->getsize()<<std::endl; 59 std::cout<<"|语音打招呼|:" ; 60 t->greet(); 61 std::cout<<"|角色特色动作|:"; 62 t->action(); 63 std::cout<<"==============喜欢我就快带我回家吧=============\n\n"; 64 } 65 } 66 ToyFactory::~ToyFactory(){ 67 for(Toy* t:toys) 68 delete t; 69 } 70 ToyName str_to_ToyName(const std::string &s){ 71 std::string t=s; 72 std::transform(s.begin(),s.end(),t.begin(), 73 [](unsigned char c){return std::tolower(c);}); 74 if(t=="judy") 75 return ToyName::Judy; 76 if(t=="nick") 77 return ToyName::Nick; 78 if(t=="gary") 79 return ToyName::Gary; 80 if(t=="fuzzby") 81 return ToyName::Fuzzby; 82 return ToyName::Judy; 83 } 84 Toy* make_toy(const std::string &name){ 85 switch(str_to_ToyName(name)){ 86 case ToyName::Judy: return new Judy; 87 case ToyName::Nick: return new Nick; 88 case ToyName::Gary: return new Gary; 89 case ToyName::Fuzzby: return new Fuzzby; 90 default:return nullptr; 91 } 92 }
源代码task4.cpp
1 #include<string> 2 #include"Toy.hpp" 3 void test(){ 4 ToyFactory toyfactory; 5 toyfactory.add("Judy"); 6 toyfactory.add("Nick"); 7 toyfactory.add("Gary"); 8 toyfactory.add("Fuzzby"); 9 toyfactory.display(); 10 } 11 int main(){ 12 test(); 13 }
运行成果展示:

应用场景描述:我设计的玩具工厂灵感来自最近上映的疯狂动物城2,共有4种角色主题玩偶,在工厂中会展示出每个玩偶的名称、类型、型号、打招呼语音和特色动作,可以帮助消费者进行挑选。
类的设计:1.因为每个角色类都象征着一个玩具,符合“is-a”的关系,所以我用继承的方法,以Toy类为基类,Judy、Nick、Gary、Fuzzby为派生类,继承基类玩具类的基本接口,可以减少代码的书写量,看起来更简洁。
2.玩具工厂里会具有很多玩具,符合“has-a”的关系,所以在ToyFactory类中我选择组合方法std::vector<Toy*> toys;,表示玩具工厂包含有不同的玩具。
实验总结:
一、本次实验中主要了解到继承和组合的不同用法,我觉得很多时候好像可以通用,但需要结合题目所给的具体要求选择一个更好的,"is-a"关系用继承,"has-a"关系用继承。
二、在实验四需要自己完成全部的代码编写时,出现了一些细节错误:
1、t->greet()函数返回类型是void,不能直接用std::cout输出void类型的函数值;还有std::endl本身是一个函数,不能单独一行使用。
2、函数声明和定义分开写时要小心不要重定义,在Judy类的声明中我一开始写成void greet() const{},导致打招呼函数被重定义了。
3.基类的构造函数不能是虚函数,因为在创建对象时已知要创建什么类型的;而析构函数和一些功能函数要定义成虚函数,这样才能完成多态。


浙公网安备 33010602011771号