类成员和成员函数内存位置

Posted

技术标签:

【中文标题】类成员和成员函数内存位置【英文标题】:Class members and member functions memory location 【发布时间】:2014-04-02 21:12:24 【问题描述】:

这是一个简单的 C++ 类,命名为A

class A

public:
    explicit A() : m_a(0)  
    explicit A(int a) m_a(a)  
    int getA() const  return m_a; 
    void setA(int a)  m_a = a; 

private:
    int m_a;

这是我目前所知道的:

当您声明类实例的对象时,会为该对象分配内存。分配的内存相当于其成员的内存总和。所以就我而言,它是:sizeof(A) = sizeof(int) = sizeof(m_a) A 类的所有成员函数都存储在内存中的某个位置,A 类的所有实例都使用相同的成员函数。

这是我知道的:

成员函数存储在哪里以及它们实际上是如何存储的?例如,假设int 存储在 4 个字节上;我可以想象具有 4 个连续单元的 RAM 内存布局,每个单元存储该 int 的一部分。我怎么能想象这个函数的布局?(这听起来很傻,但我想函数必须在内存中占有一席之地,因为你可以有一个指向它们的指针)。另外,功能指令是如何以及在哪里存储的?我的第一感觉是函数和函数指令存储在程序可执行文件(及其动态或静态库)中,但如果这是真的,当您创建函数指针时会发生什么? AFAIK 函数指针指向 RAM 内存中的位置,它们可以指向程序二进制文件中的位置吗?如果是,这是如何工作的?

谁能向我解释这是如何工作的,并指出我所知道的是对还是错?

【问题讨论】:

好吧,如果不先将代码加载到 RAM 中,您将无法执行代码...... 指针不指向 RAM,而是指向虚拟内存。 【参考方案1】:

首先,你需要了解linker的作用以及executables(通常在virtual memory中执行)和address spaces&processes是什么。在 Linux 上,阅读 ELF 和 execve(2) 系统调用。另请阅读 Levine 的 Linkers & Loaders 书籍和 Operating Systems: Three Easy Pieces,以及 C++11 标准 n3337 和 this draft 报告和一本好的 C++ programming 书籍,以及 this reference 网站。

成员函数可以是虚函数或普通函数。

一个普通的(非virtual)成员函数就像一个C函数(除了它有this作为一个隐含的,通常是第一个参数)。例如,您的 getA 方法的实现类似于以下 C 函数(在对象之外,例如在二进制可执行文件的 code segment 中):

int C$getA(A*thisptr) const  return thisptr->m_a; 

然后想象编译器正在将p->getA() 翻译成C$getA(p)

virtual member function 通常通过 vtable (virtual method table) 实现。具有一些虚拟成员函数(包括析构函数)的对象通常具有指向此类表的指针作为其第一个(隐式)成员字段(由编译器在其他地方生成)。您的 class A 没有任何虚拟方法,但想象一下如果它有一个额外的 virtual void print(std::ostream&); 方法,那么您的 class A 将具有与

相同的布局
struct A$ 
   struct A$virtualmethodtable* _vptr;
   int m_a;
;

虚拟表可能是

struct A$virtualmethodtable 
  void (*print$fun) (struct A$*, std::ostream*);
;

(因此添加其他虚拟功能意味着只需在 vtable 中添加插槽); 然后像p->print(std::cout); 这样的电话会被翻译成 p->_vptr.print$fun(p,&std::cout); ... 此外,编译器将生成各种虚拟方法表作为常量表(每个类一个)。

注意:多重继承或虚拟继承会使事情变得更复杂。

在这两种情况下,成员函数都不会占用对象中的任何额外空间。如果它是非虚拟的,它只是一个普通的函数(在代码段中)。如果是虚拟的,则共享虚拟方法表中的一个槽。

注意。如果你用最近的GCC(即g++)或Clang(所以clang++)编译,你可以传递它,例如-fdump-tree-all 标志:它将生成数百个转储文件,其中部分以转储的文本形式显示编译器的一些内部表示,您可以使用寻呼机(例如 less)或文本编辑器对其进行检查。您也可以使用MELT 或查看使用g++ -S -fverbose-asm -O1 生成的汇编代码 ....

【讨论】:

很好的答案,我不敢相信它有这么少的赞成票! 对所提到的所有其他书籍的另一个补充和可能的先决条件是:David Patterson 等人的计算机组织和设计。本书从一开始就介绍了计算机的结构,并带您踏上计算机设计的岁月之旅。【参考方案2】:

要理解这一点,您需要了解程序的内存布局。代码将由对象共享。并且所有对象都有自己的数据副本。

【讨论】:

link 了解代码和数据段可以读取c/c++程序的内存布局cs.uwm.edu/classes/cs315/Bacon/Lecture/html/ch10s04.html 提供您在回答中提到的资源/主题的链接会很有帮助。【参考方案3】:

所有局部非静态变量和非虚函数都保存在代码/文本段中。

所有静态和全局变量都保存在静态数据段中。

具有虚函数的类或从具有虚函数的类继承的类将由编译器插入一个 vptr 指针。 Vptr 指向一个虚函数表,它有多个函数槽。每个slot包含存储在代码段中的函数地址。

【讨论】:

以上是关于类成员和成员函数内存位置的主要内容,如果未能解决你的问题,请参考以下文章

Java面向对象:成员变量—OOP中的内存管理—构造函数

C++|详解类成员指针:数据成员指针和成员函数指针及应用场合

java_类与对象的关系

Java_成员变量和局部变量的区别

第四章:虚成员函数(虚函数表thunksplit function和多接入点函数)

Java类的实例化对象成员在内存空间怎么分配,调用构造函数又是在内存中怎么分配