【C++】---多态
一、多态的概念
多态就是:多种形态。通俗的来说,多态的概念就是:同一件事情,不同的对象去做会有不同的结果。
二、多态的定义及实现
1、构成多态的2个必要条件
1. 子类必须对父类的虚函数进行重写(重写,包括三同:返回值相同,函数名相同,参数列表相同。协变除外)
2. 必须是父类的指针或者引用去调用虚函数,而且被调用的函数必须是虚函数。
2、什么叫做虚函数的重写?
1、什么叫做虚函数?
虚函数:在 成员函数 前面加 virtual
2、虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
#include<iostream>
using namespace std;
class Person
{
public:
virtual void Buy_ticket()
{
cout << "成人-全价" << endl;
}
};
class Student : public Person
{
public:
virtual void Buy_ticket()
{
cout << "学生-半价" << endl;
}
};
void Func(Person& p)
{
p.Buy_ticket();
}
int main()
{
Person p1;
Student s1;
Func(p1);
Func(s1);
return 0;
}
3、虚函数重写的3个例外
- 协变:基类 与 派生类 虚函数返回值类型 可以不同。派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(了解)
协变 的代码
class A{};
class B : public A {};
class Person {
public:
virtual A* f() {return new A;}
};
class Student : public Person {
public:
virtual B* f() {return new B;}
};
- 子类的虚函数可以不写:virtual (因为形成多态后,如果存在调用子类的虚函数,实际上,只看子类虚函数的内部函数实现,不看头部,头部继承的父类,所以:子类虚函数可以不写:virtual)
但是该种写法不是很规范,不建议这样使用
3、子类的虚函数中:析构函数,即使函数名不同,也会构成:重写!
解释:
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,
都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,
看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊
处理,编译后析构函数的名称统一处理成destructor!!!
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
4、建议把 析构函数 都定义为:虚函数
注意:如果不把析构函数都定义为虚函数的话,可能会发生内存泄漏!
1、自己好好想为什么建议把析构函数定义虚函数,防止发生内存泄漏?
因为如果所有的析构函数都定义为虚函数之后,它们所有的析构函数就会形成多态的关系,形成多态之后,就会达到我们的期望:delete的时候,子类对象调用子类的析构函数,父类对象要用父类的析构函数
如果在子类里面定义一个私有成员变量,如果所有析构函数不写成虚函数,无法满足多态的条件,没有形成多态的话,像上面子类有一个私成员变量,这个私有成员变量,无法被释放掉,就会导致内存泄漏。
三、C++11的两个关键字:final override
1、final:修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
2、override: 检查是否重写
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
四、重载、隐藏、重写 的比较
五、多态的原理
1、虚表(虚函数表)
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
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;
}
- 虚表:存在于哪里?常量区
- 虚函数:和普通函数一样,存在代码段!
- 虚函数指针:存在于:虚表
2、虚表指针:
虚表指针:
① 只要类里面有虚函数,就一定有虚表指针!
②同类型的对象,共用一张虚表
3、底层原理:满足多态后,在调用的指令中,指向谁 就去谁的虚表 里面 找对应的 虚函数 进行调用!
上面分析了这个半天了那么多态的原理到底是什么?还记得这里Func函数传Person调用的
Person::BuyTicket,传Student调用的是Student::BuyTicket
①满足多态后,在调用的指令中,指向谁 就去谁的虚表 里面 找对应的 虚函数 进行调用!
②如果函数不满足多态,那么就与指向的对象没有关系,跟你函数的形参类型有关系。
不满足多态,在编译链接时就直接,确定好调用函数的类型,地址!
六、抽象类
1、纯虚函数
定义:在虚函数的后面写上 =0
2、抽象类(接口类)
定义:包含纯虚函数的类
性质:抽象类不能实例化出对象。子类继承抽象类后也不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象。
意义:
① 能够更好地去表示现实世界中没有实例对象是我抽象类型,如:植物、人、动物
② 体现接口继承,强制子类去重写虚函数(就算不重写,子类也是抽象类)
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;
}
};
void Test()
{
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}
抽象类不能实例化出对象:
int main()
{
Animal a;
return 0;
}
3、接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
七、单继承关系中的虚函数表
1、单继承中的虚函数表
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;}
virtual void func4() {cout<<"Derive::func4" <<endl;}
private :
int b;
};
八、继承和多态常见的面试问题
1、选择题
- 下面哪种面向对象的方法可以让你变得富有( )
A: 继承 B: 封装 C: 多态 D: 抽象
答案: A 当然是继承更富有啦
- ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定
答案: D 动态绑定是函数调用时关联到具体对象
- 面向对象设计中的继承和组合,下面说法错误的是?()
A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用
B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用
C:优先使用继承,而不是组合,是面向对象设计的第二原则
D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现
答案: C 尽量少用继承,会破坏封装原则,多用组合,能降低耦合度
- 以下关于纯虚函数的说法,正确的是( )
A:声明纯虚函数的类不能实例化对象 B:声明纯虚函数的类是虚基类
C:子类必须实现基类的纯虚函数 D:纯虚函数必须是空函数
答案: A 包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象
- 关于虚函数的描述正确的是( )
A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型 B:内联函数不能是虚函数
C:派生类必须重新定义基类的虚函数 D:虚函数可以是一个static型的函数
答案: B inline函数没有地址,当inline成为虚函数后,虚表里面要放它的地址,构成多态时,根据虚函数表指针去call这个地址,就不能展开了,就忽略了内联属性,加了虚函数以后就不再是内联函数了。
- 关于虚表说法正确的是( )
A:一个类只能有一张虚表
B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C:虚表是在运行期间动态生成的
D:一个类的不同对象共享该类的虚表
对于A如果是多继承,那么这个类的对象会有多张虚表
对于B,监视:
#include<iostream>
using namespace std;
class Animal
{
public:
virtual void Color()//颜色
{
cout << "virtual Animal::color" << endl;
}
virtual void Name()//名称
{
cout << "virtual Animal::name" << endl;
}
};
class Coral :public Animal
{};
int main()
{
Animal a;
Coral c;
return 0;
}
发现虚表指针不同,虽然虚表指针中存放的虚函数地址相同:
对于C:虚表在编译时就已经生成了
对于D:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Animal
{
public:
virtual void Color()//颜色
{
cout << "virtual Animal::color" << endl;
}
virtual void Name()//名称
{
cout << "virtual Animal::name" << endl;
}
};
int main()
{
Animal a;
Animal a1;
return 0;
}
监视发现:a和a1的虚表指针地址相同,虚表指针中存放的虚函数地址也相同
- 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,正确的是()
A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B:A类对象和B类对象前4个字节存储的都是虚基表的地址
C:A类对象和B类对象前4个字节存储的虚表地址相同
D:A类和B类中的虚函数个数相同,但是A类和B类使用的不是同一张虚表
接下来是我对这一题的查缺补漏。一个选项一个选项,详细的解析一下
A、对于a选项来说,只要一个类里面有虚函数,这个类的内存里前4个字节都储存的是虚表的地址
B、 b选项和a选项是一样的。
C、关于这个虚表地址相同与否,可有的说了!
①:虽然现在有两个不同的类,但是如果子类没有对父类的虚函数进行重写行为,那么子类和父类 可能 共用一张虚表。
②:与①对立如果两个不同的类子类对父类的虚函数进行了重写行为,那么两者就用各自的去表。(因为这样就会构成多态,如果构成多态,调用谁,就去自己的虚表里面,找自己的虚函数地址)
答案:D 因为题目中已经说了子类对父类的虚函数进行了重写,所以说两者用的是各自的虚表。
- 下面程序输出结果是什么? ()
#include<iostream>
using namespace std;
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
答案:A 子类构造函数必须调用父类的构造函数初始化父类的成员,因此执行D的构造函数前必须执行B和C的构造函数,执行B和C的构造函数前必须执行A的构造函数
- 多继承中指针偏移问题?下面说法正确的是( )
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
10. 以下程序输出结果是什么()
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: 以上都不正确
这里考察的一个点就是:虚函数重写,重写的是函数体实现,但函数结构部分(virtual 返回值 函数名 参数列表)用的还是父类的。
这就是为什么第3个例外:子类的虚函数可以不加virtual,因为他压根就不看子类前面的函数结构,他用是父类结构(virtual 返回值 函数名 参数列表)。
2、问答题
-
什么是多态?(不同的对象去完成同一件事情,产生了不同的结果)
-
多态的原理?(一句话:形成多态之后,指向谁,就去谁的虚表里面找对应的虚函数地址进行调用对应的虚函数。)
当指针或引用指向父类对象时,调用的就是父类的虚表中的虚函数,当指针或引用指向子类对象时,调用的就是子类虚表中的虚函数。 -
inline函数可以是虚函数吗?
答:不能,因为inline函数没有地址,无法把地址放到虚函数表中。
inline函数没有地址,当inline成为虚函数后,虚表里面要放它的地址,构成多态时,根据虚函数表指针去call这个地址,就不能展开了,就忽略了内联属性,加了虚函数以后就不再是内联函数了。
- 静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
- 构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
假如构造函数是虚函数:
①调用构造函数虚函数必须要去虚表里面找,这就要求对象必须已经被初始化出来了。
②要初始化对象,就要调构造函数虚函数,但是对象还没有构造出来,虚表还没有初始化,还找不到构造函数虚函数地址。
这就陷入了死循环
- 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,最好把基类的析构函数定义成虚函数。
- 对象访问普通函数快还是虚函数更快?
答:
如果是普通对象,那么访问普通函数和虚函数是一样快的。
如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找,需要耗时。
- 虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
- C++菱形继承的问题?虚继承的原理?
答:子类对象会有两份父类的成员,菱形继承会导致数据冗余和二义性。
虚继承通过虚基表指针的偏移量计算出父类成员的起始地址,这样就只需要在内存中存一份父类成员,解决了数据冗余和二义性的问题。
- 什么是抽象类?抽象类的作用?
答:包含纯虚函数的类叫做抽象类。
抽象类不能实例化出对象。子类继承抽象类后也是抽象类,没有重写虚函数,不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象。
① 能够更好地去表示现实世界中没有实例对象是我抽象类型,如:植物、人、动物
② 体现接口继承,强制子类去重写虚函数(就算不重写,子类也是抽象类)
好了,今天的分享就到这里了
如果对你有帮助,记得点赞👍+关注哦!
我的主页还有其他文章,欢迎学习指点。关注我,让我们一起学习,一起成长吧!