目录 Table of Contents
8 堆 栈 与内存管理
output 函数
#include <iostream.h>
ostream& operator<<(ostream& os, const String&)
{
os << str.get_c_str();
return os;
}
{
String s1("hello ");
cout<< s1;
}
测试程序里面想要对字符串进行输出, 打印到屏幕上
所以和之前复数的想法一样, 我们也要写一个操作符重载
我们的思考方式和以前还是一模一样, 这个不可以写成成员函数, 因为如果写成成员函数的话 s1 >> cout;
cout 写在右边, 和一般的习惯相反, 使用者无法接受
然后它的参数应该有两个, 左边那个是 cout, 右边就是我们的字符串例子
我们的想法都和之前一样, 任何的东西只要能丢给 ostream 并被接受的话, 那你就直接丢过去就好了
现在我们有什么东西能被 <<
接受的呢 ? 我们的字符串里面有一个指针, 就是传统的 C 的 string, 指针指向一个字符组成的数组. 所以只要我们得到那种指针, cout 这种东西本来就可以接受那种指针并且打印出来, 所以我们这里直接写了这个函数
这个函数很简单, 所以没怎么提到过
class String
{
public:
String(const char* cstr = 0);
String(const String& str);
String& operator=(const String& str);
~String();
char* get_c_str() const { return m_data; } // 就是这个函数
private:
char* m_data;
};
所谓栈 (stack), 所谓堆 (heap)
Stack, 是存在于某作用域 (scope) 的一块内存空间 (memory space). 例如, 当你调用函数, 函数本身即会形成一个 stack 用来放它所接收的参数, 以及返回地址.
在函数本体 (function body) 内声明的任何变量, 其所使用的内存块都取自上述 stack.
Heap, 或谓 system heap, 是指由操作系统提供的一块 global 内存空间, 程序可动态分配 (dynamic allocated) 从某中获得若干区块 (blocks).
class Complex { /*...*/ };
//...
{
Complex c1(1, 2); // c1 所占用的空间来自 stack
Complex* p = new Complex(3); // Complex(3) 是个临时对象, 其所占用的空间是以 `new` 自 heap 动态分配而得, 并由 p 指向
}
new
这种创建方式和我们一般的方式, 区别在于, 你用 new
创建的, 你可以在程序任何地方以这种方式动态获得一块, 而你动态获得, 你自己就有责任去释放 delete
它, 我们现在这里还没有 delete
的写法
这两个对象, 当离开这个作用域的时候, c1 会自然消失了, 因为它在栈里面
但是你自己是用 new
的话你都必须要手动去释放掉
stack object 的生命期
class Complex { /*...*/ };
//...
{
Complex c1(1, 2);
}
c1 便是所谓 stack object, 其生命期在作用域 (scope) 结束之际结束. 结束的时候它的析构函数就会被调用起来, 这种作用域内的 object, 又称为 auto object, 因为它会被 "自动" 清理. 自动清理就是说它析构函数会自动调用起来
static local object 的生命期
class Copmlex { /*...*/ };
//...
{
static Complex c2(1, 2);
}
c2 便是所谓 static object, 其生命期在作用域 (scope) 结束之后仍然存在, 直到整个程序结束. 也就是离开这个大括号之后, 它还在那里, 它的析构函数在程序结束的时候调用起来.
global object 的生命期
class Complex { /*...*/ };
//...
Copmlex c3(1, 2);
int main()
{
//...
}
c3 便是所谓 global object, 其生命期在整个程序结束之后才结束. 你也可以把它视为一种 static object, 其作用域是 "整个程序".
heap objects 的生命期
class Copmlex { /*...*/ };
//...
{
Complex* p = new Complex;
//...
delete p;
}
p 就是 heap object, 其生命期在它被 delete
之际结束
class Complex { /*...*/ };
//...
{
Copmlex* p = new Copmlex;
// 没有 delete
}
上述代码会出现内存泄露 (memory leak), 因为当作用域结束, p 所指的 heap object 仍然存在, 但是指针 p 的生命却结束了, 作用域之外再也看不到 p (也就没机会 delete p
)
内存泄露就是你本来有一块内存, 经过某段时间之后某些作用域之后, 你对它失去了控制, 以至于你没有任何办法把它还给操作系统, 这当然是不行的, 因为内存是一个很重要并且很有限的资源
new : 先分配 memory, 再调用 ctor
new 先分配一块空间, 然后调用构造函数
看图, 现在我 new
一个复数, 得到指针
这个 new
被编译器转化为分解为 3 个动作
void* mem = operator new( sizeof (Complex) );
分配内存
第一个就是分配内存, 看上去有点绕, 本身是 new
现在又要调用 operator new
, 其实这个 operator new
只是一个名字比较奇特的函数, 这个函数内部调用 malloc()
然后编译器自动给我们添加 sizeof
这样的代码, 于是我们然后就到了上面的 double 那里
因为我们设计复数, 实部虚部是两个 double, 所以分配出来的空间就是两个 double 的大小, 8 个字节, 这是第一个动作
pc = static_cast<Complex*>(mem);
转型
第二个动作是次要的, 不是我们讲的重点
它只是把第一个动作得到的 void*
类型指针转型为我们复数的 Copmlex*
指针
pc->Complex::Complex(1, 2);
构造函数
第三个动作, 通过刚刚第二步得到的指针, 调用这个构造函数
构造函数是个成员函数, 所以一定要有一个 this pointer, 所以这个 pc 也就是这里的隐藏的参数, 而这个 pc 也是上面那一块内存的起始位置
delete : 先调用 dtor, 再释放 memory
现在我们看一下 new
的逆向过程 delete
delete
先调用析构函数, 再释放内存
它被编译器转换为图上的两个动作
Complex::~Complex(pc);
析构函数
我们去看看我们的这个例子的代码
这个析构函数做的就是把字符串里面动态分配的那一块杀掉
字符串本身只是一个指针
operator delete(pc);
释放内存
这个 operator delete
也是一个特殊的函数
源代码里面调用的是 free(ps)
这里面有两个删除动作, 第一个就是外部我们删除这个对象的操作, 另外一个就是这个对象本身里面有动态分配的操作, 我们要删除它
动态分配所得的内存块 (memory block), in VC
刚刚提到的分配内存和释放内存, 实际上都使用了 C 语言的 malloc()
和 free()
函数
那么使用这两个函数, 到底得到多大的内存呢 ?
你 new
一个复数, 获得的内存是 8 个字节 (两个 double), 在调试模式下面, 你会得到上面 8 个内存, 每一格都是 4 个字节, 下面还有一个 4 字节的
除此之外, 还要带着两个红色的这两个内存, 这个叫 cookie, 作用等会再说
所以在 VC 底下, 调试模式下一个复数会获得 52 个字节, 由于内存对齐, 所以要填补一些东西进去, 就是这里深绿色的 pad 东西, 填到 64 字节
在其它编译器下面, 内存大小可能不一样, 但是原理差不多
刚才我们说 new
一个复数只需要 8 个字节, 这多出来的内存不是浪费了吗 ? 是浪费了, 不过这方便了操作系统进行后续的内存回收工作
如果是在非调试模式下面, 一个复数 8 个字节, 加上两头 cookie 总共 16 字节, 16 字节已经是不用对齐了, 所以不用填充绿色的内存
上下 cookie 最主要的是记录系统给你这一整块内存的大小, 因为系统将来回收这块内存的时候, 你只给它一个指针, 它必须要一个东西记录这个长度, 不然它就不知道该怎么回收
复数说完了, 来看字符串
字符串内部本身就只有一个指针, 所以它的大小是 4 字节
在调试模式下面要加上这些灰色部分的内存, 总共加出来得到的大小是 48 字节, 48 是 16 的倍数, 所以不用填充 pad
48 的 16 进制是 0x30, 这里应该写的是 31, 但是这一块给你了, 所以写成 31
如果你把这一块内存还给操作系统, 这里就会写成 30
如果是在非调试模式下的话, 就没有灰色部分了, 算出来结果是 12 字节, 因为内存对齐所以填充 pad 到 16 字节
动态分配所得的 array
现在看看如果你动态分配的是数组会怎样
当时我们写字符串的构造函数的时候, 只是说 new[]
下面 delete[]
, 上面要中括号下面删除的时候也要中括号, 标准术语是 array new / array delete
所以一个良好的编程习惯是 array new 要搭配 array delete, 不然会出错
现在我们看看为什么会出错
我们先看左边的, 复数, 就是 2 个 double, 现在有 3 个复数形成一个数组, 那就是 8*3 字节
在调试模式下要加上 header, 上面是 32 下面是 4
还有上下 cookie
最后这个加 4 是刚刚没有出现过的, 是因为这里是一个数组, VC 的做法是用一个整数记录这里有 3 个东西, 整数的大小是 4 字节, 其它编译器会不会这么做就不知道了, 但是 VC 是这样的
最后 72 字节内存对齐填充 pad 到 80 字节
80 的十六进制是 50H, 现在内存给出去了, 所以是 51H
非调试模式下同理
字符串的计算方式完全一样
字符串里面只有一根指针, 所以这里画一格, 里面有一个指针, 现在是三个字符串
如果你不搭配用 array delete 来释放内存的话, 会出现什么结果呢 ?
array new 一定要搭配 array delete
图画得很清楚了
问题不是这一块出现内存泄露, 而是你这么 delete [] p;
写的话, 编译器才知道你要删除的是一个数组, 它才知道下面原来不止一个而是有三个, 所以它才知道要调用三次析构函数
如果没写中括号, 它以为下面就一个, 剩下的两个它不知道
如果你没有 array new 搭配 array delete 的话, 的确会内存泄露, 它泄露的是图上打上 ?!
标记的地方
换句话说, 如果你这里数组是复数, 复数没有指针, array new 不搭配 array delete, 其实也没啥问题, 因为复数没有指针, 所以也不存在指针指向的那一块内存
当然我们还是要养成一个好的习惯