cpp的堆和栈

栈(stack) 和 堆(heap)

  • 由系统自动管理,以执行函数为单位
  • 空间大小编译时确定(参数+局部变量)
    如果你做反汇编,你可以看到栈的大小是通过编译器写在信息里面,这个时候的内存都是row memory。row memory是上一次内存使用过后留下的01数据,就是没有初始化的。
  • 函数执行时,系统自动分配一个stack
  • 函数执行结束,系统立即回收stack

栈是属于函数,比如现在有一个函数,A函数调用A函数就建立一个栈,调用结束,则该栈就被销毁了。


MyClass* func()
{
    MyClass c(10);//栈内存,函数结束就会被回收
    return &c;//返回悬浮指针,也叫幽灵指针
}

上面这个程序问题还是比较明显,现在来看另一个程序.

MyClass func()
{
	MyClass c(10);
	AClass a(100);
	c.pa = &a;//拷贝构造函数,pa指针指向栈对象
	return c;
}

指针指向栈对象,仍然很危险。

  • 在cpp中由程序员手动控制
  • 手动分配newmalloc
  • 手动释放deletefree
  • 具有全局性,总体无大小限制
  • 容易造成内存泄露

关于堆对象的代码如下

MyClass *func()
{
	MyClass * pa = new MyClass();
	return pa;
}
MyClass *p = func();

这样做虽然没有问题,但是new毕竟是在函数里面用的,如果在函数外有另外一个人接收这个指针,接收这个指针的知不知道要delete这个指针呢?显然是不清楚的。
所以对于函数而言,返回值尽量不要返回指针,返回栈对象指针显然是错误,但是反悔堆对象指针也是不好的。记住cpp有一个原则:谁分配谁释放

堆对象的空间分析

一般情况下,栈上存储指针,堆上存储真正的对象。
绝对不可以将指针指向栈对象。

栈对象的空间分析

对象内存直接存储于栈空间。
栈里面伸出指针不要指向堆对象,如果一定要指向栈对象,也一定要是同栈对象。比如A栈的指针不能指向B栈的对象。

变量模型与使用

三种变量模型

  • 对象 MyClass c;,只能生成栈对象
  • 指针 MyClass *pc;有栈有堆,注意和c=*pc;的*符号是完全不一样的,后者是解引用符号
  • 引用 MyClass& c2 = c;有栈有堆,和pc=&c中的&符号也是完全不一样的,后者是取地址符号

注意:指针有双重性,可指栈可指堆,因此拿到一个指针首先一定要区分它是栈指针还是堆指针,如果用new的形式就是堆指针。

MyClass * pc2 = new MyClass();//堆指针
MyClass& c3 = *pc2;//指向堆对象的引用

三种使用场景

声明对象

传参

传参有三种方式

void func1(MyClass c) {}//传对象,涉及到拷贝构造函数,但是这样做不好
void func2(MyClass* pc) {}//传指针,效率可以,但是问题不少
void func3(MyClass& mc) {}//传引用,一般会加const,防止函数修改外传的引用
MyClass c1;
func1(c1);
func2(&c1);
func3(c1);//千万不要加&符号

注意:

  1. 学会计算对象的大小,涉及到字段的多少,字段的大小,字节对齐问题。在32位机器上,如果有虚函数,就要再多4个byte(64位是8个byte)来存储v_table的地址
  2. 传指针就涉及要知道堆指针还是栈指针,要不要在func2做delete操作等问题,给了函数使用者无限的想象空间。
  3. 但凡对象比int大,传引用都是有价值的。

返回值

MyClass func1()
{
    //第一种方式
    MyClass c1;
    return c1;
    //第二种方式
    MyClass* pc2 = new MyClass();
    return *pc2;//不推荐,违背“谁创建谁负责”的原则
}

如果我们用第二种方式,那么返回的就是一个堆对象的拷贝。代码如下:

MyClass c = func1();

这样做的后果就是,func1里面生成的堆对象永远都无法被释放了,从而造成内存泄露。 来看另一个版本的代码。

MyClass* func2()
{
    //第一种方式,错误
    MyClass c1;
    return &c1;
    //第二种方式,不算错,但是不推荐
    MyClass* pc2 = new MyClass();
    return pc2;
}

程序里面一定要慎用返回指针。

MyClass& func3()
{
   //第一种方式,错误,引用指向了栈对象
    MyClass c1;
    return c1;
    //第二种方式
    MyClass *pc2 = new MyClass();
    return *pc2;
}
//在第二种方式的前提下
MyClass c = func3();//内存泄露!
MyClass & c4 = func3();//可以这么做,但是不常用
delete &c4;

但是返回一个传进入的参数的引用是OK的,不管传进来的参数是栈对象还是堆对象。相当于c在函数里走了一遭然后又出来了。

MyClass& func4(MyClass & c)
{
   return c;
}

总结:

  1. 传参推荐const引用
  2. 返回推荐返回指针或者对象,返回引用是极少极少的。

栈是编译时就要确定的,具有静态特征。所谓静态特征就是编译器编译完之后它的所有东西都是定的。而堆具有灵活性。因此不能没有堆。但是也不能没有栈。本质上来讲,没有栈就没有函数,没有栈就没有变量。所以堆和栈是缺一不可的。

课程总结

  • 掌握内存模型分析方法–画运行时内存图,搞清楚是在堆上还是栈上
  • 掌握堆、栈概念
  • 掌握指针、引用、对象
  • 探微知著: 魔鬼尽在细节里
打赏还是要有的,万一有人打赏呢!