C++PrimerPlus学习之类继承

版权声明:转载之前请说你是博主的粉丝哦。 https://blog.csdn.net/qq_34921856/article/details/83784035

公有派生

  • 基类的公有成员及私有成员都会成为派生类的一部分
  • 基类的私有成员只能通过基类的公有和保护方法访问
  • 基类指针或引用可以在不显式类型转换的情况下指向派生类对象
  • 派生类的构造函数
    • 首先会创建基类对象,派生类的构造函数应通过成员初始化列表将基类信息传递给基类构造函数
    • 一个需要注意的地方
      Point::Point(int tk,const Base &b):Base(b)
      {
      	k=tk;
      }
      
      由于b的类型为Base &,因此将调用基类的复制构造函数,如果基类使用了动态内存分配(new)的话,则需要定义基类的复制构造函数。
  • 派生类的析构函数
    • 派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数

继承的一些关系

  • C++有3种继承方式:公有继承,保护继承,私有继承
  • is-a(is-a-kind-of)关系:如香蕉是水果。建模方式:那么香蕉类可以继承水果类
  • has-a关系:如午餐有水果。建模方式:午餐类里有水果类这个数据成员
  • is-like-a关系:如律师像鲨鱼。建模方式:设计一个包含共有特征的类,然后以is-ahas-a关系,在这个类的基础上定义相关的类。(以is-a关系建立有点抽象类的感觉
  • is-implemented-as-a(作为......来实现)关系:如使用数组来实现栈。建模方式:让栈包含一个私有Array对象。
  • uses-a关系:如计算机可以使用激光打印机。建模方式:使用友元函数或类来处理打印机和计算机之间的关系。

多态-动态多态

  • 定义

    • 同一个方法在派生类和基类中的行为是不同的
    • 方法取决于调用该方法的对象
    • 前面所学的重载和函数模板是在编译时间便确定的联编,称为静态多态
  • 重写基类方法

    • 基类的方法可以在派生类中重写 – 使用classname::来说明是基类的还是派生类的

    • 可在派生类中使用基类名作为限定符调用同名的基类函数

      void Point::show()
      {
      	Base::show();
      	cout<<z<<endl;
      }
      
    • 如果基类中的函数有多个重载,则继承过来的时候不能只重新定义一个版本的 – 另外的会被隐藏

      
      class Base
      {
      public:
      	void show(int a)const
      	{
      		cout<<a<<endl;
      	}
      	void show()const
      	{
      		cout<<10<<endl;
      	}
      	void show(double a)const
      	{
      		cout<<a<<endl;
      	}
      };
      class P : public Base
      {
      public:
      	void show(int a)const
      	{
      		cout<<a+10<<endl;
      	}
      	/*void show()const
      	{
      		cout<<20<<endl;
      	}
      	void show(double a)const
      	{
      		cout<<a+10<<endl;
      	}*/
      
      };
      int main()
      {
      	P a;
      	a.show();//invalid
      }
      
  • 虚函数

    • 声明前加virtual,定义不用加。

    • 如果方法是通过引用或指针而不是对象调用的,程序将根据引用或指针指向的对象的类型来选择方法。

    • 如果没有加virtual(一般方法),那么程序将根据引用或指针的类型来选择方法。

    • 友元函数不能是虚函数,因为友元函数不是类成员,而只有成员函数才能是虚函数。

    • 可以创建指向基类的指针数组,那么这个数组既可以指向基类,也可以指向派生类。

      int main() {
      	Base b(10, 20);
      	Point x(1, 2, 3);
      	Base* bs[3];
      	bs[0] = new Base(1, 2);
      	bs[1] = &x;
      	bs[2] = &b;
      	rep(i, 0, 3) bs[i]->show(), cout << endl;
      	return 0;
      }
      /*
      output:
      1 2
      1 2 3
      10 20
      */
      
    • 一个需要注意的地方:基类需要声明一个虚析构函数,这样做是为了保证在释放对象时,可以调用相应对象类型的析构函数。

    • 没有重新定义

      • 重新定义一个不接受参数的show函数,那么将会隐藏同名基类的方法。
      • 两条经验规则
        • 如果重新定义继承的方法,应确保与原来的原型完全相同。
        • 如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的)。这种特性被称为返回类型协变。
      class Base
      {
      public:
      	virtual void show(int a)const;
      ...
      };
      class Point
      {
      public:
      	virtual void show()const;
      ...
      };
      Point tmp;
      tmp.show()//valid
      tmp.show(1);//invalid
      

静态联编和动态联编

  • 将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编
  • 函数重载和函数模板C/C++编译器可以在编译过程完成这种联编。在编译过程中进行联编被称为静态联编。
  • 虚函数,因为编译器不知道用户将选择哪种类型的对象。所以编译器必须生成能够在程序运行时选择正确的虚函数方法,这被称为动态联编。
  • C++默认选择为静态联编,因为效率更高。

虚函数的实现原理

  • 给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针
  • 该地址数组称为虚函数表
  • 对于基类来说,基类对象包含了一个指针,该指针指向基类中所有虚函数的地址表
  • 对于派生类来说,派生类对象也包含了一个指针,该指针指向派生类中所有虚函数的地址表
    • 如果派生类没有重新定义某虚函数,则此表保留此虚函数基类版本的地址
    • 如果派生类重新定义了某虚函数,则此表将更新此虚函数的新地址
    • 如果派生类增加了新的虚函数,则此表也增加该函数的地址
  • 不管虚函数有多少个,都只需要在对象里添加一个指针成员
  • 调用虚函数时
    • 程序将查看存储在对象中的虚函数表头地址
    • 然后转身相应的函数地址表
    • 然后根据该虚函数在类中声明的位置找到其在表中的位置
    • 然后跳到该地址指向的函数地址,执行函数
      在这里插入图片描述

抽象基类(abc)

  • abstract base class

  • 使用纯虚函数提供未实现的函数 – 在声明的结尾处加 =0

    class Base {
    private:
    ...;
    public:
    virtual double get_area() = 0;
    };
    
  • 包含纯虚函数的类只用作基类 – 不能实例化 – 但是能声明(但不初始化)指针

  • 即ABC必须至少包含一个纯虚函数

  • 如果在基类中声明了纯虚函数,而派生类中并没有对其定义,则该函数仍为纯虚函数,派生类也为抽象类

  • 基类中也可以定义(实现)纯虚函数,但在派生类中必需也要定义并且显示地调用(使用类名限定符)

    • 这样可以将不同子类中公共的事务放在父类中完成
    • 只有声明而没有定义的纯虚函数派生类是无法调用的
    • 如果要把基类的析构函数声明为纯虚函数(有时候这么做只是为了说明此类为抽象类),则必须定义这个纯虚析构函数的实现 – 因为派生类析构时会自动调用它
  • 纯虚函数作为一种“接口约定”, 在基于组件的编程模式中很常见 (华哥这么说

    • 派生组件至少会实现基类组件的所有接口(纯虚函数)

继承和动态内存分配

  • 如果基类使用了动态内存分配 – 即在构造中使用new分配空间
    • 该基类需要声明其构造函数, 析构函数,复制构造,赋值运算符
  • 如果此时子类中没有使用new分配的内存
    • 此子类默认的复制构造会显式地调用基类的复制构造, 同时根据成员变量类型进行复制
    • 此子类默认的赋值运算符会显式地调用基类的赋值运算符
  • 如果子类使用new分配的内存
    • 必须为子类定义显式析构函数

    • 必须为子类定义复制构造函数

      • 基类的复制构造函数中的参数是基类的引用,所以可以传递进来派生类对象。
      Point::Point(const Point &a):Base(a)	
      {
      	z=a.z;
      }
      
    • 必须为子类重载赋值运算符

      • 显式调用基类的赋值运算符,以完成基类部分的赋值
      String& String::operator=(const String& s) {    
      if(this == &s) return *this;
      Base::operator=(s);             // 注意此句 -- 必须显示调用基类的赋值运算符
      delete[] str;                           
      len = s.len;
      str = new char[len+1];
      strcpy(str, s.str);
      return *this;
      }
      

继承与友元函数

// Base.h
class Base {
    ...;
    friend ostream& operator<< (ostream& out, const Base& p) {
        ...;
    } // 基类的友元函数
};

// Point.cpp
ostream& oeprator<< (ostream& out, const Point& p) {
    out << (const Base&) p; // 输出基类部分
    ...;                    // 输出派生部分
} // 派生类的友元函数
  • 在派生类的友元函数中, 只能访问派生类的派生部分,而不能访问基类的私有成员
  • 可使用基类的友元函数来负责对派生类的基类部分的访问
  • 需要显式转换类型。

猜你喜欢

转载自blog.csdn.net/qq_34921856/article/details/83784035