写点什么

类继承

作者:Maybe_fl
  • 2022-11-11
    陕西
  • 本文字数:2185 字

    阅读完需:约 7 分钟

继承:is-a 关系

C++有三种继承方式:公有继承、保护继承和私有继承。公有继承是最常用的方式,它建立 is-a 关系。公有继承不能建立 has-a 关系、is-like-a 关系、is-implemented-as-a 和 uses-a 关系。将公有继承的关系描述为 is-a-kind-of 关系可能更准确。但通常使用术语 is-a。


多态公有继承

多态的方法在这里不赘述,多态的本质就是类中多了一个虚函数表指针,每个虚函数的调用,都需要执行一项额外的操作,即到表中查找地址。本质就是(*(this->vptr)[n])(this)


静态联编和动态联编

程序调用函数时,将使用哪个可执行代码块呢?这是由编译器决定的。C 语言中,这非常简单,因为每个函数名都对应一个不同的函数,使用函数名联编即可。但在 C++中,由于虚函数的原因,编译过程复杂了很多。那到底调用哪一个函数呢?在编译过程中进行联编称为静态联编。在执行阶段时才知道函数具体调用的,称为动态联编。编译器对非虚方法使用静态联编,对虚方法使用动态联编。

必须使用指针和引用才能触发虚函数,将派生类引用或指针转换为基类引用或指针被称为向上强制转换。公有继承的一个重要特性是这种转换不需要进行显示类型转换。这个规则可以用来判断 is-a 关系。

相反的过程---将基类指针或引用转换为派生类引用或指针成为向下强制转换,需要进行显示类型转换。

除了虚函数使用动态联编,其他都使用静态联编,所有的成员函数调用只与成员类型相关。触发虚函数机制必须是引用和指针,不适当的代码将阻止动态联编。

void show1(const Base& rba)const{  rba.func();    //调用Derived::func()}
void show2(const Base ba)const{ rba.func(); //调用Base::func()}
复制代码


有关虚函数的注意事项

  • 构造函数

构造函数不能是虚函数,创建对象时,调用的是派生类的构造函数,而不是基类的构造函数,然后,派生类会调用一个基类的构造函数,这种顺序不同于继承机制。

derived::derived(tyep1 x, type2 y){  ...};
复制代码

上述函数,其实调用的就是默认构造函数,等价于下面函数

derived::derived(tyep1 x, type2 y) : base(){  ...};
复制代码


  • 析构函数

析构函数应当是虚函数,主要是为了保证基类指针或引用触发析构函数时。能释放派生类新申请的内存。


  • 友元

友元不能是虚函数,因为友元函数就不是类成员,只有类成员才能成为虚函数。友元函数不能被继承,可以利用静态联编,调用基类的友元函数

ostream& operator<<(ostream& os , const Derived& rde){  os << (const Base&) rs;  os << ...;  return os;}
复制代码


  • 重新定义将隐藏方法

class Dwelling{public:  virtual void showperks(int a)const;  ...};
class Hovel : public Dwelling{public: virtual void showperks()const; ...};
复制代码

重新定义继承的方法并不是重载。如果重新定义派生类中的函数,将不只是使用相同的函数列表覆盖基类声明,无论参数列表是否相同,该操作将隐藏所有的同名基类方法。

  Hovel hovel;  hovel.showperks();     //ok  hovel.showperks(1);    //error

Dwelling& dwelling = hovel; dwelling.showperks(1); //ok dwelling.showperks(); //error
复制代码

这就引出了两条规则:

第一就是如果重新定义继承方法,应确保与原来的类型完全相同,如果返回值是基类引用或者指针,则可以将其修改为派生引用或者指针,这种特性被称为返回类型协变

class Dwelling{public:  virtual Dwelling& build(int a)const;  ...};
class Hovel : public Dwelling{public: virtual Hovel& build(int a)const; ...};
复制代码

第二,如果基类声明被重载了,则应在派生类中重新定义所有的基类版本

class Dwelling{public:  virtual void showperks(int a)const;  virtual void showperks(double a)const;  virtual void showperks()const;  ...};
class Hovel : public Dwelling{public: virtual void showperks(int a)const; virtual void showperks(double a)const; virtual void showperks()const; ...};
复制代码

如果只定义一个,其他的会被隐藏,派生类将无法使用。如果不需要修改,则新定义可只调用基类的版本

virtual void Hovel::showperks()const {Dwelling::showperks()};
复制代码


继承和动态内存分配

继承是怎样与动态内存分配(new 和 delete)进行互动的呢?分为以下两种情况


  • 派生类不使用 new

是否需要为派生类定义显示的析构函数、复制构造函数和赋值运算符呢?不需要

析构函数

派生类不需要手动管理内存,默认析构函数是合适的

复制构造函数

派生类的默认复制构造函数使用基类的显示复制构造函数来复制派生类的基类部分

赋值运算符

派生类的默认赋值运算符将自动使用基类的赋值运算符来对基类组件进行赋值


  • 派生类使用 new

是否需要为派生类定义显示的析构函数、复制构造函数和赋值运算符呢?需要

析构函数

需要显示定义析构函数来手动释放派生类部分的内存。

复制构造函数

派生类复制构造函数只能访问派生类的数据,因为必须调用基类的复制构造函数来处理共享的基类数据

Derived::Derived(const Derived& de) : Base(de){  m_str = new string(...);};
复制代码

上述代码直接将派生类传给基类,这个是向上强制转换,自动进行的,不需要显示调用。

赋值运算符

同样的道理,赋值运算符调用基类的赋值运算符来处理共享的基类数据

Derived::operator=(const Derived& de){  if(this == &de)    return *this;    Base::operator=(de);  delete m_str;  m_str = new string(...);  return *this;};
复制代码


用户头像

Maybe_fl

关注

还未添加个人签名 2019-11-11 加入

还未添加个人简介

评论

发布
暂无评论
类继承_Maybe_fl_InfoQ写作社区