frankfan的胡思乱想

学海无涯,回头是岸

继承

继承

本章讲授的主要内容是C++中的继承 分别从应用场景、语法、内存结构等方面进行阐述。

继承是面向对象的三大特性之一(封装、继承、多态),继承更多的是先有其设计模式后有其语法支持,在设计模式的角度,与继承通常被一起讨论的是组合

C++中继承的场景

面向对象中封装的目的是提高代码的复用度,封装是手段,最终要达到的状态是高内聚、低耦合,只有这样的状态才能更好的实现代码复用。

继承的目的也是代码的复用,不过可以说继承是代码复用的结果。

在面向对象的语言中,思考逻辑是基于『类』的,一类为单位进行封装,构建面向对象中的运行逻辑,类与类的关系既可以是平行的,也可以是派生的,并没有统一的规则,这些通常都是基于业务逻辑进行考量的。

类的继承是复用代码最简单的方式之一,因此很容易导致滥用。不加限制的对类进行派生会导致产生一个臃肿庞大的类,这样实质是对高内聚低耦合原则的破坏,大类的产生会降低面向对象的封装与灵活性,得不偿失。

与类不同,组合 是另一种代码复用的方式。这种方式不是直接通过继承的关系来复用代码,而是通过将不同的类汇聚到一起的方式来复用代码,代码的复用单位是类。通过这种方式组合在一起产生的新类既拥有了其他类的代码,又不会产生大类,造成臃肿,非常灵活。

关于到底是继承 还是组合 ,业界一直都有讨论,通常认为,从逻辑上而言,当类与类之间的关系是is-a时,则适合用继承,而类与类的关系是has-a的关系是,则适合用组合。

而更为激进的是,现在有些编程语言,更为推荐使用组合而非继承的方式类构建面向对象的世界)如golang语言,就推荐使用组合而非继承的方式类复用代码)

C++面向对象继承的语法支持

#include <iostream>
using namespace std;
class Node{
public:
  Node(int v = 0):value(v){}
  ~Node(){}
  void showValue(){
    cout<<value<<endl;
  }
  int getValue()const{return value;}
private:
  int value;
};
#include <iostream>
#include "Node.h"
using namespace std;
class TTNode:Node{
  
};
int main(){
  
  TTNode ttnode;
  //ttnode.showValue();编译报错。因为默认继承关系是private,子类既无法访问父类的private成员,也无法访问父类的public成员
  return 0;
}

private protect public 三个权限关键字,权限大小依次放开。

private仅本类有权限访问。

protect 仅本类以及子类有权限访问。

public 完全放开,不设限制。

#include <iostream>
#include "Node.h"
using namespace std;
class TTNode:public Node{
public:
  TTNode(int v = 0):Node(v){}//通过参数列表的形式来使用父类的构造器
};

int main(){
  
  //复用了Node类的成员变量与成员函数
  TTNode ttnode(11);
  ttnode.showValue();//11
  return 0;
}

对父类成员函数的重写,C++子类会默认继承父类的public成员函数,同时,子类也可以支持重写同名父类函数,构成函数重写,这样子类就能默认使用自己的成员方法了

#include <iostream>
#include "Node.h"
using namespace std;
class TTNode:public Node{
public:
  TTNode(int v = 0):Node(v){}//通过参数列表的形式来使用父类的构造器
  
  //与父类函数同名,只要同名,而无论其参数是否相同,都构成重写关系,子类会默认调用自身的成员函数,而非父类的(除非指定调用父类的
  void showValue(){
    cout<<"ttnode value "<<this->getValue()<<endl;
  }
};

int main(){
  
  //复用了Node类的成员变量与成员函数
  TTNode ttnode(11);
  ttnode.showValue();//ttnode value 11
  
  //指定父类的成员函数
  ttndoe.Node::showValue();//11
  return 0;
}

在类的派生关系中,基类先产生构造,派生类后构造。析构时则派生类先析构,基类后析构。

继承的内存关系

C++的类继承中,子类是完全将父类的内存成员直接拷贝进自己的内存空间中,而无论父类的成员变量是何种访问权限(priavte权限的成员变量也会被拷贝进子类) 。基类的成员地址在低地址区,派生类的成员地址在高地址区,这样的好处在于可以直接使用父类指针指向子类对象,能通过父类指针直接访问子类数据,而无需担心数据越界的问题。

实际是,无论是结构体还是类,通过结构体名或者对象名访问成员变量,本质都是访问其内存偏移地址处的内存,以结构体或者对象首地址为基地址,不同的成员变量名就是不同的偏移值,编译时就已经决定。

#include <iostream>
using namespace std;

class Node{
public:
  void showValue(){
    cout<<value<<endl;
  }
  void showMeteValue(){
    cout<<meteValue<<endl;
  }
private:
  int value;
  int meteValue;
};

class TTNode{
public:
  void showId(){
    cout<<mid<<endl;
  }
private:
  int mid;
};

int main(){
  
  Node node;
  
  //通过直接访问内存偏移值来访问成员变量,无视其语法层面的private
  *(int*)&node = 11;
  node.showvalue();//11
  *((int*)&node+1) = 12;
  node.showMetaValue();//12
  
  //同理,派生类中继承自父类的成员内存与父类本身结构一致
  TTNode ttnode;
  *(int*)&ttnode = 11;
  ttnode.showvalue();//11
  *((int*)&ttnode+1) = 12;
  ttnode.showMetaValue();//12
  
  //自己的成员在父类成员之后,依次排列,当然,C++类中成员也遵守内存对齐原则
  *((int*)&ttnode+2) = 33;
 	ttnode.showId();//33
  
  return 0;
}

因为C++中基类与派生类的内存结构是一脉相承的,因此父类指针可以直接指向子类,并且访问子类中的成员而无需担心错误.

#include <iostream>
using namespace std;
class Node{
public:
  int a;
  int b;
  void showAValue(){
    cout<<a<<endl;
  }
};

class TTNode:public Node{
public:
  int c;

}

int main(){
  Node *node = new TTnode;
  node->a = 11;
  a->showAValue();//11
  return 0;
}

posted on 2021-12-28 00:24  shadow_fan  阅读(36)  评论(0编辑  收藏  举报

导航