C++:13.多态、虚函数

多态与虚函数:

什么是虚函数:

用virtual关键字声明的函数都是虚函数。虚函数存在的唯一目的,就是为了实现多态(动态绑定/运行时绑定)。

virtual 关键字只在类定义中的成员函数声明处使用,不能在类外部写成员函数体时使用。所有派生类中具有覆盖关系的同名函数都将自动成为虚函数。(派生类转化为的虚函数,最好也写上virtual,清晰么。)

静态成员函数不能是虚函数。

再说简单点:有virtual声明的函数都是虚函数。如果没有virtual,那么派生类中的同名函数会把基类中的同名函数隐藏了。如果有,那么派生类的同名函数(同参同返回)会在虚函数表中将基类的同名函数覆盖掉。

什么是多态:

        多态性可以简单概括为“一个接口,多种行为”。

动态绑定(运行阶段)是多态的基础。

基类指针或引用,指向一系列的派生类对象, 调用派生类对象的同名覆盖方法(也就是那个与基类虚函数同名同参同返回的函数),指针指向谁,就会调用谁的方法。

多态分为两种:

        (1)编译时多态(也叫静态的多态):主要通过函数的重载和模板来实现。

        (2)运行时多态(也叫动态的多态):主要通过虚函数来实现。

覆盖:

       基类的某个成员函数为虚函数,派生类又定义一个成员函数,函数名、形参、返回类型都与基类的成员函数相同。那么就会用派生类的函数覆盖掉基类的虚函数


说一下多态是如何实现的:

在代码编译阶段产生一张虚函数表vftable。运行的时候,会载入内存,加载到数据段(rodata段,只读),在程序的整个声生命周期都存在。一个类的虚函数表中列出了该类的全部虚函数地址。

如果成员里有虚函数,在成员变量里只会多一个虚函数指针(对象的前4个字节),指向虚函数表vftable()存放虚函数地址。虚函数的个数只会影响表的大小。不会影响对象的大小。

基类有虚函数,派生类如果有同名同参数列表同返回值的函数,派生类的函数会自动变成虚函数,在虚函数表中派生类会覆盖掉原有的函数。

补充:在虚函数表中还有一项RTTI(运行时类型) 指针。

打印类型  #include <typeinfo>
cout << typeid(p).name() << endl;   该函数打印p的类型,因为有RTTI才能在继承派生中实现

举个栗子:

    #include <iostream>
    using namespace std;
    class A
    {
    public:
        int i;
        virtual void func() {}
        virtual void func2() {}
    };
    class B : public A
    {
        int j;
        void func() {}
    };
    int main()
    {
        cout << sizeof(A) << ", " << sizeof(B);
        return 0;
    }

 设 pa 的类型是 A*,则 pa->func() 这条语句的执行过程如下:

1) 取出 pa 指针所指位置的前 4 个字节,也就是虚函数指针。如果 pa 指向的是类 A 的对象,则这个地址就是类 A 的虚函数表的地址;类 B 同

2) 根据虚函数指针找到虚函数表,在其中查找要调用的虚函数的地址。

如果 pa 指向的是类 A 的对象,自然就会在类 A 的虚函数表中查出 A::func 的地址;类 B 同

类 B 没有自己的 func2 函数,因此在类 B 的虚函数表中保存的是 A::func2 的地址,这样,即便 pa 指向类 B 的对象,pa->func2();这条语句在执行过程中也能在类 B 的虚函数表中找到 A::func2 的地址。

3) 根据找到的虚函数的地址调用虚函数。

由以上过程可以看出,只要是通过基类指针或基类引用调用虚函数的语句,就一定是多态的,也一定会执行上面的查表过程,哪怕这个虚函数仅在基类中有,在派生类中没有。

在虚函数表中还有一个:RTTI  运行时的类型信息
Base *p =  new Derive();
cout<<typeid(*p).name()<<endl;


动态绑定与静态绑定:

绑定就是函数调用。

在使用的时候,用一个基类的指针指向了一个派生类的对象,如果调用这个虚函数,会先找虚函数指针,再找虚函数表,再再找虚函数地址。而这个绑定过程就是动态绑定(运行时期)。

如果你不用指针调用,而是用对象本身调用函数,不论是否是虚函数,都是静态绑定(编译时期)。

用指针调用如果是虚函数,指针识别的就是运行时期的类型;如果调用的是一般的函数,指针识别就是在编译时期。

  • 没有virtual -> 静态绑定
  • 有 virtual 用引用或指针  ->   动态绑定
  • 有 virtual 但用对象调用->   动态绑定

纯虚函数:

一般情况下,基类是不希望定义对象的。基类只是为了将共有的属性统一起来。

为了实现这一目的:在基类提供的一个虚函数,为所有派生类提供统一的虚函数接口,具体实现让派生类自己去重写的。

virtual void show() = 0;  // 在虚函数后面加   =  0    就是纯虚函数,不用去实现。

纯虚函数实际上是不存在的,引入纯虚函数就是为了便于实现多态。

拥有纯虚函数的类叫做抽象类。抽象类不能实例化。一般基类都应该实现为抽象类。

当不知道用哪个函数定义为纯虚函数的时候,我们可以将析构函数定义为纯虚函数,但需要注意的是,析构函数成纯虚函数了,它在类内就不能实现了。当然编译也就没法通过了。解决办法:类内不能实现,我类外实现啊。

还有一点,如果你基类写了纯虚函数,但在派生类中没有写对应基类纯虚函数,那么由于继承的关系,会导致派生类也成为纯虚函数。


那么问题来了:

1、基类在没有更多方法的时候,把谁实现成纯虚函数呢?-------->  析构函数

首先明确一个函数想要成为虚函数     1、它得有地址,有地址才能放入虚函数表;2、得依赖对象,有对象才会有指针,有指针才能找到虚函数表。

1)构造函数能不能是虚函数?

构造函数不依赖对象,构造函数执行完才有对象,有对象才有虚函数指针,所以不能是虚函数。

2)static成员方法能不能是虚函数?  virtual static

静态函数也不依赖对象,可以直接使用类名调用,也不能是虚函数。

3)inline函数能不能是虚函数?  =>  virtual inline  

内联函数直接在程序内展开,没有地址,无法往虚函数表放。也不能是虚函数。

4)析构函数能不能实现成虚函数?

析构函数依赖对象,有地址。可以写成虚函数。我们知道如果将一个函数写为虚函数,那么其派生类会有一个同名的函数也成为虚函数,两者成覆盖关系。但是析构函数的函数名是类名前加~。所以名字是不同的,但其实这是可以的,因为一个类只会有一个虚函数。

2、什么时候必须将析构函数定义为虚函数呢?

当基类指针,指向堆上的派生类对象时。

int main()
{
	Base *p = new Derive(20);
	p->show();
	delete p;      如果析构函数不是虚函数的话,调用的时候由于p是基类指针,所以只会调用基类的析构函数,
                       而不会调用派生类的析构函数。导致资源泄露。所以必须将基类的析构函数声明为虚函数。
	return 0;
}

3、坑1

class Base
{
public:
	Base(int data) :ma(data){ cout << "Base()" << endl; }
	virtual ~Base() = 0;
	virtual void show(int i=10)
	{
		cout << "Base::show" << endl;
	}
private:
	int ma;
};
Base::~Base()
{
	cout << "~Base()" << endl;
}
///
class Derive : public Base
{
public:
	Derive(int data) :mb(data), Base(data)
	{
		cout << "Derive()" << endl;
	}
	~Derive(){ cout << "~Derive()" << endl; }
	void show(int i=20)
	{
		cout << "Derive::show i:" << i<<endl;
	}
private:
	int mb;
};

int main()
{
	const int a = 10;      用const进行类比,我们知道const定义的变量的值是不能修改的,直接修改会报错。
	int *q = (int*)&a;        但通过指针修改地址的值,还是可以将其更改。
	*q = 20;     原因就是,编译的时候确实没有检测出来修改,但运行的时候就可以将他改了。
        //
	Base *p = new Derive(20);  
	p->show();  
        结果:打印出来的是Derive::show i:10。诶呦奇怪了,我派生类定义的明明是20,为啥调的是派生类的函数,打印出来的却是10?
        解释:由于参数的压栈,是编译阶段确定的。具体调用哪个方法,是经过动态绑定,运行时才确定。
        所以可能导致使用的是基类的参数,而调用的是派生类的函数。(所以虚函数最好不要写默认值,写的话就写成一样的)
	delete p;
	return 0;
}

在问个问题:如何调用派生类的私有成员函数?
跟const是一样的。由于方法的访问权限是在编译阶段确定的。所以如果将基类对应的函数写为虚函数,那么在使用方法时是动态绑定,在运行时确定使用哪个方法,所以由此可以调用派生类的私有成员函数。

4、坑2

class Animal
{
public:
	Animal(string name) :_name(name){}
	virtual void bark() = 0;
protected:
	string _name;
};

class Cat : public Animal
{
public:
	Cat(string name) :Animal(name){}
	virtual void bark()
	{
		cout << _name << " miao miao!" << endl;
	}
};

class Dog : public Animal
{
public:
	Dog(string name) :Animal(name){}
	virtual void bark()
	{
		cout << _name << " wang wang!" << endl;
	}
};
int main()
{
	Animal *p1 = new Cat("猫");
	Animal *p2 = new Dog("狗");

	int *pp1 = (int*)p1;
	int *pp2 = (int*)p2;

	int tmp = pp1[0];将两个虚函数指针交换
	pp1[0] = pp2[0];
	pp2[0] = tmp;

	p1->bark();   猫 vfptr  ->   狗vftable
	p2->bark();   导致猫叫出了狗的声音

	delete p1;
	delete p2;

	return 0;
}

5、在构造函数中,调用虚函数,是静态绑定还是动态绑定?

析构函数和构造函数内部都不会发生动态绑定(多态)。前面提到了调用虚函数一定要有对象,而对象的生命周期在构造函数结束后一直到析构函数开始前。所以在构造与析构内部不会发生动态绑定。

6、在看一个题:

class Base
{
public:
	Base(int data) :ma(data)
	{ 
		// 1. 栈帧开辟  2.栈帧初始化 3.vftable=》vfptr里面
		cout << "Base()" << endl; 
		clear();
		this->show();
	}
	virtual ~Base()
	{
		this->show();
		cout << "~Base()" << endl;
	}
	void clear()
	{ 
                memset(this, 0, sizeof(*this)); 
        }
	virtual void show(int i=10)
	{
		cout << "Base::show" << endl;
	}
private:
	int ma;
};
///
class Derive : public Base
{
public:
	Derive(int data) :mb(data), Base(data)
	{
		cout << "Derive()" << endl;
	}
	~Derive()
	{ 
		cout << "~Derive()" << endl; 
	}
	void show(int i=10)
	{
		cout << "Derive::show i:" << i<<endl;
	}
private:
	int mb;
};
int main()
{
        帧的开辟,栈帧的初始化  将虚函数表的地址写入虚函数指针中。都是进入构造函数一开始时进行的,
        如果清空了虚函数指针,在动态绑定的时候指向基类虚函数对象的指针会报错。
	Base *p1 = new Base(10);
	p1->show();
	delete p1;

	Base *p2 = new Derive(10);
	p2->show();
	delete p2;
	// 继承结构中,每一层构造函数都会把自己类型的虚函数表的地址,写到vfptr里面

	return 0;
}

栈帧的开辟,栈帧的初始化  将虚函数表的地址写入虚函数指针中。都是进入构造函数一开始时进行的,如果清空了虚函数指针,在动态绑定的时候指向基类虚函数对象的指针会报错。

写的有点多了,但还没写完,后续内容在下一篇里。

猜你喜欢

转载自blog.csdn.net/qq_41214278/article/details/83996143