zoukankan      html  css  js  c++  java
  • 【C++ 系列笔记】03 C++ 面向对象进阶

    C++ 面向对象进阶

    继承 - 基础

    class Base;
    class Type :public Base{
       public:
        Type(a, b, c):Base(a, b){
    		// ...
        }
    };
    
    • 继承方式( 派生类均不可访问基类私有成员

      • public(父类访问权限不变)

        最常用的方式

      • private(父类访问权限全变成私有)

        当不希望本类对象访问基类任何成员时,可以考虑使用 private 继承。

      • protected(父类访问权限全变成保护)

        使用 private 继承存在一个严重的问题:

        当该派生类进一步派生时,该子类将完全无法访问其父类成员。

        故一般使用 protected 进行派生,保证该派生类的可派生特性。

    继承中的对象模型

    • 类结构

      class Base{
         private:
          int __private;
         public:
          int __public;
         protected:
          int __protected;
      };
      class Type :public Base{
         public:
          int __sonPublic;
      };
      
    • 内存结构

      class Base	size(12):
      	+---
       0	| __private
       4	| __public
       8	| __protected
      	+---
      
      class Type	size(16):
      	+---
          |
       0	| +--- (base class Base)
       0	| | __private
       4	| | __public
       8	| | __protected
      	| +---
          |
       12	| __sonPublic
      	+---
      

    继承中的构造和析构

    • 调用父类构造

      class Base{
         private:
          int member;
         public:
      	Base(m): member(m){
              // ...
          }
      };
      class Type :public Base{
         private:
          int sonMember;
         public:
          Type(m, m1):Base(m), sonMember(m1){
      		// ...
          }
      };
      

      不显式地调用父类构造时,会隐式调用默认的无参构造。

    • 调用顺序

      • 父类构造(多继承按照顺序构造)
      • 本类构造
      • 本类析构
      • 父类析构(多继承按照反序析构)

    继承中的同名处理

    • 同名属性

      class Base{
         public:
          int member;
      	Base(m): member(m){}
      };
      class Type :public Base{
         public:
          int member;
          Type(m, m1):Base(m), member(m1){}
      };
      int main(){
      	Type obj(100, 200);
          cout << obj.member << endl;
          // > 200
          
          cout << obj.Type::member << endl;
          // > 200
          cout << obj.Base::member << endl;
          // > 100
      }
      

      当直接引用该同名成员属性时,会隐式调用本类的成员属性。

      也可以通过作用域运算符显式地指定成员属性。

    • 同名方法

      class Base{
         public:
          void fun();
      };
      class Type :public Base{
         public:
          // 子类重载
          void fun(int a);
      };
      int main(){
      	Type obj;
      	obj.fun();
          // 报错
          
          obj.fun(1);
          // 调用正常
      }
      

      子类重载了父类的方法,父类的方法将被隐藏,无法通过重载调用。

      想调用父类方法,可以通过作用域运算符显式指定。

    不会继承的函数

    • 构造和额析构函数
    • 等号操作符重载函数

    继承 - 进阶

    多继承

    class Base1 {
    	// ...
    };
    class Base2 {
    	// ...
    };
    
    class Type: public Base1, public Base2 {
        // ...
    };
    
    • 多继承的二义性

      通过作用域访问不同父类的成员。

      class Base1 {
         public:
          int member;
      };
      class Base2 {
         public:
          int member;
      };
      
      class Type: public Base1, public Base2 {
      	// ...
      };
      
      int main(){
          Type obj;
          obj.member;
          // 报错,存在多个父类的 member
          
          // 通过作用域访问
          obj.Base1::member;
          obj.Base2::member;
      }
      

    虚继承

    • 引出 - 菱形继承

      Grandson 的实例中存在两份基类实例的数据。

      class Base {
         public:
          int member;
      };
      
      class Son1: public Base {
      	// ...
      };
      class Son2: public Base {
      	// ...
      };
      
      class Grandson: public Son1, public Son2 {
      	// ...
      };
      
      int main(){
      	
      }
      

      内存结构

      这两份基类数据均可由不同的作用域访问到,且是相互独立的。

      class Grandson	size(8):
      	+---
          |
       0	| +--- (base class Son1) // 父类
          | |   
       0	| | +--- (base class Base) // 基类
       0	| | | member // 两份基类实例数据
      	| | +---
          | |   
      	| +---
          |
       4	| +--- (base class Son2) // 父类
          | | 
       4	| | +--- (base class Base) // 基类
       4	| | | member // 两份基类实例数据
      	| | +---
          | |   
      	| +---
          |  
      	+---
      

      为了解决这个问题,使用虚继承

    • 虚继承

      当一个类对父类的继承被声明为虚拟的(virtual),它就成为了一个 虚基类

      这里的 虚基类 是指该派生类继承自的基类是虚拟的,而不是说该派生类类是基类。

      class Base {
       public:
        int member;
      };
      
      // 虚基类
      class Son1 : virtual public Base {
        // ...
      };
      // 虚基类
      class Son2 : virtual public Base {
        // ...
      };
      
      class Grandson : public Son1, public Son2 {
        // ...
      };
      

      内存结构

      class Grandson	size(12):
      	+---
          |
       0	| +--- (base class Son1) // 父类
       0	| | {vbptr} // 虚基指针
      	| +---
          |
       4	| +--- (base class Son2) // 父类
       4	| | {vbptr} // 虚基指针
      	| +---
          |
      	+---
      
      	+--- (virtual base Base) // 基类
       8	| member
      	+---
      
      // 此处的内存空间并不接壤
      
      // Son1 的虚基表
      Grandson::$vbtable@Son1@:
       0	| 0
       1	| 8 (Grandsond(Son1+0)Base) // 偏移量
      
      // Son2 的虚基表
      Grandson::$vbtable@Son2@:
       0	| 0
       1	| 4 (Grandsond(Son2+0)Base) // 偏移量
      vbi:	   class  offset o.vbptr  o.vbte fVtorDisp
                Base       8       0       4 0
      

      可以看到,原本存放基类实例的内存空间被一个 {vbptr} 占用了。

      vbptr(virtual base pointer):虚拟基类指针

      该指针指向一个 vbtable

      vbtable(virtual base table):虚拟基类表,结构是一个数组

      这个表中记录着 该虚基指针所在派生类实例与基类实例的偏移量

      注意!虚基表的内存空间与类实例并不接壤。

      尝试取到该虚基表:

      Grandson obj;
      
      // Son1 的虚基表数组
      cout << ((int*)*((int*)&obj)) << endl;
      // Son2 的虚基表数组
      cout << ((int*)*((int*)&obj + 1)) << endl;
      

    多态 - 基础

    静态多态和动态多态

    • 静态多态

      编译时多态,静态联编,包括函数重载在内的多态。

    • 动态多态

      运行时多态,动态联编,通过虚函数实现的多态。

    静态和动态多态的本质区别就是 静态联编动态联编

    分别指在编译时绑定函数入口和在运行时寻找函数入口。

    动态多态的本质:父类的引用或指针指向了子类对象。从而发生动态多态。

    静态联编的例子:

    class Person {
       public:
        int member;
        void speak() {
    		cout << "I'm a person." << endl;
        }
    };
    
    class Programmer: public Person {
       public:
        int member;
        void speak() {
    		cout << "Life is shot, I'm a programmer." << endl;
        }
    };
    
    void doSpeak(Person& person){
        person.speak();
    }
    int main(){
        Programmer programmer;
        doSpeak(programmer);
        // > "I'm a person."
    }
    

    I’m a person.

    doSpeak 函数的实现在编译期就已经确定了需要调用的方法。

    要实现动态多态,只需将基类的方法声明为虚拟的。

    virtual void speak() {
        cout << "I'm a person." << endl;
    }
    

    Life is shot, I’m a programmer.

    动态多态原理解析

    重复一遍

    动态多态的本质:父类的引用或指针指向了子类对象。从而发生动态多态。

    上例静态联编的例子,其内存结构如下:

    class Programmer	size(8):
    	+---
        |  
     0	| +--- (base class Person)
     0	| | member
    	| +---
        |  
     4	| member
    	+---
    

    现在将基类的 speak 方法声明为虚拟的:

    class Person {
       public:
        int member;
        // 声明为虚拟的
        virtual void speak() {
    		cout << "I'm a person." << endl;
        }
    };
    
    class Programmer: public Person {
       public:
        int member;
        void speak() {
    		cout << "Life is shot, I'm a programmer." << endl;
        }
    };
    
    void doSpeak(Person& person){
        person.speak();
    }
    int main(){
        Programmer programmer;
        doSpeak(programmer);
        // > "Life is shot, I'm a programmer."
    }
    

    内存结构

    • 父类

      class Person	size(8):
      	+---
       0	| {vfptr} // 虚函数表指针
       4	| member
      	+---
      
      // 此处内存空间并不接壤
      
      Person::$vftable@: // 虚函数表
      	| &Person_meta
      	|  0
      

    0 | &Person::speak

    Person::speak this adjustor: 0

    
    可以发现,基类实例多了一个 {vfptr}
    
    **vfptr**(virtual function pointer):虚拟函数表指针
    
    
    
    虚函数表指针指向一张虚函数表
    
    **vftable**(virtual function table):虚拟函数表,结构是一个数组
    
    - 子类
    
    ```cpp
    class Programmer	size(12):
    	+---
        |  
     0	| +--- (base class Person)
     0	| | {vfptr} // 虚函数表指针
     4	| | member
    	| +---
        |  
     8	| member
    	+---
    
    // 此处内存空间并不接壤
    
    Programmer::$vftable@: // 虚函数表
    	| &Programmer_meta
    	|  0
     0	| &Programmer::speak
    
    Programmer::speak this adjustor: 0
    

    子类继承后会创建一个新表,该表内会指向基类的虚函数。

    若子类重新实现了某些虚函数 ,该表中被重新实现的函数将被覆盖,指向新函数的入口。

    这种 重新实现 被称为 重写。被重写的函数的参数列表和返回值类型需要对应完全相同。

    尝试取到该函数并调用:

    Programmer programmer;
    
    ((void (*)())*(int*)*(int*)&programmer)();
    // > Life is shot, I'm a programmer.
    

    开闭原则

    即对扩展开放,对修改关闭。

    修改需求并不应去修改实现,而应对原有类进行派生,重写方法,最后通过多态实现需求。

    方便维护方便扩展。

    由此引出抽象类。

    多态 - 进阶

    纯虚函数和抽象类

    纯虚函数就是没有定义的函数,仅作为一个接口,为子类重写占位,实现多态。

    class Type (){
       public:
        // 纯虚函数
        virtual voie fun() = 0;
    };
    

    virtual voie fun() = 0;告诉编译器在 vftable (虚函数表)中保留一个位置,但不放地址。

    当一个类中含有纯虚函数时,这个类就被称为 抽象类,无法进行实例化。

    且其派生类必须实现该纯虚函数。

    虚析构和纯虚析构

    • 虚析构

      当父类指针指向子类实例时,该子类实例析构会调用父类析构。

      如果子类需要在堆区开辟空间,那么析构时就会造成内存泄漏。

      class Base () {
         public:
          ~Base(){};
      };
      class Son (): public Base {
         public:
          char* mName;
          Son(const char* name){
              // 构造时在堆区托管了额数据
              this->mName = new char[strlen(name)];
              strcpy(this->mName, name);
          }
          ~Son(){
              // 析构时释放堆区数据
              delete[] this->mName;
          };
      };
      

      当 Son 类实例析构时,会调用其父类 Base 的析构函数,导致mName没有正确释放,从而导致内存泄漏。

      为了解决这个问题,我们就要使用 虚析构,让子类重写析构。

      virtual ~Base(){};
      

      注意!父类虚构是一定会调用的,尽管将父类析构声明为虚拟的,父类也需要做收尾工作。

    • 纯虚析构

      与纯虚函数类似,当一个类中存在纯虚析构时,该类也是一个抽象类,无法实例化。

      但有一点稍稍不同,首先,作为抽象类需要派生才能使用,但任意一个派生类对象在释放时一定会调用父类的构造。

      所以 纯虚析构要求有定义,在类内声明,在类外定义。

      class Base () {
         public:
         virtual ~Base() = 0;
          // 纯虚析构的声明
      };
      Base::~Base(){
          // 纯虚析构的定义
      }
      class Son (): public Base {
         public:
          char* mName;
          Son(const char* name){
              // 构造时在堆区托管了额数据
              this->mName = new char[strlen(name)];
              strcpy(this->mName, name);
          }
          ~Son(){
              // 析构时释放堆区数据
              delete[] this->mName;
          };
      };
      

    类型转换

    • 未发生多态

      向下类型转换即子类指针指向父类实例,会导致指针寻址范围较大,不安全。

      向上类型转换是安全的。

    • 发生了多态

      多态是父类的指针或引用指向了子类实例,此时一定是安全的。

    静态成员方法实现多态

    只有非静态成员方法可以被声明为虚拟的,那么静态成员看起来就无法实现多态,但实际上还是有方法的,这里提供一个思路:

    通过虚函数包装静态成员方法

    代码实现:

    #include<iostream>
    using namespace std;
    class Person {
     public:
      int member;
      static void __speak() { cout << "I'm a person." << endl; }
      // 虚函数包装
      virtual void speak() { __speak(); }
    };
    
    class Programmer : public Person {
     public:
      int member;
      static void __speak() { cout << "Life is shot, I'm a programmer." << endl; }
      // 虚函数包装
      virtual void speak() { __speak(); }
    };
    
    
    void test(Person& glh) {
      glh.speak();
      // > "Life is shot, I'm a programmer."
    }
    int main() {
      Programmer glh;
      test(glh);
    
      system("pause");
      return 0;
    }
    
  • 相关阅读:
    Structured Streaming watermark总结
    技术实践第三期|HashTag在Redis集群环境下的使用
    元宇宙带来沉浸式智能登录?你学会了吗?
    技术实践第一期|友盟+开发者平台Node.js重构之路
    2021年度友盟+ APP消息推送白皮书:工作日68点通勤时间消息送达率每日最高
    技术实践第二期|Flutter异常捕获
    注意啦!还没有支持64位系统的App开发者,务必在12月底前完成这件事!
    一位大牛对于写技术博客的一些建议
    国内常用源开发环境换源(flutter换源,python换源,Linux换源,npm换源)
    初识Socket通讯编程(一)
  • 原文地址:https://www.cnblogs.com/gaolihai/p/13149746.html
Copyright © 2011-2022 走看看