【C++】 多态与虚函数

总结1——多态

(1) 通过基类的指针既可以访问基类的成员,也可以访问派生类的成员

(2) 试图使用不变的代码来实现可变的算法让父类的指针有 “多种形态”

(2) 用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数

构成多态的条件:

  • 必须存在继承关系;
  • 继承关系中必须有同名的虚函数,并且它们是覆盖关系(函数原型相同)。
  • 存在基类指针,通过该指针调用虚函数。

总结2——什么时候声明虚函数

先看成员函数所在的类是否会作为基类。
再看成员函数在类的继承后有无可能被更改功能。

如果希望更改其功能的,一般应该将它声明为虚函数。如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。

总结3——虚函数的注意事项。

(1) 只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加。
(2) 为了方便,可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽(覆盖)关系的同名函数都将自动成为虚函数。
(3) 当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。
(4) 只有派生类的虚函数覆盖基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)

例如基类虚函数的原型为

virtual void func();

派生类虚函数的原型为

virtual void func(int);

那么当基类指针 p 指向派生类对象时,语句

p -> func(100);

将会出错,而语句

p -> func();

将调用基类的函数。

(5) 构造函数不能是虚函数

对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于继承。也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义。

(6) 析构函数可以声明为虚函数,而且有时候必须要声明为虚函数

01 虚函数的简介

由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为 “虚” 函数。

用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数

这种技术可以让父类的指针有 “多种形态”,这是一种泛型技术。

所谓泛型技术,就是试图使用不变的代码来实现可变的算法。

虚函数主要通过V-Table虚函数表来实现,该表主要包含一个类的虚函数的地址表,可解决继承、覆盖的问题。

当使用一个父类的指针去操作一个子类时,虚函数表就像一个地图一样,可指明实际所应该调用的函数。

(每一个virtual函数的class都有一个相应的 vtbl,当对象调用某一 virtual 函数,实际被调用的函数取决于该对象的 vptr 所指的那个 vtbl ——编译器在其中寻找适当的函数指针。)

防止多重派生时,使用指针调用同名函数时已基类函数(父类)为准。

02 虚函数的核心概念

某基类中声明为 virtual 并在一个或多个派生类中重新定义的成员函数叫做虚函数。

03 虚函数的核心作用

(1) 实现动态联编,在函数运行阶段动态的选择合适的成员函数。

(2) 在定义了虚函数后,可实现在派生类中对虚函数进行重写,从而实现统一的接口和不同的执行过程。

通过三个虚函数的应用实例来说明虚函数的主要作用。

🌰 继承关系中非虚函数
#include <iostream>

class Base{
public:
    Base(){};
    ~Base(){};
    void show(void){
        std::cout << "基类成员函数 show ......" << std::endl;
    }
};

class Derive:public Base{
public:
    Derive(){};
    ~Derive(){};
    void show(void){
        std::cout<< "派生类成员函数 show ......" << std::endl;    
    }        
};

int main(){
    // 基类实例化
    Base atr;
    // 派生类实例化
    Derive btr;
    
    // 定义基类指针
    Base *ptr;
    ptr = &atr;
    ptr->show();
    
    ptr = &btr;
    ptr->show();
    return 0;
}

输出结果

基类 show ......
基类 show ......
Note

即使 ptr 指向 B 对象后,第二个 show() 的结果也并不是 “I am B!”,

主要原因该程序使用的是静态联编,而静态联编选择函数是基于指向对象的指针类型,而 ptr 是指向 Base 的指针,因此也会一直调用 Base 的成员函数 show()。

🌰-1 继承关系中虚函数
#include <iostream>

class Base{
public:
    Base(){
			std::cout << "Base Constructor Function" << std::endl;
		}
    ~Base(){
			std::cout << "Base Destructor Function" << std::endl;
		}
    virtual void show(void){
        std::cout << "基类成员函数 show ......" << std::endl;
    }
};

class Derive:public Base{
public:
    Derive(){
			std::cout << "Derive Constructor Function" << std::endl;
		}
    ~Derive(){
			std::cout << "Derive Destructor Function" << std::endl;
		}
    void show(void){
        std::cout<< "派生类成员函数 show ......" << std::endl;    
    }        
};

int main(){
    // 基类实例化
    Base base;
    // 定义基类指针
    Base *ptr_base;
    ptr_base= &base;
    ptr_base->show();
    std::cout << "----------------------------" << std::endl;
    // 派生类实例化
    Derive* pb = new Derive(); 
    pb->show();
    delete pb;

    return 0;
}

输出结果

Base Constructor Function
基类成员函数 show ......
----------------------------
Base Constructor Function
Derive Constructor Function
派生类成员函数 show ......
Bae Destructor Function
Base Destructor Function
🌰-2 继承关系中虚函数
#include <iostream>

class Base{
public:
    Base(){
			std::cout << "Base Constructor Function" << std::endl;
		}
    virtual ~Base(){
			std::cout << "Base Destructor Function" << std::endl;
		}
    virtual void show(void){
        std::cout << "基类成员函数 show ......" << std::endl;
    }
};

class Derive:public Base{
public:
    Derive(){
			std::cout << "Derive Constructor Function" << std::endl;
		}
    ~Derive(){
			std::cout << "Derive Destructor Function" << std::endl;
		}
    void show(void){
        std::cout<< "派生类成员函数 show ......" << std::endl;    
    }        
};

int main(){
    // 基类实例化
    Base base;
    // 定义基类指针
    Base *ptr_base;
    ptr_base= &base;
    ptr_base->show();
    std::cout << "----------------------------" << std::endl;
    // 派生类实例化
    Derive* pb = new Derive(); 
    pb->show();
    delete pb;

    return 0;
}

输出结果

Base Constructor Function
基类成员函数 show ......
----------------------------
Base Constructor Function
Derive Constructor Function
派生类成员函数 show ......
Derive Destructor Function
Base Destructor Function
Note

在基类 Base 中对 show() 这一成员函数变为 virtual 虚函数后,会采用动态编译,只有调用它时才用根据它的对象类型去匹配对应的函数体。

🌰 非继承关系中虚函数
#include<iostream>

class A{
public:
    A(){};
    ~A(){};
    virtual void show(void){
        std::cout << "I am A!"<< std::endl;
    }
};
class B{
public:
    B(){};
    ~B(){};
    virtual void show(void){
        std::cout << "I am B!" << std::endl;    
    }        
};

int main(){
    std::cout << "hello world"<< std::endl;
    A atr, *ptr;
    B btr;
    
    ptr = &atr;
    ptr->show();
    
    ptr = (A*)&btr;
    ptr->show();
    
    system("pause>nul");
}
小结

(1) 在使用继承的方式实现运行时多态时,基类需要将与派生类相同函数名的函数加上 virtual 关键字,这样才可以在运行时精准识别出子类的虚函数。

(2) 如果要不通过继承关系也实现出运行时多态的效果,则需要将两个不同类的同名函数都加上 virtual 关键字;同时,需要将定义的指针指向其他对象时,要进行强制类型转换。(因为两个类已经没有继承关系了,不能通过赋值兼容规则进行自动转换,所以要强制转化。)

(3) 带有多态性质的基类均应该声明一个virtual析构函数。同时如果任一class带有任何virtual函数,它就应该拥有一个virtual析构函数。

(4) 当 class 的设计目的如果不是作为 base class 使用,或不是为了具备多态性,则就不该声明 virtual 析构函数。

因为如果 class 中含有 virutal 函数会使得该 class 的体积增加,因为添加一个vptr(virtual table pointer)会增加其 class 大小达 50%-100%。

拓展问题:

(1) 为什么调用普通函数比调用虚函数的效率高?

因为普通函数是静态联编的,而调用虚函数是动态联编的。
联编的作用:程序调用函数,编译器决定使用哪个可执行代码块。(所谓联编就是将函数名和函数体的程序连接到一起的过程)
静态联编 :在编译的时候就确定了函数的地址,然后call就调用了。

静态联编本质是系统用实参与形参进行匹配,对于重名的重载函数根据参数上的差异进行区分,然后进行联编,从而实现编译时的多态。函数的选择基于指向对象的指针类型或者引用类型。

动态联编 : 首先需要取到对象的首地址,然后再解引用取到虚函数表的首地址后,再加上偏移量才能找到要调的虚函数,然后call调用。

动态联编本质上是运行阶段执行的联编,当程序调用某一个函数时,系统会根据当前的对象类型去寻找和连接其程序的代码。函数的选择基于对象的类型。

(2) 为什么要用虚函数表(存函数指针的数组)?

同一个类的多个对象的虚函数表是同一个,所以这样就可以节省空间,一个类自己的虚函数和继承的虚函数还有重写父类的虚函数都会存在自己的虚函数表。

同时,虚函数表本质是一个地图导航,可以清楚告诉一个想要操作子类的父类指针到底该使用哪个函数。

(3) 为什么要把基类的析构函数定义为虚函数?
C++ 虚析构函数
构造函数不能是虚函数
原因1

派生类不能继承基类的构造函数,将构造函数声明为虚函数没有意义。

原因2

C++ 中的构造函数用于在创建对象时进行初始化工作,在执行构造函数之前对象尚未创建完成,虚函数表尚不存在,也没有指向虚函数表的指针,所以此时无法查询虚函数表,也就不知道要调用哪一个构造函数。

析构函数可以是或有时必须是析构函数

析构函数用于在销毁对象时进行清理工作,可以声明为虚函数,而且有时候必须要声明为虚函数。

在用基类操作派生类时,为了防止执行基类的析构函数,不执行派生类的析构函数。

因为这样的删除只能够删除基类对象, 而不能删除子类对象, 形成了删除一半形象, 会造成内存泄漏。

🌰 析构函数可以是或有时必须是析构函数
#include <iostream>

class Base{
public:
    Base(){
        std::cout << "基类构造函数 ..." << std::endl;
        base_ptr = new int[100];
    }
    ~Base(){
        std::cout << "基类析构函数 ......" << std::endl;
        delete[] base_ptr;
    }
protected:
    int* base_ptr;
};

class Derive:public Base{
public:
    Derive(){
        std::cout << "Derive 构造函数 ..." << std::endl;
        derive_ptr = new int[100];
    }
    ~Derive(){
        std::cout<< "Derive 析构函数 ......" << std::endl;
        delete[] derive_ptr;
    }   
private:
    int* deprive_ptr;
};

int main(){
    Base* pb = new Derive();
    delete pb;
    
    std::cout << "---------------------" << std::endl;
    
    Derive* pd = new Derive();
    delete pd;
    
    return 0;
}

输出结果

基类构造函数 ...
派生类构造函数 ...
基类析构函数 ......
---------------------
基类构造函数 ...
派生类构造函数 ...
派生类析构函数 ......
基类析构函数 ......

上栗中定义了两个类,基类 Base 和派生类 Derive,它们都有自己的构造函数和析构函数。

在构造函数中,会分配 100 个 int 类型的内存空间;在析构函数中,会把这些内存释放掉。

pb、pd 分别是基类指针和派生类指针,它们都指向派生类对象,最后使用 delete 销毁 pb、pd 所指向的对象。

从运行结果可以看出,语句delete pb;只调用了基类的析构函数,没有调用派生类的析构函数;而语句delete pd;同时调用了派生类和基类的析构函数。

不调用派生类的析构函数会导致 derive_ptr 指向的 100 个 int 类型的内存空间得不到释放;除非程序运行结束由操作系统回收,否则就再也没有机会释放这些内存。这是典型的内存泄露。

(1) 为什么 delete pb;不会调用派生类的析构函数呢?

因为这里的析构函数是非虚函数,通过指针访问非虚函数时,编译器会根据指针的类型来确定要调用的函数;

也就是说,指针指向哪个类就调用哪个类的函数

pb 是基类的指针,所以不管它指向基类的对象还是派生类的对象,始终都是调用基类的析构函数。

(2) 为什么delete pd;会同时调用派生类和基类的析构函数呢?

pd 是派生类的指针,编译器会根据它的类型匹配到派生类的析构函数,在执行派生类的析构函数的过程中,又会调用基类的析构函数。

派生类析构函数始终会调用基类的析构函数,并且这个过程是隐式完成的

更改上面的代码,将基类的析构函数声明为虚函数:

#include <iostream>

class Base{
public:
    Base(){
        std::cout << "基类构造函数 ..." << std::endl;
        base_ptr = new int[100];
    }
    virtual ~Base(){
        std::cout << "基类析构函数 ......" << std::endl;
        delete[] base_ptr;
    }
protected:
    int* base_ptr;
};

class Derive:public Base{
public:
    Derive(){
        std::cout << "派生类构造函数 ..." << std::endl;
        derive_ptr = new int[100];
    }
    ~Derive(){
        std::cout<< "派生类析构函数 ......" << std::endl;
        delete[] derive_ptr;
    }   
private:
    int* deprive_ptr;
};

int main(){
    Base* pb = new Derive();
    delete pb;
    
    std::cout << "---------------------" << std::endl;
    
    Derive* pd = new Derive();
    delete pd;
    
    return 0;
}

输出结果

基类构造函数 ...
派生类构造函数 ...
派生类析构函数 ......
基类析构函数 ......
---------------------
基类构造函数 ...
派生类构造函数 ...
派生类析构函数 ......
基类析构函数 ......
将基类的析构函数声明为虚函数发生了什么变化?

将基类的析构函数声明为虚函数后,派生类的析构函数也会自动成为虚函数。

这个时候编译器会忽略指针的类型,而根据指针的指向来选择函数。

将基类的析构函数声明为虚函数,指针指向哪个类的对象就调用哪个类的函数。

pb、pd 都指向了派生类的对象,所以会调用派生类的析构函数,继而再调用基类的析构函数。如此一来也就解决了内存泄露的问题。

在实际开发中,一旦定义了析构函数,就是希望在对象销毁时用它来进行清理工作,比如释放内存、关闭文件等,如果这个类又是一个基类,那么,就必须将该析构函数声明为虚函数,否则就有内存泄露的风险。

也就是说,大部分情况下都应该将基类的析构函数声明为虚函数

注意,这里强调的是基类,如果一个类是最终的类,那就没必要再声明为虚函数了。

这里子类的析构函数重写了父类的析构函数,虽然子类和父类的析构函数名不一样,

但是,编译器对析构函数做了特殊处理,在内部子类和父类的析构函数名是一样的。

所以,如果不把父类的析构函数定义成虚函数,就不构成多态。

由于父类的析构函数隐藏了子类的析构函数,所以只能调到父类的析构函数。

但是,若把父类的析构函数定义成虚函数,那么,调用时就会直接调用子类的析构函数。

由于子类析构先要去析构父类,再析构子类,这样就会把父类和继承的子类都析构。

(4) 虚函数可以是内联的吗?

要多态的时候不内联,不多态的时候(也就是非指针、引用,也就是传值)可以内联。

(5) 多态的栗子

🌰

#include <iostream>

// 基类Base
class Base{
public:
    virtual void func(){
        std::cout<<"void Base::func()"<<std::endl;
    }
    virtual void func(int){
        std::cout<<"void Base::func(int)"<<std::endl;
    }
};

// 派生类 Derived
class Derived: public Base{
public:
    void func(){
        std::cout<<"void Derived::func()"<<std::endl;
    }
    void func(char *){
        std::cout<<"void Derived::func(char *)"<<std::endl;
    }
};

int main(){
    Base *p = new Derived();
    
    std::cout << "p -> func() 输出:" << std::endl;
    p -> func();  
    std::cout << std::endl;
    
    std::cout << "p -> func(10) 输出:" << std::endl;
    p -> func(10); 
    std::cout << std::endl;
    
    std::cout << "p -> func("hello world") 输出:" << std::endl;
    p -> func("hello world");  

    return 0;
}

输出结果

p -> func() 输出:
void Derived::func()

p -> func(10) 输出:
void Base::func(int)

p -> func("hello world") 输出:
compile error

在基类 Base 中,将void func()声明为虚函数,这样派生类 Derived 中的void func()就会自动成为虚函数。p 是基类 Base 的指针,但是指向了派生类 Derived 的对象。

(1) 语句

p -> func();

调用的是派生类的虚函数,构成了多态。

(2) 语句

p -> func(10);

调用的是基类的虚函数,因为派生类中没有函数覆盖它。

(3) 语句

p -> func("hello world");

编译错误,因为通过基类的指针只能访问从基类继承过去的成员,不能访问派生类新增的成员。

参考链接

原文链接1:https://blog.csdn.net/weixin_38537639/article/details/117821816

原文链接2:http://c.biancheng.net/cplus/polymorphism/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值