文章目录
- 1.多态的概念
- 1.1多态的概念
- 2.动态的定义及实现
- 2.1多态的构成条件
- 2.2虚函数
- 2.3虚函数的重写
- 虚函数重写的两个例外
- 2.4 C++11 override和final
- 2.5重载、覆盖(重写)、隐藏(重定义)的对比
- 3.抽象类
- 3.1概念
- 3.2接口继承和实现继承
- 4.多态的原理
- 4.1虚函数表
- 4.2多态的原理
1.多态的概念
1.1多态的概念
多态的概念:一般来说,就是多种状态。具体来讲就是去完成某个行为,当不同对象去完成时会产生不同的状态。
举个例子:比如买票这个行为,当普通人买票时,为全价买票;当学生买票时,为半价买票;军人买票时,为优先买票。
2.动态的定义及实现
2.1多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生不同的行为。比如student基础了person。person对象买票全价,student对象买票半价。
那么在继承中要构成多态还有两个条件:
1.必须通过基类的指针或者引用调用虚函数
2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数重写
#include <iostream>
using namespace std;
//多态
class person
{
public:
virtual void BuyTicket()
{
cout << "全价买票" << endl;
}
private:
};
class student :public person
{
public:
virtual void BuyTicket()
{
cout << "半价买票" << endl;
}
private:
};
void test(person& p)
{
p.BuyTicket();
}
int main()
{
person p;
student s;
test(p);
test(s);
return 0;
}
2.2虚函数
虚函数:即被virtual修饰的类成员被称为虚函数。
class person
{
virtual void BuyTicket()
{
cout<<"全价买票"<<endl;
}
}
2.3虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型,函数名字,参数列表完全相同),称子类虚函数重写了基类的虚函数。
#include <iostream>
using namespace std;
//多态
class person
{
public:
virtual void BuyTicket()
{
cout << "全价买票" << endl;
}
private:
};
class student :public person
{
public:
/*在重写基类虚函数时,派生类的虚函数前可以不加virtual关键字,
虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性)但是这种写法不是很规范,不建议这样写。*/
virtual void BuyTicket()
{
cout << "半价买票" << endl;
}
private:
};
void test(person& p)
{
p.BuyTicket();
}
int main()
{
person p;
student s;
test(p);
test(s);
return 0;
}
虚函数重写的两个例外
1.协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
class A{};
class B:pubilc A{};
class person
{
virtual A* f()
{
return new A;
}
}
class student: pubilc person
{
virtual B* f()
{
return new B;
}
}
2.析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类的析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然名字不同,看起来违背了重写的规则,其实不然,因为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class person
{
public:
virtual ~person()
{
cout << "~person" << endl;
}
private:
};
class student :public person
{
public:
virtual ~student()
{
cout << "~student" << endl;
}
private:
};
int main()
{
person p;
student s;
return 0;
}
2.4 C++11 override和final
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下可以由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报错的,只有在程序运行时没有得到预期结果才来debug,由此C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
1.final:修饰虚函数,表示该虚函数不能再被重写
class person
{
public:
virtual void BuyTicket() final
{
cout << "全价买票" << endl;
}
};
class student :public person
{
public:
virtual void BuyTicket()
{
cout << "半价买票" << endl;
}
};
编译报错
2.override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写则编译报错
class person
{
public:
virtual void BuyTicket() final
{
cout << "全价买票" << endl;
}
};
class student :public person
{
public:
virtual void BuyTicket() override
{
cout << "半价买票" << endl;
}
};
2.5重载、覆盖(重写)、隐藏(重定义)的对比
三个概念的对比:
重载:1.两个函数再同一作用域
2.函数名/参数相同
重写(覆盖):1.两个函数分别再基类和派生类的作用域
2.函数名/参数/返回值都必须相同(协变除外)
3.两个函数必须是虚函数
重定义:1.两个函数分别再基类和派生类的作用域
2.函数名相同
3.两个基类和派生类的同名函数不构成重写就是重定义
3.抽象类
3.1概念
在虚函数的后面写上 =0 ,则这个函数为纯虚函数**包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能示例话出对象。**派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象,纯虚函数规范;1派生类必须重写,另外纯虚函数更能体现出接口继承。
class car
{
public:
virtual void Drive() = 0;
};
class Tesla :public car
{
public:
virtual void Drive()
{
cout << "Tesla" << endl;
}
};
class Benz :public car
{
virtual void Drive()
{
cout << "Benz" << endl;
}
};
int main()
{
car* pBenz = new Benz;
pBenz->Drive();
car* pTesla = new Tesla;
pTesla->Drive();
return 0;
}
3.2接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现,虚函数的继承是一种借口继承,派生类继承的基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
4.多态的原理
4.1虚函数表
计算sizeof(a)的大小,注意:程序在x86环境下运行
class A
{
public:
virtual void f()
{
cout << "f()" << endl;
}
private:
int _a = 0;
};
int main()
{
A a;
cout << sizeof(a) << endl;
return 0;
}
通过运行我们发现了结果为8
为什么呢?函数不是存在公共区域的吗?
我们调试一下
可以看到,除了_a成员,还多出了 _ vfptr放在了对象的前面(不同平台可能不同,博主使用的为vs2022),对象中的这个指针我叫做虚函数指针,一个含有虚函数的类都至少有一个虚函数指针,因为虚函数的递增要被放到虚函数表中,虚函数也称虚表,那么派生类中这个表会放写什么呢?
class A
{
public:
virtual void f()
{
cout << "f()-A" << endl;
}
virtual void f2()
{
}
void f3()
{
}
private:
int _a = 0;
};
class B :public A
{
public:
virtual void f()
{
cout << "f()-B" << endl;
}
private:
int _b = 1;
};
int main()
{
A a;
cout << sizeof(a) << endl;
B b;
return 0;
}
通过观察和测试,我们可以发现
1.派生类对象b中也存在一个虚表指针,b对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
2.基类a对象和派生类b对象虚表是不一样的,这里我们发现f1()完成了重写,所以b的虚表中存的是重写的A::f1(),所以虚函数的重写也叫做覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3.另外f2()继承下来后是虚函数,所以放进了虚表,**f3()**也继承下来了,但不是虚函数,所以不会放进虚表。
4.虚函数表的本质是一个存放虚函数指针的指针数组,一般情况下析构数组最后放一个nullptr。
5.总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份放到派生类虚表中。b.如果派生类重写基类中某个函数,用派生类总结的虚函数覆盖虚表中基类的虚函数 c.派生类总结新增的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
6.这里还有一个容易混淆的问题:虚函数存在哪?虚表存在哪?
**虚函数存在虚表,虚表存在对象中。注意这个回答是错误的。**很多同学都是这样深信不疑。注意虚表,存的是虚函数指针,不是虚函数,虚函数和普通函数一样,都是存在代码段的,只是他的这种又存到了虚表。另外对象中存的也不是虚表,而是虚表指针。那么虚表存在哪呢?实际是存在代码段的。
4.2多态的原理
- 观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚
函数是Person::BuyTicket。 - 观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中
找到虚函数是Student::BuyTicket。 - 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
- 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调
用虚函数。反思一下为什么?
完