- 多态的概念
- 1.多态的定义和实现
- ①虚函数重写的三个例外
- ②C++11: override 和 final
- 2.重载、覆盖、隐藏对比
- 3.抽象类
- 4.接口继承和实现继承
- 多态原理
- 1.探究虚表存放的位置
- 2.打印虚表
- 3.菱形虚继承、虚函数
多态:多种形态;不同的对象完成同一件事情会发生不同的行为,产生不同的结果
1.多态的定义和实现多态包括
静态的多态:函数重载(静态绑定:静态指编译时)
动态的多态:父类指针或引用调用重写了的虚函数(动态绑定:是指运行时)
构成多态还需要两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
虚函数:即被virtual修饰的类非静态成员函数称为虚函数
静态函数没有this指针,无法形成切片,那就无法调用派生类重写了的虚函数,无法形成多态
虚函数是为了形成多态
虚函数的重写(覆盖)
派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同)
注意与重定义区分:
如果函数名相同,不是重写就是重定义(隐藏)
虚函数重写要求重写函数与基类完全相同
但也有例外:
①协变(基类与派生类虚函数返回值类型不同)
基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变
#includeusing 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不写,编译器也认为它是虚函数完成了重写
-
final:修饰虚函数,表示该虚函数不能再被重写
-
override: 检查派生类虚函数是否重写
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象
纯虚函数派生类必须重写
C类就是抽象类
1.抽象类能更好地表示没有实例对象对应的抽象类型 比如动物
2.体现了接口继承(跟Java的接口相似),强制子类重写虚函数
通过父类对象的指针或引用指向子类对象来使用
4.接口继承和实现继承普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态。
不用多态,就不要被函数定义成虚函数
多态原理虚表指针(铺垫):
只要类中有虚函数就会有虚表指针
这个指针存放的是函数指针数组
#includeusing 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就会去传入对象中找到虚函数指针,调用的是该对象的虚函数(如果重写)
例如:
#includeusing 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类的虚函数表
对象的前四个字节就是虚表的地址
#includeusing 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的虚表:%pn", *((int*)c));//取前四个字节 int i; printf("栈上地址:%pn", &i); printf("数据段地址:%pn", &a); int* arr = new int; printf("堆地址:%pn", arr); const char* str = "hello world"; printf("代码段地址:%pn", str); return 0; }
所以虚函数在代码段
2.打印虚表includeusing 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编译器监视做了特殊的处理只能看到从父类继承下来的函数指针
但内存窗口里面真实说明了虚函数表里面有三个函数指针
打印:
多态虚表就非常清晰了
多继承:
#includeusing 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("虚表指针:%pn", ptr); for (int i = 0; ptr[i] != nullptr; i++) { printf("VF[%d]:%pn",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只放第一张虚表
#includeusing 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的偏移量
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)