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

简介: 【C++】非常重要的——多态

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.

目录
相关文章
|
28天前
|
存储 编译器 数据安全/隐私保护
【C++】多态
多态是面向对象编程中的重要特性,允许通过基类引用调用派生类的具体方法,实现代码的灵活性和扩展性。其核心机制包括虚函数、动态绑定及继承。通过声明虚函数并让派生类重写这些函数,可以在运行时决定具体调用哪个版本的方法。此外,多态还涉及虚函数表(vtable)的使用,其中存储了虚函数的指针,确保调用正确的实现。为了防止资源泄露,基类的析构函数应声明为虚函数。多态的底层实现涉及对象内部的虚函数表指针,指向特定于类的虚函数表,支持动态方法解析。
32 1
|
2月前
|
编译器 C++
C++入门12——详解多态1
C++入门12——详解多态1
47 2
C++入门12——详解多态1
|
7月前
|
C++
C++中的封装、继承与多态:深入理解与应用
C++中的封装、继承与多态:深入理解与应用
171 1
|
2月前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
88 1
|
4月前
|
存储 编译器 C++
|
5月前
|
存储 编译器 C++
【C++】深度解剖多态(下)
【C++】深度解剖多态(下)
57 1
【C++】深度解剖多态(下)
|
5月前
|
存储 编译器 C++
|
5月前
|
机器学习/深度学习 算法 C++
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
|
5月前
|
Java 编译器 C++
【C++】深度解剖多态(上)
【C++】深度解剖多态(上)
58 2
|
5月前
|
程序员 C++
【C++】揭开C++多态的神秘面纱
【C++】揭开C++多态的神秘面纱