C++使用多态时要注意的用法细节
继承派生
继承是一种允许我们根据一个已有的类来定义一个新类的机制。已有的类称为基类,新定义的类称为派生类。派生类继承了基类的所有特征(数据成员和成员函数),并可以添加自己的新特性或重新定义继承来的特性。

1. 核心功能与目的
代码复用:避免重复编写相同的代码。通用的属性和行为可以放在基类中,特定的类通过继承获得这些功能。
建立类之间的层次关系,派生类对象是基类对象的一种特殊化。例如,Car 是一种 Vehicle,Dog 是一种 Animal。这种关系是继承的前提。
2. 使用示例
#include <iostream>
#include <string>
using namespace std;
// 基类
class Vehicle {
protected:
string brand;
int speed;
public:
Vehicle(const string& b, int s) : brand(b), speed(s) {}
void displayInfo() const {
cout << "Brand: " << brand << ", Speed: " << speed << " km/h" << endl;
}
void honk() const {
cout << "Some generic vehicle sound!" << endl;
}
};
// 派生类:公共继承自 Vehicle
class Car : public Vehicle {
private:
int numDoors;
public:
// 派生类构造函数:使用初始化列表调用基类构造函数
Car(const string& b, int s, int doors) : Vehicle(b, s), numDoors(doors) {}
// 添加派生类特有的行为
void drive() const {
cout << "Driving the " << brand << " car with " << numDoors << " doors." << endl;
}
// 重写基类方法 (早期绑定/静态多态,后面会讲)
void honk() const {
cout << "Car honk: Beep Beep!" << endl;
}
};
// 另一个派生类
class Bike : public Vehicle {
public:
Bike(const string& b, int s) : Vehicle(b, s) {}
void ringBell() const {
cout << "Bike bell: Ring Ring!" << endl;
}
};
int main() {
Car myCar("Toyota", 120, 4);
Bike myBike("Giant", 30);
myCar.displayInfo(); // 继承自 Vehicle
myCar.drive(); // Car 自己的方法
myCar.honk(); // 调用的是 Car 的 honk()
cout << endl;
myBike.displayInfo(); // 继承自 Vehicle
myBike.ringBell(); // Bike 自己的方法
myBike.honk(); // 调用的是 Vehicle 的 honk()
return 0;
}
输出:
Brand: Toyota, Speed: 120 km/h
Driving the Toyota car with 4 doors.
Car honk: Beep Beep!
Brand: Giant, Speed: 30 km/h
Bike bell: Ring Ring!
Some generic vehicle sound!


多态
多态的字面意思是“多种形态”。在 C++ 中,它指的是使用基类的指针或引用来操作派生类对象,并根据对象的实际类型来调用相应的方法。

1. 核心功能与目的
接口与实现分离:基类定义接口(虚函数),派生类提供不同的实现。
运行时灵活性:程序在运行时(而非编译时)才能决定调用哪个函数,这使得程序更加通用和可扩展。
2. 虚函数
多态是通过虚函数实现的。
virtual 关键字:在基类中使用 virtual 声明的成员函数就是虚函数。
重写:在派生类中,使用相同的函数签名(函数名、参数列表、常量性)重新定义虚函数。通常使用 override 关键字(C++11引入)来显式注明,提高代码可读性和安全性。
3.纯虚函数
纯虚函数与抽象类:
在基类中,虚函数可以没有实现,称为纯虚函数(语法:virtual ReturnType Func() = 0;)。
包含至少一个纯虚函数的类称为抽象类。
抽象类不能实例化对象,它只能作为接口被继承。
派生类必须重写所有纯虚函数,否则它也会成为抽象类。

4. 使用示例(结合继承)
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// 抽象基类
class Vehicle {
protected:
string brand;
int speed;
public:
Vehicle(const string& b, int s) : brand(b), speed(s) {}
virtual ~Vehicle() {} // 虚析构函数,极其重要!
// 普通的虚函数,有默认实现
virtual void honk() const {
cout << "Some generic vehicle sound!" << endl;
}
// 纯虚函数,使 Vehicle 成为抽象类
virtual void displayInfo() const = 0;
// 另一个普通的成员函数
void setSpeed(int s) {
speed = s;
}
};
class Car : public Vehicle {
private:
int numDoors;
public:
Car(const string& b, int s, int doors) : Vehicle(b, s), numDoors(doors) {}
// 重写纯虚函数,提供实现
void displayInfo() const override {
cout << "[Car] Brand: " << brand << ", Speed: " << speed << " km/h, Doors: " << numDoors << endl;
}
// 重写虚函数
void honk() const override {
cout << "Car honk: Beep Beep!" << endl;
}
};
class Bike : public Vehicle {
public:
Bike(const string& b, int s) : Vehicle(b, s) {}
// 重写纯虚函数,提供实现
void displayInfo() const override {
cout << "[Bike] Brand: " << brand << ", Speed: " << speed << " km/h" << endl;
}
// 注意:这里没有重写 honk(),它将使用基类 Vehicle 的 honk() 实现
};
int main() {
// Vehicle v; // 错误!不能实例化抽象类
// 多态的魔力:使用基类指针指向派生类对象
Vehicle* vehicle1 = new Car("Toyota", 120, 4);
Vehicle* vehicle2 = new Bike("Giant", 30);
// 根据对象的实际类型调用正确的函数
vehicle1->displayInfo(); // 调用 Car::displayInfo()
vehicle1->honk(); // 调用 Car::honk()
cout << endl;
vehicle2->displayInfo(); // 调用 Bike::displayInfo()
vehicle2->honk(); // 调用 Vehicle::honk() (因为Bike没重写)
cout << "\n--- Using a collection ---\n";
// 多态的巨大优势:可以统一处理不同类型的对象
vector<Vehicle*> garage;
garage.push_back(new Car("BMW", 200, 2));
garage.push_back(new Bike("Trek", 25));
garage.push_back(new Car("Honda", 90, 4));
for (const auto& v : garage) {
v->displayInfo(); // 对每个对象调用其自己的 displayInfo()
}
// 清理内存
for (auto& v : garage) delete v;
delete vehicle1;
delete vehicle2;
return 0;
}
输出
[Car] Brand: Toyota, Speed: 120 km/h, Doors: 4
Car honk: Beep Beep!
[Bike] Brand: Giant, Speed: 30 km/h
Some generic vehicle sound!
--- Using a collection ---
[Car] Brand: BMW, Speed: 200 km/h, Doors: 2
[Bike] Brand: Trek, Speed: 25 km/h
[Car] Brand: Honda, Speed: 90 km/h, Doors: 4
使用细节
1.使用多态一定要定义虚析构函数:
规则: 如果一个类打算被用作基类(即会被其他类继承),并且会通过基类指针来删除派生类对象,那么它的析构函数必须是 virtual。
为什么:如果析构函数不是虚的,当我们delete 一个指向派生类对象的基类指针时,只会调用基类的析构函数,而不会调用派生类的析构函数。这会导致派生类部分的资源泄漏。
class Base {
public:
Base() { std::cout << "Base Constructor\n"; }
~Base() { std::cout << "Base Destructor\n"; } // 非虚析构函数 - 大问题!
};
class Derived : public Base {
public:
Derived() { std::cout << "Derived Constructor\n"; }
~Derived() { std::cout << "Derived Destructor\n"; }
};
int main() {
Base* ptr = new Derived(); // 基类指针指向派生类对象
delete ptr; // 灾难!只输出 "Base Destructor"
// Derived 的析构函数没被调用!
return 0;
}
///应该写成 virtual ~Base() { std::cout << "Base Destructor\n"; } // 虚析构函数
解决方案:如果一个类有任何一个虚函数,就应该给它加上一个虚析构函数。这几乎是一条铁律。


2.小心“隐藏”而不是“重写”
规则: 派生类的函数并不会自动重写基类的虚函数。只有函数签名(包括函数名、参数列表和常量性)完全一致时,才会发生重写。
为什么:如果签名不一致,派生类的函数会隐藏基类中同名的函数,而不是重写它。这通常不是我们想要的行为,并且容易导致错误。
class Base {
public:
virtual void print(int x) const {
std::cout << "Base: " << x << std::endl;
}
};
class Derived : public Base {
public:
// 注意:参数是 double,且缺少 const
virtual void print(double x) {
std::cout << "Derived: " << x << std::endl;
}
// 这个函数隐藏了 Base::print(int),而不是重写它!
};
int main() {
Derived d;
d.print(5); // 输出 "Derived: 5" (发生了隐式转换 int->double)
// 但调用的是 Derived::print(double),不是我们期望的多态
Base* bPtr = &d;
bPtr->print(5); // 输出 "Base: 5"
// 因为 Derived 没有重写 print(int),所以仍然调用基类版本
return 0;
}
解决方案: 在意图重写虚函数的任何地方,都使用 override 关键字。
override 关键字明确告诉编译器:“我意图重写基类的一个虚函数”。如果签名不匹配导致没有成功重写,编译器会报错。这是极其重要的编译时检查工具。
class Derived : public Base {
public:
// 编译器会在这里报错,因为找不到可以重写的 void print(double)
virtual void print(double x) override {
std::cout << "Derived: " << x << std::endl;
}
// 正确的重写
virtual void print(int x) const override {
std::cout << "Derived: " << x << std::endl;
}
};
3.避免在虚函数中使用默认参数
规则: 虚函数是动态绑定的(在运行时决定调用哪个),但默认参数是静态绑定的(在编译时根据指针或引用的类型决定)。
这意味著通过基类指针调用派生类重写的虚函数时,使用的默认参数是基类中定义的版本,而不是派生类中的版本。
class Base {
public:
virtual void print(std::string msg = "Base") {
std::cout << msg << std::endl;
}
};
class Derived : public Base {
public:
// 重写虚函数,但使用不同的默认参数
void print(std::string msg = "Derived") override {
std::cout << msg << std::endl;
}
};
int main() {
Derived d;
Base* bp = &d;
bp->print(); // 输出 "Base" !!!
// 函数调用是 Derived::print (动态绑定)
// 但默认参数是 Base 的 (静态绑定)
d.print(); // 输出 "Derived" (符合预期)
return 0;
}
解决方案: 避免在虚函数中使用默认参数。如果必须使用,确保基类和所有派生类使用相同的默认值。通常可以用重载函数或其他设计模式来替代。
4.不要在构造/析构函数中调用虚函数
规则: 在构造函数和析构函数中调用虚函数,不会发生多态行为。它会调用当前正在构造的类的版本。
为什么:在构建派生类对象时,基类构造函数先执行。此时派生类部分还未初始化。如果基类构造函数调用了一个虚函数,而它又向下调用派生类的版本,可能会访问未初始化的派生类成员,导致未定义行为。
C++通过机制确保在构造期间,虚函数机制不会“下降”到派生层。在基类构造函数中,对象的类型被视为基类类型。
class Base {
public:
Base() {
setup(); // 在构造函数中调用虚函数
}
virtual void setup() {
std::cout << "Base::setup()\n";
}
};
class Derived : public Base {
public:
Derived() : Base() {}
void setup() override {
std::cout << "Derived::setup()\n";
m_value = 42; // 假设这里初始化派生类成员
}
private:
int m_value;
};
int main() {
Derived d; // 输出什么?
return 0;
}
// 输出: "Base::setup()"
// 即使在构造 Derived 对象,Base 构造函数中的 setup() 也调用的是 Base 的版本
解决方案: 将初始化工作分解到非虚函数中,或者在对象完全构造后,再通过外部代码调用初始化方法。
5.避对象切片
规则: 将派生类对象按值赋值给一个基类对象时,会发生“对象切片”。派生类特有的部分会被“切掉”,只留下基类部分。
这是值语义语言(如C++)的一个特性,但常常是bug的来源。
class Base {
public:
int base_data = 10;
};
class Derived : public Base {
public:
int derived_data = 20;
};
int main() {
Derived d;
Base b = d; // 对象切片发生在这里!
std::cout << b.base_data; // 输出 10,没问题
// std::cout << b.derived_data; // 错误!Base 类没有 derived_data
// b 只是一个普通的 Base 对象,Derived 的部分丢失了。
return 0;
}
解决方案:多使用基类的指针 (Base*) 或引用 (Base&) 来操作派生类对象,这样可以避免拷贝,也不会发生切片。
继承与多态机制背后的原理(虚函数表 vtable)
简单来说,编译器会为每个包含虚函数的类创建一个虚函数表。这是一个函数指针数组,存放着该类所有虚函数的地址。
当一个类被实例化时,对象中会包含一个隐藏的指针(vptr),指向该类的虚函数表。
当通过基类指针调用虚函数时,程序会通过对象的 vptr 找到对应的虚函数表,然后在表中查找函数的地址并进行调用。
派生类有自己的虚函数表。如果重写了虚函数,它的表项就是派生类的函数地址;如果没有重写,表项就是基类的函数地址。
这就是动态绑定或晚期绑定:函数调用在运行时通过查表解析。
总结
1.虚析构是必须:多态基类的析构函数必须是 virtual。
2.使用 override:始终用 override 关键字明确重写意图,让编译器帮你检查。
3.慎用默认参数:避免在虚函数中使用默认参数,否则容易混淆。
4.构造/析构中不调虚函数:这时多态不会生效。
5.提防对象切片:多使用指针和引用来传递多态对象。
使用继承来构建对象的层次结构,使用多态来让这些对象在接收到相同消息时“如何做”,从而实现基于同一接口的不同行为。
补充:实战中遇到的问题和解答
定义一个派生类变量a,
基类*b=new 派生类(a)是可以的,属于多态的复制构造,
派生类 *c= new 派生类(b)是不行的,因为b是一个基类指针,而这个指针对象b可能不一定是派生类型。即使b实际上指向了一个派生类对象,但编译器在编译时不知道这一点,除非你进行强制类型转换。所以这样是不安全的,编译器会报错。需要用函数来让基类和派生类彼此兼容。
基类和派生类之间已经有一种兼容关系:派生类对象可以被视为基类对象(即向上转换,upcasting),这是安全的,并且可以由编译器自动完成。
但是,反过来(向下转换,downcasting)是不安全的,因为基类指针可能并不指向一个派生类对象。因此,需要显式地进行类型转换,并且要确保转换是安全的。
可以使用dynamic_cast来进行安全的向下转换,使用要求就是有多态(基类存在虚函数)。
Player *b = new Char(A); // 假设A是一个Char对象
Char *c = dynamic_cast<Char*>(b);
if (c != nullptr) {
// 转换成功,b确实指向一个Char对象
} else {
// 转换失败,b并不指向Char对象
}
在C++中,通过基类指针指向派生类对象时,该指针只能直接访问基类中定义的成员(包括虚函数,通过动态绑定调用派生类的重写版本)。如果派生类中有基类没有的成员函数(即派生类专有的函数),那么通过基类指针是无法直接调用这些函数的。
同样可以通过将基类指针向下转换(downcast)为派生类指针,然后通过派生类指针来调用派生类专有的函数。向下转换可以使用dynamic_cast或static_cast,但推荐使用dynamic_cast,因为它具有类型检查功能,更安全。
#include <iostream>
class Base {
public:
virtual ~Base() {} // 虚析构函数,确保基类是多态的,以便使用dynamic_cast
};
class Derived : public Base {
public:
void specificFunction() {
std::cout << "Derived class specific function." << std::endl;
}
};
int main() {
Base* basePtr = new Derived(); // 基类指针指向派生类对象
// basePtr->specificFunction(); // 错误:Base类中没有specificFunction成员
// 向下转换
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
if (derivedPtr) {
derivedPtr->specificFunction(); // 正确:通过派生类指针调用派生类专有函数
} else {
std::cout << "Downcast failed." << std::endl;
}
delete basePtr; // 释放内存
return 0;
}
补充:protected的使用场合
一般来说类的属性我们倾向于设置为private最小权限,通过public的方法来间接操控他们。
但在使用多态的时候,创建了子类,子类继承了父类所有的属性和方法,如果这个时候父类的属性还设置为private的话,子类里所有新定义的个性方法都无法修改子类继承下来的属性了,而这些属性往往是需要随不同子类而改变的,所以将父类的那些需要传给子类而且需要有所变动的属性设置为protected。
更多推荐


所有评论(0)