目录
前言
在前面我们对C++的封装,继承等特性都有了了解和学习,接下来我们将对C++的第三大特性-多态进行认识和掌握。内容分为来两大部分,第一个是对多态的认识和运用,第二大部分是对多态原理的了解和扩展。
1.多态的概念
多态(Polymorphism)是面向对象编程(OOP)中的一个核心概念,它指的是同一个行为具有多个不同表现形式或形态的能力。在编程中,多态通常通过继承(inheritance)和接(interfaces来实现。
以下是多态的几个主要方面:
-
编译时多态(静态多态):这是在编译时确定的多态性,通常通过函数重载(function overloading)和模板(templates)来实现。编译器根据函数的参数类型或数量来决定调用哪个函数。
-
运行时多态(动态多态):这是在程序运行时确定的多态性,主要通过虚函数(virtual functions)和继承来实现。在运行时,根据对象的实际类型来调用相应的成员函数。
多态的关键特性包括:
- 继承:子类继承父类的属性和行为,可以对这些行为进行重写(override)。
- 虚函数:在基类中声明为虚的成员函数,可以在派生类中被重写,使得通过基类指针或引用调用函数时,能够根据对象的实际类型来调用相应的函数版本。
- 虚函数表:用于实现运行时多态的数据结构,它存储了虚函数的地址,使得程序能够在运行时确定调用哪个函数。
- 向上转型:将派生类对象的引用或指针转换为基类类型的引用或指针,这是多态实现的基础。
2.多态的定义及实现
2.1多态的构成条件
2.1.1重要条件
2.1.2 虚函数
class Person {
public:
virtual void BuyTicket() {
cout << "买票全额" << endl;
}
};
2.1.3 虚函数的重写/覆盖
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Person {
public:
virtual void BuyTicket() {
cout << "买票全额" << endl;
}
};
class Student : public Person {
public:
virtual void BuyTicket() {
cout << "学生票半价" << endl;
}
};
//引用调用
void func(Person& p) {
p.BuyTicket();
}
//指针调用
void func1(Person* p) {
p->BuyTicket();
// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
}
int main() {
Person p1;
Student s1;
Person* p2 = new Person();
Student* s2 = new Student();
func(p1);
func(s1);
p1.BuyTicket();
s1.BuyTicket();
func1(&p1);
func1(&s1);
p2->BuyTicket();
s2->BuyTicket();
return 0;
}
void func(Student& p) {
p.BuyTicket();
}
//指针调用
void func1(Student* p) {
p->BuyTicket();
}
如果改成Student,就会出问题,就不是多态了,也就不能传Person对象了。
#include <iostream>
using namespace std;
class Pet {
public:
virtual void eat() const{
cout << "Eat food" << endl;
}
};
class Dog : public Pet{
public:
virtual void eat() const {
cout << "Dog eats meat!" << endl;
}
};
class Cat :public Pet {
public:
virtual void eat()const {
cout << "Cat eats fish!" << endl;
}
};
void func(const Pet& p) {
p.eat();
}
int main() {
Pet p;
Dog g;
Cat c;
func(p);
func(g);
func(c);
return 0;
}
上述是宠物的一个多态实现。
这里我们测试一下,基类函数不加virtual会怎样,
class Pet {
public:
void eat() const{
cout << "Eat food" << endl;
}
};
我们会发现多态效果没有实现,所以一定要加上virtual.
2.1.4 选择题
下面程序输出结果是什么?(B)
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* p = new B;
创建了一个B
类型的对象,并通过基类指针p
指向它。p->test();
调用了A
类的test
方法(因为B
类没有重写test
方法)。- 在
A
类的test
方法中,func(val)
被调用,没有指定val
的值,因此它使用A
类func
方法的默认参数1
。 - 由于
func
是虚函数,并且p
指向一个B
类型的对象,所以B
类的func
方法被调用,接收到的参数是1
。
2.1.5 虚函数其他知识
协变(了解)
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class A {};
class B : public A {};
class Person {
public:
virtual A* BuyTicket()
{
cout << "买票-全价" << endl;
return nullptr;
}
};
class Student : public Person {
public:
virtual B* BuyTicket()
{
cout << "买票-打折" << endl;
return nullptr;
}
};
void Func(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}
析构函数的重写
故在C++中,当一个基类的析构函数被声明为虚函数时,它确保了当通过基类指针或引用删除派生类对象时,会调用正确的析构函数,即派生类的析构函数,然后再调用基类的析构函数。这是因为虚析构函数允许动态绑定,确保了派生类对象被正确地销毁。
#include <iostream>
using namespace std;
class A {
public:
virtual ~A() {
cout << "delete A" << endl;
}
};
class B :public A {
public:
~B() {
cout << "~B()->delete:" << _p << endl;
delete _p;
}
protected:
int* _p = new int[10];
};
int main() {
A* a = new A;
A* b = new B;
delete a;
delete b;
return 0;
}
当我们不把基类析构函数设置成virtual时, 会发现没有调用B的析构,该释放的资源没有释放掉。
public:
~A() {
cout << "delete A" << endl;
}
};
故基类的析构函数我们要设置成虚函数。
override 和 final关键字
override 关键字用于明确指出一个成员函数旨在重写(覆盖)其基类中的一个虚函数。如果该函数没有正确地重写基类中的任何虚函数,编译器将报错。这有助于避免因拼写错误或参数列表不匹配而意外地没有重写虚函数的情况。
class Car {
public:
virtual void Dirve()
{}
};
class Benz :public Car {
public:
virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
比如上面这个例子,函数名写错了,重写失败,编译报错。
final 关键字用于防止类被进一步派生,或者防止虚函数被重写。当应用于类时,它表示这个类不能被继承。当应用于虚函数时,它表示这个虚函数不能在派生类中被重写。
class Car {
public:
virtual void Dirve() final
{}
};
class Benz :public Car {
public:
virtual void Dirve(){ cout << "Benz-舒适" << endl; }
};
class Base final { // 不能从这个类派生其他类
public:
virtual void doSomething() const final {} // 这个虚函数不能被重写
};
// 下面的类声明会导致编译错误,因为 Base 是 final 的
// class Derived : public Base {};
// 下面的函数声明也会导致编译错误,因为 doSomething 是 final 的
// class Derived : public Base {
// public:
// void doSomething() const override {} // 错误:不能重写 final 函数
// };
使用 final 关键字可以确保类或虚函数的行为不会被意外的继承或重写改变,这对于设计那些不打算被扩展的类或函数非常有用。
3. 重载,重写,隐藏的对比
重载(Overloading)
- 定义:在同一作用域内,可以定义多个同名函数,只要它们的参数列表(参数的数量、类型或顺序)不同。
- 特点:
- 发生在同一类中。
- 参数列表必须不同。
- 返回类型可以不同,但不是区分重载的主要因素。
重写(Overriding)
- 定义:在派生类中提供一个与基类中虚函数同名、参数列表和返回类型相同的函数,以实现多态。
- 特点:
- 发生在基类和派生类之间。
- 参数列表和返回类型必须相同。
- 基类函数必须是虚函数。
- 使用
override
关键字可以明确指出重写意图。
隐藏(Hiding)
- 定义:在派生类中定义一个与基类中成员(非虚函数或非静态成员变量)同名的成员,导致基类中的同名成员在派生类中不可见。
- 特点:
- 发生在基类和派生类之间。
- 可以是函数或变量。
- 如果是函数,参数列表不必相同。
- 如果派生类中的成员与基类中的成员具有相同的名称,但不同的参数列表,则基类成员被隐藏,而不是重载或重写。
4.纯虚函数和抽象类
#include <iostream>
using namespace std;
class Car {
public:
virtual void Drive() = 0;
};
class Benchi :public Car {
public:
virtual void Drive() {
cout << "Benchi-舒适" << endl;
}
};
class Baoma :public Car {
public:
virtual void Drive() {
cout << "Baoma-上手" << endl;
}
};
int main() {
Car car;
Car* b = new Benchi();
b->Drive();
Car* m = new Baoma();
m->Drive();
return 0;
}
这里Car是抽象类,所以无法实例化对象。
结束语
本期内容就到此结束了,内容有点多,下节我们将对多态的原理进行补充讲解。
最后感谢各位友友的支持!!!