即多种形态:这里又分为静态多态和动态多态。
其中静态多态:即编译时呈现的多态如:函数模版,函数参数等。
动态多态:运行时呈现的多态如这里要讲的虚函数呈现的多态。
那么会有个疑问满足多态的条件是什么呢?
下面引出这两个条件:即多态出现的不得有继承关系么:
1.即基类对象指针或引用调用虚函数,(这时看传的对象如果是派生类对象就调派生类虚函数,如果是基类对象就去调基类虚函数)
2.即需要在父类的虚函数前加上virtual,而子类也可加也可不加但是一般要加,其次就是要满足虚函数的重写(覆盖):即函数名同,返回类型同,参数类型同(比如 int x=1;另一个是int x=2,这也是相同的),简记:三同。
下面举个实现简单多态的代码例子:
class A {
public: virtual void talk(int x = 1) {
cout << "我来咯" <<"->"<<x << endl;
}
};
class B : public A{
public:
virtual void talk (int x = 0) {
cout << "你来咯" << "->" << x<< endl;
}
};
//void f(A& p) {
// p.talk();
//}
int main() {
A a;
B b;
/*f(a);
f(b);*/
A& aa = a;
A& bb = b;
aa.talk();
bb.talk();
return 0;
}
这样就形成了简单的多态。
解答:这里A是父类,B是子类,然后p指针是父类的指针,这里用p去访问子类继承过来的父类的虚函数test()然后继承抽象理解成照搬过来但是应该是存了个提醒,然后切片变成A的指针通过B的虚表(后续讲解,这里放着test函数地址,以及A的func声明+B的func定义覆盖过去)(记:覆盖父的声明加上子类的定义覆盖过去),因此是父类指针调用虚函数,满足多态条件:第一种理解:由于是父类的指针调用虚函数,传的是子类的对象,故调用子类虚表的这个虚函数故是B->1.
第二种理解:就是它是在B类内切片成A的指针去访问的,然后整体还是可以理解为在B类内操作,故这里调用的虚表还是B的。
派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引 ⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。
注:各自返回的是指针或者引用:
即有资源是否被完全清理完的问题。
涉及隐藏和虚函数这两点。
这里为了可以完全清理掉资源故把析构设计成虚函数:这样的话父与子类的虚表中就会存有这两个虚函数(后期都被处理成destructor形式满足多态)这样用父类的指针或引用,当传递不同对象就调用不同的析构都清理掉资源了。
但是如果父未加virtual也就是不构成虚函数,也就是隐藏关系。这时子隐藏父,但是这里用的是A类型指针也就是父类型指针接收,这时切片到访问父的析构,故子类没清理资源。
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A {
public:
~B()
{
cout << "~B()->delete:" << _p << endl;
delete _p;
}
protected:
int* _p = new int[10];
};
int main()
{
A* p1 = new A;
A* p2 = new B;
delete p1;
delete p2;
return 0;
}
因此要把析构搞成虚函数并重写当用的是父指针或引用操作时,如果用父与子分别操作,那么子类析构完会多一次调用父类的析构,如果有资源的话,两次父的析构必然出问题,故析构就要虚函数这么搞。
C++对函数重写的要求⽐较严格,但是有些情况下由于疏忽,⽐如函数名写错参数写错等导致⽆法构成重载,⽽这种错误在编译期间是不会报出的,只有在程序运⾏时没有得到预期结果 因此C++11提供了override,可以帮助⽤⼾检测是否重写。如果我们不想让派⽣类继承基类或者重写虚函数那么可以⽤final去修饰。
final无法继承:
无法重写虚函数:
这里重写比隐藏条件更严格,也就是如果不是重写就是隐藏,隐藏又称重定义,下面有张表方便我们记忆:
纯虚函数就是我们在父类的虚函数后加上=0,而包含这个纯虚函数的类就是抽象类,不能实例化处对象如:
virtual void talk() = 0;
这里也许会说为什么没内容,因为它已经是纯虚函数了,后面要想使用必须通过子类给它的定义重写了,故父类的定义没什么意义了可以不写,而子类如果补充些便去继承它,那么子类也是抽象类不能实例化出对象,因此这种操作便强制了子类必须对这个纯虚函数完成重写操作了。
class per
{
public:
virtual void talk () = 0;
};
class zhangsan:public per
{
public:
virtual void talk()
{
cout << "我是张三" << endl;
}
};
class lisi:public per
{
public:
virtual void talk()
{
cout << "我是李四" << endl;
}
};
int main()
{
//per s;
per* zs = new zhangsan;
zs->talk();
per* ls = new lisi;
ls->talk();
return 0;
}
抽象类无法实例化对象:
被继承后完成重写:
这里我们也许会说这种多态操作是怎么样的,下面一道例题带我们进入正题:
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
protected:
int _b = 1;
char _ch = 'x';
};
int main()
{
Base b;
cout << sizeof(b) << endl;
return 0;
}
思考一下这道题,一般我们肯定会认为是8,思路:这个int类型从偏移量从0开始排到5,最后放的是char,然后输出的字节应该是成员最大对齐数的整数倍也就是8,但是事实确实12,为什么呢?这里就涉及到了虚表的概念。
虚表的全称就是虚函数的一个数组的指针(__vfptr->virtual function ptr),这里面放的是虚函数地址,方便查找:
这个指针大小也就是地址大小和平台有关,32位机器是4字节,64位机器是8字节,这样下面这道题就不难解决了(默认的是x86,32位机器)。
没有virtual,它确实8,因为不是虚函数无虚表。
这里还需要补充一点:
①父类的虚表和子类重写后的虚表不是同一张但是内容可能有相同的。
②当子类如果继承了多个父类,则分别在继承的子类中的父类处有个虚表,则继承几个父类,有几个虚表但是没完成重写的虚函数直接加到第一个继承的父类的虚表中。
③派生类的虚表包括基类的虚函数地址(找基类的声明),子类重写父类的虚函数地址(找子类虚函数的定义),子类自己的虚函数地址。
④虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标 记(vs)。而g++没有操作,不同编译器处理不同。
⑤虚表放在常量区。
⑥虚函数放在代码段(因为虚函数也是函数最后汇编成指令也就是放代码段)。
静态绑定:即是不满足多态,仅是指针引用调用函数,在编译的时候确定的函数地址(如:模版,函数重载等)。
动态绑定(又叫做晚期绑定):即满足多态的条件,在运行的时候在虚表中找到对应函数的地址。
父类指针或者引用调用虚函数(访问谁的虚函数由传递给它的对象决定)子类继承父类在子类对应继承放父类的位置生成虚表,这个虚表中放的是父类虚函数的地址,然后子类如果能进行虚函数重写就给它重写定义完成覆盖操作,最后这个虚表中也就是父的声明+子的定义(存放它们对应函数地址),当使用不同对象调用不同虚表中的虚函数。
以下程序输出结果是()
class A
{
public:
A() :m_iVal(0) { test(); }
virtual void func() { std::cout << m_iVal << ‘ ’; }
void test() { func(); }
public:
int m_iVal;
};
class B : public A
{
public:
B() { test(); }
virtual void func()
{
++m_iVal;
std::cout << m_iVal << ‘ ’;
}
};
int main(int argc, char* argv[])
{
A* p = new B;
p->test();
return 0;
}
解答:
首先这道题涉及了继承和多态的知识点(继承的构造,多态的条件,以及操作等):
第一步:A*p = new B:首先它构造了一个B类型的对象,我们就要给它初始化,但是它继承了A类,故先给父类初始化然后再给子类初始化,父类初始化就是把m_iVal=0,然后通过父类的指针调用func函数,而这里对象明显是A类的(这里构成了多态),故打印0,然后就是B类即子类再初始化,然后调用test函数,而它属于A类,故进行“切片”然后变成A类的指针去访问test,这里又是多态效应,然后对象是B类的故访问B的虚函数即m_iVal++变为1,然后打印,接着完成了初始化。
第二步:p->test():就是利用A类的指针去访问test然后又是多态即对象是B类的对象故访问B类的虚表中虚函数,m_iVal++变为2,打印。
故输出0,1,2。