C++多态二三事
作者:互联网
多态:
多态的概念
多态:多种形态;不同的对象完成同一件事情会发生不同的行为,产生不同的结果
多态包括
静态的多态:函数重载(静态绑定:静态指编译时)
动态的多态:父类指针或引用调用重写了的虚函数(动态绑定:是指运行时)
1.多态的定义和实现
构成多态还需要两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
虚函数:即被virtual修饰的类非静态成员函数称为虚函数
静态函数没有this指针,无法形成切片,那就无法调用派生类重写了的虚函数,无法形成多态
虚函数是为了形成多态
虚函数的重写(覆盖)
派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同)
注意与重定义区分:
如果函数名相同,不是重写就是重定义(隐藏)
①虚函数重写的三个例外
虚函数重写要求重写函数与基类完全相同
但也有例外:
①协变(基类与派生类虚函数返回值类型不同)
基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变
#include<iostream>
using namespace std;
class A{};
class B :public A{};
class C
{
public:
virtual A* show()
{
cout << "C" << endl;
return new A;
}
};
class D :public C
{
public:
virtual B* show()
{
cout << "D" << endl;
return new B;
}
};
int main()
{
D d;
C* c = &d;
c->show();
return 0;
}
运行结果:
②析构函数的重写
编译器会自动对基类,派生类的析构函数名统一处理成destructor,所以父子类析构函数自动构成重定义(不重写的话)
一般情况下,重不重写并没有影响:
无论重不重写并没有影响,都是先调用D的析构,D析构调用完后调用父类析构,接着C再析构
但是在特殊情况下,重写就十分必要
在这种情况下,我们是想通过delete分别调用它们的析构函数
但是c2的析构函数没重写,发生切片后,c2只能调用从父类继承下来的成员,所以也调用了父类的方法,并没有析构释放动态开辟的空间,这样就容易造成资源泄漏
重写后:
所以编译器为什么要对子类和父类的析构函数名进行处理
想必已经很明确了,就是让父子类的析构函数构成重写,让它们调用指向的对象的析构函数
③不写子类的virtual
子类的virtual不写,编译器也认为它是虚函数完成了重写
②C++11: override 和 final
-
final:修饰虚函数,表示该虚函数不能再被重写
-
override: 检查派生类虚函数是否重写
2.重载、覆盖、隐藏对比
3.抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象
纯虚函数派生类必须重写
C类就是抽象类
1.抽象类能更好地表示没有实例对象对应的抽象类型 比如动物
2.体现了接口继承(跟Java的接口相似),强制子类重写虚函数
通过父类对象的指针或引用指向子类对象来使用
4.接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态。
不用多态,就不要被函数定义成虚函数
多态原理
虚表指针(铺垫):
只要类中有虚函数就会有虚表指针
这个指针存放的是函数指针数组
#include<iostream>
using namespace std;
class C
{
public:
virtual void show() = 0;
virtual ~C()
{
cout << "C" << endl;
}
};
class D :public C
{
public:
virtual void show( )override
{
cout << "hello" << endl;
}
virtual ~D()
{
cout << "D" << endl;
}
int* a;
};
int main()
{
D d;
return 0;
}
注意:
虚继承和这里完全是不同的两个东西,虽然都有virtual
虚继承的叫虚基表,存放的是偏移量
而这里叫虚表,存放的是函数指针
完成重写:
如果没完成重写:
可以看到c跟d里面的虚函数指针指向同一个函数,那这个函数一定是父类的
说明这个虚函数指针是来自父类的拷贝,如果子类重写了该虚函数,就把这个函数的地址拷贝到对应位置上
所以满足多态后,虚函数表被修改了,在运行时,再到指向的对象中的虚表中去找对应的虚函数调用。
所以指向父类就调用的是父类的虚函数,指向子类就调用的是子类的虚函数
如果不构成多态:
比如这样修改一下
void test(C c)
{
c.show();
}
调用的就是父类的show函数,与传入的是什么对象无关
总结:
以我的理解,本质上就是在找这个虚函数指针,虚表里面函数指针存的是谁的地址,就调用的是谁
比如C c=d/c 这样根据切片是重新开辟了一份空间,此时c是一个对象了,虚表指针在C上(每个类实例化出的对象共用一个虚表指针),调用的就是C的虚函数
如果形成了多态,这个c是指向传入对象的,c就会去传入对象中找到虚函数指针,调用的是该对象的虚函数(如果重写)
例如:
#include<iostream>
using namespace std;
class C
{
public:
virtual void show()
{
cout << "C" << endl;
}
int c;
};
class D :public C
{
public:
virtual void show(int )
{
cout << "D" << endl;
}
int d;
};
void test(C c)
{
c.show();
}
int main()
{
D d;
test(d);
C c;
test(c);
C c1;
return 0;
}
所以要形成多态必须父类的指针或引用指向子类对象
在拷贝构造时并不会拷贝vfptr,容易造成紊乱,vfptr只与所属类有关
注意:
1.对象中的虚表指针是在构造函数初始化列表初始化的,虚表是在编译时就生成好的
2.虚函数表存放的是类中所有的虚函数的地址,虚函数跟普通函数一样,编译完成后放在代码段
3.虚函数的重写,也叫覆盖,子类会先调用父类的构造函数,父类会将虚函数表内容拷贝给子类,子类重写了再逐一修改
C类的虚函数表
1.探究虚表存放的位置
对象的前四个字节就是虚表的地址
#include<iostream>
using namespace std;
class C
{
public:
C()
{
}
virtual void show()
{
cout << "C" << endl;
}
virtual void show1()
{
}
int c;
};
int a = 0;
int main()
{
C* c = new C;
printf("C的虚表:%p\n", *((int*)c));//取前四个字节
int i;
printf("栈上地址:%p\n", &i);
printf("数据段地址:%p\n", &a);
int* arr = new int;
printf("堆地址:%p\n", arr);
const char* str = "hello world";
printf("代码段地址:%p\n", str);
return 0;
}
所以虚函数在代码段
2.打印虚表
include<iostream>
using namespace std;
class A
{
public:
virtual void test1()
{
cout << "A1" << endl;
}
virtual void test2()
{
cout << "A2" << endl;
}
int _a;
};
class B:public A
{
public:
virtual void test1()
{
cout << "B1" << endl;
}
virtual void test3()
{
cout << "B3" << endl;
}
int _b;
};
int main()
{
A a;
B b;
return 0;
}
可以看到VS编译器监视做了特殊的处理只能看到从父类继承下来的函数指针
但内存窗口里面真实说明了虚函数表里面有三个函数指针
打印:
多态虚表就非常清晰了
多继承:
#include<iostream>
using namespace std;
class A
{
public:
virtual void test1()
{
cout << "A1" << endl;
}
virtual void test2()
{
cout << "A2" << endl;
}
int _a;
};
class B
{
public:
virtual void test1()
{
cout << "B1" << endl;
}
virtual void test2()
{
cout << "B2" << endl;
}
int _b;
};
class C :public A, public B
{
public:
virtual void test1()
{
cout << "C1" << endl;
}
virtual void test3()
{
cout << "C3" << endl;
}
int _c;
};
typedef void (*VFunc)();//定义函数指针VFunc
void PrintVF(VFunc* ptr)//函数指针的数组指针
{
printf("虚表指针:%p\n", ptr);
for (int i = 0; ptr[i] != nullptr; i++)
{
printf("VF[%d]:%p\n",i, ptr[i]);
ptr[i]();
}
printf("\n");
}
int main()
{
C c;
PrintVF((VFunc*)(*(int*)&c));//打印A继承下来的虚表
PrintVF((VFunc*)(*(int*)((char*)&c+sizeof(A))));//打印B继承下来的虚表
return 0;
}
可以看到test3只放第一张虚表
3.菱形虚继承、虚函数
#include<iostream>
using namespace std;
class A
{
public:
virtual void show()
{
cout << "A" << endl;
}
int _a;
};
class B :virtual public A
{
public:
virtual void show()
{
cout << "B" << endl;
}
int _b;
};
class C :virtual public A
{
public:
virtual void show()
{
cout << "C" << endl;
}
int _c;
};
class D :public B, public C
{
public:
virtual void show()
{
cout << "D" << endl;
}
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
作如下修改,B,C各增加一个虚函数,A并不修改
修改后 修改前
此时虚基表的第一行就存了到B作用域的虚函数表的偏移量
而虚基表的18 00 00 00是存的到作用域公共基类A的偏移量
标签:虚表,函数,多态,C++,二三,virtual,重写,cout 来源: https://blog.csdn.net/hbbfvv1h/article/details/120972454