多态
一、多态的定义及实现
多态: 完成某个行为时,当不同的对象去完成时会产生出不同的状态。
1.1 多态的构成条件
在继承中要构成多态还有两个条件:
必须通过基类的指针或者引用调用虚函数( 父类指针或者引用去调用虚函数 ) 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写( 子类虚函数重写的父类虚函数 (重写:三同(函数名/参数/返回值)+虚函数) )
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票原价" << endl;
}
};
class Student :public Person
{
public:
//子类虚函数重写的父类虚函数
//虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的
//返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
virtual void BuyTicket()
{
cout << "买票半价" << endl;
}
};
//父类指针或者引用去调用虚函数
void test(Person& people)
{
people.BuyTicket();
}
int main()
{
Person Cris;
test(Cris);
Student Tiano;
test(Tiano);
return 0;
}
注意:
在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然 也可以构成重写 ,因为继承后基类的虚函数被 继承 下来了在派生类依旧保持虚函数属性。( 我们自己写时,子类虚函数应该写上virtual )class Person { public: virtual void BuyTicket() { cout << "买票原价" << endl; } }; class Student :public Person { public: //子类虚函数没有写virtual,BuyTicket依旧是虚函数,因为先继承了父类函数接口声明 void BuyTicket() { cout << "买票半价" << endl; } };
1.2 虚函数重写的两个例外
1.2.1 协变
虚函数重写对返回值要求唯一一个例外:协变
派生类重写基类虚函数时,与基类虚函数返回值类型不同
父类虚函数 返回 父类对象 的指针或者引用, 子类虚函数 返回 子类对象 的指针或者引用时,称为协变。
//A B 父子关系
class A{};
class B : public A{};
//基类Person虚函数返回基类对象A的指针或者引用
class Person
{
public:
virtual A* f()
{
return new A;
}
};
//派生类Student虚函数返回派生类对象B的指针或者引用
class Student :public Person
{
public:
virtual B* f()
{
return new B;
}
};
1.2.2 析构函数的重写
基类与派生类析构函数的名字不同
如果设计一个类,可能会作为基类,其次析构函数最好定义为虚函数
class Person
{
public:
virtual ~Person()
{}
};
class Student :public Person
{
public:
//如果父类的析构函数为虚函数,此时子类析构函数只要定义,
//无论是否加virtual关键字,都与父类的析构函数构成重写
//virtual ~Student()
~Student()
{}
};
int main()
{
Person* a = new Person;
Person* b = new Student;
//这里的 delete对象调用析构函数,构成多态
delete a;
delete b;
return 0;
}
1.3 C++11 override 和 fifinal
1.3.1 final
修饰虚函数,表示该虚函数不能再被重写
class Person
{
public:
//用final修饰
virtual void f() final
{}
};
class Student :public Person
{
public:
//这里的f函数不能再被重写
void f()
{}
};
1.3.2 override
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Person
{
public:
virtual void f()
{}
};
class Student : public Person
{
public:
//检查是否重写,没重写则报错
void f() override
{}
};
1.4 重载、覆盖(重写)、隐藏(重定义)三个概念的对比
重载:
- 两个函数在同一作用域
- 函数名、参数相同
重写 :
- 两个函数分别在基类和派生类的作用域
- 函数名、参数值、返回值必须相同(协变除外)
- 两个函数必须是虚函数
重定义:
- 两个函数分别在基类和派生类的作用域
- 函数名相同
- 两个基类和派生类的同名函数不构成重写就是重定义
二、抽象类
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象子类继承后也不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象class Person { public: //在虚函数的后面写上 =0 ,则这个函数为纯虚函数 virtual void f() = 0; //实现没有价值,因为没有对象会调用它 /*virtual void f() = 0 { cout << "f" << endl; }*/ }; class Student :public Person { public: virtual void f() { cout << " Student " << endl; } }; class Teacher :public Person { public: virtual void f() { cout << " Teacher " << endl; } }; int main() { Person* a = new Student; a->f(); Person* b = new Teacher; b->f(); return 0; }
2.1 接口继承和实现继承
- 普通函数的继承是一种实现继承,子类继承了父类函数,可以使用函数,继承的是函数的实现。
- 虚函数的继承是一种接口继承,子类继承的是父类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
三、多态的原理
3.1 虚函数表
class Cris { public: virtual void f() { cout << "f()" << endl; } private: int _a = 1; }; int main() { cout << sizeof(Cris) << endl; return 0; }
sizeof(Cris)值为16,不为4的原因:
除了_a成员,还多一个 __vfptr 放在对象的前面,对象中的这个指针叫做虚函数表指针一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr
3.1.1 派生类和基类的虚数表
class Cris
{
public:
virtual void f()
{
cout << "f()" << endl;
}
private:
int _a = 1;
};
class Tiano : public Cris
{
public:
virtual void f()
{
cout << "Tiano::f" << endl;
}
private:
int _b = 2;
};
int main()
{
Cris a;
Tiano b;
return 0;
}
可以知道的是:
- 子类对象b中也有一个虚表指针,b对象有两部分。一部分是父类a继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
- 父类a对象和子类b对象虚表是不一样的,f()完成了重写,所以b的虚表中存的是重写的f(),所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
- 子类虚表生成:先将父类中的虚表内容拷贝一份到子类虚表中。如果子类重写了父类中某个虚函数,用子类自己的虚函数覆盖虚表中父类的虚函数。子类自己新增加的虚函数按其在子类中的声明次序增加到子类虚表的最后。
注意虚表存的是 虚函数指针 ,不是虚函数。虚函数和普通函数一样的,都是存在代码段的,只是它的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针,虚表存在代码段上。