C++基础——多态(上)

发布于:2024-05-17 ⋅ 阅读:(128) ⋅ 点赞:(0)

一、多态的概念

多态的概念:望文生义,就是多种状态,具体点就是对于一件事情,不同的对象去完成时会产生出不同的状态 (结果)。

举个例子:比如买票这个行为,当普通人买票时,是全价买票;儿童买票时,是免费买票;而军人买票时是优先买票。

二、多态的定义及实现

C++的多态必须满足两个条件:
1 必须通过基类的指针或者引用调用虚函数
2 被调用的函数是虚函数,且必须完成对基类虚函数的重写

子类虚函数可以不写 virtual (实际中最好加上),但是父类虚函数必须写 virtual,才能满足虚函数重写;

返回值可以不同,但必须是父子关系的指针或者引用 (在这里的父子没有限制,可以是其他的父子关系),这也称之为协变

具体实现:

class Person //成人
{
  public:
  virtual void fun()
   {
       cout << "全价票" << endl; //成人票全价
   }
};
class Student : public Person //学生
{
   public:
   virtual void fun() //子类完成对父类虚函数的重写
   {
       cout << "半价票" << endl;//学生票半价
   }
};
void BuyTicket(Person* p)
{
   p->fun();
}

int main()
{
   Student st;
   Person p;
   BuyTicket(&st);//子类对象切片过去
   BuyTicket(&p);//父类对象传地址
}

以上例子为什么要传地址,直接传对象不行吗?

答案当然是不行的,因为多态构成的第一个条件不满足。

如果满足多态,编译器会调用指针指向对象的虚函数,而与指针的类型无关。如果不满足多态,编译器会直接根据指针的类型去调用虚函数。

三、虚函数

用virtual修饰的关键字就是虚函数。
虚函数只能是类中非静态的成员函数。

在类外面的函数不能是虚函数。

虚函数的重写(覆盖)

虚函数的重写:虚函数 + 三同 (基类和派生类的虚函数的返回值、参数、函数名都相同)。

class A
{
public:
	virtual void Print() { std::cout << "haha" << std::endl; }
};
 
class B : public A
{
public:
	virtual void Print() { std::cout << "hehe" << std::endl; }
};

虚函数重写的两个例外:

1.协变

子类的虚函数和父类的虚函数的返回值可以不同,也能构成重载。但需要子类的返回值是一个子类的指针或者引用,父类的返回值是一个父类的指针或者引用,且返回值代表的两个类也成继承关系。这个叫做协变。

class A
{};
class B : public A
{};   //B继承A
class Person
{
  public:
   virtual A* fun()//返回A类指针
   {
      return nullptr;
   }
};
class Student
{
   public:
            //返回B类指针,虽然返回值不同,也构成重写
   virtual B* fun()//子类重写父类虚函数
   {
     return nullptr;
   }
};

2.析构函数的重写

子类的虚函数可以不写 virtual,但是父类虚函数必须写 virtual,才能符合虚函数重写的条件。

只要父类的析构函数用virtual修饰,无论子类是否有virtual,都构成析构。

class A
{
public:
	// 基类的虚函数必须加上 virtual
	virtual void Print() { std::cout << "haha" << std::endl; }
};
 
class B : public A
{
public:
	// 派生类的虚函数可以不写 virtual, 但实际中最好加上
	void Print() { std::cout << "hehe" << std::endl; }
};

四、多态调用和普通调用

普通调用, 即调用函数的对象的类型是什么,就调用什么类型的函数。

上面之所以是普通调用,因为它不满足构成多态的条件:

  • 虚函数的重写;
  • 基类的指针或者引用调用虚函数。

只要符合上面的两个条件就构成多态调用。

多态调用和普通调用是相对立的:

  • 如果是普通调用,就看对象的类型决定调用什么接口;
  • 如果是多态调用,就看指向的是什么对象,如果指向派生类,就调派生类的接口;如果指向基类,就调基类的接口。

五、C++11 中 override 和 final

1. final关键字

在学习继承的时候,我们知道, final 可以修饰一个类,表明这个类不可被继承;

实际上,final 也可以修饰一个虚函数,表示这个虚函数不可以被重写。

因此,final 的作用有两个:

  • 修饰一个类,表明该类不可被继承;
  • 修饰一个虚函数,表明该虚函数不可被重写
class A
{
public:
	virtual void print() final
	{
		std::cout << "haha" << std::endl;
	}
};
 
class B : public A
{
public:
	virtual void print()
	{
		std::cout << "hehe" << std::endl;
	}
};

2. override关键字

用 override 修饰的虚函数,如果没有完成重写,则会编译报错。

class A
{
public:
	virtual void print() 
	{
		std::cout << "haha" << std::endl;
	}
};
 
class B : public A
{
public:
    // 此时就会编译报错, 因为派生类没有完成对 print 这个虚函数的重写
	virtual void print(int val) override
	{
		std::cout << "hehe" << std::endl;
	}
};

六、重载,覆盖(重写),重定义(隐藏)

七、接口继承和实现继承

  • 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现;
  • 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口 (即函数的声明),目的是为了虚函数的重写 (重写函数的实现),达成多态,继承的是虚函数的接口;
  • 强调一下虚函数的继承是一种接口继承,更准确地说,应该是,虚函数的继承, 在构成多态的条件下是接口继承;而如果派生类没有重写虚函数,但是继承了该虚函数,那么这仍然是继承了函数的实现,不会产生多态效果,称为实现继承;
  • 补充一点,派生类继承基类的函数,并不是说基类的函数在派生类中也有一份,而是说派生类可以使用基类的函数 (当然受访问权限和继承方式的约束),继承后的基类函数依旧属于基类;
  • 最后,一般情况下,如果目的不是实现多态,不要把函数定义成虚函数,可以避免不必要的开销和复杂性。

网站公告

今日签到

点亮在社区的每一天
去签到