一些关于C++内存布局的知识

普通成员函数,静态成员函数,及静态成员变量皆不会在类的对象中有所表示,成员函数和对象的关联由编译器在编译时处理,正如我们会在后面看到的那样,编译器会在编译时决议出正确的普通成员函数地址,并将对象的地址以this指针的方式,做为第一个参数传递给普通成员函数,以此来进行关联。静态成员函数类似于全局函数,不和具体的对象关联。静态成员变量也一样。静态成员函数和静态成员变量和普通的全局函数及全局变量不同之处在于它们多了一层名字限定。

三种内存对象的比较

  栈对象的优势是在适当的时候自动生成,又在适当的时候自动销毁,不需要程序员操心;而且栈对象的创建速度一般较堆对象快,因为分配堆对象时,会调用operator new操作,operator new会采用某种内存空间搜索算法,而该搜索过程可能是很费时间的,产生栈对象则没有这么麻烦,它仅仅需要移动栈顶指针就可以了。但是要注意的是,通常栈空间容量比较小,一般是1MB~2MB,所以体积比较大的对象不适合在栈中分配。特别要注意递归函数中最好不要使用栈对象,因为随着递归调用深度的增加,所需的栈空间也会线性增加,当所需栈空间不够时,便会导致栈溢出,这样就会产生运行时错误。

  堆对象,其产生时刻和销毁时刻都要程序员精确定义,也就是说,程序员对堆对象的生命具有完全的控制权。我们常常需要这样的对象,比如,我们需要创建一个对象,能够被多个函数所访问,但是又不想使其成为全局的,那么这个时候创建一个堆对象无疑是良好的选择,然后在各个函数之间传递这个堆对象的指针,便可以实现对该对象的共享。另外,相比于栈空间,堆的容量要大得多。实际上,当物理内存不够时,如果这时还需要生成新的堆对象,通常不会产生运行时错误,而是系统会使用虚拟内存来扩展实际的物理内存。
接下来看看static对象。

  首先是全局对象。全局对象为类间通信和函数间通信提供了一种最简单的方式,虽然这种方式并不优雅。一般而言,在完全的面向对象语言中,是不存在全局对象的,比如C#,因为全局对象意味着不安全和高耦合,在程序中过多地使用全局对象将大大降低程序的健壮性、稳定性、可维护性和可复用性。C++也完全可以剔除全局对象,但是最终没有,我想原因之一是为了兼容C。

  其次是类的静态成员,上面已经提到,基类及其派生类的所有对象都共享这个静态成员对象,所以当需要在这些class之间或这些class objects之间进行数据共享或通信时,这样的静态成员无疑是很好的选择。

  接着是静态局部对象,主要可用于保存该对象所在函数被屡次调用期间的中间状态,其中一个最显著的例子就是递归函数,我们都知道递归函数是自己调用自己的函数,如果在递归函数中定义一个nonstatic局部对象,那么当递归次数相当大时,所产生的开销也是巨大的。这是因为nonstatic局部对象是栈对象,每递归调用一次,就会产生一个这样的对象,每返回一次,就会释放这个对象,而且,这样的对象只局限于当前调用层,对于更深入的嵌套层和更浅露的外层,都是不可见的。每个层都有自己的局部对象和参数。

  在递归函数设计中,可以使用static对象替代nonstatic局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放nonstatic对象的开销,而且static对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。

 

不产生堆对象的方法:
怎样禁止产生堆对象了?我们已经知道,产生堆对象的唯一方法是使用new操作,如果我们禁止使用new不就行了么。再进一步,new操作执行时会调用operator new,而operator new是可以重载的。方法有了,就是使new operator 为private,为了对称,最好将operator delete也重载为private。现在,你也许又有疑问了,难道创建栈对象不需要调用new吗?是的,不需要,因为创建栈对象不需要搜索内存,而是直接调整堆栈指针,将对象压栈,而operator new的主要任务是搜索合适的堆内存,为堆对象分配空间。好,让我们看看下面的示例代码:

#include <stdlib.h> //需要用到C式内存分配函数
class Resource ; //代表需要被封装的资源类
class NoHashObject
{
 private:
  Resource* ptr ;//指向被封装的资源
  … … //其它数据成员
  void* operator new(size_t size) //非严格实现,仅作示意之用
  {
   return malloc(size) ;
  }
  void operator delete(void* pp) //非严格实现,仅作示意之用
  {
   free(pp) ;
  }
 public:
  NoHashObject()
  {
   //此处可以获得需要封装的资源,并让ptr指针指向该资源
   ptr = new Resource() ;
  }
  ~NoHashObject()
  {
   delete ptr ; //释放封装的资源
  }
}; 

  NoHashObject现在就是一个禁止堆对象的类了,如果你写下如下代码:

NoHashObject* fp = new NoHashObject() ; //编译期错误!
delete fp ; 

不产生栈对象的方法:
创建栈对象时会移动栈顶指针以“挪出”适当大小的空间,然后在这个空间上直接调用对应的构造函数以形成一个栈对象,而当函数返回时,会调用其析构函数释放这个对象,然后再调整栈顶指针收回那块栈内存。在这个过程中是不需要operator new/delete操作的,所以将operator new/delete设置为private不能达到目的。当然从上面的叙述中,你也许已经想到了:将构造函数或析构函数设为私有的,这样系统就不能调用构造/析构函数了,当然就不能在栈中生成对象了。

  这样的确可以,而且我也打算采用这种方案。但是在此之前,有一点需要考虑清楚,那就是,如果我们将构造函数设置为私有,那么我们也就不能用new来直接产生堆对象了,因为new在为对象分配空间后也会调用它的构造函数啊。所以,我打算只将析构函数设置为private。再进一步,将析构函数设为private除了会限制栈对象生成外,还有其它影响吗?是的,这还会限制继承。

  如果一个类不打算作为基类,通常采用的方案就是将其析构函数声明为private。

  为了限制栈对象,却不限制继承,我们可以将析构函数声明为protected,这样就两全其美了。如下代码所示:

class NoStackObject
{
 protected:
  ~NoStackObject() { }
 public:
  void destroy()
  {
   delete this ;//调用保护析构函数
  }
}; 

  接着,可以像这样使用NoStackObject类:

NoStackObject* hash_ptr = new NoStackObject() ;
… … //对hash_ptr指向的对象进行操作
hash_ptr->destroy() ; 

  呵呵,是不是觉得有点怪怪的,我们用new创建一个对象,却不是用delete去删除它,而是要用destroy方法。很显然,用户是不习惯这种怪异的使用方式的。所以,我决定将构造函数也设为private或protected。这又回到了上面曾试图避免的问题,即不用new,那么该用什么方式来生成一个对象了?我们可以用间接的办法完成,即让这个类提供一个static成员函数专门用于产生该类型的堆对象。(设计模式中的singleton模式就可以用这种方式实现。)让我们来看看:

class NoStackObject
{
 protected:
  NoStackObject() { }
  ~NoStackObject() { }
 public:
  static NoStackObject* creatInstance()
  {
   return new NoStackObject() ;//调用保护的构造函数
  }
  void destroy()
  {
   delete this ;//调用保护的析构函数
  }
};

  现在可以这样使用NoStackObject类了:

NoStackObject* hash_ptr = NoStackObject::creatInstance() ;
… … //对hash_ptr指向的对象进行操作
hash_ptr->destroy() ;
hash_ptr = NULL ; //防止使用悬挂指针 

  现在感觉是不是好多了,生成对象和释放对象的操作一致了。

 

By Lu Jun

80后男,就职于软件行业。习于F*** GFW。人生48%时间陪同电子设备和互联网,美剧迷,高清视频狂热者,游戏菜鸟,长期谷粉,临时果粉,略知摄影。

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.