C++ 多态(多态定义 多态应用 多态底层||final override关键字||抽象类)

多态概念

多态 面向对象三大基本特性的最后一个,多态可以实现**"一个接口,多种方法"** ,比如父类和子类中的同名方法,在增加了多态后,调用同名函数时候,可以根据不同的对象调用属于自己的函数,实现不同的方法,因为 多态 的实现依赖于继承

多态分为编译时多态(静态多态)运行时多态(动态多态)编译时多态(静态多态) 主要就是我们前⾯讲的函数重载和函数模板 ,他们传不同类型的参数就可以调⽤不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在 编译时完成的,我们把编译时⼀般归为静态,运⾏时归为动态

运⾏时多态,具体点就是去完成某个⾏为(函数),可以传不同的对象就会完成不同的⾏为,就达到多种 形态。⽐如买票这个⾏为,当普通⼈买票时,是全价买票;学⽣买票时,是优惠买票(5折或75折);军 ⼈买票时是优先买票。再⽐如,同样是动物叫的⼀个⾏为(函数),传猫对象过去,就是"(>^ω^<)

多态应用

买票场景

cpp 复制代码
class Person {
public:
//  [virtual] + [返回值] + [函数名] + [参数] 相同 = 构成多态
        👇         👇         👇       👇
      virtual     void     BuyTicket   ()  {
          cout << "Person: 买票-全价 100¥" << endl;
      }
};
 
class Student : public Person {
public:
    // 这里也都相同
    virtual void BuyTicket() {
        cout << "Student: 买票-半价 50¥" << endl;
    }
};
 
class Soldier : public Person {
public:
    // 这里也都相同
    virtual void BuyTicket() {
        cout << "Soldier: 优先买预留票-全价 100¥" << endl;
    }
};

宠物

cpp 复制代码
class Pet {
public:
    virtual void makeSound() {
        cout << "Some generic pet sound" << endl;
    }
};
 
class Cat : public Pet {
public:
    void makeSound() override {
        cout << "Meow" << endl;
    }
};
 
class Dog : public Pet {
public:
    void makeSound() override {
        cout << "Woof" << endl;
    }
};
 
class Bird : public Pet {
public:
    void makeSound() override {
        cout << "Tweet" << endl;
    }
};

就宠物场景进行多态讲解,我们现在有pet类型的指针,它可以指向 任何一种具体的宠物 对象:

cpp 复制代码
Pet* myPet = new Dog();
myPet->makeSound();  // 输出 "Woof"

尽管 myPetPet 类型的指针,但它指向的是一个**Dog** 对象。因此,当你调用 makeSound() 方法时,会调用 Dog 类的 makeSound() 方法,而不是 Pet 类的(这就是多态)

当你将mypet指向一个Cat对象:

cpp 复制代码
myPet = new Cat();
myPet->makeSound();  // 输出 "Meow"

多态形成条件

多态是 在不同继承关系的类对象中去调用同一个函数,产生了不同的行为。

比如我们刚才的 Student 继承了 Person,Person 买票是全价,但 Student 买票却是半价:

**📌 注意:**继承中想要构成多态,必须满足以下两个条件:

  • ① 必须是子类的虚函数 重写成父类函数(重写:三同 + 虚函数)

  • ② 必须是父类的指针 或者引用去调用虚函数。

  • * 三同指的是:同函数名、同参数、同返回值。

    * 虚函数:即被 virtual 修饰的类成员函数。

cpp 复制代码
// 基类
class Base {
public:
    virtual void display()      // 虚函数
    {
        cout << "Display from Base" << endl;
    }
};
 
// 派生类
class Derived : public Base {
public:
    void display() override {
        cout << "Display from Derived" << endl;
    }
};

虚函数重写

在面向对象编程中,虚函数重写(覆盖) 是指派生类重新定义基类中的虚函数**(返回值、函数名、参数列表,均相同)** 。当通过基类指针或引用调用虚函数时,程序会根据实际对象类型调用对应的重写函数,而不是基类中的函数。

具体实现步骤

  • 定义基类和虚函数
  • 定义派生类并重写虚函数
  • 通过基类指针或引用调用虚函数

虚函数重写的例外

💬 观察下面的代码,并没有达到 "三同" 的标准,它的返回值是不同的,但依旧构成多态:

cpp 复制代码
class A {};
class B : public A {};
 
class Person {
public:
	virtual A* f() {
		cout << "virtual A* Person::f()" << endl;
		return nullptr;
	}
};
 
class Student : public Person {
public:
	virtual B* f() {
		cout << "virtual B* Student:::f()" << endl;
		return nullptr;
	};
};
 
int main(void)
{
	Person p;
	Student s;
	Person* ptr = &p;
	ptr->f();
 
	ptr = &s;
	ptr->f();
 
	return 0;
}

结果如下:

💡 因为虚函数的重写要求有一个例外 ------ 协变(Covariant)。

但是协变也是有条件的,协变的类型必须是父子关系

cpp 复制代码
class A{};
class B{};  //我们取消 A B 的父子关系

理所当然多态不复存在,代码报错

🚩 运行结果:(报错)

error C2555: "Student::f": 重写虚函数返回类型有差异,且不是来自"Person::f"的协 message : 参见"Person::f"的声明

💬 父类的虚函数没了无法构成多态:

cpp 复制代码
class Person {
public:
	A* f() {
		cout << "virtual A* Person::f()" << endl;
		return nullptr;
	}
};
 
class Student : public Person {
public:
	virtual B* f() {
		cout << "virtual B* Student:::f()" << endl;
		return nullptr;
	};
};
 
int main(void)
{
	Person p;
	Student s;
	Person* ptr = &p;
	ptr->f();
 
	ptr = &s;
	ptr->f();
 
	return 0;
}

结果如下:

💬 但是,子类的虚函数没了却能构成多态:

cpp 复制代码
class A {};
class B : public A {};
 
class Person {
public:
	virtual A* f() {
		cout << "virtual A* Person::f()" << endl;
		return nullptr;
	}
};
 
class Student : public Person {
public:
	B* f() {
		cout << "virtual B* Student:::f()" << endl;
		return nullptr;
	};
};
 
int main(void)
{
	Person p;
	Student s;
	Person* ptr = &p;
	ptr->f();
 
	ptr = &s;
	ptr->f();
 
	return 0;
}

结果如下:

那么这时候就有人问了------这都不是虚函数了怎么还能构成多态!

**💡 解答:**子类虚函数没有写 virtual,但 f 依旧是虚函数,是因为先继承了父类的函数接口声明。

子类继承父类的虚函数是一种接口继承,所以即使子类的 virtual 没写,它也是虚函数,符合多态条件。

这是重写父类虚函数的实现,也就是说父类有 virtual 的属性,子类也就有了。

最后,虽然子类虚函数可以不加 virtual,但是我们自己写的时候 子类虚函数建议加上 virtual。

协变返回类型

协变返回类型 :指的是在 派生类中重写基类虚函数时,返回类型可以是基类返回类型的派生类 。例如,如果是基类的虚函数返回一个基类指针或引用,派生类可以重写这个函数并返回派生类的指针或引用。

首先,我们定义一个基类 Base 和一个从 Base 继承的派生类 Derived

cpp 复制代码
#include <iostream>
using namespace std;
 
class Base {
public:
    virtual Base* clone() const {
        return new Base(*this);
    }
 
    virtual void print() const {
        cout << "This is Base" << endl;
    }
};
 
class Derived : public Base {
public:
    Derived* clone() const override {
        return new Derived(*this);
    }
 
    void print() const override {
        cout << "This is Derived" << endl;
    }
};
  • Base 类定义了一个虚函数 clone ,返回一个 Base* 类型。
  • Derived 类重写了 clone 函数,但返回类型是 Derived*

使用协变返回类型

main 函数中,我们可以使用协变返回类型来创建对象的副本

cpp 复制代码
int main() {
    Base* b = new Derived();
    Base* b_clone = b->clone();  // 调用的是 Derived::clone(),返回 Derived*,但可以赋值给 Base*
    
    b->print();        // 输出 "This is Derived"
    b_clone->print();  // 输出 "This is Derived"
 
    delete b;
    delete b_clone;
    return 0;
}

解释:

基类定义 :基类 Base 中的虚函数 clone 返回一个Base* 类型的对象。
派生类重写 :派生类Derived 重写了 clone 函数,并返回一个Derived* 类型的对象。这是合法的,因为 Derived*Base* 的派生类指针。
多态性 :通过基类指针调用clone 函数时,会实际调用Derived 类的 clone 函数,并返回一个 Derived* 。尽管返回的是 Derived* ,但它可以被赋值给**Base***类型的指针,这是C++的类型兼容性特性。

析构函数的重写

cpp 复制代码
class Person {
public:
	~Person() {
		cout << "~Person()" << endl;
	}
};
 
class Student : public Person {
public:
	~Student() {
		cout << "~Student()" << endl;
	}
};
 
int main(void)
{
	Person p;
	Student s;
 
	return 0;
}

**❓ 思考:**这三行分别是谁的?

💡 解读:第一行和第二行是 Student s 的,第三行是Person p 的。我们来看看析构顺序,Student s 是后定义的析构顺序是后定义先析构 。根据子类对象析构先子后父调用子类的析构函数结束后自动调用父类的析构函数 ,所以第一行的 ~Student() 和第二行的 ~Person() 都是 Student 的,随后第三行的 ~Person() 是 Person p 自己调的。
现在这两个析构函数默认是隐藏关系

因为它们的函数名会被同一处理修改成 destructor

但是如果我用 virtual 修饰 ~Person,我们知道,如果这加了不管 ~Student 加不加 virtual,

子类都会跟着父类变身成 virtual,那么现在这两个析构函数还是隐藏关系吗?

如果 Person 的析构函数加了 virtual,它们的关系就变了:

干脆直接用一个 ptr 去演示好了:

cpp 复制代码
	Person* ptr = new Person;
	delete ptr;   // ptr->destructor() + operator delete(ptr)
 
	ptr = new Student;  
	delete ptr;   // ptr->destructor() + operator delete(ptr)

刚才我们看到了,如果这里不加 virtual,~Student 是没有调用析构的。

你可能会想这有啥,那是因为这里没场景,这其实是非常致命的,是不经意间会发生的内存泄露。

所以 析构函数为什么要重写的结果就是:

当使用基类指针或引用指向派生类对象 时,如果基类的析构函数不是虚函数,那么在删除对象时,只会调用基类的析构函数,而不会调用派生类的析构函数。这可能会导致资源泄漏,因为派生类的清理工作未被执行。

**🔺 结论:**如果设计的类可能会作为父类,析构函数最好设计成虚函数,即加上 virtual。

------------------------------------------------------------------------------------------------------------------------------

C++11 override和final关键字

override 是C++11引入的一种功能,用来**明确表示派生类中的函数是覆盖基类中的虚函数。**它有以下几个好处:

  • 增加代码可读性:明确表明这个函数是用来覆盖基类的虚函数的,方便代码阅读。
  • 编译器检查:编译器会检查这个函数是否确实覆盖了基类中的虚函数。如果没有(例如函数签名不匹配),编译器会报错。这可以帮助我们捕捉错误
cpp 复制代码
class Pet {
public:
    virtual void makeSound() {
        cout << "Some generic pet sound" << endl;
    }
};
 
class Cat : public Pet {
public:
    void makeSound() override {
        cout << "Meow" << endl;
    }
};

Cat 类中,makeSound() 函数后面的 override 关键字告诉编译器这是对基类 PetmakeSound() 函数的覆盖。如果我们不小心拼写错误或参数列表不同,编译器会报错。

如果我有个虚函数,但我不想让它被人重写:

这种情况,就可以将 C++11 的 final 关键字置于函数尾部:

cpp 复制代码
class Car {
public:
	virtual void Drive() final {}
};
 
class Benz : public Car {
public:
	virtual void Drive() {  ❌
		cout << "Benz-舒适" << endl; 
	}
};

final 不仅能让虚函数不能被重写,还能让直接把类的继承功能一刀砍了。

将 final 放在类名后面,该类就不能被继承了,因此你不用创建对象他就可以报错给你检查出来。

final 的意思是 "最终的",可以理解为是最终的类了,不能再继承了。

以后如果你想让某个类不能被继承,就可以在类名后面加上 final 关键字。

  • 防止类被继承 :将 final 关键字放在类声明之后,表示该类不能被继承。
  • 防止虚函数被重写 :将 **final**关键字放在虚函数声明之后,表示该虚函数不能在派生类中被重写。

重载 重写(覆盖) 重定义(隐藏)区别

  • 重载(Overloading):同一个作用域内同名函数的参数列表不同,构成重载。
  • 覆盖(重写)(Overriding):派生类重新定义基类中的虚函数,函数签名必须相同,构成覆盖。
  • 重定义(隐藏)(Hiding):派生类中定义一个与基类中同名但参数列表不同的函数,构成重定义或隐藏。

抽象类

纯虚函数和抽象类

在虚函数的后面写上 =0,则我们称这个函数为 "纯虚函数"。

包含纯虚函数的类,就是抽象类(abstract class),也叫接口类。

cpp 复制代码
/* 抽象类 */
class Car {
public:
	virtual void Drive() = 0;  // 纯虚函数
};

抽象类不能实例化出对象,子类即使在继承后也不能实例化出对象:

只有重写纯虚函数,子类才能实例化出对象:

cpp 复制代码
/* 抽象类 */
class Car {
public:
	virtual void Drive() = 0;
};
 
// 如果父类是抽象类,子类必须重写才能实例化
class BMW : public Car {
public:
	virtual void Drive() {   // 重写
		cout << "BMW-操控性" << endl;
	}
};
 
int main(void)
{
	BMW b;
	b.Drive();
 
	return 0;
}

**🔺 总结:**抽象类不能实例化出对象,子类即使在继承后也不能实例化出对象,除非子类重写。

抽象类指针

虽然父类是抽象类不能定义对象,但是可以定义指针。

定义指针时如果 new 父类对象因为是纯虚函数,自然是 new 不出来的,但是可以 new 子类对象:

cpp 复制代码
/* 抽象类 */
class Car {
public:
	virtual void Drive() = 0;
};
 
class Benz : public Car {
public:
	virtual void Drive() {
		cout << "Benz-舒适" << endl;
	}
};
 
int main(void)
{
	Car* pBenz1 = new Benz;
	pBenz1->Drive();
 
	Benz* pBenz2 = new Benz;
	pBenz2->Drive();
 
	return 0;
}

关于接口继承和实现继承

普通函数的继承是一种实现继承 ,子类继承了父类函数,可以使用函数,继承的是函数的实现

虚函数的继承是一种接口继承 ,子类继承的是父类虚函数的接口 ,目的是为了重写

达成多态,继承的是接口 。所以如果不实现多态,不要把函数定义成虚函数。

出现虚函数就是为了提醒你重写的,以实现多态。如果虚函数不重写,那写成虚函数就没价值了。

相关推荐
进击的荆棘2 小时前
C++起始之路——unordered_map和unordered_set的使用
开发语言·c++·stl·unordered_map·unordered_set
进击的荆棘2 小时前
C++起始之路——封装红黑树实现map和set
c++·stl·set·map
云深麋鹿2 小时前
C++ | 模板
开发语言·c++
Bat U2 小时前
JavaEE|多线程(三)
java·前端·java-ee
卷到起飞的数分3 小时前
JVM探究
java·服务器·jvm
Geek攻城猫3 小时前
Java生产环境问题排查实战指南
java·jvm
t***54410 小时前
Clang 编译器在 Orwell Dev-C++ 中的局限性
开发语言·c++
OtIo TALL10 小时前
redis7 for windows的安装教程
java
oy_mail10 小时前
QoS质量配置
开发语言·智能路由器·php