欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 新车 > c++之多态

c++之多态

2025/2/22 17:02:00 来源:https://blog.csdn.net/2401_85878549/article/details/145741806  浏览:    关键词:c++之多态

1.概念

多态分为编译时多态(静态多态)运行时多态(动态多态)

编译时多态(静态多态)主要就是我们前面讲的函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态。示例代码如下所示:

1)函数重载

void print(int x) {std::cout << "Integer: " << x << std::endl;
}void print(double x) {std::cout << "Double: " << x << std::endl;
}void print(const std::string& x) {std::cout << "String: " << x << std::endl;
}

2)函数模板

template <typename T>
void print(T x) {std::cout << "Value: " << x << std::endl;
}

 运行时多态,具体点就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种形态。也就是说,是在程序运行时才确定函数调用的具体实现形式,主要通过虚函数和继承机制实现。

2.虚函数


虚函数是实现运行时多态的关键。通过在基类中用virtual声明虚函数,派生类可以重写这些函数。调用虚函数时,程序会根据对象的实际类型动态选择调用的函数版本。

2.1 虚函数的重写/覆盖:

派生类中有⼀个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。例如:

class Base {
public:virtual void show() {std::cout << "Base::show" << std::endl;}
};class Derived : public Base {
public:void show() override {  // 重写虚函数std::cout << "Derived::show" << std::endl;}
};

在运行时,通过基类指针或引用调用虚函数时,会根据对象的实际类型调用对应的函数版本:

    Base* ptr = new Derived();Base* ptr1 = new Base();ptr->show();  // 输出 "Derived::show"ptr1->show();//输出"Base::show"

 2.2  析构函数的重写

基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,所以基类的析构函数加了vialtual修饰,派生类的析构函数就构成重写。

下面的代码我们可以看到,如果~A(),不加virtual,那么delete p2时只调用的A的析构函数,没有调用B的析构函数,就会导致内存泄漏问题,因为~B()中在释放资源。

class A
{public :virtual ~A(){std::cout << "~A()" << std::endl;}
};
class B : public A {
public:~B(){std::cout << "~B()->delete:" << _p << std::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;
}

这是运行结果

 有些人可能会好奇为什么输出结果是这样的,这里我解释一下:第一句delete p1 执行后,调用A的析构函数,输出~A(),之后执行delete p2 命令,先调用B的析构函数,输出

~B()->delete:0000027202410FB0,之后由于B继承了A,所以还要调用A的析构函数,输出~A()

 3.override关键字

作用:1)明确告诉编译器,当前函数是用于覆盖基类中的虚函数。

         2)避免错误:

    如果没有使用 override,编译器不会检查当前函数是否正确覆盖了基类的虚函数。如果基类中的虚函数签名发生变化(例如参数类型或数量改变),派生类中的函数可能无法正确覆盖,但编译器不会报错。这种情况下,可能会导致运行时错误或逻辑问题。而使用 override 后,如果派生类中的函数无法正确覆盖基类的虚函数,编译器会报错,从而避免潜在的错误。例如 

class Base {
public:virtual void display(int x) {  // 基类虚函数的参数改变了std::cout << "Base::display" << std::endl;}
};class Derived : public Base {
public:void display() override {  // 编译错误,无法覆盖基类的虚函数std::cout << "Derived::display" << std::endl;}
};

 在这种情况下,编译器会报错,提示 display 函数无法覆盖基类中的虚函数,从而避免了潜在的错误。

4.final关键字

作用:如果我们不想让派生类重写这个虚函数,那么可以用final去修饰。
例如:

// error C3248 : “Car::Drive” : 声明为“final”的函数⽆法被“Benz::Drive”重写
class Car
{public :virtual void Drive() final {}
};
class Benz :public Car
{public :virtual void Drive() { std::cout << "Benz-舒适" << std::endl; }
};

5.纯虚函数和抽象类

如果希望在基类中声明一个虚函数,但不提供具体实现,可以将其定义为纯虚函数(在后面加=0)。包含纯虚函数的类称为抽象类,不能直接实例化。例如:

class Shape {
public:virtual void draw() = 0;  // 纯虚函数
};class Circle : public Shape {
public:void draw() override {std::cout << "Drawing a Circle" << std::endl;}
};class Square : public Shape {
public:void draw() override {std::cout << "Drawing a Square" << std::endl;}
};

通过抽象类的指针或引用,可以统一调用派生类的实现:

Shape* shape1 = new Circle();
Shape* shape2 = new Square();shape1->draw();  // 输出 "Drawing a Circle"
shape2->draw();  // 输出 "Drawing a Square"

6.小结

综上所述,实现多态的两个必须重要条件是

1)是基类的指针或者引用调用虚函数

2)被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖。

7.原理

1)虚函数表(V-Table):编译器为每个包含虚函数的类生成一个虚函数表(V-Table),表中存储了类中所有虚函数的地址。

2)对象的V-Pointer:每个对象都包含一个指向其类的V-Table的指针(V-Pointer)。当通过基类指针或引用调用虚函数时,程序会通过对象的V-Pointer找到对应的V-Table,再通过V-Table找到实际的函数地址

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词