十五、多态与虚函数
十五、多态与虚函数
15.1 引言
- 面向对象编程的基本特征:数据抽象(封装)、继承、多态
- 基于对象:我们创建类和对象,并向这些对象发送消息
- 多态(Polymorphism):指的是相同的接口、不同的实现。简单来说,多态允许不同的类对象 对 同一个函数调用 做出 不同的响应 。
- 动态多态性(Dynamical polymorphism) 是通过 虚函数 实现的。虚函数是迈向真正 面向对象编程(OOP) 的关键一步
多态的分类
-
编译时多态(静态多态/Static Polymorphism)
-
特点:在编译阶段就能确定调用哪个函数
-
实现方式:
- 函数重载(Function Overloading)
- 运算符重载(Operator Overloading)
-
示例:
#include <iostream> using namespace std; void print(int x){cout << x << endl;} void print(double x){cout << x << endl;}int main(){print(5); //在编译时就知道调用int版本print(9.8);//在编译时就知道调用double版本 }
-
-
运行时多态(动态多态/Dynamical polymorphism)
-
特点:在程序运行时决定调用哪个函数,是程序运行起来后根据实际对象决定的。
-
实现方式:虚函数(virtual function) + 基类指针或引用
-
示例:
#include <iostream> using namespace std; class Animal { public:virtual void speak() { cout << "Animal sound" << endl; } }; class Dog :public Animal { public:void speak() override { cout << "Woof!" << endl; } };void makeSound(Animal* a) {a->speak(); //在运行时才知道是哪个版本 } int main() {Animal a;Dog d;makeSound(&a);//输出"Animal sound"makeSound(&d);//输出 "Woof!",调用的是子类的函数 }
-
15.2 向上转型(Upcasting)
- 当通过指针或引用(指向或引用基类)操作时,派生类的对象可以被当作其基类对象 来处理。
- 向上转型(Upcasting): 获取一个对象的地址(无论是指针 还是 引用),并将其当作 基类类型 使用,就叫做向上转型(Upcasting)。
- 也就是说,” 新类是现有类的一种类型 “。
示例
class Instrument {
public:void play() const {}
};
//Wind是Instrument的派生类
class Wind :public Instrument {};
void tune(Instrument& i) { i.play(); }
void main() {Wind flute;tune(flute); //向上转型(Upcasting)Instrument* p = &flute;//UpcastingInstrument& l = flute;//Upcasting
}
这里将一个 Wind 类型的引用或指针 转换为 一个 Instument 类型的引用或指针的行为,就是向上转型(Upcasting)。
下面给出一个有疑问的示例
#include <iostream>
using namespace std;
class Instrument {
public:void play() { cout << "Instrument::play" << endl; }
};
class Wind:public Instrument {
public://重新定义接口函数void play() const { cout << "Wind::play" << endl; }
};void tune(Instrument& i) { i.play(); }void main() {Wind flute;tune(flute);//向上转型
}
输出
Instrument::play
问题
- 此调用本应产生
Wind::play
,但实际调用了Instrument::play
。 - 为了解决这个问题,我们需要使用 虚函数(virtual function) 来解决这个问题。
15.3 虚函数(virtual functions)
什么是虚函数
-
格式:
virtual type function-name(arguments);
-
如果一个函数在其基类中被声明为
virtual
, 那么它在所有派生类中也是virtual
的。 -
在派生类中重新定义一个
virtual
函数,通常称之为重写(Overriding)
。 -
多态(Polymorphism):
同名但不同实现的函数。虚函数(通过重写)是动态决定 调用哪一个函数的。
-
函数重载(Function overloading):
是静态决定调用哪个版本的函数。
示例 C15:Instrument2.cpp
//C15:Instrument2.cpp
#include <iostream>
using namespace std;class Instrument{
public:virtual void play() const{cout << "Instrument::play" << endl;}
};
class Wind:public Instrument{
public://重写虚函数virtual void play() const //省略"virtual"是可以的{cout << "Wind::play" << endl;}
};void tune(Instrument& i){ i.play();}void main(){Wind flute;tune(flute);//向上转型
}
输出
Wind::play
这样就达到我们的期望了——Wind::play
可扩展性
- 如果在基类中将
play()
定义为 虚函数(virtual) ,那么我们可以在不修改tune()
函数的前提下,添加任意多的新类型。 - 在一个设计良好的面向对象程序中,我们的大多数甚至全部函数,都会遵循
tune()
的模式,并只通过基类接口进行通信。这样的程序是可扩展的,因为我们可以通过从共同的基类继承新的数据类型 来添加新功能。
示例 Extensibility in OOP
//Extensibility in OOP
#include <iostream>
#include <string>
using namespace std;
class Instrument {
public:virtual void play() const { cout << "Instrument::play" << endl; }virtual string what() const { return "Instrument"; }//下面这个函数会修改对象virtual void adjust(int) {}
};class Wind :public Instrument {
public:void play() const { cout << "Wind::play" << endl; }string what() const { return "Wind"; }void adjust(int) {}
};class Stringed :public Instrument {
public:void play() const { cout << "Stringed::play" << endl; }string what() const { return "Stringed"; }
};class Brass :public Wind {
public:void play() const { cout << "Brass::play" << endl; }string what() const { return "Brass"; }
};void tune(Instrument& i) { i.play(); }void f(Instrument& i) { i.adjust(1); }int main() {Wind flute;Stringed violin;Brass horn;tune(flute);//Wind::play;tune(violin);//Stringed::play;tune(horn);//Brass::playf(horn);//Wind::adjustreturn 0;
}
- 我们可以看到,
virtual(虚函数)
机制 无论有多少层继承都能正常运行。 adjust()
函数在Brass
类中没有别重写。当这种情况发生时,继承层次结构中 ”最近的“‘定义会被自动使用(即:Wind::adjust
)。
注意
-
虚函数 是一个 非静态成员函数 。
-
如果一个虚函数是在类体外定义的,那么关键字
virtual
只在声明时需要写明。class Instrument{ public:virtual void play() const; }; void Instrument::play() const {cout << "Instrument::play" endl; }
-
当使用 **作用域解析运算符
::
** 时,虚函数机制将不会被使用。……………… void tune(Instrumnet& i){//……i.Instrument::play(); //显示调用基类版本,禁用虚机制 }void main(){Wind flute;tune(flute); }
输出
Instrument::play
-
在派生类中,如果要重写基类的虚函数,那么要重写的函数的类型必须与基类中虚函数的类型完全相同,这样才称为 重写(Overriding) 基类版本的虚函数。
-
如果类型不相同,那么这叫做重定义,是在派生类里面重定义了一个全新的函数,就不会重写基类的虚函数,而是会名字隐藏基类的虚函数。
-
要实现多态行为:
- 派生类必须是 公有继承(public) 自基类
- 被调用的成员函数必须是虚函数
- 必须通过指针或引用来操作对象。(如果是直接操作对象而不是通过指针或引用,编译器在编译时就已知对象的确切类型,因此不需要运行时多态机制)
示例
#include <iostream>
using namespace std;
class A{
public:virtual void f1(){cout << "A::f1" << endl;}virtual void f2(){cout << "A::f2" << endl;}void f3() {cout << "A::f3" << endl;}void f4() {cout << "A::f4" << endl;}
};class B:public A
{
public:virtual void f1() //虚函数的重写{cout << "B::f1" << endl;}virtual void f2(int) //新定义了一个虚函数{cout << "B::f2" << endl;}virtual void f3() //在B中,f3是虚函数,但在A,f3并不是{cout << "B::f3" << endl;}void f4() //重定义{cout << "B::f4" << endl;}
};
下面是针对上面示例的不同main()
函数测试
main1
void main(){B b;A* p = &b;p->f1();p->f2();p->f3();p->f4();
}
输出
B::f1
A::f2
A::f3
A::f4
main2
void main(){B b;A& a1 = b;a1.f1();a1.f2();a1.f3();a1.f4();
}
输出
B::f1
A::f2
A::f3
A::f4
main3
void main(){B b;A a = b;a.f1();a.f2();a.f3();a.f4();
}
输出
A::f1
A::f2
A::f3
A::f4
15.4 C++ 如何实现晚绑定(late binding)
绑定: 在C++中,”绑定“”就是指函数调用与实际执行代码之间建立联系的过程。
绑定有两种:
类型 | 绑定时间 | 说明 |
---|---|---|
早绑定(Early Binding) | 编译时决定 | 编译器在编译时就知道要调用哪个函数。适用于普通函数、非虚函数等。效率高,但不灵活 |
晚绑定(Late Binding) | 运行时决定 | 编译时不确定,运行时根据对象的实际类型决定调用哪个函数。适用于虚函数。灵活,支持多态 |
C++晚绑定的机制
- 编译器为每个包含 虚函数 的类创建一个 虚函数表(VTABLE)。
- 编译器会将类的所有虚函数地址存放到它对应的 虚函数表 中。
- 在每个包含虚函数的类中,编译器会 偷偷地添加一个 VPTR 指针 ,它指向该对象所属类的 VTABLE。
- 为每个类设置 VTABLE ,初始化 VPTR ,插入虚函数调用代码——这些操作都会自动完成。
- 当我们用基类指针(或引用)指向派生类对象时, 基类中的 虚指针(vptr)会被设置为指向派生类的虚函数表(vtable),
以便实现动态多态(运行时绑定)。
//C15:Early & Late Binding.cpp
#include <iostream>
#include <string>
using namespace std;
class Pet{
public:virtual string speak() const{return "Pet::speak";}
};class Dog:public Pet{
public:virtual string speak() const {return "Dog::speak";}
};void main(){Dog ralph;Pet* p1 = &ralph; //有类型歧义Pet& p2 = ralph; //有类型歧义Pet p3 = ralph; //无类型歧义//晚绑定cout << p1->speak() << endl;cout << p2.speak() << endl;//早绑定cout << p3.speak() << endl;
}
输出
Dog::speak
Dog::speak
Pet::speak
15.5 为什么使用虚函数(virtual functions)
- 虚函数是一种选择(并不是强制的)。
virtual
关键字的设计,是为了方便效率优化。当我们想提升代码执行速度时,只需要查找哪些函数可以改为非虚函数即可。
15.6 抽象基类与纯虚函数
-
有时候我们希望基类仅仅作为接口供其派生类使用,而不希望任何人实际创建这个基类的对象。
-
一个 抽象类(abstract class) 至少包含一个 纯虚函数(pure virtual function)。并且抽象类不可以拿来创建对象。
-
纯虚函数: 使用
virtual
关键字,并且以= 0
结尾。 -
纯抽象类(pure abstract class): 是指其中只包含纯虚函数,没有其他函数实现。也不可以拿来创建对象。
-
不能创建抽象类的对象。
-
将一个类设计为抽象类,可以确保在向上转型时只能通过指针或引用来使用这个类。
-
当一个抽象类别继承时,所有纯虚函数都必须在子列中实现(也就要定义),否则这个子类也会变成为一个抽象类。
#include <iostream>
using namespace std;
class Point //抽象类(不是纯抽象类)
{
public:Point(int i = 0,int j = 0) { x0 = i; y0 = j; }virtual void Set() = 0;virtual void Draw() = 0;
protected:int x0, y0;
};class Line :public Point
{
public:Line(int i = 0, int j = 0, int m = 0, int n = 0) :Point(i, j){x1 = m; y1 = n;}virtual void Set(){cout << "Line::Set() called." << endl;}virtual void Draw(){cout << "Line::Draw()." << endl;}
protected:int x1, y1;
};
//抽象类
class Ellipse :public Point
{
public:Ellipse(int i = 0, int j = 0, int p = 0, int q = 0) :Point(i, j){x2 = p; y2 = q;}
protected:int x2, y2;
};void main() {Line line(0, 1);//Elipse elipse(0,1,2,3);//错误,因为Wllipse是抽象类Point& p = line;p.Set();p.Draw();
}
输出
Line::Set() called.
Line::Draw().
15.7 继承与虚函数表(VTABLE)
虚函数表
- 编译器会为派生类自动创建一个新的 虚函数表(VTABLE) ,并将你新重写的函数地址插入其中。对于那些没有重写的虚函数 ,则使用基类中的函数地址。
//C15:AddingVirtuals.cpp
#include <iostream>
#include <string>
using namespace std;
class Pet {string pname;
public:Pet(const string& petName) :pname(petName) {}virtual string name() const { return pname; }virtual string speak() const { return ""; }
};class Dog :public Pet {string name;
public:Dog(const string& petName) :Pet(petName) {}virtual string sit() const { return Pet::name() + "sits"; } //新的虚函数string speak () const { return Pet::name() + " says 'Bark!'"; }//重写虚函数
};
int main() {Pet* p[] = { new Pet("generic"),new Dog("bob") };//创建一个Pet*数组cout << "p[0]->speak() = " << p[0]->speak() << endl;cout << "p[1]->speak() = " << p[1]->speak() << endl;//!cout << "p[1]->sit() = " << p[1]->sit() << endl;//非法,因为Pet型指针的Dog,会在Pet的虚函数表里面找sit(),但这是找不到的delete p[0];delete p[1];return 0;
}
输出
p[0]->speak() =
p[1]->speak() = bob says 'Bark!'
对象切片(Object slicing)
- 通过地址进行向上转型是自动且安全的,但通过值进行向上转型是不安全的。(向下转型同样不安全)
- 对象切片: 对象切片会在复制对象到新对象时丢失原有对象的一部分信息(即派生类特有的部分会被“切掉”)。
- 如果你将对象向上转型为另一个对象 (而不是指针或引用),就会发生对象切片。
//C15:ObjectSlicing.cpp
#include <iostream>
#include <string>
using namespace std;class Pet{string pname;
public:Pet(const string& name):pname(name){}virtual string name() const{return pname;}virtual string description() const{return "This is " + pname;}
};class Dog:public Pet{string favoriteActivity;
public:Dog(const string& name,const string& activity):Pet(name),favoriteActivity(activity){}virtual string description() const{return Pet::name() + "likes to " + favoriteActivity;}
};void describe(Pet a) //对象切片
{cout << a.description() << endl;
}
void main(){Pet p("Zhang");Dog d("Li","sleep");describe(p);describe(d);//Pet::pname::pname,name(),description()
}
输出
This is Zhang
This is Li
这里就要对
This is Li
这段文字产生提出问题,我们并不希望这样,因此需要将desctibe()
函数进行修改
void describe(Pet& a)
{cout << a.description() << endl;
}
输出
This is Zhang
Lilikes to sleep
15.8 重载和重写(Overloading & Overriding)
- 在派生类中,如果我们重写或者重新定义了基类中某个重载的成员函数,那么基类里其他重载版本将会被隐藏。
- 编译器不允许我们通过更改基类虚函数的返回类型来**“重新定义”**该函数。
//C15:NameHiding2.cpp
#include <iostream>
#include <string>
using namespace std;
class Base {
public:virtual int f() const { cout << "Base::f()" << endl; return 1; }virtual void f(string) const {}virtual void g() const {}
};class Derived1 :public Base
{
public:void g() const {}
};class Derived2 :public Base {
public://重写int f() const { cout << "Derived2::f()" << endl; return 2; }
};class Derived3 :public Base {
public://不被允许,因为它在通过改变return类型来重新定义基类的虚函数f()//!void f() const { cout << "Derived3::f()" << endl; }
};class Derived4 :public Base {
public://重新定义,因为改变了参数列表int f(int) const { cout << "Derived4::f()" << endl; return 4; }
};int main() {string s("Hello");Derived1 d1;int x = d1.f(); //调用Base的int f()d1.f(s); //调用Base的void f(string)Derived2 d2;x = d2.f(); //调用Derived2的int f()//!d2.f(s);//Derived2的int f()将其他版本隐藏了Derived3 d3;d3.f();//调用Base的int f()Derived4 d4;x = d4.f(1);//!x = d4.f();//Base的f()版本都被隐藏//!d4.f(s);//void f(string)被隐藏Base& br = d4;//向上转型//!!br.f(1);//因为转型到了Base,所以派生类的非虚函数不能在使用br.f();//可以使用Base版本br.f(s);
}
输出
Base::f()
Derived2::f()
Base::f()
Derived4::f()
Base::f()
15.9 虚函数和构造函数
-
当一个包含虚函数的对象被创建时,它的虚函数指针(VPTR)必须被初始化为指向正确的虚函数表(VTABLE)。
-
构造函数负责初始化这个虚函数指针(VPTR)。
-
构造函数不能是虚函数。
原因:构造函数负责设置 VPTR,但虚函数调用又依赖 VPTR,所以构造函数不能是 虚函数,否则逻辑自相矛盾。如果构造函数是虚函数,那调用构造函数需要去使用 VPTR,那使用VPTR又需要去调用构造函数,就像“先有鸡还是先有蛋”一样,会是悖论。
15.10 虚函数和析构函数
- 析构函数可以是虚函数,而且通常必须是虚函数(如果要通过“基类或指针”来删除派生类对象)。
- 如果基类中的析构函数被声明为
virtual
,那么即使派生类的析构函数没有加virtual
关键字,它们也依然是virtual
的。 - 这是为了确保析构函数能够被准确地调用。
下面给出原因
示例
#include <iostream>
using namespace std;class A {
public:A() { cout << "A::A()" << endl; }~A() { cout << "A::~A()" << endl; }
};class B :public A
{
public:B(int i) {buf = new char[i];cout << "B::B(int)" << endl;}~B() {delete[]buf;cout << "B::~B() called." << endl;}
private:char* buf;
};void main() {A* a = new B(15);delete a;
}
输出
A::A()
B::B(int)
A::~A()
**问题:**我们通过基类指针删除派生类对象,但 A::~A()
不是虚函数。
所以编译器在执行 delete p;
时:
- 只知道
a
是个A*
类型 - 查的是
A
的析构函数 -->~A()
,不是虚函数 - 所以它不会进行“虚调用”跳转到
~B()
- 最终只调用
A::~A()
,而B::~B()
根本没被调用
后果: 导致buf
的内存没有被释放——内存泄漏。
那么为什么呢?原因就主要在于它不会进行“虚调转”跳转到~B()
虚调用 的跳转
-
虚函数的跳转:当通过基类指针或引用调用虚函数时,程序会根据对象的真实类型,通过虚函数表(vtable)**跳转到**最派生类的函数版本。
-
正向跳转:调用一个虚函数时,跳转到最派生类重写的版本来执行。
特点:只跳一次,执行最底层的版本,不会执行上层版本。
示例
class A { public:virtual void f() { cout << "A::f" << endl; } };class B : public A { public:void f() override { cout << "B::f" << endl; } };class C : public B { public:void f() override { cout << "C::f" << endl; } };
当我们写:
A* p = new C(); p->f(); // 会执行谁?
这里会执行的是
C::f()
,不是A
的也不是B
的而是C
的,这就是因为虚函数的跳转。 -
反向跳转:析构对象时,虚函数表引导从最派生类开始,逐级调用所有析构函数(派生 → 基类)。
特点:逐级调用每一层的析构函数,不能省略。
示例
#include <iostream> using namespace std; class A { public:virtual void f() { cout << "A::f" << endl; }virtual ~A() { cout << "~A()" << endl; } };class B : public A { public:void f() override { cout << "B::f" << endl; }~B() { cout << "~B()" << endl; } };class C : public B { public:void f() override { cout << "C::f" << endl; }~C() { cout << "~C()" << endl; } }; void main() {A* a = new C();delete a; }
输出
~C() ~B() ~A()
所以我们如果要解决上述的那个问题:就需要将 A
的析构函数改成虚函数
#include <iostream>
using namespace std;class A {
public:A() { cout << "A::A()" << endl; }virtual ~A() { cout << "A::~A()" << endl; }
};class B :public A
{
public:B(int i) {buf = new char[i];cout << "B::B(int)" << endl;}~B() {delete[]buf;cout << "B::~B() called." << endl;}
private:char* buf;
};void main() {A* a = new B(15);delete a;
}
输出
A::A()
B::B(int)
B::~B() called.
A::~A()
注意:
- 最好避免在构造函数和析构函数中调用虚函数。
- 在构造函数或析构函数中调用虚函数时,不会启用运行时多态性。
#include <iostream>
using namespace std;
class Base{
public:Base(){cout << "Bse constructor start" << endl;call();//调用虚函数cout << "Bse constructor end" << endl;}virtual void call(){cout << "Base::call()" << endl;}virtual ~Base(){cout << "Base destructor start" << endl;call();//析构再次调用虚函数cout << "Base destructor end" << endl;}
};class Derived : public Base {
public:Derived() {cout << "Derived constructor\n";}void call() override {cout << "Derived::call()\n";}~Derived() {cout << "Derived destructor\n";}
};
int main() {Derived d;return 0;
}
输出
Base constructor start
Base::call()
Base constructor end
Derived constructor
Derived destructor
Base destructor start
Base::call()
Base destructor end
说明:
- 即使
Derived
中重写了call()
函数,构造函数和析构函数中调用的都是Base::call()
。 - 这是因为:
- 在构造
Base
的时候,Derived
还没构造完,因此不会使用它的虚函数表。 - 在析构
Base
时,Derived
已经开始析构,也不会再用它的虚函数表。
- 在构造
15.11 运算符重载
- 我们可以像其他成员函数一样,把运算符函数声明为虚函数。
15.12 向下转型
- 向下转型是不安全的
dynamic_cast
:会在运行时检查类型安全(前提是基类中至少有一个虚函数),如果转换失败会返回nullptr
(指针)或抛出异常(引用),相对更安全。static_cast
:不做运行时检查,速度快但风险大,只应在你确信类型匹配时使用。
15.13 总结
- 虚数调用的绑定方式:早期绑定、晚期绑定
- 虚函数、多态
- 向上转型(Upcasting)
- 重写(Overriding)、重载(Overloading)
- 纯虚函数、抽象类、纯抽象类