【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;
}
posted @ 2020-06-06 18:43  高厉害  阅读(100)  评论(0编辑  收藏  举报