C++虚函数, 纯虚函数

首先看这么一个例子:

#include <iostream>
using namespace std;

class Animal{

public:
     void speak(){
        cout << "Animal speak" << endl;
    }

};

class Dog :public Animal{
    public:
    void speak(){
        cout << "Dog speak" << endl;
    }
};

class Cat :public Animal{
    public:
    void speak(){
        cout << "Cat speak" << endl;
    }
};

void doSpeak(Animal &animal){
    animal.speak();
}



int main(int argc, const char * argv[]) {
    // insert code here...
    
    Animal animal1;
    doSpeak(animal1);
    
    
    Dog dog;
    doSpeak(dog);
    
    Cat cat;
    doSpeak(cat);
    
    return 0;
}

打印结果是:

Animal speak
Animal speak
Animal speak

那么如何让子类调用自己定义的方法呢?在基类前面加 virtual

class Animal{

public:
    virtual void speak(){
        cout << "Animal speak" << endl;
    }

};

再次打印:

Animal speak
Dog speak
Cat speak

C++的多态性用一句话概括就是:

在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数,

如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。

virtual关键字在使用时有哪些限制?
1.这个函数必须是某一个类的成员函数,不可以是一个全局函数,否则会导致编译错误。
2.静态成员函数不能是虚函数 static成员函数是和类同生共处的,他不属于任何对象,使用virtual也将导致错误。
3.内联函数不能是虚函数 如果修饰内联函数 如果内联函数被virtual修饰,计算机会忽略inline使它变成存粹的虚函数。
4.构造函数不能是虚函数,否则会出现编译错误。

多态中虚函数使用时的内存泄漏问题?

父类指针操作子类对象的成员函数的时候是没有问题的,
可是在销毁对象内存的时候则只是执行了父类的析构函数,子类的析构函数却没有执行,这会导致内存泄漏。
解决这个问题要用到虚析构函数virtual ~ClassName();

下面这个例子还包含了 虚析构和纯虚析构函数

#include <iostream>
using namespace std;

class Animal{
    
public:
    
//普通析构不会释放子类
//    ~Animal(){
//        cout << "Animal 析构
" << endl;
//    }
    
//加了virtual 会先调子类析构 再调基类析构
    virtual ~Animal(){
        cout << "Animal 虚析构
" << endl;
    }
    
    //纯虚析构 类内声明 类外实现
//    virtual ~Animal() = 0;
    
    virtual void speak(){
        cout << "Animal speak
" << endl;
    }
    
};

//Animal:: ~Animal(){
//    cout << "Animal 纯虚析构
" << endl;
//}

class Cat:public Animal{
    
public:
    
    Cat(const char *name){
        this->m_Name = new char[strlen(name)+1];
        strcpy(this->m_Name, name);
    }
    
    virtual ~Cat(){ //此处virtual可以不写,系统将会自动添加,建议写上
        
        cout << "Cat 析构
" << endl;
        if (this->m_Name != NULL) {
            delete [] this->m_Name;
            this->m_Name = NULL;
        }
        
    }
    void speak(){
        cout << "Cat speak
" << endl;
    }
    
    char *m_Name;
    
};


int main(int argc, const char * argv[]) {
    // insert code here...
    
    Animal *animal = new Cat("Tom");
    animal->speak();// 在这里,animal虽然是指向Animal的指针,但是被调用的函数(foo)却是Cat的! 因为Animal中speak为虚函数
    delete animal;
    
    return 0;
}

那虚函数的实现原理是什么?

首先要说函数指针,多态的原理。

指针指向对象称为对象指针,指针除了指向对象还可以指向函数,函数的本质就是一段二进制代码,我们可以通过指针指向这段代码的开头,计算机就会从这个开头一直往下执行,直到函数结束,并且通过指令返回回来。函数的指针与普通的指针本质上是一样的,也是由四个基本的内存单元组成,存储着内存的地址,这个地址就是函数的首地址。

虚函数表指针:类中除了定义的函数成员,还有一个成员是虚函数表指针(占四个基本内存单元),这个指针指向一个虚函数表的起始位置,这个表会与类的定义同时出现,这个表存放着该类的虚函数指针,调用的时候可以找到该类的虚函数表指针,通过虚函数表指针找到虚函数表,通过虚函数表的偏移找到函数的入口地址,从而找到要使用的虚函数。虚拟函数表是在编译期就建立了,各个虚拟函数这时被组织成了一个虚拟函数的入口地址的数组.而对象的隐藏成员--虚拟函数表指针是
在运行期--也就是构造函数被调用时进行初始化的。当实例化一个该类的子类对象的时候,(如果)该类的子类并没有定义虚函数,但是却从父类中继承了虚函数,所以在实例化该类子类对象的时候也会产生一个虚函数表,这个虚函数表是子类的虚函数表,但是记录的子类的虚函数地址却是与父类的是一样的。所以通过子类对象的虚函数表指针找到自己的虚函数表,在自己的虚函数表找到的要执行的函数指针也是父类的相应函数入口的地址。如果我们在子类中定义了从父类继承来的虚函数,对于父类来说情况是不变的,对于子类来说它的虚函数表与之前的虚函数表是一样的,但是此时子类定义了自己的(从父类那继承来的)相应函数,所以它的虚函数表当中管于这个函数的指针就会覆盖掉原有的指向父类函数的指针的值,换句话说就是指向了自己定义的相应函数,这样如果用父类的指针,指向子类的对象,就会通过子类对象当中的虚函数表指针找到子类的虚函数表,从而通过子类的虚函数表找到子类的相应虚函数地址,而此时的地址已经是该函数自己定义的虚函数入口地址,而不是父类的相应虚函数入口地址,所以执行的将会是子类当中的虚函数。这就是多态的原理。

如果父类当中定义了虚析构函数,那么父类的虚函数表当中就会有一个父类的虚析构函数的入口指针,指向的是父类的虚析构函数,子类虚函数表当中也会产生一个子类的虚析构函数的入口指针,指向的是子类的虚析构函数,这个时候使用父类的指针指向子类的对象,delete接父类指针,就会通过指向的子类的对象找到子类的虚函数表指针,从而找到虚函数表,再虚函数表中找到子类的虚析构函数,从而使得子类的析构函数得以执行,子类的析构函数执行之后系统会自动执行父类的虚析构函数。这个是虚析构函数的实现原理。

参考了:https://www.cnblogs.com/zhengfa-af/p/8134633.html

纯虚函数

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”
virtual void funtion1()=0

包含纯虚函数的基类又叫抽象类/接口类,基类本身不用实现虚函数,交给子类实现。

所以引入纯虚函数的目的大概就是:

1、为了安全,因为避免任何需要明确但是因为不小心而导致的未知的结果,提醒子类去做应做的实现。
2、为了效率,不是程序执行的效率,而是为了编码的效率。

#include <iostream>

using namespace std;

class A
{
public:
    virtual void foo()
    {
        cout<<"A::foo() is called"<<endl;
    }
    virtual void eat() = 0; //纯虚函数 必须在派生类中实现 且基类为抽象类 不能new
    
};

class B:public A
{
public:
    void foo()
    {
        cout<<"B::foo() is called"<<endl;
    }
    
    void eat(){
        
        
    }
};


//作者:wuxinliulei
//链接:https://www.zhihu.com/question/23971699/answer/69592611
//来源:知乎
//著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

int main(int argc, const char * argv[]) {
    // insert code here...
    
    
    A *a = new B();
    a->foo();   // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
    return 0;
    
}

参考了:https://www.zhihu.com/question/23971699/answer/69592611

 
此文仅为鄙人学习笔记之用,朋友你来了,如有不明白或者建议又或者想给我指点一二,请私信我。liuw_flexi@163.com/QQ群:582039935. 我的gitHub: (学习代码都在gitHub) https://github.com/nwgdegitHub/
原文地址:https://www.cnblogs.com/liuw-flexi/p/13819991.html