Skip to main content

封装-继承-多态

·799 words·4 mins· loading
Raven005
Author
Raven005
A little bit about you
Table of Contents

继承
#

C++面向对象的三大特性:分装、继承、多台。分装就是类的管理

介绍
#

C++非继承的类相互是没有关系的,假设现在需要设计医生、教师、公务员三个类,需要定义很多重复的内容而且相互没有关联,调用也没有规律,如果这还算好,那一个游戏有几千件物品,调用时也要写几千个函数,于是继承能力就应运而生了

原理
#

C++继承原理: C++的继承可以理解为在创建子类成员把变量之前先创建父类的成员变量,实际上,C 语言就是这么模仿出来的

#include <iostream>
#include <string>

class Spear {
protected:
  std::string name;
  std::string icon;

public:
  Spear(const std::string &name_, const std::string &icon_)
      : name(name_), icon(icon_) {
    std::cout << "Spear()" << std::endl;
  }
  ~Spear() {
    std::cout << "Delete Spear" << std::endl;
  }
};

class FireSpear : public Spear {
private:
  int i;


public:
  FireSpear(const std::string &name_, const std::string &icon_, int i_)
      : Spear(name_, icon_), i(i_) { // NOTE: 先调用父类的构造函数,初始化父类的部分,然后调用自己的构造函数,初始化自己的部分
    std::cout << "FireSpear()" << std::endl;
  }
  void outPut() { std::cout << name << std::endl; }
  ~FireSpear() {
    std::cout << "Delete FireSpear" << std::endl;
  }
};

int main() {

  FireSpear firespear("fire", "iron", 10);
  // Spear *pFather = new Spear("father" , "10"); // NOTE:可以创建父类对象指针指向自己
  // Spear *pSon = new FireSpear("son" , "10" , 10); // NOTE: 也可以创建父类对象指针指向子类对象

  return 0;
}

注意事项:

  • C++子类对象的构造过程中,先调用父类的构造函数,再调用子类的构造函数,也就是说先初始化父类成员再初始化子类成员
  • 若父类没有默认构造函数,子类的构造函数又未调用父类的构造函数,则无法编译
  • C++子类对象的析构过程是先调用父类的析构函数又调用子类的析构函数

总结:

  • 面向对象的三大特性的继承就这么简单,很多人觉得类继承很复杂,其实完全不是,这样的,只要明白子类在内存上其实就相当于把父类的成员变量放在子类的成员变量前面罢了,构造和析构过程也是为了这个机制而设计的

虚函数及其实现原理,override关键字
#

  1. 虚函数就是面向对象的第三大特点 多态 ,多态非常重要,它完美解决了上面游戏装备设计的难题,我们可以只设计一个函数,函数参数是基类指针,就可以调用子类的功能。比如射击游戏,所有的枪都继承自一个枪的基类,人类只要有一个开枪的函数就可以实现所有枪打出不同的子弹
  2. 父类指针可以指向子类对象,这个是自然而然的,因为子类对象的内存前面就是父类成员,类型完全匹配
  3. 当父类指针指向子类对象,且子类重写父类的某一函数时,父类指针调用该函数,就会产生一下可能
    • 该函数为虚函数父类指针调用的是子类的成员函数
    • 该函数不是虚函数父类指针调用的是父类的成员函数
#include <iostream>
#include <string>

class Spear {
protected:
  std::string name;
  std::string icon;

public:
  Spear(const std::string &name_, const std::string &icon_)
      : name(name_), icon(icon_) {
    std::cout << "Spear()" << std::endl;
  }

  virtual void openFire() const { std::cout << "Spear OpenFire!" << std::endl; }

  virtual ~Spear() { std::cout << "Delete Spear" << std::endl; } // NOTE: 父类的析构函数必须为虚函数,才能调用子类的析构函数
};

class FireSpear : public Spear {
private:
  int i;

public:
  FireSpear(const std::string &name_, const std::string &icon_, int i_)
      : Spear(name_, icon_), i(i_) {
    std::cout << "FireSpear()" << std::endl;
  }
  void outPut() { std::cout << name << std::endl; }

  virtual void openFire() const override { // NOTE: 防止开发人员将函数名写错,所以加上override关键字以防万一
    // NOTE: 如果将子类的virtual关键字删掉,
    // NOTE: 父类指针依旧会调用该函数,因为父类的该函数有virtual关键字,在编译时子类也会自动加上virtual关键字
    std::cout << "FireSpear OpenFire!" << std::endl;
  }

  ~FireSpear() { std::cout << "Delete FireSpear" << std::endl; }
};

void openFire(const Spear *pFather) {
  pFather->openFire();
  delete pFather;
}

int main() {

  openFire(new FireSpear("Fire" , "flame" , 10));
  // NOTE: 加了virtual关键字后,多态,父类的指针可以调用子类的函数
  // NOTE:  不加virtual关键字时,静态绑定,在编译时就能确定函数的地址


  return 0;
}

注意事项

  • 子父类的虚函数必须 完全相同 ,为了防止开发人员一不小心将函数写错,于是有了 override 关键字

  • 父类的析构函数必须为虚函数,这一点很重要,当父类对象指向子类对象时,容易使独属于子类的内存泄露,会造成内存泄漏的严重问题

  • override关键字的作用

    1. 它向代码的读者表明“这是一个虚拟方法,它重写了基类的虚拟方法”。
    2. 编译器还知道它是重写,因此它可以“检查”您是否没有更改/添加您认为是重写的新方法。
  • 虚函数实现原理的介绍 详细介绍

    1. 静态绑定和动态绑定

      • 静态绑定: 程序在编译时就已经确定了函数的地址,比如非虚函数就是静态绑定

      • 动态绑定: 程序在编译时确定的是程序寻找函数地址的方法,只有程序运行时才可以真正确定程序的地址,比如虚函数就是动态绑定

    2. 虚函数是如何实现动态绑定?

      • 每个有虚函数的类都会有一个虚函数表,对象其实就是指向虚函数的指针,编译时编译器只告诉了程序在运行时查找虚函数表的对应函数。每个类都会有自己的虚函数,所以当父类指针引用的是子类虚函数表时,自然调用的就是子类的函数

纯虚函数
#

  1. 之前枪械射击的例子,基础的枪类没有对应的对象,它唯一的作用就是被子类继承
  2. 基类的 openfire 函数实现过程有意义吗?没有,它就是用来被重写的
  3. 所以纯虚函数的语法就诞生了,只要将一个虚函数写为纯虚函数,那么该类将被认为无实际意义的类,无法产生对象,纯虚函数也不用去写实际部分,写了编译器也会自动忽略
#include <iostream>
#include <string>

class Spear { // WARNING: 构造函数和析构函数不能省略,构造子类的时候要用的
protected:
  std::string name;
  std::string icon;

public:
  Spear(const std::string &name_, const std::string &icon_)
      : name(name_), icon(icon_) {
    std::cout << "Spear()" << std::endl;
  }

  // NOTE: 可以把这个看作是java中的接口
  virtual void openFire() const = 0; // NOTE: 只需要在虚函数后面赋值=0,这个函数就是纯虚函数,不需要函数实现过程,它就是个虚基类,虚基类无法产生对象

  virtual ~Spear() { std::cout << "Delete Spear" << std::endl; }
};

class FireSpear : public Spear {
private:
  int i;

public:
  FireSpear(const std::string &name_, const std::string &icon_, int i_)
      : Spear(name_, icon_), i(i_) {
    std::cout << "FireSpear()" << std::endl;
  }
  void outPut() { std::cout << name << std::endl; }

  virtual void openFire() const override {
    std::cout << "FireSpear OpenFire!" << std::endl;
  }

  ~FireSpear() { std::cout << "Delete FireSpear" << std::endl; }
};

void openFire(const Spear *pFather) {
  pFather->openFire();
  delete pFather;
}

int main() {

  openFire(new FireSpear("Fire" , "flame" , 10));
  // Spear *pSear = new Spear("awdjk" , "awdk");
  // pSear->openFire(); // NOTE: 这种对象没有任何意义

  return 0;
}

静态成员变量与静态成员函数
#

静态成员变量
#

  • 在编译期就已经在静态变量区明确了地址,所以生命周期为程序开始到程序结束,作用范围与普通的成员变量相同。这些对于类的静态成员变量同样适用

  • 类的静态成员变量因为创建在静态变量区,所以直接属于类,也就是我们可以直接通过类名来调用,也可以通过对象调用

#include <iostream>
#include <string>

class Test {
private:
  static unsigned i;
public:
  static unsigned getI() {return i; } // NOTE: 为静态成员变量服务,保护封装性
};

unsigned Test::i = 20; // NOTE: 一定要在类外初始化

int main() {

  Test test;

  std::cout << test.getI() << std::endl; // NOTE: 通过对象来调用静态成员变量
  // std::cout << Test::i << std::endl; // NOTE: 通过类名来调用静态成员变量
  return 0;
}

注意事项:

  • 静态成员变量必须在类外进行初始化,否则会报未定义的错误,不能用构造函数进行初始化,因为静态成员变量在静态变量区,只有一份,而且静态成员变量在编译期就要被创建,成员函数那都是运行期的事情了

静态成员函数
#

  • 静态成员函数就是为静态成员变量设计的,就是为了维持封装性
#include <mutex>
#include <memory>
#include <iostream>

template <typename T> class SingleTon {
protected:
    explicit SingleTon()                      = default;
    SingleTon( const SingleTon & )            = delete;
    SingleTon &operator=( const SingleTon & ) = delete;
    static std::shared_ptr<T> single;

public:
    static std::shared_ptr<T> getInstance() {
        static std::once_flag flag;
        std::call_once( flag, [&]() {
            single = std::shared_ptr<T>( new T() );
            // 注意这里不要是使用std::make_shared,因为这要求被构造的对象的构造函数是public
        } );
        return single;
    }

    void PrintAddress() { std::cout << single.get() << std::endl; }

    ~SingleTon() {
        // std::cout << "This is SingleTon deletor" << std::endl;
    }
};

template <typename T> std::shared_ptr<T> SingleTon<T>::single = nullptr;

class Base : public SingleTon<Base> {
    friend class SingleTon<Base>;

public:
    ~Base() = default;
    void output() { std::cout << "This is Base::output()" << std::endl; }

private:
    Base() = default;
};

int main( int argc, char *argv[] ) {

    Base::getInstance()->output();

    return 0;
}

多继承
#

了解就行

  • 概念:就是一个类同时继承多个类,在内存上,该类对象前面依次为第一个继承的类,第二个继承的类,依次类推
#include <iostream>
#include <string>


// NOTE: 构造函数的运行过程是从第一个祖宗到儿子再到孙子,析构函数则是反过来
class Base1 {
public:
  Base1(int base1I_) : base1I(base1I_) { std::cout << "base1" << std::endl; }
  ~Base1() {
    std::cout << "Bye Base1" << std::endl;
  }
protected:
  int base1I;
};

class Base2 {
public:
  Base2(int base2I_) : base2I(base2I_) { std::cout << "base2" << std::endl; }
  ~Base2() {
    std::cout << "Bye Base2" << std::endl;
  }

protected:
  int base2I;
};

class HENCE : public Base1, public Base2 {

public:
  HENCE(int base1I_, int base2I_, int i_)
      : Base1(base1I_), Base2(base2I_), i(i_) {
    std::cout << "HENCE" << std::endl;
  }
  ~HENCE() {
    std::cout << "Bye HENCE" << std::endl;
  }

private:
  int i;
};

int main() {

  HENCE hence(10, 20, 30);
  return 0;
}

注意事项:

  1. 多继承最需要注意的点就是重复继承的问题
  2. 多继承会使整个程序设计更加复杂,平常不推荐使用

虚继承
#

虚继承就是为了避免多重继承时产生的二义性问题这个语法就是典型的语法简单,但在游戏开发领域经常使用的语法,其他领域使用频率会低一点

实现原理
#

具体介绍

  1. 使用了虚继承的类会有一个虚继承表,表中存放了父类所有成员变量相对于类的偏移地址

  2. 当`C`类同时继承`B1`和`B2`类时,每继承一个就会用虚继承表进行比对,发现该变量在虚继承表中偏移地址相同,就只会继承一份。