多态
1.概念:多态就是多种形态。具体来说就是完成某个行为,当不同的对象去完成的时候会产生不同的状态。
2.构成条件
-
(1)被调用的对象必须是指针或者是引用。
-
(2)被调用的对象必须是虚函数,且完成了虚函数的重写。
-
(3)虚函数:就是在类的成员函数上面加上virtual关键字
-
(4)虚函数的重写:派生类中有一个基类完全相同的虚函数,我们就称子类的虚函数重写了基类的虚函数,完全相同是指:函数名,参数,返回值都相同。另外,虚函数的重写也叫做虚函数的覆盖。
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票 - 全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票 - 半价" << endl;
}
};
void Fun(Person& p)
{
p.BuyTicket();
}
int main()
{
Person pe;
Student st;
Fun(pe);
Fun(st);
system("pause");
return 0;
}
- (5)虚函数重写的例外:协变 — 重写的虚函数的返回值可以不同,但是必须分别是基类指针和派生类指针或者基类引用和派生类引用。
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;
}
};
- (6)不规范的重写行为:在派生类中重写的成员函数可以不加上virtual关键字,也是构成重写的,因为继承后基类的虚函数被继承下来,在派生类中依然保持虚函数的特性,我们只是重写了它。
lass Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
(7)析构函数的重写行为:基类中的析构函数如果是虚函数,那么派生类的析构函数就重写了基类的析构函数。这里他们的函数名不同,看起来违背了重写的规则。其实不然,这里可以理解为编译器对析构函数的名称做了特殊的处理,编译后析构函数的名称统一处理为destructor,这也说明基类的析构函数最好写成虚函数。
二.重载,覆盖(重写),隐藏(重定义)的对比
三.抽象类
在虚函数的后面写上 = 0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承之后也不能实例化出对象,只有重写纯虚函数才能实例化出对象。纯虚函数规定了派生类必须重写,另外纯虚函数更体现了接口继承。
四.c++11中override和final
实际中我们建议多用纯虚函数 + override的方式强制重写虚函数,因为虚函数的意义就是实现多态,如果没有重写,虚函数就没有意义。
//final 修饰的基类的虚函数不能被派生类重写
class Car
{
public:
virtual void Driver()final
{}
};
class Benz : public Car
{
public:
virtual void Driver()
{
cout <<"Benz - 舒适” <<endl;
}
};
class Car
{
public:
virtual void Drive()
{}
};
//2.override 修饰派生类虚函数强制完成重写,如果没有重写就会强制报错。
class Benz : public Car
{
public:
virtual void Drive() override
{
cout << "Benz - 舒适" << endl;
}
};
五.多态的原理
1.虚函数表
//这里常考的一道笔试题:sizeof(Base)的大小是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func()" << endl;
}
private:
int _b = 1;
}
通过观察调试我们发现b对象是8比特,除了_b成员,还多一个_vfptr放在对象的前面,对象中的指针我们叫做虚函数表指针,一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表简称虚表
// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
class Base
{
public:
virtual void Fun1()
{
cout << "Base::Fun1()" << endl;
}
virtual void Fun2()
{
cout << "Base::Fun2()" << endl;
}
void Fun3()
{
cout << "base::Fun3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Fun1()
{
cout << "Derive::Fun1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
system("pause");
return 0;
}
结论:
- 1.派生类对象d中也有一个虚表指针,d对象是由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
- 2.基类b对象和派生类d独享虚表指针是不一样的,这里我们发现Fun1完成了重写,所有d的虚表中存的是重写的Derive::Fun1,所以虚函数的重写也叫覆盖,覆盖就是指虚表中虚函数的覆盖。
- 3.Fun2()继承下来后也就是虚函数,所以放进了虚表中,Fun3也继承下来了,但是不是虚函数,所以不会放进虚表。
- 4.虚函数表本质上是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
- 派生类的虚表生成:
(1)先将基类的虚表中的内容拷贝一份到派生类虚表中。
(2)如果派生类重写了基类中的某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数。
(3)派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的后面。 - 5.虚表存的是虚函数的指针,不是虚函数。虚函数和普通函数一样,都是存在代码段的,只是它的指针又存在虚表中。对象中存的不是虚表,存的是虚表指针。虚表经过实际验证发现在vs中是存在代码段的。
2.两个条件:(1)虚函数覆盖(2)对象的指针或引用调用虚函数。
六.内联函数
面试题:
1.什么是多态?
-
多种形态,具体就是完成某个行为,当不同的对象去完成时会产生出不同的状态。
-
多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候已经确定了;动态多态是用虚函数机制实现的,在运行期间进行动态绑定。举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类的虚函数的时候。会调用子类重写过后的函数,在父类中声明了virtual关键字的函数,在子类中重写不需要加上virtual也是虚函数。
-
虚函数的实现:在有虚函数的类中,类中最开始的部分是一个虚函数表指针,这个指针指向了一个虚函数表,虚函数表中放的是虚函数的地址,实际上虚函数存在代码段。当子类继承父类的时候,也会继承父类的虚函数表,当子类重写父类的虚函数的时候,会将其继承到的虚函数表的地址替换为重新写的函数地址,使用了虚函数,会增加访问内存的开销,降低效率。
2.什么是重载,重写(覆盖),重定义(隐藏)
- 1.重载:就是在同一个作用域中,函数名相同,参数相同就构成了重载。
- 2.重写(覆盖):在基类和派生类中同时存在,且必须是虚函数,函数名相同,参数相同,返回值相同(协变除外)
- 2.重定义(隐藏):两个函数分别在基类和派生类中。函数名相同
3.多态的实现原理?
- 1.虚函数覆盖
- 2.对象的指针或引用调用虚函数
4.inline函数可以是虚函数吗?
不能,因为inline函数没有地址,无法把地址放到虚函数表中。
5.静态成员可以是虚函数吗?
不能,因为静态成员没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
6.构造函数可以是虚函数吗?
不能,因为对象中的徐哈桑农户表指针是在构造函数初始化列表阶段才初始化的。
7.析构函数可以是虚函数吗?
可以,并且最好把基类的析构函数定义为虚函数。将可能会被继承的父类的析构函数设置成为虚函数。可以保证当我们new出一个子类的时候,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
8.对象访问普通函数快还是虚函数快?
如果时普通函数的话是一样快的。如果是指针对象或者是引用对象,则调用普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表里面去查找。