作者:@小萌新
专栏:@C++进阶
作者简介:大二学生 希望能和大家一起进步!
本篇博客简介:简单介绍C++中多态的概念
多态
- 多态的概念
- 多态的定义及实现
- 多态的构成条件
- 虚函数
- 虚函数的重写
- 虚函数重写的两个例外
- 协变
- 析构函数的重写
- C++11 override和final
- final
- override
- 重载、覆盖(重写)、隐藏(重定义)的对比
- 抽象类
- 抽象类的概念
- 接口继承和实现继承
- 总结
多态的概念
多态就是函数调用的多种形态,使用多态能够使得不同的对象去完成同一件事时,产生不同的动作和结果
例如 我们去吃海底捞的时候 普通人去就是原价 学生去就会有学生优惠 这就叫做多态
多态的定义及实现
多态的构成条件
多态是指不同继承关系的类对象,去调用同一函数,产生了不同的行为。语法上 我们这里要满足两个条件
- 必须通过基类的指针或者引用调用虚函数。
我们会在文章的后面解释 为什么只能用指针或者是引用 不能使用对象
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
还是一样 我们下面会解释 为什么是虚函数 为什么必须要重写
虚函数
被virtual修饰的类成员函数被称为虚函数。
例如下面的这段代码
class Person
{
// 虚函数
virtual void Print();
};
int main()
{
return 0;
}
我们的Print就是虚函数
这里有两点需要注意的:
- 只有类的非静态成员函数前可以加virtual,普通函数前不能加virtual。
关于这个问题 因为静态成员和普通成员函数是没有this指针的
- 虚函数这里的virtual和虚继承中的virtual是同一个关键字,但是它们之间没有任何关系。虚函数这里的virtual是为了实现多态,而虚继承的virtual是为了解决菱形继承的数据冗余和二义性。
这个是关于virtual的用法 就不用过多解释了
虚函数的重写
虚函数的重写在语法层面上叫做重写
在原理层面上叫做覆盖 后面的例子会让大家明白这一点
它有两个必要条件
-
必须是虚函数
-
三同 即 函数名相同 参数相同 返回值相同
还是一样 我们来看代码
class Person
{
public:
virtual void buy_ticket()
{
cout << "买票 - 原价" << endl;
}
private:
};
class child : public Person
{
public:
// 这里的virtual也可以不写 因为语法规定 只要三同 实际上这里的函数就继承了父类的虚函数属性
// 但是不管我们平时敲代码 或者写项目的时候都要加上去 保证代码的可读性
virtual void buy_ticket()
{
cout << "买票 - 半价" << endl;
}
private:
};
class soldier : public Person
{
public:
// 为了证明上面说可以省略 virtual 的正确性 这里省略之
void buy_ticket()
{
cout << "买票 - 优先" << endl;
}
private:
};
现在我们通过父类的对象指针还有引用调用看看能不能完成多态
void func1(Person& p)
{
p.buy_ticket();
}
void func2(Person* p)
{
p->buy_ticket();
}
void test_vritual()
{
Person p;
child c;
soldier s;
func1(p);
func1(c);
func1(s);
cout << "test ------ ptr" << endl;
func2(&p);
func2(&c);
func2(&s);
}
int main()
{
test_vritual();
return 0;
}
显示效果如下
虚函数重写的两个例外
协变
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,称为协变。
比如说我们改写下之前写的代码
我们可以看到这里它们的返回值并不相同 但是依然满足多态 可以运行 这就是协变机制
要记住的一点是 协变的返回值必须是基类或者派生类的指针或引用 不然会报错 类似这样
析构函数的重写
如果父类的析构函数为虚函数 那么只要子类的析构函数定义了 那么它就与父类中的析构函数构成重写
比如说我们看下面的代码
class a
{
public:
virtual ~a()
{
cout << "~a" << endl;
}
};
class b : public a
{
public:
virtual ~b()
{
cout << "~b" << endl;
}
};
其中 a和b的析构函数就构成多态
怎么证明呢? 我们再来看下面的一段代码
void func(a& p)
{
p.~a();
}
int main()
{
a a1;
b b1;
cout << "start test" << endl;
func(a1);
func(b1);
cout << "test end" << endl;
return 0;
}
运行结果如下
我们可以发现 我们输入不同的对象引用确实触发了不同的析构函数
至于为什么出现了三次析构函数 可以参考下我上一篇继承的博客
派生类对象在析构时,会先调用派生类的析构函数再调用基类的析构函数。
至于后面的三次析构则是 a1 和 b1的生命周期结束了 自动调用的
那么这里的问题就来了
父类和子类的析构函数构成重写的意义何在呢?
我们试想下面的场景
我们创建一个父类对象和一个子类对象 并且使用父类的指针指向它们
然后全部delete掉
a* a1 = new a;
a* b1 = new b;
delete a1;
delete b1;
此时如果没有重写析构函数的话 两次析构其实都是析构的父类的
这样子就会造成一个内存泄漏的情况
而我们期望的是 delete a1 就是析构父类
delete b1 就是析构父类加子类
本质上是一种多态 所以我们要重写
记不记得我们上面继承提过一个知识点
析构函数的名字会被统一处理成destructor();
现在应该能充分理解为什么这么做的原因了吧 为了多态开路
C++11 override和final
我们从上面的博文中就可以看出 C++对于函数重写比较严格 但是我们有可能由于自身的疏忽 导致字符写反 或者返回值写错等原因无法构成重写
而这种错误要在程序运行之后才能被编译器发现 我们觉得有点太慢了
为了解决这个问题 C++中给出了两个关键字 这里我们来一个个学习下它们
final
final:修饰虚函数,表示该虚函数不能再被重写。
我们来看下面的代码
class Person
{
public:
virtual void print() final;
private:
};
class child : public Person
{
public:
void print()
{
;
}
private:
};
运行下我们可以发现
编译的时候会报错 不能够重写
override
override:检查派生类虚函数是否重写了基类的某个虚函数,如果没有重写则编译报错。
我们来看下面的两组对比
重载、覆盖(重写)、隐藏(重定义)的对比
具体的内容看上面这张图就好
抽象类
抽象类的概念
在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类)
抽象类不能实例化出对象
为了证明这个概念 我们写出下面的代码
class person
{
public:
virtual void print() = 0;
private:
};
int main()
{
person p;
return 0;
}
我们可以发现 符合我们上面的结论
派生类继承抽象类后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象
比如说这样子
class child : public person
{
public:
private:
};
接着我们重写下虚函数试试
class child : public person
{
public:
virtual void print()
{
cout << "child" << endl;
}
private:
};
我们发现 这样子就可以运行了
抽象类既然不能实例化出对象,那抽象类存在的意义是什么?
我们说 意义有二
-
抽象类可以更好的去表示现实世界中,没有实例对象对应的抽象类型,比如:植物、人、动物等。
-
抽象类很好的体现了虚函数的继承是一种接口继承,强制子类去重写纯虚函数,因为子类若是不重写从父类继承下来的纯虚函数,那么子类也是抽象类也不能实例化出对象。
接口继承和实现继承
实现继承: 普通函数的继承是一种实现继承,派生类继承了基类函数的实现,可以使用该函数。
接口继承: 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态
总结
本文主要讲解了C++中多态的一些使用