多态:
-
多态的目的:
不同继承关系的类对象去调用同一个函数,能达到不同效果。 -
条件:
- 必须通过基类的指针或者引用调用虚函数。
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
- 虚函数:
被virtual修饰的类成员函数被称为虚函数。
class Person
{
public:
//被virtual修饰的类成员函数
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
虚函数注意点:
- 只有类的非静态成员函数前可以加virtual,普通函数前不能加virtual,static类型成员函数没有this指针,所以不能构成虚表中成员函数。
- 虚函数这里的virtual和虚继承中的virtual是同一个关键字,但是它们之间没有任何关系。虚函数这里的virtual是为了实现多态,而虚继承的virtual是为了解决菱形继承的数据冗余和二义性。
虚函数的重写和多态的使用方式
虚函数的重写也叫做虚函数的覆盖,若派生类中有一个和基类完全相同的虚函数(返回值类型相同、函数名相同以及参数列表完全相同),此时我们称该派生类的虚函数重写了基类的虚函数。
//父类
class Person
{
public:
//父类的虚函数
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
//子类
class Student : public Person
{
public:
//子类的虚函数重写了父类的虚函数
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
//子类
class Soldier : public Person
{
public:
//子类的虚函数重写了父类的虚函数
virtual void BuyTicket()
{
cout << "优先-买票" << endl;
}
};
以上需要满足三同,以下需要通过父类的引用或指针来接收子类对象。
void Func(Person& p)
{
//通过父类的引用调用虚函数
p.BuyTicket();
}
void Func(Person* p)
{
//通过父类的指针调用虚函数
p->BuyTicket();
}
int main()
{
Person p; //普通人
Student st; //学生
Soldier sd; //军人
Func(p); //买票-全价
Func(st); //买票-半价
Func(sd); //优先买票
Func(&p); //买票-全价
Func(&st); //买票-半价
Func(&sd); //优先买票
return 0;
}
- 多态和协变:
上面说了,协变的父子关系,也可以满足多态,协变指的是重写时的返回值类型如果满足非该类但也是继承的父子关系即可。
如下,子类中重写虚函数时,使用了B类的指针,而父类虚函数用了A类指针。调用也符合
//基类
class A
{};
//子类
class B : public A
{};
//基类
class Person
{
public:
//返回基类A的指针
virtual A* fun()
{
cout << "A* Person::f()" << endl;
return new A;
}
};
//子类
class Student : public Person
{
public:
//返回子类B的指针
virtual B* fun()
{
cout << "B* Student::f()" << endl;
return new B;
}
};
int main()
{
Person p;
Student st;
//父类指针指向父类对象
Person* ptr1 = &p;
//父类指针指向子类对象
Person* ptr2 = &st;
//父类指针ptr1指向的p是父类对象,调用父类的虚函数
ptr1->fun(); //A* Person::f()
//父类指针ptr2指向的st是子类对象,调用子类的虚函数
ptr2->fun(); //B* Student::f()
return 0;
}
- 析构函数的重写:
析构函数也需要构成多态,对于一个继承得到的子类做析构时,先调用自己的析构函数再调用父类的析构函数,分别去销毁自己和父类内容中申请的空间。
//父类
class Person
{
public:
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
//子类
class Student : public Person
{
public:
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
析构函数如果不重写。
int main()
{
//分别new一个父类对象和子类对象,并均用父类指针指向它们
Person* p1 = new Person;
Person* p2 = new Student;
//使用delete调用析构函数并释放对象空间
delete p1;
delete p2;
return 0;
}
delete p2时,delete语句会执行 p2->destructor(); operator delete(ptr2); 调用p2所指向的析构函数,此时若不将析构函数定义为虚函数,则不会发生重写,而是隐藏。则第二个delete时,事实上,Derived类中会有两个析构函数,一个是基类的,一个是自己的。p2类型为Base*,如果没有发生多态,只能调用基类的析构函数。有了多态,delete子类对象时,先调用子类的析构函数,再自动调用父类的析构函数,这样分别使得两部分内容都做了析构。(此外,我们不需要手动在子类的析构中调用父类析构,都是自动的)
- 回忆:
为了构成重写,编译后析构函数的名字会被统一处理成destructor()。
C++11的override和final
final:修饰虚函数,表示该虚函数不能再被重写。
override:检查派生类虚函数是否重写了基类的某个虚函数,如果没有重写则编译报错。
- final使用:
如下,会发生编译报错:
//父类
class Person
{
public:
//被final修饰,该虚函数不能再被重写
virtual void BuyTicket() final
{
cout << "买票-全价" << endl;
}
};
//子类
class Student : public Person
{
public:
//重写,编译报错
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
//子类
class Soldier : public Person
{
public:
//重写,编译报错
virtual void BuyTicket()
{
cout << "优先-买票" << endl;
}
};
- override:检查派生类虚函数是否重写了基类的某个虚函数,如果没有重写则编译报错。
//父类
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
//子类
class Student : public Person
{
public:
//子类完成了父类虚函数的重写,编译通过
virtual void BuyTicket() override
{
cout << "买票-半价" << endl;
}
};
//子类
class Soldier : public Person
{
public:
//子类没有完成了父类虚函数的重写,编译报错
virtual void BuyTicket(int i) override
{
cout << "优先-买票" << endl;
}
};
重载、覆盖(重写)、隐藏(重定义)的对比
重载:两个函数在同一定义域发生在同一个类中,函数名相同,参数列表不同。
重定义(隐藏):两个函数分别在基类和派生类的作用域中发生在两个类中,函数名相同,当再次用子类直接去调用父类的方法时,就找不到父类的方法了,会编译错误,(如果子和父函数的类型或参数不一样时,因为调不到子类,两个不同的作用域中)。
重写(覆盖):两个函数分别在基类和派生类的作用域中,函数名,参数,返回值相同(协变除外)
事实上,基类和派生类中,两个函数如果函数名相同,若没有构成重写,就是重定义(隐藏)
抽象类
纯虚函数
- 概念:
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类,抽象类不能实例化对象,只有重写纯虚函数,派生类才能实例化出对象。另外纯虚函数更体现出了接口继承。
class Base
{
public:
virtual void Drive() = 0;
};
-
接口继承和实现继承:
普通函数的继承是实现继承因为继承了函数实现,而虚函数继承是接口继承,因为继承的内容需要重写。 -
抽象类既然不能实例化出对象,那抽象类存在的意义是什么?
- 抽象类可以更好的去表示现实世界中,没有实例对象对应的抽象类型,比如:植物、人、动物等。
- 抽象类很好的体现了虚函数的继承是一种接口继承,强制子类去重写纯虚函数,因为子类若是不重写从父类继承下来的纯虚函数,那么子类也是抽象类也不能实例化出对象。
多态的原理
派生类的计算
求下面base对象:
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
我们发现base对象b是8个字节
int main()
{
Base b;
cout << sizeof(b) << endl; //8
return 0;
}
我们发现对象只有一个成员变量,但是大小是8,那是因为它有虚函数表指针。虚表指针指向的是虚表,每个含有虚函数的类至少有一个虚表指针。
实际上,父类对象和子类对象都会有自己的虚表指针,分别指向自己的虚表。
#include <iostream>
using namespace std;
//父类
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:
//重写虚函数Func1
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
如上,子类重写了父类的fun1,所以在虚表中fun1不同而fun2地址相同。所以子类和父类有两个不同的虚表,虚表是存虚函数地址的,相同虚函数,地址肯定一样。
- 派生类虚表生成步骤:
- 先将基类中的虚表内容拷贝一份到派生类的虚表。
- 派生类重写了基类中的某个虚函数,则用派生类自己的虚函数地址覆盖虚表中基类的虚函数地址。
- 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
- 虚表是什么阶段初始化的,虚函数存在哪里,虚表在哪里?
虚表实际上是在构造函数初始化列表阶段进行初始化的,虚函数和普通函数一样,在代码段,而虚表也在代码段。
看虚表的方法
首先,对于一个重写了虚函数的派生类,我们都可以通过运行查看到虚表指针指向的地址 。
我们在类函数中,输出一下是哪个函数。
再通过拿函数地址,直接获取到虚表中的函数地址。
代码如下:把虚表中每个函数地址打印。
函数指针的定义方法:\*和ptr一起,字母和\*一起,说明是个指针,而左边是void,是个类型,右边(),是个参数。所以是函数指针。
void(*ptr)();
typedef void(*VFPTR)();
而typedef定义函数指针时,直接把这一串放上即可。
// 参数:函数指针数组,直接函数指针加【】
void PrintVFT(VFPTR table[])
{
// 指向不为nullptr时,VS下虚表中最后是个空指针标志结束
for(size_t i = 0; table[i]!=nullptr; ++i)
{
// table[i]就能把上几个函数地址打印,为了方便看调用的是谁,在每个函数中打印输出class_name::fun();
printf("vft[%d]:%p", i, table[i]);// 打印出了函数地址,再调用函数
//table[i]();
VFPTR pf = table[i];
pf(); // 这样私有的也能调用,这样已经越过了正常调用方式。
}
}
如何传虚表地址,虚表地址在对象头部4个地址或前8个,VS中建议使用64位,前4个。回忆以前看大小端int时,拿前一个字节,借助指针。把student地址转为int*,取到头4个字节地址,再解引用得到头4个地址中存的值,这个值是虚表地址。
Student s1;
// *(int*)&s1; // 拿虚表地址 int:4个字节, 8个字节用long long
// PrintVFT(*(int*)&s1); // 传不过去,因为传入的是int*解引用,是个int,而那边形参是一个VFPTR [],也就是VFPTR*,数组名其实是指针,所以再做一次强转。
PrintVFT((VFPTR*)*(int*)&s1);
运行效果:
派生类新增的虚函数会按照声明顺序添加到自己虚表的后端。同一个类贡献一个虚表。
多态原理解释
父类指针接收父类对象或子类对象,这两个对象的虚表中存的虚函数不同,根据虚表查找到的虚函数就不同,所以形成了多态。切片并不影响非多态以外的部分,切片切到的部分正好构成多态。
动态绑定和静态绑定
静态绑定: 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也成为静态多态,比如:函数重载。
动态绑定: 动态绑定又称为后期绑定(晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
菱形继承、菱形虚拟继承
- 菱形虚拟继承的好处:
上一节继承中,我们知道为了菱形继承的二义性,使用了菱形虚拟继承,而使用菱形虚拟继承后,孙子类对象中存的两个父亲对象的首地址存的本身是都是同一个爷爷类的两个成员变量,有两个,而使用了菱形虚拟继承会把这个共同变量只存一个,且原来的位置存偏移量,这样的好处是当共同部分很大时,只存一份,偏移指针的空间代价远小于存两份同样的,且没有二义性。 - D类对象:
D类对象新增虚函数,会存到B类的虚表中,(哪个父类继承在前,就存哪个父类中)。
习题
- 关于虚函数说法正确的是( )
A.被virtual修饰的函数称为虚函数
B.虚函数的作用是用来实现多态
C.虚函数在类中声明和类外定义时候,都必须加虚拟关键字
D.静态虚成员函数没有this指针
A:被virtual修饰的成员函数称为虚函数。
B:实现多态,我觉得只能算一个吧,解决菱形继承问题时候也涉及了。
C:virtual只能在类内。正确
D:静态成员函数没有this指针,而静态虚函数概念不存在,virtual虚函数两个条件:只能加在非static和类成员函数前面。
2. 关于多态,说法不正确的是( )
A.C++语言的多态性分为编译时的多态性和运行时的多态性
B.编译时的多态性可通过函数重载实现
C.运行时的多态性可通过模板和虚函数实现
D.实现运行时多态性的机制称为动态绑定
A:这里说的是C++的多态,正确。
B:早期绑定是重载。
C:模板属于编译时多态,运行时多态分是虚函数多态。
D:正确。
- 关于虚表说法正确的是( )
A.一个类只能有一张虚表
B.基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C.虚表是在运行期间动态生成的
D.一个类的不同对象共享该类的虚表
A:一个类只能有一张虚表。
B:父类对象的虚表和子类对象的虚表没有任何关系。
C:虚表在编译期间生成
D:一个类的不同对象共享该类虚表,一个类只有一个虚表。
- 如果类B继承类A,A::x()被声明为虚函数,B::x()重写了A::x()方法,下述语句中哪个x()方法会被调用:( )
B b;
b.x();
A.A::x()
B.B::x()
C.A::x() B::x()
D.B::x() A::x()
重写,通过子的x方法,肯定调用的是子的。
- 程序输出结果是:
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;
}
第一行,已经发生了切片,创建B,先调用A构造函数,所以先输出一次0,再用B构造函数,test(),而test()是A的test(),A的fun()会去调用B的fun(),加完后输出1,然后p的test(),是A的test(),又去B的fun(),透你妈,这种题无聊至极,又加了一次,输出2。
考查知识点总结:
- A* p = new B:会产生切片
- 父子继承关系中,创建B对象,会先调用A构造,再调用B构造。
- 子类重定义(同名隐藏或说重写也一样)后,父类会用自类的方法。
- 子类中,可以直接调用父类的方法。自己如果没有重写的话。或者不是什么虚函数之类的。
- 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )
A.A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B.A类对象和B类对象前4个字节存储的都是虚表的地址
C.A类对象和B类对象前4个字节存储的虚表地址相同
D.A类和B类中的内容完全一样,但是A类和B类使用的不是同一张虚表。
B继承A,A、B中前4字节存的是虚表地址。且两个不是同一张虚表,父类、子类不是同一个类,虚表不一样,内容也不一样。
7. 假设D类先继承B1,然后继承B2,B1和B2基类均包含虚函数,D类对B1和B2基类的虚函数重写了,并且D类增加了新的虚函数,则:( )
B:D类对象有两个虚表,D类新增加的虚函数放在第一张虚表最后
其它错误选项别看了。
- 关于不能设置成虚函数的说法正确的是( )
A.友元函数可以作为虚函数,因为友元函数出现在类中
B.成员函数都可以设置为虚函数
C.静态成员函数不能设置成虚函数,因为静态成员函数不能被重写
D.析构函数建议设置成虚函数,因为有时可能利用多态方式通过基类指针调用子类析构函数
A:友元不是成员函数,不能。
B:静态成员函数不能设置为虚函数。
C:因为静态成员函数与具体对象无关,属于整个类,没有this,没有this无法拿到虚表,就无法实现多态,不能设置为虚函数。
D:正确。
9 。 要实现多态类型的调用,必须( )
A.基类和派生类原型相同的函数至少有一个是虚函数即可
B.假设重写成功,通过指针或者引用调用虚函数就可以实现多态
C.在编译期间,通过传递不同类的对象,编译器选择调用不同类的虚函数
D.只有在需要实现多态时,才需要将成员函数设置成虚函数,否则没有必要
A:必须是父类。
B:文字游戏,需要说是父类。
C:虚函数多态又称为运行时多态。
D:正确。
- 下面函数输出结果是( )
class A
{
public:
virtual void f()
{
cout<<"A::f()"<<endl;
}
};
class B : public A
{
private:
virtual void f()
{
cout<<"B::f()"<<endl;
}
};
A* pa = (A*)new B;
pa->f();
A.B::f()
B.A::f(),因为子类的f()函数是私有的
C.A::f(),因为强制类型转化后,生成一个基类的临时对象,pa实际指向的是一个基类的临时对象
D.编译错误,私有的成员函数不能在类外调用
因为会用B的覆盖A的,而不影响A的访问限定符,因为这里做切片,只是把B的内容给了A,但是一切按A的规则。
所以选A。
-
关于重载、重写和重定义的区别说法正确的是( )
A.重写和重定义都发生在继承体系中
B.重载既可以在一个类中,也可以在继承体系中
C.它们都要求原型相同
D.重写就是重定义
E.重定义就是重写
F.重写比重定义条件更严格
G.以上说法全错误
C:重写要原型相同(三同),重定义是同名隐藏,不需要过多,所以C错误,
D:重写是覆盖,是多态。重定义是隐藏。 -
关于重载和多态正确的是 ( )
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
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。
C:重载不涉及子类。
D:构成了虚函数重写。所以切片完调用子类方法。
- 纯虚函数的声明以"=0"结束,是语法要求。纯虚函数可以有函数体。
- 假设A为抽象类,下列声明( )是
抽象类可以定义指针,是一种多态,且我见过的,抽象类也只能是个指针了。
A.A fun(int);
B.A*p;
C.int fun(A);
D.A obj;
只有B符合规范。