多态
- 1.多态的概念
- 2. 多态的定义及实现
- 2.1虚函数
- 2.2虚函数的重写/覆盖
- 2.3析构函数的重写
- 2.4override 和 final关键字
- 3.重载/重写/隐藏的对比
- 3.1重载(overload)
- 3.2重写(override)
- 3.3隐藏(hide)
1.多态的概念
在C++中,多态是指用同一个函数名或操作符在不同的上下文中执行不同的操作。主要分为两种:
(1)编译时多态(静态多态)
主要通过函数重载,运算符重载和函数模板来实现,他们传不同类型的参数就可以调⽤不同的函数,通过参数不同达到多种形态。函数重载是指在同一个作用域内,可以定义多个同名函数,但它们的参数列表(参数个数、类型、顺序)不同。例如,你可以定义两个名为 add 的函数,一个用于 int 类型相加,一个用于 double 类型相加。
(2)运行时多态,具体点就是去完成某个⾏为(函数),可以传不同的对象就会完成不同的⾏为,就达到多种形态。比如买票这个⾏为,当普通⼈买票时,是全价买票;学⽣买票时,是优惠买票(5折或75折);军⼈买票时是优先买票。
2. 多态的定义及实现
多态还有两个必须重要条件:
1.必须指针或者引用调用虚函数
2.被调用的函数必须是虚函数。
运行时多态的构成条件(虚函数实现)
(1)存在继承关系:必须有基类和派生类的层次结构,派生类继承自基类。
(2)虚函数声明:在基类中,将希望在派生类中重新定义的函数声明为虚函数,使用关键字 virtual
。例如, virtual void func();
。
(3)函数重定义:派生类对基类中的虚函数进行重新定义,函数签名(函数名、参数列表、返回值类型要和基类虚函数一致)必须相同,函数体可以不同。派生类必须对基类的虚函数重写/覆盖,重写或者覆盖了,派⽣类才能有不同的函数,多态的不同形态效果才能达到。
(3)通过基类指针或引用调用:使用基类的指针或引用指向派生类对象,必须是基类的指针或引用,因为只有基类的指针或引用才能既指向派⽣类对象,然后通过这个指针或引用调用虚函数,才能实现运行时多态。比如 Base* ptr = new Derived(); ptr->virtualFunc();
,这里 Base
是基类, Derived
是派生类, virtualFunc
是虚函数。
2.1虚函数
2.1类成员函数前⾯加virtual修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修饰。
class Person
{public:virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
2.2虚函数的重写/覆盖
虚函数的重写/覆盖:派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派⽣类的虚函数重写了基类的虚函数。
注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。
不同人买的票
class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-打折" << endl; }
};
void Func(Person* ptr)
{
// 这里可以看到虽然都是Person指针Ptr在调用BuyTicket
// 但是跟ptr没关系,而是由ptr指向的对象决定的。ptr->BuyTicket();
}
int main()
{Person ps;Student st;Func(&ps);Func(&st);return 0;
}
不同动物的叫的声音
class Animal
{
public:virtual void talk() const
{}
};
class Dog : public Animal
{
public:virtual void talk() const
{std::cout << "汪汪" << std::endl;
}
};
class Cat : public Animal{
public:virtual void talk() const
{std::cout << "(>^ω^<)喵" << std::endl;
}
};
void letsHear(const Animal& animal)
{animal.talk();
}
int main()
{Cat cat;Dog dog;letsHear(cat);letsHear(dog);return 0;
}
2.3析构函数的重写
基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor
,所以基类的析构函数加了virtual
修饰,派生类的析构函数就构成重写。
class A
{
public:virtual ~A(){cout << "~A()" << endl;}
};
class B : public A {
public:~B(){cout << "~B()->delete:"<<_p<< endl;delete _p;}
protected:int* _p = new int[10];
};
// 只有派⽣类Student的析构函数重写了Person的析构函数,
//下⾯的delete对象调⽤析构函数,
//才能构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。
int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}
上⾯的代码我们可以看到,如果~A()
,不加virtual
,那么delete p2
时只调⽤的A的析构函数,没有调用B的析构函数,就会导致内存泄漏问题,因为~B()
中在释放资源。
2.4override 和 final关键字
从上⾯可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错参数写错等导致无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运⾏时没有得到预期结果才来debug会得不偿失,因此C++11提供了override
,可以帮助⽤户检测是否重写。
// error C3668: “Benz::Drive”: 包含重写说明符“override”的⽅法没有重写任何基类⽅法
class Car {
public:virtual void Dirve(){}
};
class Benz :public Car {
public:virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
int main()
{return 0;
}
如果我们不想让派生类重写这个虚函数,那么可以⽤final
去修饰。
// error C3248: “Car::Drive”: 声明为“final”的函数⽆法被“Benz::Drive”重写
class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() { cout << "Benz-舒适" << endl; }
};
int main()
{return 0;
}
3.重载/重写/隐藏的对比
3.1重载(overload)
概念: 发生在同一个类内部。函数名相同,但参数列表(参数个数、参数类型、参数顺序)不同,以此来提供多个同名函数的不同实现。返回值类型不同不能作为重载的条件。
class MyClass {
public:void func(int x) {std::cout << "func with int: " << x << std::endl;}void func(double x) {std::cout << "func with double: " << x << std::endl;}
};
**调用方式:**在调用函数时,编译器会根据传入的实际参数类型来决定调用哪个重载版本
3.2重写(override)
概念: 发生在基类和派生类之间,用于实现运行时多态。当在派生类中重新定义基类的虚函数,且函数签名(函数名、参数列表、返回值类型)和基类虚函数相匹配时,就是重写。
class Base {
public:virtual void func(int x) {std::cout << "Base func with int: " << x << std::endl;}
};
class Derived : public Base {
public:void func(int x) override {std::cout << "Derived func with int: " << x << std::endl;}
};
调用方式: 通过基类指针或引用指向派生类对象,调用虚函数时实现运行时多态,会根据对象实际类型调用对应的函数。
3.3隐藏(hide)
概念: 主要发生在基类和派生类之间。当派生类中的函数和基类中的函数同名(不论参数列表是否相同),在某些情况下,派生类函数会隐藏基类函数。如果派生类函数和基类函数同名且参数列表不同,这是一种隐藏情况;另外,如果派生类函数和基类函数同名,参数列表相同,但基类函数不是虚函数,这也是隐藏。
class MyClass {
public:void func(int x) {std::cout << "func with int: " << x << std::endl;}void func(double x) {std::cout << "func with double: " << x << std::endl;}
};
调用方式: 如果派生类对象调用同名函数,编译器优先使用派生类中的函数。如果要调用被隐藏的基类函数,需要通过基类的作用域限定符如 Derived d; d.Base::func(3);