目录
virtual与析构函数
C++11 override 和 final
重载,重写(覆盖),隐藏(重定义)对比
抽象类
子类和父类虚表
多继承
习题
多态总结
习题
这里的父类和子类析构完成了重写。
因为析构函数名会被处理成destructor,所以这里析构函数完成虚函数重写。
不加virtual,子类对象没被析构,因为这里是一个普通调用,
delete b,变成b->destructor(); operator delete(b);
满足多态时,此时子类调用子类析构,父类调用父类析构。
子类的析构函数重写父类析构函数,才能正确调用,这里对父类析构了俩次是因为,Student里面也继承了一个父类,而我们又创建了一个父类对象,所以对父类析构了俩次。不存在重复析构。由于先delete a所以先析构子类对象。
.final:修饰虚函数,表示该虚函数不能再被重写
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
重载:俩个函数在同一作用域,要求函数名相同,类型不同(包括类型不同,顺序不同,个数不同)
重写(覆盖): 俩函数分别在基类和派生类的作用域,函数名/参数/返回值都必须相同(协变例外),俩个函数必须是虚函数。
重定义(隐藏):俩个函数分别在基类和派生类的作用域,函数名相同,俩个基类和派生类的同名函数不构成重写就是重定义。
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
这种情况下,子类也没办法实例化
因为子类继承了纯虚函数,如果没有重写,则子类也是抽象类
子类重写后正常运行
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
这里p1和p2共用一个虚表
student和person创建的对象用的不是同一个虚表,父类虚表存父类虚函数,子类虚表存子类虚函数。
通过观察和测试,我们发现了以下几点问题:
1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表
中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数
的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函
数,所以不会放进虚表。4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
在vs下,调试的时候,虚表中没有子类自己写的虚函数,其实是fun4进了虚表,只不过没显示内存中此时多出来一个地址,这个地址就FUN4这个虚函数的地址
虚函数表是一个函数指针数组,我们可以打印出来
上面这些函数都没有返回值,没有参数
这是一个函数指针类型
这个函数指针类型名字我们设置为ptr,ptr代表没有返回值,没有参数的函数指针类型
传过来的参数类型是函数指针数组
*(int*)&s1 是取前四个字节
但传参穿的是函数指针类型,还要再类型转换
#include
using namespace std; typedef void(*ptr)(); void PrinVFTable(ptr table[]) { for (size_t i = 0; table[i] != nullptr; ++i) { printf("vft[%d]:%p", i, table[i]);//打印地址和函数名 table[i]();//调用函数,由于是无返回值,无参数,table[i]就是虚表里对应的函数 } } class Base { public: virtual void Func1() { cout << "Base::Func1()" << endl; } virtual void Func2() { cout << "Base::Func2()" << endl; } void Func3() { cout << "Base::Func3()" << endl; } private: int _b = 1; }; class Derive : public Base { public: virtual void Func1() { cout << "Derive::Func1()" << endl; } virtual void Func4() { cout << "Derive::Func4()" << endl; } private: int _d = 2; }; int main() { Derive s1; PrinVFTable((ptr*)*(int*)&s1);//虚表的地址在对象的前四个或前八个字节 return 0; } Linux下不支持虚表后面给空指针
这样写即可
using namespace std; typedef void(*ptr)(); void PrinVFTable(ptr table[]) { for (size_t i = 0; table[i]!=nullptr; ++i) { printf("vft[%d]:%p", i, table[i]);//打印地址和函数名 table[i]();//调用函数,由于是无返回值,无参数,table[i]就是虚表里对应的函数 } } class Base1 { public: virtual void func1() { cout << "Base1::func1" << endl; } virtual void func2() { cout << "Base1::func2" << endl; } private: int b1; }; class Base2 { public: virtual void func1() { cout << "Base2::func1" << endl; } virtual void func2() { cout << "Base2::func2" << endl; } private: int b2; }; class Derive : public Base1, public Base2 { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << endl; } private: int d1; };
Derive大小=Base1+Base2+Derive成员d1
所以整体是20
Derive创建的对象里有俩个虚表
一个是Base1的,一个是Base2的
Derive继承下来的虚表中,func1是自己重写的func1,所以Base1和Base2里面的func1都是自己重写的。
由于没有重写func2,所以俩个表里面的func2都是父类的。
func3此时没有显示,因为这是子类自己增加的,这里func3是被放进了哪个虚表里?
这里打印的是第一个虚表(因为第二行打印的是Base1),说明第一个虚表里面有func3
接下来我们打印第二个虚表,第一个虚表在头四个字节,第二个虚表在中间,因为声明的时候谁先继承谁在前面
由于int *强制转换的级别高,不能直接(int*)&d+sizeof Base1,因为这样没加一次会加4个字节,而我们要取中间四个字节,这样一加会直接将该四个字节跳过
所以要转换为char*,char*一次加1个字节
我们此时发现没有func3
说明自己写的func3在第一个虚表中
这种方式也可以,利用切片,ptr2是Base2的指针,指向Base2这一块,把子类对象给Base2,完成切片
这里都是Derive的func1但地址不一样(这个问题后面说)
调用的时候去指向对象虚函数表中去找func1地址调用
切片后ptr1和ptr2指向各自的区域
打印func3地址
func3地址跟这俩个虚表里的地址都不一样
直接调用d.func1()
观察这俩条语句
Base1* ptr1 = &d;
ptr1->func1();这是多态调用,先找到虚表我们发现直接用子类对象调用func1和用父类指针调用func1,只有一个步骤不同,就是子类对象直接call func1的地址,而父类指针要先找到虚表
执行这俩条语句
Base2* ptr2 = &d;
ptr2->func1();这里的eax跟前面的不一样,eax此时是第二个虚表的第0个虚函数地址,但是执行完eax之后,进行了sub ecx,8这里是给ecx-8,ecx是第二个虚表的地址-8,8是第一个虚表的大小,
ptr2调用ecx时,ecx指向ptr2这个位置,然后-8让ptr2指向跟ptr1同一块地方
最终执行结果调的都是Derive func1.
using namespace std; class A{ public: A(char *s) { cout<
A:class A class B class C class D B:class D class B class C class A
C:class D class C class B class A D:class A class C class B class D子类必须调用父类的构造函数,这里不调用A的构造函数会报错,因为是虚继承,A是B和C共享的,但是B和C都不能初始化A,此时应该由D去初始化A,因为D里面的A只有一份,而且这个A是B和C共享的。
注意B和C里面都有对A构造
调用顺序:编译器会特殊处理,不会重复对A进行构造,D里面只有一份A,只在D里面初始化,编译器按照声明的顺序进行初始化,所以先初始化A,然后B,C,D。
B和C初始化A的意义,可能会单独定义B或C对象。
inline函数可以是虚函数吗?(内联函数是直接展开,内联函数没有地址,汇编时候不进行call操作,虚函数要放进虚函数表,虚函数表中填的是函数地址)
答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。
static函数可以是虚函数吗
首先测试一下不行,编译器直接报错,因为静态成员函数没有this指针,直接使用A::FUNC2()类域指定的形式调用,虚函数是为了实现多态,多态都是运行时直接去虚表中决议(寻找),static成员函数都是在编译时决议(直接call函数地址,不通过虚表),所以静态函数不能成为虚函数
构造函数可以是虚函数吗
运行程序直接报错,对创建好后,对象没有被初始化,虚表也没有被初始化
在初始化列表阶段会对对象和虚表进行初始化(找到虚表的地址再初始化)
虚函数是为了实现多态,运行时去虚表找对应的虚函数进行调用,对象中虚表指针都是构造函数初始化列表阶段才初始化,构造函数是虚函数是没有意义的
析构函数可以是虚函数吗
可以,建议基类的析构函数定义成虚函数,博客里有提到过。
拷贝构造和operator=可不可以是虚函数?
拷贝构造不可以,拷贝构造也是构造函数,拷贝构造也有初始化列表。
operator=:可以,子类B的赋值不是A类赋值的重写,因为参数不同,但这种重写可以让子类对象接收父类对象
class A { public: virtual A& operator=(const A& aa) { return *this; } }; class B:public A { public: virtual B& operator=(const B& aa) { return *this; } };
这种赋值重写,可以把父类赋值给子类
但如果去掉赋值重写,这种写法就会报错,因为这是C++的语法规定
对象访问普通函数快还是虚函数快
首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
虚函数表是在什么阶段生成的,存在哪的?虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
构造函数初始化列表阶段初始化的是虚函数表指针,对象中存的也虚函数表指针
1.关于重载、重写和重定义的区别说法正确的是( A F)
A.重写和重定义都发生在继承体系中
B.重载既可以在一个类中,也可以在继承体系中
C.它们都要求原型相同
D.重写就是重定义
E.重定义就是重写
F.重写比重定义条件更严格
G.以上说法全错误
A.重写即覆盖,针对多态, 重定义即隐藏, 两者都发生在继承体系中
B.重载只能在一个范围内,不能在不同的类里
C.只有重写要求原型相同
D.重写和重定义是两码事,重写即覆盖,针对多态, 重定义即隐藏
E.重写和重定义是两码事,重写即覆盖,针对多态, 重定义即隐藏
F.重写要求函数完全相同,重定义只需函数名相同即可
G.很明显有说法正确的答案
2.关于重载和多态正确的是 ( B)
A.如果父类和子类都有相同的方法,参数个数不同, 将子类对象赋给父类对象后, 采用父类对象调用该同名方法时,实际调用的是子类的方法
B.选项全部都不正确
C.重载和多态在C++面向对象编程中经常用到的方法,都只在实现子类的方法时才会使用
D.class A{ public: void test(float a) { cout << a; } }; class B :public A{ public: void test(int b){ cout << b; } }; void main() { A *a = new A; B *b = new B; a = b; a->test(1.1); } 结果是1
A.使用父类对象调用的方法永远是父类的方法
B.正确
C.重载不涉及子类
D.输入结果为1.1
3.以下哪项说法时正确的(D )
class A { public: void f1(){cout<<"A::f1()"<
A.基类和子类的f1函数构成重写
B.基类和子类的f3函数没有构成重写,因为子类f3前没有增加virtual关键字
C.如果基类指针引用子类对象后,通过基类对象调用f2时,调用的是子类的f2
D.f2和f3都是重写,f1是重定义
4.关于抽象类和纯虚函数的描述中,错误的是 ( D)
A.纯虚函数的声明以“=0;”结束
B.有纯虚函数的类叫抽象类,它不能用来定义对象
C.抽象类的派生类如果不实现纯虚函数,它也是抽象类
D.纯虚函数不能有函数体
A.纯虚函数的声明以“=0;”结束,这是语法要求
B.有纯虚函数的类叫抽象类,它不能用来定义对象,一般用于接口的定义
C.子类不实现父类所有的纯虚函数,则子类还属于抽象类,仍然不能实例化对象
D.纯虚函数可以有函数体,只是意义不大
5.假设A为抽象类,下列声明( )是正确的
A.A fun(int);
B.A*p;
C.int fun(A);
D.A obj;
A.抽象类不能实例化对象,所以以对象返回是错误
B.抽象类可以定义指针,而且经常这样做,其目的就是用父类指针指向子类从而实现多态
C.参数为对象,所以错误
D.直接实例化对象,这是不允许的