多态的基本概念
多态是一种允许使用相同的接口来访问不同的底层形式(类型)的对象的能力。C++中的多态主要通过以下两种方式实现:
编译时多态(静态多态):通过函数重载和运算符重载实现。
运行时多态(动态多态):通过虚函数和继承实现。
编译时多态(静态多态)
编译时多态是在编译期决定的,主要通过函数重载和运算符重载来实现。
函数重载:在同一个作用域内,可以声明几个功能相似的同名函数,但是这些函数的参数类型和/或数量不同,编译器根据函数调用时的参数类型和数量来决定具体调用哪个函数。
运算符重载:允许定义或重新定义大部分C++内置的运算符作用于自定义类型的操作,这使得运算符可以根据操作数的类型来执行不同的操作。
运行时多态(动态多态)
运行时多态是在程序运行时决定的,主要依靠虚函数(virtual function)和继承机制来实现。
虚函数:在基类中用virtual关键字声明的函数,可以在派生类中被重写(override)。这样,基类指针或引用指向派生类对象时,调用的是派生类中的函数,这种机制称为动态链接或后期绑定。
纯虚函数:在基类中声明为纯虚函数(通过virtual 返回类型 函数名() = 0;
的方式声明),这样的基类称为抽象基类,不能直接实例化。纯虚函数必须在派生类中被实现,除非派生类也是一个抽象类。
多态的使用
多态使得我们可以使用基类的指针或引用来操作不同的派生类对象,而具体调用哪个类的哪个方法,则是在运行时决定的。这种机制使得代码更加灵活和通用,易于扩展和维护。
#include <iostream>
using namespace std;class Shape {
public:virtual void draw() = 0; // 纯虚函数
};class Circle : public Shape {
public:void draw() override {cout << "Drawing Circle" << endl;}
};class Rectangle : public Shape {
public:void draw() override {cout << "Drawing Rectangle" << endl;}
};int main() {Shape* shape1 = new Circle();Shape* shape2 = new Rectangle();shape1->draw(); // 输出: Drawing Circleshape2->draw(); // 输出: Drawing Rectangledelete shape1;delete shape2;return 0;
}
在这个示例中,通过Shape
类的指针调用draw()
函数时,具体调用哪个版本的draw()
(是Circle
的还是Rectangle
的),取决于指针实际指向的对象类型。这就是运行时多态的体现。
Shape
类的指针,属于范围小的类型,在自己的地址上寻找相符合的信息部分。但是虚函数重写,导致虚基类中的draw
函数变化,Shape
类的指针在地址上找到的相符合的信息部分是被重写过的函数。
不使用多态的版本(隐藏)
#if 1
#include <iostream>
using namespace std;class Shape {
public:void draw() {cout << "Drawing Shape" << endl;};};class Circle : public Shape {
public:void draw() {cout << "Drawing Circle" << endl;}};class Rectangle : public Shape {
public:void draw() {cout << "Drawing Rectangle" << endl;}};int main() {Shape* shape1 = new Circle();Shape* shape2 = new Rectangle();shape1->draw(); shape2->draw(); return 0;}
#endif
Shape
类的指针,属于范围小的类型,在自己的地址上寻找相符合的信息部分。派生类和基类有名字相同的成员,会发生隐藏,隐藏基类的成员,此时派生类有两个相同的成员,如果要访问基类成员需要使用作用域限定符。
虚函数
虚函数是C++中支持多态性的一种机制。它允许在派生类中重写基类的方法,使得通过基类指针或引用调用这些方法时,能够执行到派生类中相应的重写版本,实现运行时的多态性。这意味着在程序执行期间,可以根据对象的实际类型来决定调用哪个函数,而不是在编译时。
定义虚函数
在基类中,使用virtual
关键字声明的成员函数就是虚函数。声明虚函数的目的是允许在派生类中对其进行重写(override)。
class Base {
public:virtual void show() {cout << "Base class show" << endl;}
};
在上面的代码中,show()
函数是一个虚函数。
虚函数的重写
虚函数重写(Override)允许子类重新定义基类中的虚函数实现。这是实现多态性的关键机制之一。通过虚函数重写,可以在运行时根据对象的实际类型调用相应类的方法,而不是在编译时确定。
虚函数重写的实现
要实现虚函数重写,需要遵循以下几个步骤:
在基类中声明虚函数:首先,你需要在基类中使用 virtual
关键字声明一个虚函数。这表示该函数可以在任何派生类中被重写。
class Base {
public:virtual void show() {cout << "Base class show function called." << endl;}
};
这里我们声明了一个基类 Base
,其中包含一个虚函数 show()
。使用 virtual
关键字标记了 show()
函数,表示它可以在派生类中被重写。
在派生类中重写虚函数:在派生类中,你可以重写基类中声明的虚函数。重写时,函数的返回类型、名称以及参数列表必须与基类中声明的虚函数完全相同。
class Derived : public Base {
public:void show() override { // C++11引入了override关键字,确保了函数重写的正确性cout << "Derived class show function called." << endl;}
};
这里我们创建了一个 Derived
类,它从 Base
类继承。在 Derived
类中,我们重写了 show()
函数。使用 override
关键字(C++11引入)表明这是一个重写的函数,这有助于编译器检查函数签名的一致性,确保我们正确地重写了函数。
虚函数重写的特定要求
函数签名必须匹配:重写的虚函数必须与基类中的原始虚函数有相同的函数签名(即相同的返回类型、函数名称和参数列表)。
基类函数必须是虚函数:只有被声明为 virtual
的基类函数才可以被派生类重写。
使用 override
关键字(可使用,也可不使用,但推荐):在C++11及以后的版本中,可以在派生类的函数声明后使用 override
关键字。这不是必需的,但它可以帮助编译器检查派生类是否正确地重写了基类的虚函数。
协变
在C++中,协变主要应用于虚函数的返回类型。协变允许派生类中重写的虚函数拥有与基类虚函数不同的返回类型,但这两个返回类型必须保持一定的继承关系。
协变的基本规则
只适用于返回类型:协变仅适用于方法返回类型的变化。
派生关系:派生类中重写的虚函数的返回类型,必须是基类对应虚函数返回类型的派生类(或同类型,同类型不是协变)。
协变的实现
假设有一个基类 Base
和一个从 Base
派生的类 Derived
,同时有一个返回 Base
类型指针的虚函数。在派生类中重写该虚函数时,可以返回 Derived
类型的指针,这就是协变的体现。
示例代码
class Base {
public:virtual Base* clone() const {// 实现克隆自己的逻辑return new Base(*this);}
};class Derived : public Base {
public:Derived* clone() const override {// 实现克隆自己的逻辑return new Derived(*this);}
};
基类 Base
有一个名为 clone
的虚函数,返回类型是指向 Base
类的指针。
派生类 Derived
重写了 clone
函数,并将返回类型改为指向 Derived
类的指针。这里展示了协变:返回类型从 Base*
变为了 Derived*
。
注意,这里 Derived::clone
函数正确地重写了基类中的 clone
函数,尽管返回类型不同。这是因为 Derived*
是 Base*
的协变返回类型,且 Derived
是 Base
的派生类。
协变的特定要求和限制
只适用于返回类型:协变只能用于方法的返回类型。
继承关系:派生类方法的返回类型必须是基类方法返回类型的派生类型。
指针和引用:协变仅适用于返回类型为指针或引用的情况。对于返回具体对象的情况,不支持协变。
虚析构函数的重写
虚析构函数允许通过基类指针来正确地删除派生类对象。当一个基类声明了虚析构函数时,派生类的析构函数自动成为虚函数,无论是否显式地使用 virtual
关键字。虚析构函数确保了当通过基类指针删除派生类对象时,能够先调用派生类的析构函数,然后是基类的析构函数,从而正确地释放资源。
虚析构函数的重要性
如果基类的析构函数不是虚函数,则删除派生类对象时可能只会调用基类的析构函数,从而导致派生类分配的资源没有被正确释放,引发内存泄露。声明虚析构函数可以防止这种情况。
重写虚析构函数
/*虚析构函数*/
#if 1
#include <iostream>
using namespace std;
class Base {
public:virtual ~Base() {std::cout << "Base destructor called." << std::endl;}};class Derived : public Base {
public:~Derived() {std::cout << "Derived destructor called." << std::endl;}};int main(){Base* p1=new Base;Base* p2=new Derived;delete p1;delete p2;}#endif
基类 Base
有一个虚析构函数。使用 virtual
关键字确保了析构函数是虚拟的。
派生类 Derived
重写了析构函数。虽然没有显式使用 virtual
关键字,但由于基类的析构函数是虚的,派生类的析构函数自动成为虚函数。
派生类Derived
析构函数的调用顺序,是先调用派生类的析构函数再调用基类的虚构函数。
不重写析构函数
/*不重写析构函数*/
#if 1
#include <iostream>
using namespace std;
class Base {
public:~Base() {std::cout << "Base destructor called." << std::endl;}};class Derived : public Base {
public:~Derived() {std::cout << "Derived destructor called." << std::endl;}};int main(){Base* p1=new Base;Base* p2=new Derived;delete p1;delete p2;}#endif
虚析构函数的特定要求
正确的资源释放:当通过基类指针删除派生类对象时,虚析构函数确保派生类和基类的析构函数都会被正确调用,从而释放所有相关资源。顺序是先调用派生类的析构函数,再调用基类的析构函数。
基类应该有虚析构函数:如果一个类被设计为基类,并且预期它会被其他类继承,则应该声明一个虚析构函数,即使析构函数不执行任何操作。
注意点
虚析构函数的性能影响:虚函数(包括虚析构函数)可能会引入轻微的性能开销,因为它们需要通过虚表(vtable)来解析调用。但在绝大多数情况下,这种开销是可以接受的,特别是考虑到它带来的正确性和灵活性。
避免内存泄漏:即使派生类没有分配任何动态内存,如果基类有虚析构函数,派生类也应正确重写析构函数,以避免潜在的资源管理问题。
C++中 override 和 final 关键字
在C++中,override
和 final
是两个与虚函数重写相关的关键字,它们在C++11标准中引入。这两个关键字提供了额外的语义,帮助程序员更明确地表达他们的设计意图,同时也使得编译器能够提供更好的检查和优化。
override
当你在派生类中重写基类的虚函数时,可以在派生类的函数声明后面使用 override
关键字。这表明该函数旨在重写一个基类中的虚函数。如果声明的函数没有重写任何基类中的虚函数(比如由于函数签名不匹配),编译器将报错。这有助于捕捉到可能的错误,比如拼写错误或者错误的参数类型。
class Base {
public:virtual void func() {}
};class Derived : public Base {
public:void func() override {} // 正确重写// void fun() override {} // 编译错误,因为Base中没有fun()函数
};
在这个例子中,Derived
类通过 override
关键字标明它意图重写 Base
类中的 func()
函数。如果 Derived
类中的函数签名与任何基类中的虚函数都不匹配,编译器将报错。
final
final
关键字可以用于防止类被进一步派生或虚函数被进一步重写。当你将一个类标记为 final
时,任何尝试从这个类派生出新类的行为都会导致编译错误。同样,将虚函数标记为 final
会阻止任何派生类重写该函数。
class Base {
public:virtual void func() final {} // 防止进一步重写
};class Derived final : public Base { // 防止进一步派生
public:// void func() override {} // 编译错误,因为Base::func()被标记为final
};// class MoreDerived : public Derived {}; // 编译错误,因为Derived被标记为final
在这个例子中,Base
类中的 func()
被标记为 final
,这意味着任何尝试在派生类中重写 func()
的操作都会导致编译错误。同时,Derived
类被标记为 final
,防止任何进一步的派生。
使用 override 和 final 的好处
提高代码清晰度:使用 override
和 final
关键字可以让你的代码意图更加明确,使其他开发者更容易理解你的设计意图。
编译时检查:它们允许编译器在编译时进行额外的检查,捕捉潜在的错误,比如签名不匹配或错误地重写了不应该重写的函数。
优化支持:明确指出哪些函数不会被进一步重写,可以帮助编译器做出更好的优化决策。
重载、重写、重定义(隐藏)
重载 (Overloading)
重载指的是在相同作用域内有两个或多个函数拥有相同的名称,但是它们的参数列表不同(参数类型、个数或者顺序不同)。重载使得函数可以根据不同的参数执行不同的任务。
-
两个函数在同一作用域
-
函数名相同
-
参数列表不同
void print(int i) {std::cout << "Printing int: " << i << std::endl;
}void print(double f) {std::cout << "Printing float: " << f << std::endl;
}void print(const std::string &s) {std::cout << "Printing string: " << s << std::endl;
}
在这个例子中,print
函数被重载了三次,分别接受 int
、double
和 std::string
类型的参数。
重写 (Overriding)
重写是面向对象编程中的一个概念,指的是派生类中的函数重写了基类中具有相同名称、相同参数列表、相同返回值(或协变)的虚函数。重写用于实现运行时多态。
-
两个函数分别在基类和派生类的作用域
-
函数名、参数列表、返回值必须相同(协变、虚析构函数除外)
-
两个函数必须是虚函数
class Base {
public:virtual void display() {std::cout << "Display Base class" << std::endl;}
};class Derived : public Base {
public:void display() override { // 使用override确保正确重写std::cout << "Display Derived class" << std::endl;}
};
在这个例子中,Derived
类重写了 Base
类中的 display
函数。
重定义 (Hiding or Redefinition)
重定义发生在派生类中,当派生类定义了一个与基类同名的成员(不论参数列表是否相同),该成员会隐藏(或称为重定义)基类中所有同名的成员,不论其参数列表。这不是多态,而是名字隐藏。
-
两个函数分别在基类和派生类的作用域
-
函数名相同
-
不构成重写
class Base {
public:void display() {std::cout << "Display Base class" << std::endl;}
};class Derived : public Base {
public:void display(int i) { // 重定义,隐藏了基类的display()std::cout << "Display Derived class with int: " << i << std::endl;}
};
在这个例子中,尽管 Derived
类中的 display
函数的参数列表与基类中的不同,它仍然隐藏了基类中的 display()
函数。
小结论
重载
-
两个函数在同一作用域
-
函数名相同
-
参数列表不同
重写
-
两个函数分别在基类和派生类的作用域
-
函数名、参数列表、返回值必须相同(协变、虚析构函数除外)
-
两个函数必须是虚函数
重定义
-
两个函数分别在基类和派生类的作用域
-
函数名相同
-
不构成重写
结尾
最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。
同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。
谢谢您的支持,期待与您在下一篇文章中再次相遇!