C++的栈对象与堆对象

本文分析了栈对象和堆对象的构造和析构过程。

1. 测试环境

  • Linux ubuntu18arm64 4.15.0-76-generic #86-Ubuntu SMP Fri Jan 17 17:25:58 UTC 2020 aarch64 aarch64 aarch64 GNU/Linux
  • gcc version 7.4.0 (Ubuntu/Linaro 7.4.0-1ubuntu1~18.04.1)
  • c++11

2. 调试分析

在本文的栈对象和堆对象示例中,我们统一使用如下class。

class Base {
 public:
  Base(int i) : m_var(i) {
  }

  ~Base() {
    m_var = 0;
  }

 private:
  int m_var;
};

2.1 栈对象

2.1.1 测试代码

void stack() {
  Base stack_obj(1);
}

stack()的反汇编如下

(gdb) disas
Dump of assembler code for function stack():
   0x0000aaaaaaaaae84 <+0>: stp x29, x30, [sp, #-32]!
   0x0000aaaaaaaaae88 <+4>: mov x29, sp
   0x0000aaaaaaaaae8c <+8>: adrp    x0, 0xaaaaaaabb000
   0x0000aaaaaaaaae90 <+12>:    ldr x0, [x0, #4024]
   0x0000aaaaaaaaae94 <+16>:    ldr x1, [x0]
   0x0000aaaaaaaaae98 <+20>:    str x1, [x29, #24]
   0x0000aaaaaaaaae9c <+24>:    mov x1, #0x0                    // #0
   0x0000aaaaaaaaaea0 <+28>:    add x0, x29, #0x10
   0x0000aaaaaaaaaea4 <+32>:    mov w1, #0x1                    // #1
   0x0000aaaaaaaaaea8 <+36>:    bl  0xaaaaaaaab098 <Base::Base(int)>
=> 0x0000aaaaaaaaaeac <+40>:    add x0, x29, #0x10
   0x0000aaaaaaaaaeb0 <+44>:    bl  0xaaaaaaaab0bc <Base::~Base()>
   0x0000aaaaaaaaaeb4 <+48>:    nop
   0x0000aaaaaaaaaeb8 <+52>:    adrp    x0, 0xaaaaaaabb000
   0x0000aaaaaaaaaebc <+56>:    ldr x0, [x0, #4024]
   0x0000aaaaaaaaaec0 <+60>:    ldr x1, [x29, #24]
   0x0000aaaaaaaaaec4 <+64>:    ldr x0, [x0]
   0x0000aaaaaaaaaec8 <+68>:    eor x0, x1, x0
   0x0000aaaaaaaaaecc <+72>:    cmp x0, #0x0
   0x0000aaaaaaaaaed0 <+76>:    b.eq    0xaaaaaaaaaed8 <stack()+84>  // b.none
   0x0000aaaaaaaaaed4 <+80>:    bl  0xaaaaaaaaacd0 <__stack_chk_fail@plt>
   0x0000aaaaaaaaaed8 <+84>:    ldp x29, x30, [sp], #32
   0x0000aaaaaaaaaedc <+88>:    ret
End of assembler dump.

2.1.2 构造

   0x0000aaaaaaaaadc0 <+28>:    add x0, x29, #0x10
   0x0000aaaaaaaaadc4 <+32>:    mov w1, #0x1                    // #1
   0x0000aaaaaaaaadc8 <+36>:    bl  0xaaaaaaaab008 <Base::Base(int)>

在本例里,栈对象的地址是x29 + 0x10, x29就是fp, 用于标识当前栈帧的起始地址,栈对象就位于fp偏移0x10的地方。

调用构造函数Base::Base(int)时,传入的参数如下

  • 第1个参数是栈对象地址x0 = x29 + 0x10
  • 第2个参数是w1 = 1

2.1.3 析构

对于栈对象,离开作用域前,将自动调用析构函数。

=> 0x0000aaaaaaaaadcc <+40>:    add x0, x29, #0x10
   0x0000aaaaaaaaadd0 <+44>:    bl  0xaaaaaaaab08c <Base::~Base()>

调用析构函数很简单,传入栈对象地址,直接调用Base::~Base()即可。

2.2 堆对象

2.2.1 测试代码

void heap() {
  Base *heap_obj = new Base(2);

  delete heap_obj;
}

heap()的反汇编如下

(gdb) disas
Dump of assembler code for function heap():
   0x0000aaaaaaaaaee0 <+0>: stp x29, x30, [sp, #-48]!
   0x0000aaaaaaaaaee4 <+4>: mov x29, sp
   0x0000aaaaaaaaaee8 <+8>: str x19, [sp, #16]
   0x0000aaaaaaaaaeec <+12>:    mov x0, #0x4                    // #4
   0x0000aaaaaaaaaef0 <+16>:    bl  0xaaaaaaaaad20 <_Znwm@plt>
   0x0000aaaaaaaaaef4 <+20>:    mov x19, x0
   0x0000aaaaaaaaaef8 <+24>:    mov w1, #0x2                    // #2
   0x0000aaaaaaaaaefc <+28>:    mov x0, x19
   0x0000aaaaaaaaaf00 <+32>:    bl  0xaaaaaaaab098 <Base::Base(int)>
=> 0x0000aaaaaaaaaf04 <+36>:    str x19, [x29, #40]
   0x0000aaaaaaaaaf08 <+40>:    ldr x19, [x29, #40]
   0x0000aaaaaaaaaf0c <+44>:    cmp x19, #0x0
   0x0000aaaaaaaaaf10 <+48>:    b.eq    0xaaaaaaaaaf24 <heap()+68>  // b.none
   0x0000aaaaaaaaaf14 <+52>:    mov x0, x19
   0x0000aaaaaaaaaf18 <+56>:    bl  0xaaaaaaaab0bc <Base::~Base()>
   0x0000aaaaaaaaaf1c <+60>:    mov x0, x19
   0x0000aaaaaaaaaf20 <+64>:    bl  0xaaaaaaaaad10 <_ZdlPv@plt>
   0x0000aaaaaaaaaf24 <+68>:    nop
   0x0000aaaaaaaaaf28 <+72>:    ldr x19, [sp, #16]
   0x0000aaaaaaaaaf2c <+76>:    ldp x29, x30, [sp], #48
   0x0000aaaaaaaaaf30 <+80>:    ret
End of assembler dump.

2.2.2 构造

对于堆对象,通过new运算符显式构造。

  • 调用operator new分配内存
  • 调用构造函数
  • 返回堆对象指针

先看分配内存

   0x0000aaaaaaaaaeec <+12>:    mov x0, #0x4                    // #4
   0x0000aaaaaaaaaef0 <+16>:    bl  0xaaaaaaaaad20 <_Znwm@plt>
   0x0000aaaaaaaaaef4 <+20>:    mov x19, x0

Base类只有一个数据成员int m_var, 在编译期能获知其实例大小为4。 然后传给operator new函数去分配4字节大小的堆内存。若分配成功,则将堆内存地址保存到x19

operator new的实现

源文件: gcc/libstdc++-v3/libsupc++/new_op.cc

_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{
  void *p; 

  /* malloc (0) is unpredictable; avoid it.  */
  if (sz == 0)
    sz = 1;

  while (__builtin_expect ((p = malloc (sz)) == 0, false))
    {   
      new_handler handler = std::get_new_handler (); 
      if (! handler)
        _GLIBCXX_THROW_OR_ABORT(bad_alloc());
      handler (); 
    }   

  return p;
}

operator new实现如下

  • 调用c库函数malloc()尝试分配内存。若分配成功, 则返回。
  • malloc()分配失败后, 会先获取new handler(通过std::set_new_handler()设置)。若handler不为空,则调用handler,否则抛出bad_alloc异常。
    terminate called after throwing an instance of 'std::bad_alloc'
      what():  std::bad_alloc
    Aborted (core dumped)
    

注: 若在Base内将operator new重载为private, 则该类不能生成堆对象。

再看调用构造函数

   0x0000aaaaaaaaaef8 <+24>:    mov w1, #0x2                    // #2
   0x0000aaaaaaaaaefc <+28>:    mov x0, x19
   0x0000aaaaaaaaaf00 <+32>:    bl  0xaaaaaaaab098 <Base::Base(int)>

调用构造函数Base::Base(int)时,传入的参数如下

  • 第1个参数是operator new返回的堆内存地址x0
  • 第2个参数是w1 = 2

最后将堆对象指针存储到栈内

=> 0x0000aaaaaaaaaf04 <+36>:    str x19, [x29, #40]

2.2.3 析构

对于堆对象,通过delete运算符显式析构。

  • 调用堆对象的析函数
  • 调用operator delete释放内存

先看调用堆对象的析构函数

   0x0000aaaaaaaaaf08 <+40>:    ldr x19, [x29, #40]
   0x0000aaaaaaaaaf0c <+44>:    cmp x19, #0x0
   0x0000aaaaaaaaaf10 <+48>:    b.eq    0xaaaaaaaaaf24 <heap()+68>  // b.none
   0x0000aaaaaaaaaf14 <+52>:    mov x0, x19
   0x0000aaaaaaaaaf18 <+56>:    bl  0xaaaaaaaab0bc <Base::~Base()>

检查堆对象指针是否为空

  • 若是,则跳过析构函数和operator delete 函数的调用(delete空指针没问题)
  • 否则,调用析构函数

再看调用operator delete释放堆内存

   0x0000aaaaaaaaaf1c <+60>:    mov x0, x19
   0x0000aaaaaaaaaf20 <+64>:    bl  0xaaaaaaaaad10 <_ZdlPv@plt>

最后看下operator delete的实现

源文件: gcc/libstdc++-v3/libsupc++/del_op.cc

_GLIBCXX_WEAK_DEFINITION void
operator delete(void* ptr) _GLIBCXX_USE_NOEXCEPT
{
  std::free(ptr);
}

可以看到,operator delete直接调用了c库的free()函数

3. 总结

  • 栈对象位于stack,定义栈对象时自动构造完成初始化,超出作用域后自动析构,开发人员不必刻意维护栈对象。
  • 堆对象位于heap, 需要new/delete(间接调用malloc()/free())显式构造和析构,如果没有及时析构容易引起内存泄露,可借助智能指针加强堆对象的内存管理。

程序员自我修养(ID: dumphex)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容