chapter13_使用继承与动态绑定

  1. 继承

    1. 示例

       class Grad : public Core {
           ...
       }
      
    2. 继承方式

      继承方式/基类成员 public成员 protected成员 private成员
      public继承 public protected 不可见
      protected继承 protected protected 不可见
      private继承 private private 不可见

      由于 protected 和 private 继承会改变基类成员在派生类中的访问权限,导致继承关系复杂,所以实际开发中我们一般使用 public

    3. 除了“三位一体”(构造函数、复制构造函数、析构函数、赋值运算符函数)以外,基类的每个成员也是派生类的成员

    4. 派生类中调用基类中的被覆盖方法

       class Derived: public Base {
       public:
           void haha() {
               Base::haha();
               ...
           }
           ...
       };
      
    5. 构造派生类对象的步骤

      (1) 为整个对象分配内存空间(包括基类的数据和派生类的数据)

      (2) 调用基类的构造函数,以初始化对象中的基类部分数据

      (3) 用构造函数初始化器,对派生类部分数据进行初始化

      (4) 如果有的话,执行派生类构造函数的函数体

      注:如果没有指定使用哪一个基类构造函数,就使用基类的默认构造函数

  2. 多态和虚函数

    1. virtual关键字

      (1) 只在类的声明中使用

           class Core {
           public:
               virtual double grade() const;
           }
      

      (2) 如果基类中一个函数是虚函数,那么派生类中它的virtual特性会被继承

    2. 静态绑定

       bool compare_grades(Core c1, Core c2) {
           ...
           return c1.grade() < c2.grade();
       }
      

      当函数为值类型时,在编译期就会确定对象类型,因此无论传递进来的是Core类型,还是派生的Grad类型,都会只复制Core的部分进入函数,从而调用Core::grade()函数

    3. 动态绑定

      (1) 当函数为引用或者指针类型时,调用一个虚函数,这个时候会根据对象的实际类型,决定调用哪一个函数

      (2) 动态绑定的原理是,对于virtual修饰的函数,会对对象生成一个虚函数表,表中各个函数的偏移量是确定的,虚函数表中对应着实际的函数地址;在运行时,如果使用地址来调用,可以根据偏移量决定调用哪个函数

      (3) 要注意的是,使用 指针(引用) + 虚函数 是多态的必要条件,缺一不可!!

      使用指针/引用才能根据地址绑定,否则编译期就会确定类型;

      使用virtual修饰才能生成虚函数表,否则没有虚函数表的概念。

      (4) 对于非虚函数,只要程序中没有调用它,可以只声明不定义;但是virtual修饰的函数,必须要又声明又定义才能编译通过,除非声明为纯虚函数

    4. 虚拟析构函数

      (1) 调用 delete 一个指针的时候,干了2件事

       1° 调用指针所指对象的析构函数
      
       2° 释放对象占用的内存空间
      

      (2) 当出现基类指针指向派生类对象的情况时,为了调用派生类对象的析构函数,也要把析构函数定义为virtual。由于virtual特性会被继承,因此派生类的析构函数也是virtual

       class Core {
       public:
           virtual ~Core() {}
           ...
       }
      
  3. 句柄类

    1. 一个要分配空间的类,在外部使用时要考虑delete很累,解决的办法就是用一个类在外面套一层,内部包着一个private数据成员,这个数据成员的类型是基类指针,这样可以实现多态

    2. 示例

       class Core {
       public:
           std::string name() const;
      
           [[nodiscard]] virtual double grade() const {
               ...
           }
           ...
       };
      
       class Grad : public Core {
       public:
           [[nodiscard]] double grade() const override {
               ...
           }
           ...
       };
      

      这两个父子类是要用到的,并且 grade() 是虚函数,经常要new,用到它们的多态特性,忘记delete很麻烦;

      这时就可以使用句柄类的编程技巧,在外面套一层

       class Student_info {
       public:
      
           // "三位一体"的几个函数,首先都把内部的Core*指针初始化为null
           Student_info() : cp(nullptr) {}
           explicit Student_info(std::istream &is) : cp(nullptr) { read(is); }
           Student_info(const Student_info &student_Info);
           Student_info &operator=(const Student_info &);
      
           // 析构函数,只需释放cp指向的空间,根据cp的类型决定使用哪个析构函数
           ~Student_info() { delete cp; }
      
           std::istream &read(std::istream &is);
      
           [[nodiscard]] std::string name() const {
               if (cp != nullptr) {
                   return cp->name();
               } else {
                   throw std::runtime_error("uninitialized student");
               }
           }
      
           // grade()函数本质上调用了cp->grade(),实现了多态
           double grade() const {
               if (cp != nullptr) {
                   return cp->grade();
               } else {
                   throw std::runtime_error("uninitialized student");
               }
           }
      
           // 对于 Core 来说,compare函数没有改变对象状态,应该实现为全局函数而非成员函数;而Student_Info句柄类在外面包了一层,所以实现为static函数比较合适,作为谓词使用时,传递Student_Info::compare即可
           static bool compare(const Student_info &s1, const Student_info &s2) {
               return s1.name() < s2.name();
           }
      
       private:
           Core *cp;   // 运行时决定指向哪种类型
       };
      
    3. "三位一体"的几个函数,首先都把内部的Core*指针初始化为null

    4. 析构函数,只需释放cp指向的空间,根据cp的类型决定使用哪个析构函数,这样用户无需手动管理内存

    5. grade()函数本质上调用了cp->grade(),实现了多态

    6. 对于 Core 来说,compare函数没有改变对象状态,应该实现为全局函数而非成员函数;而Student_Info句柄类在外面包了一层,所以实现为static函数比较合适,作为谓词使用时,传递Student_Info::compare即可

    7. read函数决定了cp指向的对象类型

       std::istream &read(std::istream &is) {
      
           if (cp!= nullptr) {
               delete cp;
           }
      
           char ch;
           is >> ch;
      
           if (ch == 'U') {
               cp = new Core(is);
           } else {
               cp = new Grad(is);
           }
      
           return is;
       }
      
    8. Student_Info 类的复制构造函数和赋值运算符函数,由于不知道 cp指针的类型,所以要委托给Core和Grad的虚函数处理实现多态,方法就是Core类和Grad类各加一个clone的虚函数

    9. 有了 Student_Info 句柄类,使用的时候大大简化

      原来要使用 vector<Core*> 结构保存基类指针的集合,并且要手动在退出前 delete;现在使用 vector<Student_Info>,局部变量退出时,自动调用Student_Info的析构函数

  4. 继承与容器

     vector<Core> students;
     Grad grad = Grad(cin);
     students.push_back(grad);
    

    这样做不会报错,但是像students容器放置时,只会复制grad对象的Core数据部分,Grad的特有部分会被丢弃

  5. 覆盖

    基类和派生类的两个函数,必须同名、参数相同、都是或都不是const,才会发生

  6. 构造函数不能声明为virtual,因为在用到构造函数的时候,对象类型一定被确定了

发布了391 篇原创文章 · 获赞 7 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/captxb/article/details/103093000