多态的概念
多态的概念:通俗来说,去完成某个行为,当不同的对象去完成时会产生出不同的状态 。
在C++
中,多态(Polymorphism
)是面向对象编程的一个重要概念,它允许你使用统一的接口来处理不同的数据类型,从而增加代码的灵活性和可扩展性。多态分为编译时多态性(静态多态性)和运行时多态性(动态多态性)两种类型。
- 编译时多态性(静态多态性):编译时多态性是通过函数重载(
Function Overloading
)和运算符重载(Operator Overloading
)实现的。这种多态性在编译阶段就能够确定要调用的函数或操作符,根据函数或操作符的参数类型来选择执行不同的代码。
例如,函数重载允许你定义多个同名函数,但参数列表不同,编译器根据传递的参数类型来选择调用合适的函数。
void print(int num) { cout << "Printing an integer: " << num << endl; } void print(double num) { cout << "Printing a double: " << num << endl; }
- 运算符重载则允许你定义自定义类型的操作符行为。
- 运行时多态性(动态多态性):运行时多态性是通过继承和虚函数(
Virtual Function
)实现的。这种多态性允许你在运行时根据对象的实际类型来决定调用哪个函数,从而实现动态的函数分发。
在运行时多态性中,基类可以定义虚函数,并且派生类可以重写这些虚函数。通过使用基类指针或引用指向派生类对象,可以在运行时调用派生类的虚函数。
class Shape { public: virtual void draw() { cout << "Drawing a shape." << endl; } }; class Circle : public Shape { public: void draw() override { cout << "Drawing a circle." << endl; } }; class Square : public Shape { public: void draw() override { cout << "Drawing a square." << endl; } };
- 使用时:
Shape* shapePtr; Circle circle; Square square; shapePtr = &circle; shapePtr->draw(); // 调用 Circle 的 draw() shapePtr = □ shapePtr->draw(); // 调用 Square 的 draw()
- 这样,根据指针指向的实际对象类型,实现了在运行时根据对象类型调用不同的函数。
总而言之,C++的多态性允许你通过统一的接口处理不同类型的对象,无论是在编译时还是运行时。这大大提高了代码的可维护性和可扩展性。
多态的定义及实现
1.多态的构成条件
那么在继承中要构成多态还有两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
2.虚函数
虚函数:即被virtual
修饰的类成员函数称为虚函数
class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl;} };
3.虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数
class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } }; class Student : public Person { public: virtual void BuyTicket() { cout << "买票-半价" << endl; } }; void Func(Person& p) { p.BuyTicket(); } int main() { Person ps; Student st; Func(ps); Func(st); return 0; }
注意:在重写基类虚函数时,派生类的虚函数在不加virtual
关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用
多态的本质原理,符合多态的两个条件,那么调用时,会到指向对象的虚表中找到对应的虚函数地址,进行调用
多态调用运行时去指向对象的虚表中找到函数地址进行调用
普通调用在编译链接时就已经确认了函数地址,运行时直接调用
4.虚函数重写的两个例外
- 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变
class A{}; class B : public A {}; class Person { public: virtual A* f() {return new A;} }; class Student : public Person { public: virtual B* f() {return new B;} };
- 析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual
关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
。
class Person { public: virtual ~Person() {cout << "~Person()" << endl;} }; class Student : public Person { public: virtual ~Student() { cout << "~Student()" << endl; } }; int main() { Person* p1 = new Person; Person* p2 = new Student; delete p1; delete p2; return 0; }
5.C++11 override 和 final
从上面可以看出,C++
对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug
会得不偿失,因此:C++11
提供了override
和final
两个关键字,可以帮助用户检测是否重写。
- final:修饰虚函数,表示该虚函数不能再被重写
class Car { public: virtual void Drive() final {} }; class Benz :public Car { public: virtual void Drive() {cout << "Benz-舒适" << endl;} };
错误(活动) E1850 无法重写“final”函数 "Car::Drive" (已声明 所在行数:4)
- override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Car { public: virtual void Drive() {} }; class Benz :public Car { public: virtual void test() override { cout << "Benz-舒适" << endl; } };
错误 C3668 “Benz::test”: 包含重写说明符“override”的方法没有重写任何基类方法 错误(活动) E1455 使用“override”声明的成员函数不能重写基类成员
6.重载、覆盖(重写)、隐藏(重定义)的对比
需要注意的是,子类虚函数即使不加virtual依旧构成重写,重写的协变返回值可以不相同,但必须是父子关系的指针或引用
抽象类
1.概念
在虚函数的后面写上 =0
,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Car { public: virtual void Drive() = 0; }; class Benz :public Car { public: virtual void Drive() { cout << "Benz-舒适" << endl; } }; class BMW :public Car { public: virtual void Drive() { cout << "BMW-操控" << endl; } }; void Test() { Car* pBenz = new Benz; pBenz->Drive(); Car* pBMW = new BMW; pBMW->Drive(); }
2.接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数
在面向对象编程中,接口继承(Interface Inheritance)和实现继承(Implementation Inheritance)是两种不同的继承方式,用于在类之间共享功能和行为。让我们更详细地了解这两种继承方式:
- 接口继承(Interface Inheritance): 接口继承是一种继承方式,其中一个类(称为子类或派生类)从另一个类(称为父类或基类)继承方法声明而不继承实际实现。这种继承方式用于定义一组方法的标准接口,而不关心具体的实现。在C++中,接口继承通常通过定义纯虚函数(Pure Virtual Function)实现。
示例:
class Shape { public: virtual void draw() = 0; // 纯虚函数,定义接口 }; class Circle : public Shape { public: void draw() override { // 实现具体的绘制圆的代码 } }; class Square : public Shape { public: void draw() override { // 实现具体的绘制正方形的代码 } };
在上面的例子中,Shape
类定义了一个纯虚函数 draw
,它作为接口继承被继承类 Circle
和 Square
实现。
- 实现继承(Implementation Inheritance): 实现继承是一种继承方式,其中一个类从另一个类继承方法的声明和实际实现。这种继承方式用于共享已经存在的代码和实现。在C++中,通过普通的继承机制实现实现继承。
示例:
class Vehicle { public: void startEngine() { // 启动引擎的代码 } void stopEngine() { // 关闭引擎的代码 } }; class Car : public Vehicle { public: void drive() { // 具体的驾驶代码 } };
在上面的例子中,Car
类从 Vehicle
类进行实现继承,它继承了 startEngine
和 stopEngine
的实现。
需要注意的是,C++中的单继承限制了一个类只能从一个父类继承,这样有助于避免多继承可能带来的复杂性和歧义。
总结:
- 接口继承用于定义方法的标准接口,而不关心实际实现。通过纯虚函数来实现接口继承。
- 实现继承用于共享已有代码和实现,子类继承父类的方法和实现。
- 在实际编程中,应根据需要选择合适的继承方式,避免继承关系过于复杂,从而保持代码的可读性和维护性。