继承

语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass{
int m_nNum;
public:
void fun(){
cout << "MyClass::fun\n";
}
}
//MyClass2以公有方式继承MyClass
//MyClass就是MyClass2的父类,MyClass2就是MyClass的子类
//继承方式可以是:
//1.public
//2.protected
//3.private
class MyClass2 : public MyClass{};

继承后的影响

  1. 子类成员数量的变化

    子类自动拥有父类的全部成员,通过子类对象也能访问到父类定义的成员函数和成员变量

    • 父类中所有私有成员在子类内部和子类外部都无法直接访问,一般只能通过父类提供的公有接口
    • 父类中的保护成员和公有成员都能在子类中进行访问
    • 父类中公有成员都能在子类内部和外部进行访问
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class MyClass{
    int m_nNum = 0x11111111;
    public:
    void fun(){
    this->m_nNum = 10;
    cout << "MyClass::fun\n";
    }
    };
    class MyClass2 : public MyClass{
    int m_nNum2 = 0x22222222
    public:
    };
    int main()
    {
    MyClass2 obj;
    //在MyClass2中并没有直接定义fun函数,但是这个函数在父类中定义了
    //通过继承,MyClass2自动拥有该函数,通过子类对象就能调用
    obj.fun();

    //子类也自动继承了所有的成员变量,但是受到访问控制的影响,
    //一些成员变量并不能直接访问到
    obj.m_nNum=0;
    }
  1. 内存布局

    • 子类成员,父类成员在内存中的顺序
    • 继承后,成员变量在内存中的顺序是:1. 父类成员 2. 子类自身的成员
  2. 继承方式的影响

    继承方式的不同,决定了那些从父类中继承下来的成员在子类中访问方式的不同

    • 公有继承:父类成员的访问方式在子类中不变
    • 保护继承:父类中的所有公有成员在子类中变成了保护成员,其它成员保持不变
    • 私有继承:父类中所有保护成员和公有成员在父类中变成私有成员
  3. 构造和析构的调用顺序

    • 构造:先父类后子类
    • 析构:先子类后父类
    • 父类没有默认构造函数的解决方法:在子类构造函数的初始化列表中显示调用父类的其它版本的构造函数(直接用类名调用构造)
  4. 成员重名冲突

    1. 成员变量重名冲突

      • 即使重名了,内存空间依然独立存在
      • 根据就近原则(最近的作用域),在子类的作用域使用的就是子类的成员变量,在父类的作用域使用的就是父类的成员变量
    2. 成员函数重名冲突

      使用时,也会根据就近原则来选择调用不同的成员函数

      • 通过子类对象调用的同名函数,调用的就是子类的版本
      • 通过父类指针指向子类对象,父类指针调用的同名函数就是父类的版本(父类指针指向子类对象,若调用虚函数则是子类的虚函数,注意区别)
    3. 若想在子类作用域中使用父类的同名成员,就需加上作用域描述:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      class MyClass{
      public:
      int m_nNum = 0x11111111;
      MyClass(int n) : m_nNum(n){
      cout << "MyClass\n";
      }
      void fun() {
      this->m_nNum = 10;
      cout << "MyClass::fun\n";
      }
      }
      class MyClass2 : public MyClass {
      int m_nNum = 0x22222222;
      public:
      void fun( ){cout<<"Myclass2\n";}
      MyClass2() : MyClass( 0 ) {
      cout << "MyClass2\n";
      m_nNum = 100;
      MyClass::m_nNum = 100;// 访问到就是父类成员
      fun(); // 默认调用的是子类版本
      MyClass::fun();// 通过作用域指定调用父类的同名函数.
      }
      }

多继承

语法

在继承时可以同时继承多个类

1
2
3
4
5
class Myclass1{};
class Myclass2{};
//同时继承多个类
class MyClass4 : public MyClass1 ,public MyClass2
{};

影响

  1. 成员:子类拥有所有父类的成员函数和成员变量

  2. 内存布局:

    在内存中是先父类再子类,有多个父类时,按照继承顺序来排序

    多继承时,由于有多个父类,且父类成员在内存中的顺序是依次排列的,因此通过父类指针保存子类对象的首地址时,指针保存的并非子类对象的首地址,而是该父类成员在子类对象内存中的首地址

  3. 成员同名

    • 函数重名(会造成二义性的编译错误):通过作用域选择就能调用确切的函数
    • 成员变量重名:通过虚继承解决菱形继承带来的祖父类成员重名问题
  4. 虚继承的影响

    虚继承后,祖父类成员在孙子类中只有一份,但这样一来,使用父类指针指向子类对象时,就无法正确在子类对象的内存布局中找到父类/祖父类成员

    • 非虚继承下菱形继承的内存布局:

      1570627805832

    • 虚继承下菱形继承的内存布局:

      1570628700588

    虚继承后,子类对象的内存布局中依次是以下内容:

    • 所有父类成员(不包含父类的父类):虚基表指针,本父类成员
    • 子类自身成员
    • 祖父类成员

    虚基表保存的是两个偏移量,第1个偏移量是父类成员变量在子类对象的内存布局中的偏移(基于父类在子类对象中的首地址的偏移),第2个偏移是祖父类成员变量在子类对象的内存布局中的偏移(基于父类在子类对象中的首地址的偏移)

继承应用

继承的最大作用:代码重用

什么时候用继承:当两个类符合一定的逻辑时才用,当xxx类是yyy类的其中一种的时候,就用继承

什么时候用组合:当xxx类是yyy类的其中一部分的时候

  1. 扩展旧类的功能

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    class mystring : public string{
    public:
    mystring() :string() {}
    mystring(const char* pStr) :string(pStr) {}
    //变参函数(可变长参数)
    //... : 表示省略的参数
    //可以接收无限个参数
    int sprintf(const char* pszformat, ...){
    va_list pArgs = nullptr;
    //根据被省略参数的前一个参数来找到被省略参数在内存中的首地址
    va_start(pArgs, pszformat);
    char buff[1000];
    //根据格式化控制字符串和参数列表(pArgs)来自动找到那些被...省略的参数
    //自动根据格式化控制字符pszformat来输出到buff中
    int count = vsprintf_s(buff, pszformat, pArgs);
    va_end(pArgs);
    // 对string对象赋值: strObj = buff
    *this = buff;
    return count;
    }
    };
    int main()
    {
    mystring strObj;
    strObj = "23456";
    strObj += "456";
    cout << strObj<<endl;
    strObj.sprintf("参数内容: %d %lf %s",
    0x123,
    6.13,
    "456");
    cout << strObj<<endl;
    }
  1. 代码重构

    当程序中出现一些功能重合(性质相同)的代码的时候,就可以使用继承来重构代码

    • 将多个类中那些功能重合(性质相同)的代码提取到一个类中,作为基类
    • 哪些类需要用到这些功能(或者哪些类有这样的性质)就可以继承这个基类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class GameObject {
    int m_x;
    int m_y;
    int m_dir;
    public:
    int move(int dir) {
    }
    };
    class Tank : public GameObject{
    };
    class Bullset : public GameObject{
    public:
    // 子类可以重定义父类的功能
    int move(int dir) {
    }
    };
  1. 组合多个类的功能形成新功能

    1
    2
    3
    4
    5
    6
    7
    class Date {
    int m_nYear, m_nMon, m_nDay;
    };
    class Time {
    int m_nHour, m_nMin, m_nSec;
    };
    class Datetime : public Date, public Time {};

多态

  1. 如何在一个数组中保存不同类型的对象

    • 数组的元素必须是指针
    • 所有对象都必须拥有一个相同的父类
    • 数组的类型使用父类类型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Base {};
    class Myclass1 : public Base {};
    class Myclass2 : public Base {};
    class Myclass3 : public Base {};
    int main()
    {
    Base* pArr[ 3 ] = {
    new Myclass1,
    new Myclass2,
    new Myclass3
    };
    std::cout << "Hello World!\n";
    }
  2. 多态原理:

    1. 通过父类指针指向子类对象的时候,调用的不是虚函数时,一般函数的地址根据就近原则来定死的,若调用的是一个虚函数,那么将会使用动态联编的方式来调用虚函数
      • 先从对象的内存中取出前4个字节作为虚函数表首地址
      • 再根据虚函数在类中的定义顺序作为下标在虚函数表中得到这个虚函数的地址
      • 根据虚函数的地址调用函数
    2. 动态联编机制就是通过父类指针指向子类对象,然后调用虚函数时,就能够调用子类的虚函数