实验五
实验5 多态
说明
一、实验目的
- 知道什么是类模板,会正确定义和实例化
- 理解运算符重载机制,会编写运算符函数,理解编译器如何将表达式转换为对运算符函数的调用
- 知道什么是抽象类,会正确定义和使用
- 基于问题场景,能合理使用继承、虚函数、抽象类实现接口继承与运行时多态
二、实验准备
系统浏览/复习以下教材章节:
- 继承:解决的问题场景、定义和用法(第7章)
- 多态:概念、分类、典型应用场景和用法(第8章)
- 类模板(9.1.2节)
三、实验内容
1. 实验任务1
说明
验证性实验。综合应用组合、继承、多态实现出版物的分类管理。运行、理解代码,回答问题。
-
问题场景描述
模拟出版物的分类管理。抽象设计后,继承关系如下:

-
代码组织
publisher.hpp类Publisher及其派生类Book, Film, Music声明publisher.cpp类Publisher及其派生类Book, Film,Music实现task1.cpp测试模块+main
代码
publisher.hpp
pragma once
include<string>
//发行/出版物类:Publisher(抽象类)
class Publisher {
public:
Publisher(const std::string& name_ = ""); //构造函数
virtual ~Publisher() = default;
public:
virtual void publish() const = 0; //纯虚函数,作为接口继承
virtual void use() const = 0; //纯虚函数,作为接口继承
protected:
std::string name; //发行/出版物名称
};
//图书类:Book
class Book: public Publisher {
public:
Book(const std::string& name_ = "", const std::string& author_ = ""); //构造函数
public:
void publish() const override; //接口
void use() const override; //接口
private:
std::string author; //作者
};
//电影类:Film
class Film: public Publisher {
public:
Film(const std::string& name_ = "", const std::string& director_ = ""); //构造函数
public:
void publish() const override; //接口
void use() const override; //接口
private:
std::string director; //导演
};
//音乐类:Music
class Music: public Publisher {
public:
Music(const std::string& name_ = "", const std::string& artist_ = "");
public:
void publish() const override; //接口
void use() const override; //接口
private:
std::string artist; //音乐艺术家名称
};
publisher.cpp
include<iostream>
include<string>
include "publisher.hpp"
// Publisher类:实现
Publisher::Publisher(const std::string& name_): name{name_} {
}
// Book类: 实现
Book::Book(const std::string &name_ , const std::string &author_ ): Publisher{name_}, author{author_} {
}
void Book::publish() const {
std::cout << "Publishing book《" << name << "》 by " << author << '\n';
}
void Book::use() const {
std::cout << "Reading book 《" << name << "》 by " << author << '\n';
}
// Film类:实现
Film::Film(const std::string &name_, const std::string &director_):Publisher{name_},director{director_} {
}
void Film::publish() const {
std::cout << "Publishing film <" << name << "> directed by " << director << '\n';
}
void Film::use() const {
std::cout << "Watching film <" << name << "> directed by " << director << '\n';
}
// Music类:实现
Music::Music(const std::string &name_, const std::string &artist_): Publisher{name_}, artist{artist_} {
}
void Music::publish() const {
std::cout << "Publishing music <" << name << "> by " << artist << '\n';
}
void Music::use() const {
std::cout << "Listening to music <" << name << "> by " << artist << '\n';
}
task1.cpp
include <vector>
include <memory>
include "publisher.hpp"
void test1() {
std::vector<Publisher*> v;
v.push_back(new Book("Harry Potter", "J.K. Rowling"));
v.push_back(new Film("The Godfather", "Francis Ford Coppola"));
v.push_back(new Music("Blowing in the wind", "Bob Dylan"));
for(Publisher* ptr: v) {
ptr->publish();
ptr->use();
std::cout << '\n';
delete ptr;
}
}
void test2() {
std::vector<std::unique_ptr<Publisher>> v;
//devc++不支持
// v.push_back(std::make_unique<Book>("Harry Potter", "J.K. Rowling"));
// v.push_back(std::make_unique<Film>("The Godfather", "Francis Ford Coppola"));
// v.push_back(std::make_unique<Music>("Blowing in the wind", "Bob Dylan"));
//旧写法
v.push_back(std::unique_ptr<Book>(new Book("Harry Potter", "J.K. Rowling")));
v.push_back(std::unique_ptr<Film>(new Film("The Godfather", "Francis Ford Coppola")));
v.push_back(std::unique_ptr<Music>(new Music("Blowing in the wind", "Bob Dylan")));
for(const auto& ptr: v) {
ptr->publish();
ptr->use();
std::cout << '\n';
}
}
void test3() {
Book book("A Philosophy of Software Design", "John ousterhout");
book.publish();
book.use();
}
int main() {
std::cout << "运行时多态:纯虚函数、抽象类\n";
std::cout << "\n测试1:使用原始指针\n";
test1();
std::cout << "\n测试2:使用智能指针\n";
test2();
std::cout << "\n测试3:直接使用类\n";
test3();
}
运行结果

回答问题
问题1:抽象类机制
(1) 是什么决定了 Publisher是抽象类?用一句话说明,并指出代码中的具体依据。
(2) 如果在 main.cpp里直接写 Publisher p; 能否编译通过?为什么?
答:(1)Publisher里的两个纯虚函数决定了这是个抽象类。virtual void publish() const = 0;virtual void use() const = 0;(2)不能,抽象类无法实例化,因为纯虚函数没有具体实现。
问题2:纯虚函数与接口继承
(1) Book、 Film、 Music必须实现哪两个函数才能通过编译?请写出其完整函数声明
(2) 在 publisher.cpp的 Film类实现中,把两个成员函数实现里的const去掉(保持函数体不变),重新编译,报错信息是什么?
答:(1)publish()和use(),完整函数声明代码在上面代码中。(2)错误如下:
[Error] prototype for 'void Film::publish()' does not match any in class 'Film'
[Error] candidate is: virtual void Film::publish() const
[Error] prototype for 'void Music::publish()' does not match any in class 'Music'
[Error] candidate is: virtual void Music::publish() const
就是找不到对应实现的错误。
问题3:运行时多态与虚析构
(1) 在 test1()里, for(Publisher* ptr: v) 中ptr的声明类型是什么?
(2) 当循环执行到 ptr->publish(); 时,ptr实际指向的对象类型分别有哪些?(按循环顺序写出)
(3) 基类 Publisher的析构函数为何声明为 virtual?若删除 virtual,执行 delete ptr; 会出现什么问题?
答(1)声明类型就是Publisher*。(2)Book*, Film*, Music*(3)需要子类实现析构函数,因为多态调用时,使用基类指针如果不是virtual的话不会调用子类的析构函数,用virtual后可以导向子类析构函数,防止内存泄露。
2. 实验任务2
说明
验证性实验。综合应用运算符重载、组合、标准库实现图书销售统计。
-
问题场景描述
模拟出版行业图书销售统计,按指定关键字做销量统计、排序。 -
代码组织
book.hpp图书描述信息类Book声明book.cpp图书描述信息类Book实现booksale.hpp图书销售记录类BookSale声明booksale.cpp图书销售记录类BookSale实现task2.cpp测试模块+ main
代码
book.hpp
pragma once
include<string>
//图书描述信息类Book:声明
class Book {
public:
Book(const std::string& name_,
const std::string& author_,
const std::string& translator_,
const std::string& isbn_,
double price_);
friend std::ostream& operator<<(std::ostream& out, const Book& book);
private:
std::string name; //书名
std::string author; //作者
std::string translator; //译者
std::string isbn; //isbn号
double price; //定价
};
book.cpp
cpp
include <iomanip>
include <iostream>
include <string>
include "book.hpp"
//图书描述信息类Book:实现
Book::Book(const std::string& name_,
const std::string& author_,
const std::string& translator_,
const std::string& isbn_,
double price_): name{name_}, author{author_}, translator{translator_},
isbn{isbn_}, price{price_} {
}
//运算符<<重载实现
std::ostream& operator<<(std::ostream& out, const Book& book) {
using std::left;
using std::setw;
out << left;
out << setw(15) << "书名:" << book.name << '\n'
<< setw(15) << "作者:" << book.author << '\n'
<< setw(15) << "译者:" << book.translator << '\n'
<< setw(15) << "ISBN:" << book.isbn << '\n'
<< setw(15) << "定价:" << book.price;
return out;
}
booksale.hpp
pragma once
include <string>
include "book.hpp"
//图书销售记录类BookSales:声明
class BookSale {
public:
BookSale(const Book& rb_, double sales_price_, int sales_amount_);
int get_amount() const; //返回销售数量
double get_revenue() const; //返回营收
friend std::ostream& operator<<(std::ostream& out, const BookSale& item);
private:
Book rb;
double sales_price; //售价
int sales_amount; //销售数量
};
booksale.cpp
include <iomanip>
include <iostream>
include <string>
include "booksale.hpp"
//图书销售记录类BookSales:实现
BookSale::BookSale(const Book& rb_,
double sales_price_,
int sales_amount_): rb{rb_}, sales_price{sales_price_},
sales_amount{sales_amount_} {
}
int BookSale::get_amount() const {
return sales_amount;
}
double BookSale::get_revenue() const {
return sales_amount * sales_price;
}
//运算符<<重载实现
std::ostream& operator<<(std::ostream& out, const BookSale& item) {
using std::left;
using std::setw;
out << left;
out << item.rb << '\n'
<< setw(15) << "售价:" << item.sales_price << '\n'
<< setw(15) << "销售数量:" << item.sales_amount << '\n'
<< setw(15) << "营收:" << item.get_revenue();
return out;
}
task2.cpp
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>
#include "booksale.hpp"
// 按图书销售数量比较
bool compare_by_amount(const BookSale &x1, const BookSale &x2) {
return x1.get_amount() > x2.get_amount();
}
void test() {
using std::cin;
using std::cout;
using std::getline;
using std::sort;
using std::string;
using std::vector;
using std::ws;
vector<BookSale> sales_records; // 图书销售记录表
int books_number;
cout << "录入图书数量: ";
cin >> books_number;
cout << "录入图书销售记录\n";
for(int i = 0; i < books_number; ++i) {
string name, author, translator, isbn;
double price;
cout << string(20, '-') << "第" << i+1 << "本图书信息录入" << string(20, '-') << '\n';
cout << "录入书名: "; getline(cin>>ws, name);
cout << "录入作者: "; getline(cin>>ws, author);
cout << "录入译者: "; getline(cin>>ws, translator);
cout << "录入isbn: "; getline(cin>>ws, isbn);
cout << "录入定价: "; cin >> price;
Book book(name, author, translator, isbn, price);
double sales_price;
int sales_amount;
cout << "录入售价: "; cin >> sales_price;
cout << "录入销售数量: "; cin >> sales_amount;
BookSale record(book, sales_price, sales_amount);
sales_records.push_back(record);
}
// 按销售册数排序
sort(sales_records.begin(), sales_records.end(), compare_by_amount);
// 按销售册数降序输出图书销售信息
cout << string(20, '=') << "图书销售统计" << string(20, '=') << '\n';
for(auto &record: sales_records) {
cout << record << '\n';
cout << string(40, '-') << '\n';
}
}
int main() {
test();
}
运行结果

回答问题
问题1:重载运算符<<
(1) 找出运算符<<被重载了几处?分别用于什么类型?
(2) 找出使用重载<<输出对象的代码,写在下面。
答:(1)两处,重载了Book和BookSale的输出流。
(2)
std::ostream& operator<<(std::ostream& out, const Book& book) {
using std::left;
using std::setw;
out << left;
out << setw(15) << "书名:" << book.name << '\n'
<< setw(15) << "作者:" << book.author << '\n'
<< setw(15) << "译者:" << book.translator << '\n'
<< setw(15) << "ISBN:" << book.isbn << '\n'
<< setw(15) << "定价:" << book.price;
return out;
}
std::ostream& operator<<(std::ostream& out, const BookSale& item) {
using std::left;
using std::setw;
out << left;
out << item.rb << '\n'
<< setw(15) << "售价:" << item.sales_price << '\n'
<< setw(15) << "销售数量:" << item.sales_amount << '\n'
<< setw(15) << "营收:" << item.get_revenue();
return out;
}
问题2:图书销售统计
(1) 图书销售记录"按销售数量降序排序",代码是如何实现的?
(2) 拓展(选答*):如果使用lambda表达式,如何实现"按销售数量降序排序"?
答:(1)sort(sales_lst.begin(), sales_lst.end(), compare_by_amount);,其中compare_by_amount这个函数指针指向
bool compare_by_amount(const BookSale& x1, const BookSale& x2) {
return x1.get_amount() > x2.get_amount();
}
(2)可以直接用
sort(sales_lst.begin(), sales_lst.end(), [](const BookSale& x1, const BookSale& x2)->bool{
return x1.get_amount() > x2.get_amount();
});
或者lambda省略箭头和返回值类型
[](const BookSale& x1, const BookSale& x2){
return x1.get_amount() > x2.get_amount();
}
3. 实验任务3
说明
验证性实验:类模板定义和使用。阅读、理解代码,结合运行回答问题。
-
问题场景描述
类A和类B除数据成员类型不同,其它都相同。类定义存在相似性把类型参数化,让类的抽象设计更通用。 -
代码组织
task3_1.cpp类A定义+类B定义+测试模块+ maintask3_2.cpp类模板X定义+测试模块+ main
task3_1.cpp
include <iostream>
//类A的定义
class A {
public:
A(int x0, int y0);
void display() const;
private:
int x, y;
};
A::A(int x0, int y0): x{x0}, y{y0} {
}
void A::display() const {
std::cout << x << "," << y << '\n';
}
//类B的定义
class B {
public:
B(double x0, double y0);
void display() const;
private:
double x, y;
};
B::B(double x0, double y0): x{x0}, y{y0} {
}
void B::display() const {
std::cout << x << "," << y << '\n';
}
void test() {
std::cout << "测试类A:" << '\n';
A a(3, 4);
a.display();
std::cout << "\n测试类B:" << '\n';
B b(3.2, 5.6);
b.display();
}
int main() {
test();
}
task3_2.cpp
include <iostream>
include <string>
//定义类模板
template<typename T>
class X {
public:
X(T x0, T y0);
void display();
private:
T x, y;
};
template<typename T>
X<T>::X(T x0, T y0): x{x0}, y{y0} {
}
template<typename T>
void X<T>::display() {
std::cout << x << "," << y << '\n';
}
void test() {
std::cout << "测试1:用int实例化类模板X" << '\n';
X<int> x1(3, 4);
x1.display();
std::cout << "\n测试2:用double实例化类模板X" << '\n';
X<double> x2(3.2, 5.6);
x2.display();
std::cout << "\n测试3:用string实例化类模板X" << '\n';
X<std::string> x3("hello", "oop");
x3.display();
}
int main() {
test();
}
运行结果
task3_1

task3_2

说明:
- 类模板用
template<typename T>把"类型"抽象成参数,一份代码可生成多个具体类。 - 在类外实现成员函数时,必须加模板头并写完整类名
X<T>。 - 使用时要先"实例化":
X<int> x1(3,4);,编译器据此生成真正的类。 - 实例化类型必须支持模板内所有操作(如
<<),否则编译报错。 - 理解类模板后,再看标准库用法
complex<double>、vector<int>——只是现成模板的实例化而已。
4. 实验任务4
说明
设计性实验。综合应用继承、多态,模拟简单机器宠物。
-
问题场景描述
模拟机器宠物,抽象后,继承关系如下:
(此处应有继承关系图) -
设计要求
- 定义抽象类:机器宠物类
MachinePet
每个机器宠物包含如下成员:- 数据:昵称(nickname)
- 接口要求:
- 构造函数:用字符串初始化昵称
get_nickname():供外部获取昵称talk():返回叫声,须支持运行时多态(由派生类各自实现)。
- 定义电子宠物猫类
PetCat- 公有继承自
MachinePet - 无新增数据
- 构造函数:用字符串初始化昵称
- 实现
talk()返回猫叫声
- 公有继承自
- 定义电子宠物狗类
PetDog- 公有继承自
MachinePet - 无新增数据
- 构造函数:用字符串初始化昵称
- 实现
talk()返回狗叫声
- 公有继承自
- 定义抽象类:机器宠物类
-
代码组织
- pet.hpp 机器宠物抽象类
MachinePet、宠物猫类PetCat、宠物狗类PetDog定义 - task4.cpp 测试模块+ main
- pet.hpp 机器宠物抽象类
测试代码task4.cpp已给出,补足pets.hpp。
代码
task4.cpp
#include <iostream>
#include <memory>
#include <vector>
#include "pet.hpp"
void test1() {
std::vector<MachinePet *> pets;
pets.push_back(new PetCat("miku"));
pets.push_back(new PetDog("da huang"));
for(MachinePet *ptr: pets) {
std::cout << ptr->get_nickname() << " says " << ptr->talk() << '\n';
delete ptr; // 须手动释放资源
}
}
void test2() {
std::vector<std::unique_ptr<MachinePet>> pets;
// pets.push_back(std::make_unique<PetCat>("miku"));
// pets.push_back(std::make_unique<PetDog>("da huang"));
pets.push_back(std::unique_ptr<PetCat>(new PetCat("miku")));
pets.push_back(std::unique_ptr<PetDog>(new PetDog("da huang")));
for(auto const &ptr: pets)
std::cout << ptr->get_nickname() << " says " << ptr->talk() << '\n';
}
void test3() {
// MachinePet pet("little cutie"); // 编译报错:无法定义抽象类对象
const PetCat cat("miku");
std::cout << cat.get_nickname() << " says " << cat.talk() << '\n';
const PetDog dog("da huang");
std::cout << dog.get_nickname() << " says " << dog.talk() << '\n';
}
int main() {
std::cout << "测试1: 使用原始指针\n";
test1();
std::cout << "\n测试2: 使用智能指针\n";
test2();
std::cout << "\n测试3: 直接使用类\n";
test3();
}
pet.hpp
#ifndef MACHINE_PET_HEAD
#define MACHINE_PET_HEAD
#include <vector>
#include <string>
#include <iostream>
class MachinePet{
protected:
std::string nickname;
public:
MachinePet(const std::string& _nickname)
:nickname(_nickname){ }
virtual ~MachinePet() = default;
public:
const std::string get_nickname() const{return nickname;}
virtual std::string talk() const = 0;
};
class PetCat final: public MachinePet{
public:
PetCat(const std::string& _n)
:MachinePet(_n){ }
~PetCat() = default;
public:
std::string talk()const override{
return "喵";
}
};
class PetDog final: public MachinePet{
public:
PetDog(const std::string& _n)
:MachinePet(_n){ }
~PetDog() = default;
public:
std::string talk()const override{
return "汪";
}
};
#endif
运行结果

Tips:
编写机器宠物抽象类MachinePet、宠物猫类PetCat、宠物狗类PetDog定义时,如不熟练,参照任务1。
注意点:
- 抽象类定义:若test1出现
delete ptr;相关崩溃或抽象类无法实例化的报错,请复习纯虚函数和虚析构规则。 - 派生类构造函数初始化列表写法:昵称须提供给基类
- 拓展*:电子宠物猫/狗的
talk()实现,如希望叫声效果拟真,可查阅网络,使用支持.midi/.wav等音频播放的工具函数
(最后一点win32库中的PlaySound可以简单实现(但是只能单个播放,就是没法同时播放多个),mciSendString可以同时播放,但是效率稍低。其他方法可以用Directx里的DirectSound或者第三方库SDL_Mixer实现更好的控制。)
5. 实验任务5
说明
设计性实验。综合应用运算符重载、类模板实现编译时多态。
-
问题场景描述
自定义简化版类模板Complex,实现类似C++标准库类模板complex,支持对类型的参数化。具体要求如下:- 支持实例化类模板与各种构造
如complex<double> c1;Complex<double> c2(1.0, 2.0);Comlex<double> c3(c2); - 提供接口
get_real(),get_image()返回实部和虚部 - 对类模板重载运算符,支持如下操作:
c1 += c2,c1 + c2,c1 == c2,cin >> c1 >> c2,cout << c1 << c2
- 支持实例化类模板与各种构造
-
代码组织
- Complex.hpp 类模板Complex定义
- task5.cpp 测试模块+main
测试代码task5.cpp已给出。根据测试代码,补足类模板Complex定义,使代码运行后满足预期截图效果。
代码
task5.cpp
include <iostream>
include "Complex.hpp"
void test1() {
using std::cout;
using std::boolalpha;
Complex<int> c1(2, -5), c2(c1);
cout << "c1 = " << c1 << '\n';
cout << "c2 = " << c2 << '\n';
cout << "c1 + c2 = " << c1 + c2 << '\n';
c1 += c2;
cout << "c1 = " << c1 << '\n';
cout << boolalpha << (c1 == c2) << '\n';
}
void test2() {
using std::cin;
using std::cout;
Complex<double> c1, c2;
cout << "Enter c1 and c2: ";
cin >> c1 >> c2;
cout << "c1 = " << c1 << '\n';
cout << "c2 = " << c2 << '\n';
const Complex<double> c3(c1);
cout << "c3.real = " << c3.get_real() << '\n';
cout << "c3.imag = " << c3.get_imag() << '\n';
}
int main() {
std::cout << "自定义类模板Complex测试1: \n";
test1();
std::cout << "\n自定义类模板Complex测试2: \n";
test2();
}
Complex.hpp
#ifndef COMPLEX_HEAD
#define COMPLEX_HEAD
#include <iostream>
#include <string>
#include <cmath>
template<class T>
class Complex {
private:
T real;
T imag;
public:
Complex(T r = 0.0, T i = 0.0)
:real(r), imag(i) {
}
~Complex() = default;
public:
const T get_real() const { return real; }
const T get_imag() const { return imag; }
public:
Complex operator+(const Complex& o) {
return Complex(this->real + o.real, this->imag + o.imag);
}
bool operator==(const Complex& o) {
return (this->real == o.real) && (this->imag == o.imag);
}
friend std::istream& operator>>(std::istream& input, Complex& o) {
input >> o.real >> o.imag;
return input;
}
friend std::ostream& operator<<(std::ostream& out, const Complex& c) {
std::string r;
std::string i;
if (c.real == 0) {
r = "";
}
else {
r = std::to_string(c.real);
}
if (c.imag == 0) {
i = "";
}
else {
i = (c.imag > 0 ? "+ " : "- ") + std::to_string(abs(c.imag)) + 'i';
}
out << r << ' ' << i << std::endl;
return out;
}
Complex& operator+=(const Complex& o) {
this->real += o.real;
this->imag += o.imag;
return *this;
}
};
#endif
运行结果

四、实验结论
任务1
- 抽象类通过纯虚函数实现,不能直接实例化
- 派生类必须实现基类中的所有纯虚函数
- 通过基类指针/引用可以调用派生类的虚函数,实现运行时多态
- 多态环境下必须使用虚析构函数以确保资源正确释放
任务2
- 运算符重载扩展了类的功能,使其使用更自然
- 通过重载
<<运算符,可以直接输出自定义类型对象 - 标准库算法
sort与自定义比较函数配合,可以灵活排序 - 组合关系可以复用已有类的功能
任务3
- 类模板可以参数化类型,提高代码复用性
- 类模板需要实例化后才能使用
- 编译器会根据实例化类型生成具体的类
- 类模板的成员函数在类外实现时需要额外的模板声明
任务4
- 合理设计抽象基类
MachinePet,定义统一接口 - 通过纯虚函数
talk()强制派生类实现特定行为 - 利用多态性,可以通过基类指针统一管理不同类型的宠物
- 智能指针(
unique_ptr)可以自动管理内存,避免内存泄漏
任务5
- 类模板
Complex支持多种数值类型 - 运算符重载使复数运算更直观自然
- 流运算符重载支持直接输入输出
- 成员函数实现基本的复数运算功能
- 注意处理输出格式,特别是虚部的符号表示
五、实验总结
通过本次实验,我深入理解了:
- 编译时多态:通过函数重载、运算符重载和类模板实现
- 运行时多态:通过继承、虚函数和抽象类实现
- 抽象类是定义接口和规范的重要工具,强制派生类实现特定的功能
- 运算符重载可以让类的用起来更方便
- 类模板提供了强大的代码复用能力
- 智能指针可以自动释放资源,避免忘记delete

浙公网安备 33010602011771号