多态与虚函数:
什么是虚函数:
用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;
}
栈帧的开辟,栈帧的初始化 将虚函数表的地址写入虚函数指针中。都是进入构造函数一开始时进行的,如果清空了虚函数指针,在动态绑定的时候指向基类虚函数对象的指针会报错。
写的有点多了,但还没写完,后续内容在下一篇里。