多态的概念:通俗来讲,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
例如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票,军人买票时是优先买票。
多态是在不同继承关系的对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买全价票,Student对象买半价票。
class Person
{
public:
virtual void BuyTicket()
{
cout << "Person : 全价买票" << endl;
}
};
class Student :public Person
{
public:
virtual void BuyTicket()
{
cout << "Student : 半价买票" << endl;
}
};
被virtual修饰的类成员函数函数是虚函数。
在子类中有一个和父类完全相同的虚函数(即子类虚函数和父类虚函数的返回值类型,函数名字,参数列表完全相同(参数的类型相同就符合))。 称子类的虚函数重写(覆盖)了父类的虚函数。
虚函数的重写(覆盖):三同(返回值类型,函数名,参数)
在继承的时候学过一个隐藏(重定义)的概念。这里不要弄混了。
多态的条件
1.虚函数重写。
2.父类的指针或者引用去调用虚函数。
class Person
{
public:
virtual void BuyTicket()
{
cout << "Person : 全价买票" << endl;
}
};
class Student :public Person
{
public:
virtual void BuyTicket()
{
cout << "Student : 半价买票" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Student s;
Func(p);
Func(s);
return 0;
}
满足多态的条件属于多态调用:父类对象调用父类的虚函数。子类对象调用子类的虚函数。
如果用父类的普通对象去调用虚函数是什么样的。
不满足多态调用的条件。p传给p,调用自己的BuyTicket()成员函数;把s传给p,相当于把s属于p的切割然后赋值给p,还是调用p的BuyTicket();
修改一下代码再看
class Person
{
public:
void fun1()
{
cout << "Person : fun1" << endl;
}
};
class Student :public Person
{
public:
void fun1()
{
cout << "Student : fun1" << endl;
}
};
void Func(Person& p)
{
p.fun1();
}
int main()
{
Person p;
Student s;
Func(p);
Func(s);
return 0;
}
也不满足多态调用。把子类中属于父类的那一部分给父类的引用,虽然这一部分还属于子类。但是p的类型是Perosn,因此调用的是Person的成员函数。
总结:
普通调用:跟调用对象类型有关
多态调用:跟指针和引用的对象有关
看下面特殊情况。
class Person
{
public:
virtual void BuyTicket()
{
cout << "Person : 全价买票" << endl;
}
};
class Student :public Person
{
public:
void BuyTicket()
{
cout << "Student : 半价买票" << endl;
}
};
把子类成员函数BuyTicket(),virtual删掉了。那这还能满足多态调用吗?
特殊情况:
再看一种特殊情况。
class Person
{
public:
virtual Person& BuyTicket()
{
cout << "Person : 全价买票" << endl;
return *this;
}
};
class Student :public Person
{
public:
virtual Student& BuyTicket()
{
cout << "Student : 半价买票" << endl;
return *this;
}
};
返回值类型竟然不同了。看是否满足多态调用。
编译报错了,不满足协变。
特殊情况:
注意要是引用都必须是引用,要是指针都必须是指针,子类的虚函数的返回值类型可以是父类或子类的引用或指针,但父类只能是父类的引用或指针。
下面也是也可以的。
class A
{};
class B : public A
{};
class Person
{
public:
virtual A* BuyTicket()
{
cout << "Person : 全价买票" << endl;
return nullptr;
}
};
class Student :public Person
{
public:
virtual B* BuyTicket()
{
cout << "Student : 半价买票" << endl;
return nullptr;
}
};
问:析构函数是否建议是虚函数,为什么?
class Person
{
public:
~Person()
{
cout << "~Person()" << endl;
}
};
class Student :public Person
{
public:
~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
Person p;
Student s;
return 0;
}
那下面这种情况去析构呢?
int main()
{
//Person p;
//Student s;
Person* ptr1 = new Person;
Person* ptr2 = new Student;
//delete行为
//1.调用析构函数完成对象中资源清理
//2.调用operator delete释放空间
delete ptr1;
delete ptr2;
return 0;
}
看运行结果
delete ptr2只释放了Student中属于父类那部分,而子类没有释放。
造成了内存泄漏。
解决方法:
给析构函数加上virtual。
子类可以不加,并且也满足三同。
这里可能有疑问,三同中不是要求函数名相同吗,是的没错,函数名必须相同,还记得在继承时析构说的,父子类析构函数名会自动转成destructor。这里就是原因,要满足多态调用。
多态调用:跟指针和引用的对象有关。
class Person
{
public:
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student :public Person
{
public:
~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
Person* ptr1 = new Person;
Person* ptr2 = new Student;
delete ptr1;
delete ptr2;
return 0;
}
总结:实现父类的时候,可以无脑给析构函数加virtual。
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
1.final:修饰虚函数,表示该虚函数不能在被重写
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() { cout << "Benz-舒适" << endl; }
};
问:如何实现出一个不能被继承的类
1.构造私有 c++98
2.类定义时加final c++11
class A virtual
{
public:
A(){}
};
class B : public A
{
public:
};
2.override:检查派生类函数是否重写了基类某个虚函数,如果没有重写编译报错
class Car{
public:
virtual void Drive(int i){}
};
class Benz :public Car {
public:
//没有重写,报错
virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
也就是说抽象类强制了子类必须重写虚函数。
class Car{
public:
virtual void Drive() = 0;
};
class Benz :public Car {
public:
//virtual void Drive() { cout << "Benz-舒适" << endl; }
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
int main()
{
//报错
Car c;
//报错
//派生类继承之后也变成抽象类。
Benz b;
BMW m;//可以实例化
return 0;
}
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
看这样一道题
以下程序输出结果是什么()
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;
}
A: A->0
B : B->1
C : A->1
D : B->0
E : 编译出错
F : 以上都不正确
// 问:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
private:
int _b = 1;
char _ch;
};
我们发现Base类除了_b,_ch成员,还多了一个_vfptr放在对象里(注意有些平台可能会放到对象的最后面,这跟平台有关),对象中这个指针我们叫做虚函数表指针,一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中。
那么派生类在这个表都放些什么呢?
修改一下代码看清楚一些。
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;
char _ch;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
void Func3()
{
cout << "Derive::Func3()" << endl;
}
private:
int _d = 2;
};
Func3不是虚函数,不进入虚表,被继承下来也不是虚函数因此最终都不会放在虚表里。
上面说了这么多,那么多态的原理到底是什么?
我们现在知道有普通调用,和多态调用。我们来对比一下它们的各个原理帮助理解多态的原理。
int main()
{
Base b;
Derive d;
//普通调用 ----编译时/静态 绑定
Base* ptr = &b;
ptr->Func3();
ptr = &d;
ptr->Func3();
//多态调用 ----运行时/动态 绑定
ptr = &b;
ptr->Func1();
ptr = &d;
ptr->Func1();
return 0;
}
普通调用:普通调用是在编译的时候,通过类型就锁定这个函数是谁,找到这个函数地址去进行调用。
多态调用在编译的时候确定不了调用函数是谁,ptr->Func1()指的到底是父类还是子类。
为什么这样说呢。因为不管指向父类还是子类反正都是指向父类的那一部分,只是一个是本身指向父类,一个是指向子类中属于父类的那一部分,真正差别就是虚表里虚函数地址不一样,如果是父类对象,那就是父类的虚函数,如果是子类对象(子类对象完成虚函数重写),放的是被重写覆盖的虚函数。
多态原理:所以无论指向父类还是子类,都是取到虚表指针去虚表里找到对应虚函数的地址然后再调用虚函数。
那这个虚表在哪里?虚函数再哪里呢?
注意虚表里面存的是虚函数地址,不是虚函数。虚函数和普通函数都是存在存在代码端。 虚表指针存在对象里。那虚表到底在哪里呢?
验证一下虚表在哪。
int main()
{
int a = 0;
cout << "栈 :"<< &a << endl;
int* ptr = new int;
cout << "堆 :" << ptr << endl;
static int b = 0;
cout << "数据段/静态区 :" << &b << endl;
const char* str = "hello world";
cout << "代码段/常量区 :" << (void*)str << endl;
Base be;
//32位机器下,指针是4byte大小,所以强转成int*,*解引用是一个10进制的值,为了打印出地址强转(void*)指针
cout << "虚表 :" << (void*)(*(int*)&be) << endl;
return 0;
}
注意:父类和子类的虚表不一样。同一个类型虚表是一样的。
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive :public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
观察上图,监视窗口只能看见重写的func1和没变的func2,func3那去了。
我们打开内存,取虚表指针看一下。
在虚表里我们看见了三个地址,分别是fun1,func2,func3。
注意vs下虚表以空指针结束。
打印虚表里地址怎么做?
虚表是一个函数指针数组,里面放的是函数指针。又以空指针结尾。
我们把第一个函数地址传给函数指针数组,循环打印地址。
//func1,func2,func3,三同,因此函数指针类型是一样的。
//函数指针类型重命名,但这种重命名方式不对
//typedef void(*)() VTprt;
typedef void(*VTptr)();
void PrintVFTable(VTptr vft[])
{
for (int i = 0; vft[i]!=nullptr; ++i)
{
printf("[%d]:%p", i, vft[i]);
vft[i]();//调用一下函数
}
cout << endl;
}
int main()
{
Base b;
PrintVFTable((VTptr*)(*(int*)&b));
Derive d;
PrintVFTable((VTptr*)(*(int*)&d));
}
这里还有一个问题,能不能写一个通用的代码,不管在多少位机器都能跑。
32位下指针大小4byte,62位下指针大小8byte。
这里推荐一种做法。
int main()
{
Base b;
//void**解引用-->void*--->指针的大小
PrintVFTable((VTptr*)(*(void**)&b));
Derive d;
PrintVFTable((VTptr*)(*(void**)&d));
}
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;
};
问func3放到哪里了?
Base1,Base2虚表都放,还是只放某一个虚表。
我们Base1虚表和Base2虚表都打印出来。
Base1虚表好打印,Base2虚表怎么打印?
int main()
{
Derive d;
//打印Base1虚表
PrintVFTable((VTptr*)(*(void**)&d));
//打印Base2虚表
PrintVFTable((VTptr*)(*(void**)((char*)&d + sizeof Base1)));
return 0;
}
注意这种情况并不是我们代码错了,这是因为虚表最后没有放空指针放了其他东西。这时我们只要清理一下解决方案,重新编译就好了。
多继承派生类未重写的虚函数放在第一个继承基类部分的虚函数表中
有多继承就可能有菱形继承,实际上我们不建议设计出菱形继承及菱形虚拟继承。一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定的性能损耗。这里有兴趣简单了解即可。
class A
{
public:
virtual void func1()
{}
public:
int _a;
};
class B : public A
{
public:
virtual void func1()
{}
virtual void func2()
{}
public:
int _b;
};
class C : public A
{
public:
virtual void func1()
{}
virtual void func3()
{}
public:
int _c;
};
class D : public B, public C
{
public:
virtual void func1()
{}
public:
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;
}
class A
{
public:
virtual void func1()
{}
public:
int _a;
};
class B : virtual public A
{
public:
virtual void func1()
{}
virtual void func2()
{}
public:
int _b;
};
class C : virtual public A
{
public:
virtual void func1()
{}
virtual void func3()
{}
public:
int _c;
};
class D : public B, public C
{
public:
virtual void func1()
{}
public:
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的func1进行了重写,在D里面必须要对func1重写,在菱形继承里B和C中的A不是同一个,但在菱形虚拟继承中只有一份A,那这么份A是放B还是放C呢?所以要求D重写,放D。
B和C中有单独的虚函数,因此B和C有单独的两个虚表
下面哪种面向对象的方法可以让你变得富有( )
A: 继承 B: 封装 C: 多态 D: 抽象
( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定
面向对象设计中的继承和组合,下面说法错误的是?()
A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用
B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用
C:优先使用继承,而不是组合,是面向对象设计的第二原则
D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现
以下关于纯虚函数的说法,正确的是( )
A:声明纯虚函数的类不能实例化对象 B:声明纯虚函数的类是虚基类
C:子类必须实现基类的纯虚函数 D:纯虚函数必须是空函数
关于虚函数的描述正确的是( )
A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型
B:内联函数可能是虚函数
C:派生类必须重新定义基类的虚函数
D:虚函数可以是一个static型的函数
关于虚表说法正确的是( )
A:一个类只能有一张虚表
B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C:虚表是在运行期间动态生成的
D:一个类的不同对象共享该类的虚表
假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )
A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B:A类对象和B类对象前4个字节存储的都是虚基表的地址
C:A类对象和B类对象前4个字节存储的虚表地址相同
D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表
下面程序输出结果是什么? ()
class A{
public:
A(char *s)
{
cout<<s<<endl;
}
~A(){}
};
class B:virtual public A
{
public:
B(char *s1,char*s2)
:A(s1)
{
cout<<s2<<endl;
}
};
class C:virtual public A
{public:
C(char *s1,char*s2)
:A(s1)
{
cout<<s2<<endl;
}
};
class D:public B,public C
{
public:
D(char *s1,char *s2,char *s3,char *s4)
:B(s1,s2),C(s1,s3),A(s1)
{
cout<<s4<<endl;
}
};
int main() {
D *p=new D("class A","class B","class C","class D");
delete p;
return 0;
}
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
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main(){
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3
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;
}
第10题我们前面讲过,第9题很简单不讲了,讲一下第8题。
8. 下面程序输出结果是什么? ()
class A{
public:
A(char *s)
{
cout<<s<<endl;
}
~A(){}
};
class B:virtual public A
{
public:
B(char *s1,char*s2)
:A(s1)
{
cout<<s2<<endl;
}
};
class C:virtual public A
{public:
C(char *s1,char*s2)
:A(s1)
{
cout<<s2<<endl;
}
};
class D:public B,public C
{
public:
D(char *s1,char *s2,char *s3,char *s4)
:B(s1,s2),C(s1,s3),A(s1)
{
cout<<s4<<endl;
}
};
int main() {
D *p=new D("class A","class B","class C","class D");
delete p;
return 0;
}
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
首先看选择,就要把B和C排除掉,对象初始化的时候先走的是初始化列表在走后面的,剩下A和D。
这是一个菱形虚拟继承,里面只有一份A,一份A不可能初始化三次,这个A是D中的,
猜也要猜这里的A初始化的。
对于成员变量来说初始化列表中的顺序并不是初始化顺序,谁先声明谁先初始化。
在继承中谁先被继承谁先走,包括菱形,菱形虚拟继承都是这样的。
B先继承,先走B,在走C。因此最终答案选A。
其实这道题还不够迷惑,把初始B和C的位置换一下就更难了。
不过最终答案还是A。
这样改的话,答案选D。
参考答案:
学了本节内容可以自己答1,2,3题。
1.什么是多态?
2.什么是重载、重写(覆盖)、重定义(隐藏)?
3.多态的实现原理?
4.inline函数可以是虚函数吗?
答:可以,虚函数虽然放在虚表中,如果是多态调用(动态绑定),在程序运行时找到虚表才能确定调用那个函数地址然后去调用。普通调用(静态绑定),在编译时就可以确定调用函数地址然后去调用,继承保存inline的属性。
5.静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
6.构造函数可以是虚函数吗?
答:不能,虽然虚表在编译的时候就生成了,但是虚表指针还没有初始化,找不到需要,因此也找不到构造函数。而虚表指针是在构造函数初始化列表阶段才初始化的。
7.析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,前面内容已经提到。
8.对象访问普通函数快还是虚函数更快?
答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
9.虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
10.C++菱形继承的问题?虚继承的原理?
答:菱形继承数据冗余和二义性的问题,为了解决这个问题有了菱形虚拟继承,菱形虚拟继承让继承的相同数据只有一份,这份数据是共享的,想要找到这个数据通过虚基类指针找到虚基表 ,虚基表里存的是到这个相同数据的偏移量。注意这里不要把虚函数表和虚基表搞混了。
答:虚函数后面加个=0,这是一个纯虚函数,包含纯虚函数的类叫做纯虚类。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。