【c++】多态(一)
hello~ 很高兴见到大家! 这次带来的是C++中关于多态这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?
个 人 主 页: 默|笙
文章目录
- 一、多态的概念
- 二、多态的定义和实现
- 1. 构成条件
- 2. 虚函数
- 3 .虚函数的重写
- 三、协变与析构函数的重写
- 1. 协变(了解)
- 2. 析构函数的重写
- 四、override和final关键字
- 五、重载、重写和隐藏
- 六、纯虚函数和抽象类
一、多态的概念
多态通俗来讲就是多种形态。多态分为编译时多态(静态多态)与运行时多态(动态多态)。
- 编译时多态就是之前我们所学的函数重载和函数模板,通过传递不同的参数来达到实现多种多种形态。名称的来历是因为实参传给形参是在编译的时候完成的,而我们一般把编译时成为静态,运行时称为动态。
- 接下来所要学习的,是运行时多态。也就是在我们需要完成某个行为时(函数),会根据传递对象的不同而达到不同的结果,从而达到多种形态。比如学生和成年人同时买火车票。行为都是去买火车票,但是因为对象的不同,学生买的学是生票,成年人买的是全价票。
二、多态的定义和实现
多态是一个继承关系下的类对象(基类对象与派生类对象),用指向它们的基类指针去调用同一个函数,由于指向的对象不同,因而产生了不同的行为。
- 比如,有基类Person与派生类Student对象p和,调用同一个买票函数,用Person类指针ptr分别指向它们,结果是对象p买全价票,对象s买学生票,它们产生了不同的结果。
class Person
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};class Student : public Person
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};void Func(Person* ptr)
{ptr->BuyTicket();
}int main()
{Person p;Student s;Func(&p);Func(&s);return 0;
}
1. 构成条件
要实现运行时多态,必须满足以下两个条件:
- 必须是基类的指针或引用调用虚函数。
- 被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖。
- 对于必须是基类的指针或引用调用虚函数,这是因为运行时多态离不开继承(也就是基类和派生类),而只有基类的指针或者引用才能既指向基类对象又能指向派生类对象。
2. 虚函数
类成员函数前面加virtual修饰,那么这个成员函数就被称为虚函数。非成员函数不能添加virtual进行修饰。
class Person
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
3 .虚函数的重写
- 虚函数的重写: 派生类中有一个跟基类完全相同的虚函数,满足三同:函数名相同,参数列表与返回值完全相同,那么称派生类的虚函数重写了基类的虚函数。
- 在重写虚函数时,派生类的虚函数可以不添加virtual关键字,这样也构成重写,但这种写法并不规范。
- 对于2,可以将派生类的虚函数看作是基类虚函数的定义(包含virtual) + 派生类虚函数的实现细节,这样派生类实际上也是有virtual关键字修饰的。比如:
class A
{
public:virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }virtual void test() { func(); }
};class B : public A
{
public:void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};int main(int argc, char* argv[])
{B* p = new B;p->test();return 0;
}
- 它的执行结果是 B->1。首先是一个B类型的指针p调用test()函数,由于B继承了A,所以p将会调用A类的test()函数(test()并没有构成重写),然后将p指针传递给test()函数里的A* const this,this是一个基类指针,且基类A与派生类B有同一个虚函数,满足重写。将会根据this指向的内容(指向B)而调用B里的虚函数,而因为B的虚函数是由A类虚函数声明+B类虚函数的实现(即 virtual void func(int val = 1) { std::cout << “B->” << val << std::endl; })。所以val的值为1。
- 继承并不是把基类成员拷贝一份到派生类中去,通过一种层次结构使得派生类对象包含一个基类子对象。
三、协变与析构函数的重写
1. 协变(了解)
协变:派生类重写基类虚函数时,与基类函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或引用。
2. 析构函数的重写
- 基类的析构函数要设计为虚函数(加virtual),这样派生类析构只要定义,无论是否加virtual关键字,都构成重写。关于函数名:编译器会做特殊处理,析构函数的名称统一会被处理为destructor。这样符合三同,构成重写。
class A
{
public:~A(){cout << "~A()" << endl;}
};
class B : public A {
public:~B(){cout << "~B()->delete:" << _p << endl;delete _p;}
protected:int* _p = new int[10];
};int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}
- 当使用基类指针指向派生类对象并通过该指针删除对象时,若基类析构函数不添加virtual关键字,不构成重写,那么B的析构函数将不会被调用,资源不被释放,这会造成内存泄露问题。所以基类中的析构函数建议设计为虚函数。
四、override和final关键字
- 由于c++对于虚函数重写的要求比较严格,若有的时候因为函数名写错参数写错等无法构成重写,这种错误在编译期间是不会报出的,如果在程序运行时没有得到预期结果才去debug会得不偿失,因此c++11提供了override关键字,可以帮助用户检测是否完成了重写,如果没有完成,那么编译器将会报错。
格式:函数声明 override {}
如:virtual BuyTicket() override { cout << "全价买票" << endl; }
- 如果我们不想让派生类重写某个虚函数,那么可以用final关键字去修饰。
格式:函数声明 final {}
如:virtual BuyTicket() final {cout << "全价买票" << endl; }
final修饰类,类不能被继承,final修饰基类虚函数,虚函数不能被重写。
五、重载、重写和隐藏
- 由于这三种都有些相似之处,我们单独把它们拿出来做一下区分:
六、纯虚函数和抽象类
- 在虚函数的后面写上 =0,则这个函数为纯虚函数,纯虚函数不需要定义实现(因为它需要被派生类重写,但可以有显式的实现,但并不常见,因为意义不大),只要声明即可。包含纯虚函数的类就做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。
- 纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。
今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~
让我们共同努力, 一起走下去!