概念:通俗的来说就是多种形态,具体就是去完成某个行为,当不同类型的对象去完成同一件事时,产生的动作是不一样的,结果也是不一样的。
举一个现实中的例子:买票这个行为,当普通人买票时是全价;学生是半价;军人是不需要排队。
多态也分为两种:
-
静态的多态:函数调用
-
动态的多态:父类指针或引用调用重写虚函数。
这里的静态是指在编译时实现多态的,而动态是在运行时完成的。
多态一定是建立在继承上的,那么除了继承还要两个条件:
-
必须通过基类(父类)的指针或引用调用函数
-
被调用的函数必须是虚函数,且派生类(子类)必须对积累的虚函数进行重写。
虚函数
概念:被virtual修饰的类成员函数称为虚函数
class Person
{
public:
virtual void BuyTicket()
{
cout<<"全价票"<
注意:
-
只有类的非静态成员函数可以是虚函数
-
虚函数这里virtual和虚继承中用的是同一个关键字,但是他们之间没有关系;虚函数这里是为了实现多态;虚继承是为了解决菱形继承的数据冗余和二义性,它们没有关联
虚函数的重写
概念:派生类(子类)中有一个跟基类(父类)完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型,函数名字,参数列表完全相同),称子类的虚函数重写了基类的虚函数。
例:
class Person
{
public:
virtual void BuyTicket()
{
cout<<"全价票"<
注意:这里子函数的虚函数可以不加virtual,也算完成了重写,但是父类的虚函数必须要加,因为子类是先继承父类的虚函数,继承下来后就有了virtual属性了,子类只是重写这个virtual函数;除了这个原因之外,还有一个原因,如果父类的析构函数加了virtual,子类加不加都一定完成了重写,就保证了delete时一定能实现多态的正确调用析构函数。
虚函数重写的两个例外
1、协变
概念:派生类重写基类虚函数时,与基类虚函数返回值类型不同。
即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变
例:
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;
}
};
2、析构函数的重写
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加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;
//这里p2指向的子类对象,应该调用子类析构函数,如果没有调用的话,就可能内存泄漏
Person* p2 = new Student;
//多态行为
delete p1;
delete p2;
//只有析构函数重写了那么这里delete父类指针调用析构函数才能实现多态。
return 0;
}
C++11 override和finel
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写
final:修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
//会在这块报错,因为基类的虚函数已经被final修饰,不能被重写了
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
重载、覆盖(重写)、隐藏(重定义)的对比
抽象类 抽象类的概念
纯虚函数:在虚函数的后面加上=0就是纯虚函数,有纯虚函数的类就是抽象类,也叫接口类,抽象类无法实例化对象。
抽象类的子类不重写父类的虚函数的话,也是一个抽象类。
//抽象类的定义
class Car
{
public:
virtual void run()=0; //不用实现只写接口就行。
};
纯虚函数不写函数体,并不意味着不能实现,只是我们不写。
因为写出来也没有人用。
虚函数的作用
接口继承和实现继承
强制子类重写虚函数,完成多态。
表示抽象类。
普通函数的继承就是实现继承,虚函数的继承就是接口继承。
子类继承了函数的实现,可以直接使用。
虚函数重写后只会继承接口,重写实现。
所以如果不用多态,就不要把函数写为虚函数。
纯虚函数就体现了接口函数。
下面我们来实现一道题,展现一下接口继承。
class A
{
public:
virtual void fun(int val=0)
{
cout<<"A->val = "<val"<Fun();
return 0;
}
结果打印为 :B->val=0
子类对象切片给父类指针,传给Fun函数,满足多态,会去调用子类的fun函数,但是子类的虚函数继承了父类的接口,所以val是父类的0。
class A
{
public:
virtual void fun()
{
}
protected:
int _a;
};
sizeof(A)是多少?
打印出来是8。
我们定义了一个A类型的对象a,打开调试窗口,发现a的内容如下
我们发现出了成员变量_a以外,还多了一个指针,这个指针是不准确的,实际上应该是 _vftptr(virtual function table pointer),虚函数表指针。
在计算类大小的时候要加上这个指针的大小。
虚表就是存放虚函数的地址地方,当我们去调用虚函数,编译器就会通过虚表指针去虚表里查找。
class A
{
public:
void fun1()
{
}
virtual void fun2()
{}
};
int main()
{
A* a=nullptr;
a->fun1();//调用函数,因为这是普通函数的调用
a->fun2();//调用失败,虚函数需要对指针 *** 作,无法 *** 作空指针。
return 0;
}
实现一个继承
class A
{
public:
virtual void fun1()
{}
virtual void fun2()
{}
};
class B : public A
{
public:
virtual void fun1()
{}
virtual void fun2()
{}
};
int main()
{
A a;
B b;
return 0;
}
子类与父类一样有一个虚表指针。
子类的虚函数表一部分继承自父类。
如果重写了虚函数,那么子类的虚函数会在虚表上覆盖父类的虚函数。
本质上虚函数表是一个虚函数指针数组,最后一个元素是nullptr,代表虚表的结束。
所以,如果继承了虚函数,那么
-
子类先拷贝一份父类虚表,然后用一个虚表指针指向这个虚表。
-
如果有虚函数重写,那么在子类的虚表上用子类的虚函数覆盖。
-
子类新增的虚函数按其在子类中的声明次序增加到子类虚表的最后。
虚函数表放在内存的那个区,虚函数又放在哪?
虚函数与虚函数表都放在代码段。
我们现在来看多态的原理
class person
{
public:
virtual void fun()
{
cout<<"全价票"<fun();
}
这样就实现了不同对象去调用同一函数,展现出不同的形态。
满足多态的函数调用是程序运行是去对象的虚表查找的,而虚表是在编译时确定的。
普通函数的调用是编译时就确定的。
1.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
2.动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
我们说的多态一般是指动态多态。
这里我附上一个有意思的问题:
就是在子类已经覆盖了父类的虚函数的情况下,为什么子类还是可以调用“被覆盖”的父类的虚函数呢?
#include
using namespace std;
class Base {
public:
virtual void func() {
cout << "Base func\n";
}
};
class Son : public Base {
public:
void func() {
Base::func();
cout << "Son func\n";
}
};
int main()
{
Son b;
b.func();
return 0;
}
输出:Base func
Son func
这是C++提供的一个回避虚函数的机制
通过加作用域(正如你所尝试的),使得函数在编译时就绑定。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)