多态及相关
- 多态的概念
- 多态实现的两个条件及特殊情况
- 虚函数
- 虚函数重写的例外
- C++11 override 和 final
- 重载、覆盖(重写)、隐藏(重定义)的对比
- 题目1
- 抽象类
- 接口继承和实现继承
- 题目2(很重要)
- 多态的原理
- 虚函数表
- 为什么Derive中的func4()在监视窗口里没有显示出来?
- 虚函数和虚表都存在于哪里?
- 静态绑定和动态绑定
- 区分虚函数表和虚基类表
- 单继承和多继承关系的虚函数表
- 菱形继承和菱形虚拟继承(了解)
- 菱形继承
- 菱形虚拟继承
- 题目3
多态的概念
多态:在C++中,多态就是不同的对象去实现同一个函数可以实现出不同的效果。多态是建立在继承的基础上的。
接着举一些生活中的例子: 例如,学生和普通人买火车高铁票,同样是去买票,学生可以享受到学生票价。 又比如,支付宝中对于新用户和老用户抢红包的奖励也是不一样的,通常新用户的红包会更大。 这些都是多态的表现。
多态实现的两个条件及特殊情况
多态是建立在继承的概念上的,多态还需要满足两个条件才能实现。
①必须通过基类的指针或者引用调用虚函数
②被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数。
class Person
{public:virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
这里强调一个点: 重写是对函数的内容进行修改,既然保持函数的返回值类型、函数名字、参数类型完全相同,那么我们重写的不就是函数的内容吗。
重写还有一些概念,我会放在下面再次提到。
class Person
{public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person
{public:virtual void BuyTicket() { cout << "买票-半价" << endl; } //构成重写
}
虚函数重写的例外
①协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指
针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
class Person
{
public:virtual Person* BuyTicket() //基类虚函数返回基类的指针或引用{ cout << "买票-全价" << endl;return nullptr;}
};
class Student : public Person
{
public:virtual Student* BuyTicket() //派生类虚函数返回派生类的指针或引用{cout << "买票-半价" << endl;return nullptr;}void Func(Person& p){p.BuyTicket();}
};
②析构函数的重写(基类与派生类析构函数的名字不同)
我们先前在模拟写析构函数的时候,都是写 ~类名()的形式 。但是我们多态调用析构函数时,如果写成同函数名的话,是不是很反常。 所以在C++有个规定,编译器会对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。 所以我们可以对析构函数进行特殊处理,基类与派生类析构函数的名字可以不同
结果符合预期,先调用Person的析构。 后面对派生类析构的时候,再自动对Person进行一次析构。
但如果,我们不对派生类进行重写会发生什么呢?
③如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写
class Person {
public:virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:~Student() { cout << "~Student()" << endl; } //省略virtual关键字
};
此时,依然构成重写。但是基类的virtual不可以省略。并且也不推崇这个写法,建议还是加上。
C++11 override 和 final
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的。因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
给派生类加了override关键字,基类成员函数没加virtual,不构成重写,报错。
final:修饰虚函数,表示该虚函数不能再被重写
给基类虚函数加了final,该虚函数就不能再被重写,派生类因重写报错了。(不加就不会报错)
重载、覆盖(重写)、隐藏(重定义)的对比
重载:重载是之前学的,函数重载的条件是函数必须在同一个作用域内,并且函数的函数名得相同参数名不同,这样构成函数重载。
重写/覆盖: 重写上面有提到,要求重写的函数在不同的作用域内,分别在基类和派生类中,函数必须是虚函数才行,并且重写的函数需满足函数名,参数(缺省函数和非缺省函数都得一致,缺省值可以不相同),返回值都相等!!(协变除外)。
重定义/隐藏: 重定义和重写属于一个互斥的关系,满足其一就不可能再符合另一种。 重定义同样要求重定义的函数分别在基类和派生类中,但对于函数的条件有所不同。首先,不要求为虚函数了,只要求函数的函数名相同就可以了。 强调,重写和重定义是互斥的!!
我们怎么分清重写和重定义的区别呢? 我的建议是,先看这两个函数能不能满足重写的条件(两个作用域,虚函数,函数名、参数、返回值都相等),如果不满足重写,但符合两个作用域且函数名相同的条件,那就叫重定义。
重写有一个点,我们在这里先铺垫一下: 函数重写,我们是对函数的定义,也就是内容进行修改。
题目1
class A
{
public:virtual void func(int val = 1){cout << val << endl;}
};
class B
{
public:virtual void func(int val = 2){cout << val << endl;}
};
int main()
{B b;b.func();A a;a.func();return 0;
}
此时,符合函数重写的条件。 打印的结果符合预期
假如我做一下修改
class A
{
public:virtual void func(int val = 1){cout << val << endl;}
};
class B
{
public:virtual void func(int val){cout << val << endl;}
};
int main()
{B b;b.func();A a;a.func();return 0;
}
此时,我们可以看到A中的函数有缺省值,而B中的函数没有,此时已经不符合函数重写的条件了,此时属于函数重定义的情况,也就是隐藏的情况。
在这里,调用B中的func(),如果不传参数,就会报错。 因为B中重定义了A中的函数,也就是B隐藏了A函数,那么再调用函数时,会去调用B的这个函数,而不会去寻找A中的这个函数了。又由于函数参数不匹配,就造成了编译不通过的问题。
抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象 。 纯虚函数规范了派生类必须重写。
此时可以看到父类是不能被实例化对象的。如果我们不对派生类进行重写函数,那派生类也会不能实例化对象,看下面!
那么我们出抽象类的意义是什么呢? 不仅仅是为了让我们对派生类中的虚函数进行重写,还有一个点就是比方说这里Car是个大类,通常有这种情况,父类是个大类,里面包含一些大的属性让派生类继承就可以,并不一定要父类来实现某些函数,这些函数会抛给子类去实现,因此也不用对父类进行实例化,而子类想要实例化就必须对函数进行重写!
接口继承和实现继承
实现继承: 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。(把函数实现的部分给一起继承下来了)
接口继承: 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。(重写,是对函数的实现进行重写,继承下来的是虚函数的声明(函数名,参数,返回值等等))
所以如果不实现多态,不要把函数定义成虚函数,因为实现多态也是要付出相应代价的。
题目2(很重要)
下面代码输出的结果是什么?
class A
{
public:virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }virtual void test() { func(); }
};
class B : public A
{
public:void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{B* p = new B;p->test();return 0;
}
我还要对这个题再进一步描述一下为什么这里this既然是A*了,在test中调用func为什么不是直接用A中的func打印出A->1,而是用A的声明,B的定义去打印出结果B->1
这里只需要理解两个点,就知道是为什么了?
①派生类重写虚函数,是对虚函数的定义进行重写,声明不会进行改变
②派生类传给父类,实际是对派生类中的父类部分进行了切割
画个简图方便大家理解一下:
下面再看一道题,和这个也是一样的。 我们再来验证一下:
class A
{
public:virtual void func(int val = 1){cout << val << endl;}
};
class B :public A
{
public:virtual void func(int val = 2){cout << val << endl;}
};
int main()
{A a;a.func();B b1;A(b1).func();B b2;b2.func();return 0;
}
结果:
我们会发现我们将B类的b1,转换为A类型,那么去调用func用的就是,A的声明val=1去实现的。 当然,不要忘记,如果直接用B类的b2去调用func,那么this指针本身就是B*,那么会直接用B类中func的声明和定义打印出val=2。
指向谁就调用谁。 b1指向B类,即使转换成A类型,调用了A的虚函数声明,但最后还是会去B中重写的虚函数定义。 b2指向B类,没有转换,直接就使用B中虚函数的声明和定义。
多态的原理
虚函数表
定义:一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
首先,通过一个例子来引出
// 题目是求sizeof(Base)
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
32位机器下,结果是8。
与之前的观念不同了,以前会认为只有一个int类型的_b,所以sizeof(Base)的大小是4,但是不然。 Base对象中出了int _b,还有一个虚函数表!
我们可以看到,b中比预期多了一个_vfptr。 这个就叫做虚函数表指针,v和f代表的是virtual fuction。 这个指针有什么用呢? 接着往下看:
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;}
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}
我们根据这个代码,还有接下来的监视,内存图看一些现象:
①Base中的func3由于不是虚函数,因此没有进入到虚函数表中去!
②b,d对象内存中第一行存放的是虚函数表的指针,通过这个指针可以找到虚函数表。
至于第二行存的是什么,我们下面也会讲到,第二行存的是虚基类表指针。
③d是b的派生类,我们可以观察到d中存放b的形式,可以看到与之前的继承很相似,但这次多出来一个虚函数表指针。派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针存在部分的有一部分是自己的成员。
④基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表
中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数
的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生
类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己
新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
那这里就产生了一个问题? 既然派生类自己新增加的虚函数会放到派生类虚表的最后,那么
为什么Derive中的func4()在监视窗口里没有显示出来?
实际上,确实监视窗口不会显示出来,那让我们来观察一下Derive中的_vfptr内存,看看是怎么一回事?
那么为了验证这个是否为func4,验证出我们上面写的结论,接下来进行详细验证:
typedef void(*VFPTR)(); //这就是函数指针的typedef的方法void PrintVFT(VFPTR* vft) //写出一个print函数,来打印对应的函数地址,并且运行出来。这样就可以直观的看到结果
{for (size_t i = 0; i < 3; i++){printf("%p->", vft[i]); //打印地址VFPTR pf = vft[i];(*pf)(); //执行这个函数 这个看起来奇怪,但函数指针就是这么用的}
}
int main()
{Base b;Derive d;// 首先我们的目的是为了取到_vfptr这个函数指针数组的地址,然后通过解引用下标的方式,//来访问到数组中的函数指针,从而打印函数地址并实现函数//VFPTR* ptr = &d; // 此时编译时不通过的,因为 &d是Derive*,不能强转成VFPTR*// 所以为了强转成功,我们得借助int来实现VFPTR* ptr = (VFPTR*)(*((int*)&d)); PrintVFT(ptr);return 0;
}
在PrintVFT函数中,通过打印地址,执行函数的方式,我们就可以得到结论,fun4是存在于派生类虚表中的,只是在监视窗口中显示不出来(可以理解成是bug)
再思考一个问题?
虚函数和虚表都存在于哪里?
虚函数存在虚表中。虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的(验证过程省略了) 虚表也同样存在于代码段!!=
静态绑定和动态绑定
静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
区分虚函数表和虚基类表
虚函数表属于一个函数指针数组,里面存放的函数指针可以找到对应的虚函数!
虚基类表我们在继承那里提到过,它是在派生类继承父类时,多加一个virtual关键字,使得成为虚基类,此时就会有一个虚基类表的指针存放在派生类所继承的父类里的内存中,指针指向的虚基类表中有一个参数会显示当前位置距离父类的偏移量。
单继承和多继承关系的虚函数表
单继承的虚函数表相信通过上面的描述,已经非常清楚了。这里再进行总结一下
继承过来的虚函数表做两件事: 更新虚函数表指针,以及虚函数如果有重新,就更新对应虚函数的定义
多继承:
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;
};
多继承呢相比单继承,一样该做那两件事。 但是呢,形式上肯定会有略微的不同。
菱形继承和菱形虚拟继承(了解)
菱形继承
class A
{
public:virtual void func1() { cout << "A::func1" << endl; }int _a;
};
class B : public A
{
public:virtual void func2() { cout << "B::func2" << endl; }int _b;
};
class C : public A
{
public:virtual void func3() { cout << "C::func3" << endl; }int _c;
};
class D : public B, public C
{
public:virtual void func4() { cout << "D::func4" << endl; }int _d;
};int main()
{D d;cout << sizeof(d) << endl; // 结论菱形继承的对象模型跟多继承类似d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
打印出来的结果会是多少呢?
为什么?怎么来的? 分析一下:
菱形虚拟继承
class A
{
public:virtual void func1() { cout << "A::func1" << endl; }int _a;
};
//class B : public A
class B : virtual public A
{
public:virtual void func2() { cout << "B::func2" << endl; }int _b;
};
//class C : public A
class C :virtual public A
{
public:virtual void func3() { cout << "C::func3" << endl; }int _c;
};
class D : public B, public C
{
public:virtual void func4() { cout << "D::func4" << endl; }int _d;
};int main()
{D d;cout << sizeof(d) << endl;// 结论菱形继承的对象模型跟多继承类似d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
这次的结果是:
同样的我们来分析一下:
此时虚基类A是共享的,内存中的形式就发生了变化!
题目3
请问输出结果为多少?
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;
}
结果: 0 1 2
思路:首先调用的是A的构造函数,此时,B还没有构造出来,因此不构成多态,直接调用A中的func(),打印出0。因为B继承了A(如果是多继承,先继承的先初始化)接着后面才构造B,此时就已经构成多态了,所以B中的A切割过去了,此时使用A的声明,B的定义完成func(),val++,打印出1。 后面通过p调用test(),同理,使用A的声明,B的定义完成func(),val++,打印出2。