c++_day003.zip
[TOC]

继承

继承的语法

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

继承之后的影响

  1. 子类成员数量的变化

    1. 子类自动拥有父类的全部成员(通过子类对象,也能访问到父类中定义的成员函数和成员方法)

      1. 父类中所有私有成员在子类内部和子类的外部都无法直接访问(一般只能通过父类提供的公有接口)
      2. 父类中的保护成员和公有成员都能在子类中进行访问
      3. 父类中公有成员都能在子类内和子类外进行访问. ```c 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; } ```

  2. 内存布局

    1. 子类成员父类成员在内存中的顺序
    2. 继承之后,成员变量在内存中的顺序是: 父类的成员然后才到子类自身的成员.
  3. 继承方式的影响
    继承方式的不同, 决定了那些从父类中继承下来的成员在子类中访问方式.
    1. 公有继承 : 父类成员的访问方式在子类中不变
    2. 保护继承 : 父类中的所有公有成员在子类中变成了保护成员, 其它成员保持不变
    3. 私有继承 : 父类中所有保护成员和公有成员在父类中变成私有成员.
  4. 构造和析构的调用顺序
    1. 构造: 先父类后子类
    2. 析构: 先子类后父类
    3. 如果父类没有默认构造函数的解决方法: 在子类的构造函数的初始化列表中显示调用父类的其它版本的构造函数(直接用类名调用构造)
  5. 成员重名冲突
    1. 成员变量重名冲突.
      1. 即使重名了, 内存空间依然是独立存在的.
      2. 根据就近原则(就最近的作用域)在子类的作用域使用的就是子类的成员变量, 在父类的作用域就使用父类的成员变量.
    2. 成员函数的重名冲突
      1. 使用时, 也会根据就近原则来选择调用不同的成员函数.
        1. 通过子类对象调用的同名函数, 调用的就是子类的版本
        2. 通过父类指针指向了子类对象, 通过父类指针调用了同名函数, 调用到的是父类的版本.
    3. 如果想要在子类的作用域中使用父类的同名成员, 那么就需要加上作用域描述了:
      1. class MyClass {
      2. public:
      3. int m_nNum = 0x11111111;
      4. MyClass(int n) :m_nNum(n)
      5. {
      6. cout << "MyClass\n";
      7. }
      8. void fun() {
      9. this->m_nNum = 10;
      10. cout << "MyClass::fun\n";
      11. }
      12. };
      13. class MyClass2 : public MyClass {
      14. int m_nNum = 0x22222222;
      15. public:
      16. void fun( ){cout<<"Myclass2\n";}
      17. MyClass2() : MyClass( 0 ) {
      18. cout << "MyClass2\n";
      19. m_nNum = 100;
      20. MyClass::m_nNum = 100;// 访问到就是父类成员
      21. fun(); // 默认调用的是子类版本
      22. MyClass::fun();// 通过作用域指定调用父类的同名函数.
      23. }
      24. };

多继承

语法 : 在继承的时候可以同时继承多个类.

  1. class Myclass{};
  2. class Myclass1{};
  3. class Myclass2{};
  4. // 同时继承多个类
  5. class Myclass4 : public MyClass ,
  6. public MyClass1
  7. {
  8. };

影响

  1. 成员 : 子类拥有所有父类的成员函数和成员变量.
  2. 内存布局 :
    1. 在内存中是先父类后子类
      1. 有多个父类的时候, 是按照继承顺序来排列.

image.png
图片备注: 在多继承的时候, 由于有多个父类, 并且父类成员在内存中的顺序是依次排列的, 因此通过父类指针保存子类对象的首地址的时候, 指针保存并不是子类对象的首地址, 而是该父类成员在子类对象内存中的首地址.

  1. 成员同名
    1. 函数重名(会造成二义性的编译错误) : 通过作用域选择就能调用确切的函数了.
    2. 成员变量的重名 : 通过虚继承的方式解决菱形继承带来的祖父类成员重复问题
  2. 虚继承的影响
    1. 虚继承之后, 爷爷类成员在孙子类中只有一份了, 但这样一来, 使用父类指针指向子类对象的时候, 就无法正确在子类对象的内存布局中找到父类/爷爷类成员.

非虚继承下菱形继承的内存布局:
image.png
虚继承下菱形继承的内存布局:
image.png
image.png
总结:

  1. 虚继承之后, 在子类对象的内存布局中依次是以下内容:
    1. 所有父类成员(不包含父类的父类)
      1. 虚基表指针
      2. 本父类的成员
    2. 子类自身的成员
    3. 爷爷类的成员
  2. 虚基表保存的是两个偏移量, 第1个偏移量是父类成员变量在子类对象的内存布局中的偏移( 基于父类在子类对象中的首地址的偏移 ), 第二个偏移是爷爷类成员变量在子类对象的内存布局中的偏移(基于父类在子类对象中的首地址的偏移)

继承应用

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

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

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

  1. 扩展旧类的功能 ```c class mystring : public string { public: mystring() :string() {} mystring(const char* pStr) :string(pStr) {}

    // 变参函数(可变长参数) // … : 表示省略的参数 // 可以接收无限个参数 int sprintf(const char* pszformat,…) {

    1. va_list pArgs= nullptr;
    2. // 根据被省略的参数的前一个参数
    3. // 来找到被省略参数在内存中的首地址
    4. va_start(pArgs, pszformat);
    5. char buff[1000];
    6. // 根据格式化控制字符串和参数列表(pArgs)
    7. // 来自动找到那些被...省略的参数. 自动
    8. // 根据格式化控制字符pszformat来输出到
    9. // buff中.
    10. int count = vsprintf_s(buff, pszformat, pArgs);
    11. va_end(pArgs);
    12. // 对string对象赋值: strObj = buff
    13. *this = buff;
    14. 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. 2. 代码的重构
  2. 1. 当程序中出现一些功能重合(性质相同)的代码的时候. 就可以使用继承来重构代码
  3. 1. 将多个类中那些功能重合(性质相同)的代码提取到一个类中. 作为基类
  4. 1. 哪些类需要用到这些功能(或者哪些类有这样的性质)就可以继承这个基类.
  5. ```c
  6. class GameObject {
  7. int m_x;
  8. int m_y;
  9. int m_dire;
  10. public:
  11. int move(int dire) {
  12. }
  13. };
  14. class Tank : public GameObject{
  15. };
  16. class Bullset : public GameObject{
  17. public:
  18. // 子类可以重定义父类的功能
  19. int move(int dire) {
  20. }
  21. };
  1. 组合多个类的功能形成新功能 ```c 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. <a name="154ae683"></a>
  2. # 多态
  3. 1. 怎么在一个数组中保存不同类型的对象?
  4. 1. 数组的元素必须是指针.
  5. 1. 所有对象都必须拥有一个相同的父类.
  6. 1. 数组的类型使用父类类型.
  7. ```c
  8. // 007_多态.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
  9. //
  10. #include "pch.h"
  11. #include <iostream>
  12. class Base {};
  13. class Myclass1 : public Base {};
  14. class Myclass2 : public Base {};
  15. class Myclass3 : public Base {};
  16. int main()
  17. {
  18. Base* pArr[ 3 ] = {
  19. new Myclass1,
  20. new Myclass2,
  21. new Myclass3
  22. };
  23. std::cout << "Hello World!\n";
  24. }

多态的原理:

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