栈(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中由程序员手动控制
- 手动分配
new
和malloc
- 手动释放
delete
和free
- 具有全局性,总体无大小限制
- 容易造成内存泄露
关于堆对象的代码如下
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);//千万不要加&符号
注意:
- 学会计算对象的大小,涉及到字段的多少,字段的大小,字节对齐问题。在32位机器上,如果有虚函数,就要再多4个byte(64位是8个byte)来存储
v_table
的地址 - 传指针就涉及要知道堆指针还是栈指针,要不要在func2做
delete
操作等问题,给了函数使用者无限的想象空间。 - 但凡对象比
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;
}
总结:
- 传参推荐const引用
- 返回推荐返回指针或者对象,返回引用是极少极少的。
栈是编译时就要确定的,具有静态特征。所谓静态特征就是编译器编译完之后它的所有东西都是定的。而堆具有灵活性。因此不能没有堆。但是也不能没有栈。本质上来讲,没有栈就没有函数,没有栈就没有变量。所以堆和栈是缺一不可的。
课程总结
- 掌握内存模型分析方法–画运行时内存图,搞清楚是在堆上还是栈上
- 掌握堆、栈概念
- 掌握指针、引用、对象
- 探微知著: 魔鬼尽在细节里