C++ 多态与流输入输出深度解析
1. 多态概述
多态性使我们能够 “泛型编程” 而非 “具体编程”。它允许我们编写程序来处理同一类层次结构中的类对象,就好像它们都是该层次结构的基类对象一样。借助多态,我们可以设计和实现易于扩展的系统,新类可以在很少或无需修改程序通用部分的情况下添加进来。只有那些需要直接了解新添加到层次结构中的类的程序部分才需要进行修改。
1.1 多态的应用场景
在多态视频游戏中,一个函数调用可以根据调用该函数的对象类型导致不同的操作发生。这使得设计和实现更具扩展性的系统成为可能,程序可以处理在开发时可能不存在的类型对象。
1.2 继承层次结构中对象的关系
C++ 支持多态性,即通过继承相关的不同类的对象对同一个成员函数调用做出不同响应的能力。多态性通过虚函数和动态绑定来实现。
- 虚函数调用 :当使用基类指针或引用调用虚函数时,C++ 会选择与对象关联的适当派生类中正确的重写函数。
- 静态绑定与动态绑定 :如果通过对象名和点成员选择运算符引用特定对象来调用虚函数,则引用在编译时解析(这称为静态绑定);调用的虚函数是为该特定对象的类定义的函数。
- 虚析构函数 :如果类包含虚函数,则应将基类析构函数声明为虚函数。这样可以确保当通过基类指针或引用删除派生类对象时,继承层次结构中所有适当的析构函数都会运行。
1.3 类型字段和 switch 语句
使用虚函数的多态编程可以消除对 switch 逻辑的需求。可以使用虚函数机制自动执行等效的逻辑,从而避免与 switch 逻辑通常相关的错误。
1.4 抽象类和纯虚函数
- 抽象类 :通常用作基类,不能实例化抽象类的对象。
- 具体类 :可以实例化对象的类。
- 纯虚函数 :通过在声明中使用纯说明符(= 0)声明一个或多个纯虚函数来创建抽象类。如果一个类从包含纯虚函数的类派生,并且该派生类没有为该纯虚函数提供定义,则该虚函数在派生类中仍然是纯虚函数,派生类也是抽象类。虽然不能实例化抽象基类的对象,但可以声明指向抽象基类对象的指针和引用,这些指针和引用可用于对从具体派生类实例化的派生类对象进行多态操作。
1.5 多态、虚函数和动态绑定的底层原理
动态绑定要求在运行时将对虚成员函数的调用路由到适合该类的虚函数版本。虚函数表(vtable)实现为一个包含函数指针的数组。每个具有虚函数的类都有一个 vtable。对于类中的每个虚函数,vtable 都有一个条目,其中包含一个函数指针,指向用于该类对象的虚函数版本。特定类使用的虚函数可以是该类中定义的函数,也可以是从层次结构中较高基类直接或间接继承的函数。
1.6 案例研究:使用多态和运行时类型信息的工资系统
- dynamic_cast 运算符 :检查指针所指向对象的类型,然后确定该类型与指针要转换的类型是否存在 “is-a” 关系。如果是,则 dynamic_cast 返回对象的地址;否则返回 nullptr。
- typeid 运算符 :返回一个对 type_info 对象的引用,该对象包含操作数类型的信息,包括类型名称。要使用 typeid,程序必须包含头文件 。
1.7 自我回顾练习
1.7.1 填空题
- a) 将基类对象视为派生类对象可能会导致错误。
- b) 多态有助于消除 switch 逻辑。
- c) 如果一个类包含至少一个纯虚函数,则它是抽象类。
- d) 可以实例化对象的类称为具体类。
- e) 可以使用 dynamic_cast 运算符安全地将基类指针向下转换。
- f) typeid 运算符返回一个对 type_info 对象的引用。
- g) 多态涉及使用基类指针或引用在基类和派生类对象上调用虚函数。
- h) 可重写的函数使用关键字 virtual 声明。
- i) 将基类指针转换为派生类指针称为向下转换。
1.7.2 判断题
- a) 错误。抽象基类可以包含具有实现的虚函数。
- b) 错误。使用派生类句柄引用基类对象是危险的。
- c) 错误。类永远不会被声明为虚类。相反,通过在类中包含至少一个纯虚函数来使类成为抽象类。
- d) 正确。
- e) 正确。
1.8 练习题
- 编程的一般性 :多态如何使你能够 “泛型编程” 而非 “具体编程”?讨论 “泛型编程” 的关键优势。
- 多态与 switch 逻辑 :讨论使用 switch 逻辑编程的问题。解释为什么多态可以是使用 switch 逻辑的有效替代方案。
- 继承接口与实现 :区分继承接口和继承实现。为继承接口设计的继承层次结构与为继承实现设计的继承层次结构有何不同?
- 虚函数 :什么是虚函数?描述一个适合使用虚函数的情况。
- 动态绑定与静态绑定 :区分静态绑定和动态绑定。解释虚函数和 vtable 在动态绑定中的使用。
- 虚函数与纯虚函数 :区分虚函数和纯虚函数。
- 抽象基类 :为形状层次结构建议一个或多个抽象基类级别。
- 多态与可扩展性 :多态如何促进可扩展性?
- 多态应用 :开发一个具有复杂图形输出的飞行模拟器,解释为什么多态编程对于这类问题特别有效。
- 工资系统修改 :修改工资系统,包括 Employee 类中的私有数据成员 birthDate。使用 Date 类表示员工的生日。假设每月处理一次工资单。创建一个 Employee 引用向量来存储各种员工对象。在循环中,多态地计算每个员工的工资,如果当前月份是员工生日所在的月份,则向该员工的工资金额中添加 100 美元的奖金。
- 包裹继承层次结构 :使用包裹继承层次结构创建一个程序,显示地址信息并计算几个包裹的运输成本。程序应包含一个指向 TwoDayPackage 和 OvernightPackage 类对象的 Package 指针向量。遍历向量以多态地处理包裹。对于每个包裹,调用 get 函数获取发件人和收件人的地址信息,然后打印这两个地址,就像它们出现在邮寄标签上一样。此外,调用每个包裹的 calculateCost 成员函数并打印结果。跟踪向量中所有包裹的总运输成本,并在循环结束时显示该总数。
- 多态银行程序 :开发一个使用账户层次结构的多态银行程序。创建一个指向 SavingsAccount 和 CheckingAccount 对象的 Account 指针向量。对于向量中的每个账户,允许用户指定要从账户中提取的金额和要存入账户的金额。在处理每个账户时,确定其类型。如果账户是 SavingsAccount,则使用成员函数 calculateInterest 计算应支付给该账户的利息,然后使用成员函数 credit 将利息添加到账户余额中。处理完一个账户后,通过调用基类成员函数 getBalance 打印更新后的账户余额。
- 工资系统修改 :修改工资系统,包括额外的 Employee 子类 PieceWorker 和 HourlyWorker。PieceWorker 表示根据生产的商品件数支付工资的员工。HourlyWorker 表示根据小时工资和工作小时数支付工资的员工。小时工对于超过 40 小时的所有工作时间支付加班费(小时工资的 1.5 倍)。在 main 函数的 Employee 指针向量中添加每个新类的对象指针。对于每个员工,显示其字符串表示形式和收入。
- 碳足迹抽象类 :使用仅包含纯虚函数的抽象类,可以为可能不同的类指定相似的行为。创建三个不相关的小类:Building、Car 和 Bicycle。为每个类赋予一些独特的适当属性和行为。编写一个抽象类 CarbonFootprint,仅包含一个纯虚函数 getCarbonFootprint。让每个类从该抽象类继承并实现 getCarbonFootprint 方法来计算该类的适当碳足迹。编写一个应用程序,创建每个类的对象,将指向这些对象的指针放入一个 CarbonFootprint 指针向量中,然后遍历该向量,多态地调用每个对象的 getCarbonFootprint 方法。对于每个对象,打印一些识别信息和该对象的碳足迹。
2. C++ 流输入输出
2.1 流输入输出概述
C++ 提供了一系列足以执行大多数常见 I/O 操作的功能,并概述了其余功能。许多 I/O 功能是面向对象的,这种 I/O 风格利用了 C++ 的其他特性,如引用、函数重载和运算符重载。
2.2 流的类型
- 经典流与标准流 :需要区分经典流和标准流的概念。
- iostream 库头文件 :使用相关的头文件来支持流输入输出操作。
- 流输入输出类和对象 :了解流输入输出类及其对象的使用。
2.3 流输出
- char * 变量的输出 :可以直接输出 char * 变量。
- 使用成员函数 put 输出字符 :通过 put 成员函数可以输出单个字符。
2.4 流输入
- get 和 getline 成员函数 :用于获取输入的字符或字符串。
- istream 成员函数 peek、putback 和 ignore :peek 函数用于查看下一个字符,putback 函数用于将字符放回输入流,ignore 函数用于忽略输入流中的字符。
- 类型安全的 I/O :C++ 使用类型安全的 I/O,每个 I/O 操作都根据数据类型执行。如果 I/O 函数已定义用于处理特定数据类型,则调用该成员函数来处理该数据类型。如果实际数据类型与处理该数据类型的函数不匹配,则编译器会生成错误。
2.5 无格式 I/O
使用 read、write 和 gcount 函数进行无格式 I/O 操作。
2.6 流操纵符
- 整数流基 :使用 dec、oct、hex 和 setbase 来设置整数的进制。
- 浮点精度 :使用 precision 和 setprecision 来设置浮点数的精度。
- 字段宽度 :使用 width 和 setw 来设置字段宽度。
- 用户定义的输出流操纵符 :可以自定义输出流操纵符。
2.7 流格式状态和流操纵符
- 尾随零和小数点 :使用 showpoint 来显示尾随零和小数点。
- 对齐方式 :使用 left、right 和 internal 来设置对齐方式。
- 填充 :使用 fill 和 setfill 来设置填充字符。
- 整数流基 :使用 dec、oct、hex、showbase 来设置整数的进制和显示进制前缀。
- 浮点数;科学和固定表示法 :使用 scientific 和 fixed 来设置浮点数的表示法。
- 大小写控制 :使用 uppercase 来控制大小写。
- 指定布尔格式 :使用 boolalpha 来指定布尔值的输出格式。
- 通过成员函数 flags 设置和重置格式状态 :可以使用 flags 成员函数来设置和重置流的格式状态。
2.8 流错误状态
需要了解流的错误状态,以便在输入输出操作失败时进行处理。
2.9 绑定输出流到输入流
可以将输出流绑定到输入流,以确保输出操作在输入操作之前完成。
2.10 总结
C++ 的流输入输出提供了丰富的功能,包括类型安全的 I/O、流操纵符和格式控制等。通过合理使用这些功能,可以更方便地进行输入输出操作,并提高程序的健壮性和可维护性。
多态和流输入输出的关系
多态和流输入输出是 C++ 中两个重要的特性,它们可以相互结合使用。例如,在处理不同类型的对象时,可以使用多态来调用不同对象的输入输出函数,从而实现统一的输入输出接口。同时,流输入输出的类型安全特性可以确保在多态操作中输入输出的数据类型正确。
以下是一个简单的多态和流输入输出结合的示例代码:
#include <iostream>
#include <vector>
// 抽象基类
class Shape {
public:
virtual void draw() const = 0;
virtual ~Shape() {}
};
// 派生类:圆形
class Circle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a circle." << std::endl;
}
};
// 派生类:矩形
class Rectangle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a rectangle." << std::endl;
}
};
int main() {
std::vector<Shape*> shapes;
shapes.push_back(new Circle());
shapes.push_back(new Rectangle());
for (auto shape : shapes) {
shape->draw();
}
// 释放内存
for (auto shape : shapes) {
delete shape;
}
return 0;
}
在这个示例中,Shape 是抽象基类,Circle 和 Rectangle 是派生类。通过多态,我们可以使用基类指针来调用不同派生类的 draw 函数。同时,使用流输出语句将结果输出到控制台。
总结
多态和流输入输出是 C++ 中非常重要的特性,它们可以帮助我们编写更灵活、可扩展和健壮的程序。通过合理使用多态和流输入输出的功能,可以提高程序的开发效率和质量。在实际应用中,我们可以根据具体的需求选择合适的方法来实现多态和流输入输出操作。
未来展望
随着 C++ 语言的不断发展,多态和流输入输出的功能可能会进一步增强和优化。例如,可能会出现更多的流操纵符和更强大的类型安全机制。同时,多态的应用场景也可能会更加广泛,为软件开发带来更多的便利。我们需要不断学习和掌握这些新特性,以跟上技术的发展步伐。
流程图
graph TD;
A[开始] --> B[创建基类指针向量];
B --> C[添加派生类对象到向量];
C --> D[遍历向量调用虚函数];
D --> E[输出结果];
E --> F[释放内存];
F --> G[结束];
表格
| 特性 | 描述 |
|---|---|
| 多态 | 允许不同类的对象对同一函数调用做出不同响应 |
| 流输入输出 | 提供类型安全的输入输出操作 |
| 虚函数 | 实现多态的关键机制 |
| 流操纵符 | 用于控制输入输出的格式 |
通过以上内容,我们对 C++ 中的多态和流输入输出有了更深入的了解。在实际编程中,我们可以根据具体需求灵活运用这些特性,提高程序的质量和可维护性。
3. 多态与流输入输出的实际应用案例
3.1 工资系统案例扩展
在之前提到的工资系统中,我们可以进一步展示多态和流输入输出的结合应用。以下是一个更完整的示例代码:
#include <iostream>
#include <vector>
#include <string>
// 基类 Employee
class Employee {
public:
Employee(const std::string& name, int birthMonth) : name(name), birthMonth(birthMonth) {}
virtual double earnings() const = 0;
virtual void print() const = 0;
int getBirthMonth() const { return birthMonth; }
virtual ~Employee() {}
protected:
std::string name;
int birthMonth;
};
// 派生类 CommissionEmployee
class CommissionEmployee : public Employee {
public:
CommissionEmployee(const std::string& name, int birthMonth, double sales, double rate)
: Employee(name, birthMonth), sales(sales), rate(rate) {}
double earnings() const override {
return sales * rate;
}
void print() const override {
std::cout << "Commission Employee: " << name << std::endl;
}
private:
double sales;
double rate;
};
// 派生类 BasePlusCommissionEmployee
class BasePlusCommissionEmployee : public CommissionEmployee {
public:
BasePlusCommissionEmployee(const std::string& name, int birthMonth, double sales, double rate, double base)
: CommissionEmployee(name, birthMonth, sales, rate), baseSalary(base) {}
double earnings() const override {
return CommissionEmployee::earnings() + baseSalary;
}
void print() const override {
std::cout << "Base Plus Commission Employee: " << name << std::endl;
}
double getBaseSalary() const { return baseSalary; }
void setBaseSalary(double base) { baseSalary = base; }
private:
double baseSalary;
};
// 处理工资系统的函数
void processPayroll(const std::vector<Employee*>& employees, int currentMonth) {
double totalPayroll = 0;
for (auto employee : employees) {
double pay = employee->earnings();
if (employee->getBirthMonth() == currentMonth) {
pay += 100;
}
employee->print();
std::cout << "Pay: " << pay << std::endl;
totalPayroll += pay;
}
std::cout << "Total Payroll: " << totalPayroll << std::endl;
}
int main() {
std::vector<Employee*> employees;
employees.push_back(new CommissionEmployee("John", 3, 1000, 0.1));
employees.push_back(new BasePlusCommissionEmployee("Jane", 6, 2000, 0.15, 500));
int currentMonth = 6;
processPayroll(employees, currentMonth);
// 释放内存
for (auto employee : employees) {
delete employee;
}
return 0;
}
在这个示例中, Employee 是抽象基类, CommissionEmployee 和 BasePlusCommissionEmployee 是派生类。通过多态,我们可以使用基类指针 Employee* 来调用不同派生类的 earnings 和 print 函数。同时,使用流输出语句将员工信息和工资信息输出到控制台。
3.2 包裹继承层次结构案例
以下是一个包裹继承层次结构的示例代码,展示了多态和流输入输出的应用:
#include <iostream>
#include <vector>
#include <string>
// 基类 Package
class Package {
public:
Package(const std::string& sender, const std::string& recipient, double weight, double costPerOunce)
: sender(sender), recipient(recipient), weight(weight), costPerOunce(costPerOunce) {}
virtual double calculateCost() const = 0;
void printAddresses() const {
std::cout << "Sender: " << sender << std::endl;
std::cout << "Recipient: " << recipient << std::endl;
}
virtual ~Package() {}
protected:
std::string sender;
std::string recipient;
double weight;
double costPerOunce;
};
// 派生类 TwoDayPackage
class TwoDayPackage : public Package {
public:
TwoDayPackage(const std::string& sender, const std::string& recipient, double weight, double costPerOunce, double flatFee)
: Package(sender, recipient, weight, costPerOunce), flatFee(flatFee) {}
double calculateCost() const override {
return weight * costPerOunce + flatFee;
}
private:
double flatFee;
};
// 派生类 OvernightPackage
class OvernightPackage : public Package {
public:
OvernightPackage(const std::string& sender, const std::string& recipient, double weight, double costPerOunce, double extraFeePerOunce)
: Package(sender, recipient, weight, costPerOunce), extraFeePerOunce(extraFeePerOunce) {}
double calculateCost() const override {
return weight * (costPerOunce + extraFeePerOunce);
}
private:
double extraFeePerOunce;
};
// 处理包裹的函数
void processPackages(const std::vector<Package*>& packages) {
double totalCost = 0;
for (auto package : packages) {
package->printAddresses();
double cost = package->calculateCost();
std::cout << "Shipping Cost: " << cost << std::endl;
totalCost += cost;
}
std::cout << "Total Shipping Cost: " << totalCost << std::endl;
}
int main() {
std::vector<Package*> packages;
packages.push_back(new TwoDayPackage("Alice", "Bob", 5, 2, 10));
packages.push_back(new OvernightPackage("Charlie", "David", 3, 3, 1));
processPackages(packages);
// 释放内存
for (auto package : packages) {
delete package;
}
return 0;
}
在这个示例中, Package 是抽象基类, TwoDayPackage 和 OvernightPackage 是派生类。通过多态,我们可以使用基类指针 Package* 来调用不同派生类的 calculateCost 函数。同时,使用流输出语句将包裹的地址信息和运输成本输出到控制台。
4. 多态和流输入输出的操作步骤总结
4.1 多态的实现步骤
- 定义抽象基类 :包含纯虚函数,作为派生类的公共接口。
- 定义派生类 :继承自抽象基类,并实现纯虚函数。
- 使用基类指针或引用 :通过基类指针或引用调用虚函数,实现多态调用。
- 释放内存 :如果使用动态内存分配,需要在适当的时候释放内存,避免内存泄漏。
4.2 流输入输出的操作步骤
- 包含必要的头文件 :例如
<iostream>用于基本的输入输出操作,<iomanip>用于流操纵符。 - 使用流对象 :如
std::cout用于输出,std::cin用于输入。 - 使用流操纵符 :根据需要设置输出格式,如设置精度、宽度、对齐方式等。
- 处理流错误状态 :检查流的错误状态,处理输入输出操作失败的情况。
5. 总结与展望
5.1 总结
多态和流输入输出是 C++ 中非常强大的特性,它们可以极大地提高程序的灵活性、可扩展性和健壮性。多态允许我们以统一的方式处理不同类型的对象,而流输入输出提供了类型安全的输入输出操作和丰富的格式控制功能。通过结合使用这两个特性,我们可以编写更加优雅和高效的代码。
5.2 展望
随着 C++ 标准的不断发展,多态和流输入输出的功能可能会进一步增强。未来可能会有更多的流操纵符和更强大的类型安全机制出现,同时多态的应用场景也可能会更加广泛。作为开发者,我们需要不断学习和掌握这些新特性,以适应不断变化的技术环境。
流程图
graph TD;
A[开始] --> B[创建包裹指针向量];
B --> C[添加不同类型包裹对象到向量];
C --> D[遍历向量处理包裹];
D --> E[输出包裹地址信息];
E --> F[计算并输出运输成本];
F --> G[累加总运输成本];
G --> H[输出总运输成本];
H --> I[释放内存];
I --> J[结束];
表格
| 特性 | 操作步骤 |
|---|---|
| 多态 | 定义抽象基类 -> 定义派生类并实现纯虚函数 -> 使用基类指针或引用调用虚函数 -> 释放内存 |
| 流输入输出 | 包含必要头文件 -> 使用流对象 -> 使用流操纵符设置格式 -> 处理流错误状态 |
通过以上的案例和总结,我们对 C++ 中的多态和流输入输出有了更全面的认识。在实际编程中,我们可以根据具体需求灵活运用这些特性,提高程序的质量和可维护性。
超级会员免费看

7440

被折叠的 条评论
为什么被折叠?



