关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

【C++】非常重要的——多态(二)

发布时间:2023-06-29 13:00:52

2. 多态的定义及实现

首先多态实现的前提必须是继承!


多态实现的两个条件:


1.必须使用父类(基类)的指针或者引用调用虚函数;


2.被调用的函数必须是虚函数,且子类(派生类)必须对虚函数进行重写;


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

Person 。 Person 对象买票全价, Student 对象买票半价。

2.1多态调用

class Person { public:  virtual void Buyticket()  {  cout << "Person:全价" << endl;  } };  class Student:public Person { public:  virtual void Buyticket()  {  cout << "Student:半价" << endl;  } };  void func(Person& p) //切片 {  p.Buyticket(); }  int main() {  Person p;  Student s;  func(p);  func(s); }

   

2.2普通调用:

不符合多态条件即可:

1.

     

void func(Person p)//不是指针或者引用,就是对象 {  p.Buyticket(); }  int main() {  Person p;  Student s;  func(p);  func(s); }

   

那么我们可以发现:

普通调用跟调用对象的类型有关;

多态调用必须是父类的指针或者引用,无论是是哪个对象传,他都会指向该对象中父类的那一部分(切片),进而调用该对象中的虚函数。一句话,多态调用跟 指针/引用 指向的对象有关

2.3析构函数建议加virtual吗? 

我们看一个例子

class Person { public:  ~Person()  {  cout << "Person delete" << endl;  delete _p;  } protected:  int* _p = new int[10]; };  class Student :public Person { public:  ~Student()  {  cout << "Student delete" << endl;  delete _s;  } protected:  int* _s = new int[20]; };  int main() {  //Person p;  //Student s;   Person* ptr1 = new Person;  Person* ptr2 = new Student;   delete ptr1;  delete ptr2; }

   

我们都知道,析构函数自动调用,在继承中,子类会先析构,调用子类的析构函数以后,自动再调用父类的析构函数。

但这用情况还适用吗?

先看一下结果:

我们发现,居然调用了两次父类的析构函数 !!!


这种情况就会造成子类对象中的成员变量没有释放,导致内存泄露!!


我们知道:


delete有两种行为:1.使用指针调用析构函数;2.operator delete(ptr)


所以使用指针调用析构函数是普通调用(不满足多态调用的条件),普通调用是跟调用的对象类型有关,类型都是Person,所以只会调用person的析构函数


但此时我们更希望的是多态调用,所以建议加virtual,指针指向的对象是哪个,就调用哪个的析构函数。但此时我们会想,析构函数名字都不一样,这能构成重写吗?当然可以,那是因为编译器会自动把父类子类的析构函数名字换成一样的:ptr->destructor()。


那么就可以实现我们预期的效果:

所以我们建议:再写析构函数时,可以无脑给父类的析构函数加virtual,防止出现上面的情况,导致内存泄露 。

普通调用时,时普通调用;父类的指针或者引用调用时,时多态调用,互不影响!

2.4抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。 包含纯虚函数的类叫做抽象类(也叫接口

类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生

类才能实例化出对象。 纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Car { public:  virtual void Drive() = 0; };  class BMW :public Car { public:  virtual void Drive()  {  cout << "BMW-操控" << endl;  } }; void Test() {  Car* pBMW = new BMW;  pBMW->Drive(); }  int main() {  Test(); }

   

总结:有些类不需要类的对象,可以在写成纯虚函数。


2.5接口继承和实现继承

接口继承针对虚函数;实现继承针对普通函数。


普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实

现。

虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成

多态,继承的是接口。

学了这么多,来做一道题温习一下: (很坑)

 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; }  A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确  若是ptr->func(),就是B类对象直接调用,就是普通调用,普通调用跟对象类型有关。 普通调用在编译时就会静态绑定,在编译时调用的函数以及函数的默认值就已经确定,子类调用子类自己的函数,跟父类没有任何关系,函数都是子类编译时就已经静态绑定的,所以缺省值依然是0。最终结果是B->0

   

答案选哪个??


首先我们了解的第一点是,继承父类的成员,会原封不动的继承到子类;


我们接下来看:创建了一个B对象的指针,指针来调用p->test(),这时候,会直接调用父类中的test,再this->func(),此时的this的类型是A*,因为test处于A类中,继承到B中,也会原封不动的继承过去,this依然是A*,所以父类的指针调用虚函数,满足多态的调用,多态调用是看指针指向的对象,又因为p调用的test,所以指针指向B对象,所以会调用B的重写的func虚函数,所以最终答案是B->1.(其实多态调用一直是调的父类的接口,再根据指向的对象去调用具体的实现,后面会详细讲到)


当B对象自己调用函数func时,当不是多态调用时,就会直接调用自己的func(),缺省值还是自己的val=0.


/template/Home/leiyu/PC/Static