frankfan的胡思乱想

学海无涯,回头是岸

C++类的各种内存结构

名词阐释 C++类的各种内存结构
关于一些C++中基础名词概念的厘清,如函数重载 函数重写 函数覆盖(隐藏)

以及C++类的相关内存结构在不同条件下的异同。

函数重载

函数重载是C++一个显著区别与C的语言特性。

这一特性的显著表现就是支持定义同名函数,只要函数的参数列表不同(或者成员函数否被const修饰),那么C++就认为这是属于不同的两个函数。

不同的参数列表与函数名(以及调用约定)构成了唯一确定函数标识的函数签名,因此参数列表不同则签名不同。(参数列表包括参数类型以及参数顺序和参数个数)

显然,定义在不同作用域下的同名函数并不需要进行重载,无论什么语言都是支持的。而需要进行重载,必然这两个函数是定义在同一作用域下,因此,要发生函数重载的前提是,函数要定义在同一作用域下

#include <iostream>
using namespace std;

class Node{
public:
  //函数重载
  void showvalue(){}
  void showvalue(int v){}
  void showvalue(float v){}
  void showvalue(int v,float a){}
  void showvalue(float a,int v){}
};

int main(){
  
}

函数重写

函数重写发生在基类与派生类之间。

所谓函数重写是指子类重新实现父类已经定义的函数,这时继承自父类的成员函数便会失效。函数重写必须要求子类与父类的成员函数同名同参数列表

#include <iostream>
using namespace std;

class Node{
public:
  void showvalue(){
    cout<<"Node showvalue"<<endl;
  }
private:
  int value;
};

class TTNode:public Node{
public:
  void showvalue(){
    cout<<"TTnode showvalue"<<endl;
  }
};

int main(){
  
  TTNode ttnode;
  ttnode.showvalue();//TTnode showvalue
  return 0;
}

函数隐藏

函数隐藏准确的讲不是一种特性,而是一种现象。

所谓函数隐藏,是指子类实现的同名函数隐藏了父类实现的函数,使得子类无法调用父类的成员函数。

子类同名函数隐藏了父类的函数,这一现象就是函数隐藏。需要说明的是,子类的同名函数参数列表不同于父类构成隐藏,如果与父类相同则构成了重写。

#include <iostream>
using namespace std;

class Node{
public:
  void showvalue(){
    cout<<"Node showvalue"<<endl;
  }
};

class TTNode:public Node{
public:
  void showvalue(int v){
    cout<<"TTNode showvalue"<<endl;
  }
};

int main(){
  
  TTNode ttnode;
  ttnode.showvalue(11);//TTNode showvalue
  
  //ttnode.showvalue();
  //编译失败,可见,TTNode并没有从父类那里继承成员函数不带参的showvalue,这时候我们说TTNode自己实现的带参成员函数隐藏了父类的showvalue函数
  //通常,这作为一种现象,而不是一种特性被使用
  return 0;
}

C++类的各种内存结构

C++是一门以面向对象为主要范式的编程语言,其对面向对象的支持非常的全面,各种面向对象的概念也层出不穷。

C++不仅仅支持普通的类的继承,在继承的基础上还有多重继承,以及由此衍生的虚继承等概念(可以认为这是C++的语言特性,也可以认为这是C++的『补丁』)

关键词:

普通继承虚函数多重继承虚继承虚继承虚基类

综述:

普通继承,子类先将父类成员原封不动的拷贝进入自己的内存空间,然后将自己的成员变量依次放在父类的成员变量后面,此外,并无额外的内存占用(内存对齐除外) ,此时,内存结构最为简单。

#include <iostream>
using namespace std;

class Node{
private:
  int value;
  int mid;
};

class TTNode:public Node{
private:
  int factor;
};

int main(){
  return 0;
}
image.png

虚函数,当类中定义一个虚函数后,那么该类内存中不仅需要保存该类的相关成员变量,此时需要一个额外的内存用来保存『虚表地址』。

当在类中定义虚函数后,编译时,编译器会为该类创建一张虚表,虚表中存放虚函数的地址。当运行时,当类调用构造函数时,在具体类的首地址处,会被存放虚表的内存地址,占内存空间4/8字节(视操作系统位数而定)

#include <iostream>
using namespace std;
class Node{
public:
  virtual void showvalue(){}
  virtual void showmid(){}
private:
  int value;
  int mid;
};
int main(){
  return 0;
}
image.png

当子类继承带有虚函数的父类后,子类会将父类的虚表原封不动的拷贝下来,在编译期根据自己对虚函数的重写情况从而重写拷贝自父类的虚表,形成属于自己的虚表。

在运行时,再将自己的虚表地址放置在自己对象的首地址处。

#include <iostream>
using namespace std;

class Node{
public:
  virtual void showvalue(){}
  virtual void showmid(){}
private:
  int value;
  int mid;
};

class TTNode:public Node{
public:
  void showvalue(){}
private:
  int factor;
};

int main(){
  
  TTnode ttnode;
  ttnode.showvalue();
  return 0;
}
image.png

当子类有多个父类,并且父类中都定义有虚函数时,那么子类会根据规则分别拷贝父类中的成员入自己的内存空间,当然,也会将父类的虚表指针拷贝进来。

#include <iostream>
using namespace std;

class Node{
public:
  virtual void showValue(){}
  virtual void showMid(){}
private:
 int value;
 int mid;
};

class Tree{
public:
  virtual void showTreeShape(){}
private:
  int depth;
};

class NodeTree:public Node,public Tree{
public:
  void showMid(){}
  void showTreeShapte(){}
  virtual void showFactor(){}
private:
  int factor;
};

int main(){
  return 0;
}
image.png

补充说明:

1、如果Node没有虚函数,Tree有虚函数,那么类Tree的数据成员则会排布在Node数据成员的前面,而忽视其继承顺序

2、继承了多少父类,就对应有多少父类的成员,继承了2个包含虚表指针的父类,那么就拥有2个虚表指针以及2张虚表,子类重写时覆盖对应虚表的成员函数

3、子类新添加的虚函数放到对象首地址虚表指针指向的虚表中(父类大于等于2个且均含有虚表指针时)

需要注意的是,子类不仅可以拷贝父类的虚表指针,当子类自己定义虚函数后,子类自己会创建属于自己的虚表,并生成自己的虚表指针,那么这样就同时拥有了父类的虚表指针和自己的虚表指针。

但是,并非只要子类定义了自己的虚函数,子类自己就会创建虚表以及虚表指针。当父类的个数大于等于2个,并且每个父类都有用虚表时,此时子类就算自定义了虚函数,那么也不会再额外的创建属于自己的虚表以及虚表指针,而是使用对象首地址处的虚表指针以及其指向的虚表


关于虚继承,这个可以认为是C++为解决菱形继承而打的『补丁』,实际使用中,几乎没有只有一个父类的虚继承,一旦使用了虚继承,那么子类对象中就会多出一个内存结构,『vbptr』(虚拟基址指针)

接下来我们我们讨论4中关于虚继承的内存情况(实际情况排列组合有更多,重在分析手段、方向)

1、无虚函数,单虚继承(尽管没意义)

2、父类有虚函数,子类没虚函数,单虚继承

3、父类有虚函数,子类有虚函数,单虚继承

4、基类无虚函数,父类A有虚函数,父类B有虚函数,子类D只有虚函数的多重继承


//windows 32bit vs2017 debug
#include <iostream>
using namespace std;
class Node{
private:
    int value = 0x11111111;
};

class TTNode:virtual public Node{//单虚继承(实际中并不意义)
private:
    int mid = 0x22222222;
};

int main(){
    
    TTNode ttnode;
    /*
    ttnode对象中包含有1个mid成员、1个value成员、以及1个vbptr表指针
    */
    cout<<sizeof(ttnode)<<endl;//12
    
    return 0;
}

image.png

虚继承后,产生了一个新的内存结构,virtual base class表,以及一个virtual base pointer。这个表中记录了2个数据。1、低地址处的虚表指针距离vbptr的距离(若无虚表指针,则值为0)。2、虚基类距离vbptr的距离。

虚继承父类后,此时对象的内存结构不同于普通的继承,普通继承的内存结构是根据继承顺序,先排列父类的成员变量,最后排列自己的成员变量。

而当虚继承后,虚基类的成员变量排在了最后。


//Windows 32bit vs2017 debug
#include <iostream>
using namespace std;

class Node{
public:
    virtual void showvalue(){}

private:
    int value = 0x11111111;
};

class TTNode:virtual public Node{
private:
    int mid = 0x22222222;
};


int main(){
    TTNode ttnode;
    //ttnode对象中包含有1个mid成员、1个value成员、以及1个vbptr表指针、和1个vfptr虚表指针
    cout<<sizeof(ttnode)<<endl;//16
    return 0;
}

image.png


#include <iostream>
using namespace std;

class Node{
public:
    virtual void showvalue(){}
private:
    int value = 0x11111111;
};

class TTNode:virtual public Node{
public:
    virtual void showmid(){}
private:
    int mid = 0x22222222;
};

int main(){
    TTnode ttnode;
    //ttnode对象中包含有1个mid成员、1个value成员、以及1个vbptr表指针、和2个vfptr虚表指针
    cout<<sizeof(ttnode)<<endl;//20
    return 0;
}

image.png


#include <iostream>
using namespace std;

class A{
private:
    int value_a = 0x11111111;
};

class B:virtual public A{
public:
    virtual void funcB(){}
private:
    int value_b = 0x22222222;
};

class C:virtual public A{
public:
    virtual void funcC(){}
private:
    int value_c = 0x33333333;
};

class D: public B, public C{
public:
    virtual void funcD(){}
};


int main(){
    
    D d;
    cout<<sizeof(d)<<endl;//28
    return 0;
}

image.png

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

导航