当前位置: 首页 > news >正文

十五、多态与虚函数

十五、多态与虚函数

15.1 引言

  • 面向对象编程的基本特征:数据抽象(封装)、继承、多态
  • 基于对象:我们创建类和对象,并向这些对象发送消息
  • 多态(Polymorphism):指的是相同的接口、不同的实现。简单来说,多态允许不同的类对象同一个函数调用 做出 不同的响应
  • 动态多态性(Dynamical polymorphism) 是通过 虚函数 实现的。虚函数是迈向真正 面向对象编程(OOP) 的关键一步

多态的分类

  1. 编译时多态(静态多态/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版本
      }
      
  2. 运行时多态(动态多态/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)。

注意

  1. 虚函数 是一个 非静态成员函数

  2. 如果一个虚函数是在类体外定义的,那么关键字 virtual 只在声明时需要写明。

    class Instrument{
    public:virtual void play() const;
    };
    void Instrument::play() const {cout << "Instrument::play" endl;
    }
    
  3. 当使用 **作用域解析运算符:: ** 时,虚函数机制将不会被使用

    ………………
    void tune(Instrumnet& i){//……i.Instrument::play();	//显示调用基类版本,禁用虚机制
    }void main(){Wind flute;tune(flute);
    }
    

    输出

    Instrument::play
    
  4. 在派生类中,如果要重写基类的虚函数,那么要重写的函数的类型必须与基类中虚函数的类型完全相同,这样才称为 重写(Overriding) 基类版本的虚函数。

  5. 如果类型不相同,那么这叫做重定义,是在派生类里面重定义了一个全新的函数,就不会重写基类的虚函数,而是会名字隐藏基类的虚函数。

  6. 要实现多态行为:

    • 派生类必须是 公有继承(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!'
Pet vtable
&Pet::name
&Pet::speak
Dog vtable
&Pet::name
&Dog::speak
&Dog::sit

对象切片(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)
  • 纯虚函数、抽象类、纯抽象类

相关文章:

  • 在MyBatis Plus里处理LocalDateTime类型
  • Termius ssh连接服务器 vim打开的文件无法复制问题
  • 【Java ee初阶】IP协议
  • 进程和线程
  • GTC2025——英伟达布局推理领域加速
  • 什么是Vim
  • 神经生物学+图论双buff,揭示大脑语言系统的拓扑结构
  • 探秘高可用负载均衡集群:企业网络架构的稳固基石
  • EnumUtils:你的枚举“变形金刚“——让枚举操作不再手工作业
  • ARM-CortexM固件升级相关问题研究
  • 模型上下文协议(MCP):AI的“万能插座”
  • Matplotlib 完全指南:从入门到精通
  • 负载均衡 ELB 在 zkmall开源商城高流量场景下的算法优化
  • 高并发内存池(三):TLS无锁访问以及Central Cache结构设计
  • [ARM][汇编] 01.基础概念
  • CentOS 和 RHEL
  • Java学习手册:服务网关与路由
  • 电子电器架构 --- 借力第五代架构,驱动汽车产业创新引擎
  • 关于mac配置hdc(鸿蒙)
  • 【25软考网工】第六章(4)VPN虚拟专用网 L2TP、PPTP、PPP认证方式;IPSec、GRE
  • 王毅人民日报撰文:共商发展振兴,共建中拉命运共同体
  • 生态环境保护督察工作条例对督察对象和内容作了哪些规定?有关负责人答问
  • 中国潜水救捞行业协会发布《呵护潜水员职业健康安全宣言》
  • 体坛联播|巴萨4比3打服皇马,利物浦2比2战平阿森纳
  • 观察|天空之外的战场:官方叙事、新闻与社交平台中的印巴冲突
  • 印控克什米尔地区再次传出爆炸声