C/C++中的内存管理

时间:2021-12-06 09:21:57

一.内存的分配方式

1. 程序代码区

2. 静态数据区

3. 动态数据区

二.动态内存

1. 在栈上创建的内存

2. 从堆上分配的内存

3. 小结

三.指针与内存

1. 操作内存

2. 指针与数组

3. 指针参数

四.malloc/free 与new/delete

1. malloc/free 的使用要点

2. new/delete 的使用要点

3. malloc/free 与new/delete 的比较

五.常见内存错误

1.内存泄露

2.内存越界访问

3.野指针

4.内存分配未成功,却使用了它

5.内存分配虽然成功,但是尚未初始化就引用它

5.返回指向临时变量的指针

6.试图修改常量

7.误解传值与传引用

六.参考文献

一.内存的分配方式

对于一个进程的内存空间而言,可以在逻辑上分成个部份:

1. 程序代码区

存放函数体的二进制代码.

2. 静态数据区

存放全局变量(global)和静态变量(static).初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域.程序结束后由系统释放.

3. 动态数据区

动态数据区一般就是"堆"和"栈","堆"和"栈"是两种不同的动态数据区,

栈(stack)是一种线性结构.由编译器自动分配释放,存放函数的参数值,局部变量,本地变量的值等.其操作方式类似于数据结构中的栈.

堆(heap)是一种链式结构.一般由程序员分配释放,若程序员不释放,则会造成内存泄漏.注意它与数据结构中的堆是两回事.

例1: 内存分配方式举例

#include <iostream>

using namespace std;

int a = 0;    // 全局初始化区

char *p1;     // 全局未初始化区

int main()

{

int b;              // 栈

char s[] = "abc";   // 栈

char *p2;           // 栈

char *p3 = "12345"; // 12345/0 在常量区, p3在栈上

static int c = 0;   // 静态初始化区

p1 = new char(10);  // 分配而来的10 和20 字节的区域就在堆区

p2 = new char(20);

strcpy(p1, "12345");    // 12345/0 在常量区

// 编译器可能会将它与p3 所指向的"12345"优化成一个地方

return 0;

}

例2: 各种变量的分配方式

#include <iostream>

using namespace std;

int g1 = 0;

int g2 = 0;

int g3 = 0;

int main()

{

static int s1 = 0;

static int s2 = 0;

static int s3 = 0;

int v1 = 0;

int v2 = 0;

int v3 = 0;

/* 打印出各个变量的内存地址 */

cout << "本地变量的内存地址:" << endl;

cout << "0x" << &v1 << endl;

cout << "0x" << &v2 << endl;

cout << "0x" << &v3 << endl;

cout << endl;

cout << "全局变量的内存地址:" << endl;

cout << "0x" << &g1 << endl;

cout << "0x" << &g2 << endl;

cout << "0x" << &g3 << endl;

cout << endl;

cout << "静态变量的内存地址:" << endl;

cout << "0x" << &s1 << endl;

cout << "0x" << &s2 << endl;

cout << "0x" << &s3 << endl;

cout << endl;

return 0;

}

/*

运行结果

本地变量的内存地址:

0x0012FF7C

0x0012FF78

0x0012FF74

全局变量的内存地址:

0x0047CDEC

0x0047CDF0

0x0047CDF4

静态变量的内存地址:

0x0047CDF8

0x0047CDFC

0x0047CE00

*/

程序中v1,v2,v3是本地变量,g1,g2,g3是全局变量,s1,s2,s3是静态变量.

可以看出每一组变量在内存中是连续分布的,但是本地变量和全局变量分配的内存地址差了十万八千里,而全局变量和静态变量分配的内存是连续的.这是因为本地变量和全局/静态变量是分配在不同类型的内存区域中的结果.

├———————┤低端内存区域

│……│

├———————┤

│动态数据区│

├———————┤

│……│

├———————┤

│代码区│

├———————┤

│静态数据区│

├———————┤

│……│

├———————┤高端内存区域

二.动态内存

在程序中,最常用的,也是最难掌握的,就是动态内存了.下面就动态内存的两种分配方式来讨论一下动态内存的使用.

1. 在栈上创建的内存.

在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放.栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限.

在栈中分配的空间的生命期与这个变量所在的函数和类相关.如果是函数中定义的局部变量,那么它的生命期就是函数被调用时,如果函数运行结束,那么这块内存就会被回收.如果是类中的成员变量,则它的生命期与类实例的生命期相同.

例3: 栈内存的生命周期

#include <iostream>

using namespace std;

/** 在栈上申请内存,函数结束时被销毁**/

char * getString1()

{

char p[] = "Hello world!";

// 编译器将提出警告, 比如

// returning address of local variable or temporary (VC++6.0)

return p;

}

/** 在常量区申请内存,函数结束时仍然可用**/

char * getString2()

{

char * p = "Hello world!";

return p;

}

int main()

{

char *str1 = getString1();

// str1 的内容是垃圾

cout << str1 << endl;

char *str2 = getString2();

// 输出Hello world!

cout << str2 << endl;

return 0;

}

在C/C++ 中,所有的方法调用都是通过栈来进行的,所有的局部变量,形式参数都是从栈中分配内存空间的.

需要注意的是,在分配的时候,比如为一个即将要调用的程序模块分配数据区时,应事先知道这个数据区的大小,也就说是虽然分配是在程序运行时进行的,但是分配的大小多少是确定的,不变的,而这个"大小多少"是在编译时确定的,不是在运行时.

如果想要在运行时才决定要分配的内存的大小,就要用到"堆内存".

2. 从堆上分配的内存

程序在运行的时候用new(malloc)申请任意多少的内存,程序员自己负责在何时用delete(free)释放内存.动态内存的生存期由我们决定,使用非常灵活,但问题也最多.其生命期是从调用new(malloc)开始,到调用delete(free)结束.如果不掉用delete或者free.则这块空间必须到软件运行结束后才能被系统回收.

例4: 堆内存的使用

#include <iostream>

using namespace std;

int main()

{

int arraySize;

int *array;

/* 堆内存的大小在程序运行时才确定*/

cin >> arraySize;

/* 开辟堆内存*/

array = new int[arraySize];

/* 对堆内存进行操作*/

int i;

for (i = 0; i < arraySize; i++)

{

array[i] = i;

}

for (i = 0; i < arraySize; i++)

{

cout << array[i] << " ";

}

cout << endl;

/* 释放堆内存*/

delete [] array;

return 0;

}

堆是应用程序在运行的时候请求操作系统分配给自己内存,由于从操作系统管理的内存分配,所以在分配和销毁时都要占用时间,因此用堆的效率非常低.但是堆的优点在于,编译器不必知道要从堆里分配多少存储空间.也不必知道存储的数据要在堆里停留多长的时间,因此,用堆保存数据时会得到更大的灵活性.

事实上,面向对象的多态性,堆内存分配是必不可少的,因为多态变量所需的存储空间只有在运行时创建了对象之后才能确定.在C++中,要求创建一个对象时,只需用new命令编制相关的代码即可.执行这些代码时,会在堆里自动进行数据的保存.当然,为达到这种灵活性,必然会付出一定的代价: 在堆里分配存储空间时会花掉更长的时间!这也正是导致效率低的原因.

3. 小结

堆和栈的区别可以用如下的比喻来看出:

使用栈就象我们去饭馆里吃饭,只管点菜(发出申请),付钱,和吃(使用),吃饱了就走,不必理会切菜,洗菜等准备工作和洗碗,刷锅等扫尾工作.他的好处是快捷,但是*度小.

使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且*度大.

三.指针与内存

1. 操作内存

什么是指针?

其实指针就像是其它变量一样,所不同的是一般的变量包含的是实际的真实的数据,而指针是一个指示器,它告诉程序在内存的哪块区域可以找到数据.指针是一个数据类型,本身也需要占用四个字节的存储空间。.所以用sizeof(void*)获得的值为.

作为一个C++程序员,指针的直接操作内存,在数据操作方面有着速度快,节约内存等优点,仍是很多C++程序员的最爱.指针是一把双刃剑,用好了它,你就会发现指针有多么的方便,反之,你可能就头疼了,往往会出现意想不到的问题.

为了方便,人们赋予了指针极大的权利,然而就是这些权利,使指针在为程序员提供种种便利的同时,也可能对程序造成巨大的破坏.

例5: 指针的权利

#include <iostream>

using namespace std;

class A

{

int value;

public:

A( int n = 0 ) : value( n ) {}

int GetValue()

{

return value;

}

};

int main()

{

A a;

*( (int *)&a ) = 5;

return 0;

}

2. 指针与数组

C/C++程序中,指针和数组在不少地方可以相互替换着用,让人产生一种错觉,以为两者是等价的.

数组要么在静态存储区被创建(如全局数组).要么在栈上被创建/数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变.

指针可以随时指向任意类型的内存块,它的特征是"可变",所以我们常用指针来操作动态内存.指针远比数组灵活,但也更危险.

例6: 指针与数组的比较-修改内容

#include <iostream>

using namespace std;

int main()

{

char a[] = "hello";

a[0] = 'X';

cout << a << endl;  // Xello

char *p = "world";  // p 指向常量字符串

p[0] = 'X';         // 编译器不能发现该错误

cout << p << endl;  // 运行错误

return 0;

}

在例中,字符数组a 的容量是个字符,其内容为hello/0.a的内容可以改变,如a[0] ='X' .

指针p 指向常量字符串"world"(位于静态存储区.内容为world/0).常量字符串的内容是不可以被修改的.从语法上看,编译器并不觉得语句p[0] = 'X'有什么不妥.但是该语句企图修改常量字符串的内容而导致运行错误.

例7: 指针与数组的比较-内容复制与比较

#include <iostream>

using namespace std;

int main()

{

/* 数组*/

char a[] = "hello";

char b[10];

strcpy(b, a);                      // 不能用b = a;

cout << strcmp(b, a) << endl;      // 不能用b == a

/* 指针*/

int len = strlen(a);

char *p = new char(len+1);

strcpy(p, a);                      // 不要用p = a;

cout << strcmp(p, a) << endl;      // 不要用p == a

return 0;

}

不能对数组名进行直接复制与比较,例中,若想把数组a的内容复制给数组b,不能用语句b = a ,否则将产生编译错误.应该用标准库函数strcpy进行复制.同理,比较b和a的内容是否相同,不能用b==a 来判断,应该用标准库函数strcmp进行比较.

语句p = a 并不能把a的内容复制指针p,而是把a的地址赋给了p.要想复制a的内容,可以先为p申请一块容量为strlen(a)+1个字符的内存,再用strcpy进行字符串复制.同理,语句p==a 比较的不是内容而是地址,应该用库函数strcmp来比较.

例8: 指针与数组的比较-内存容量

#include <iostream>

using namespace std;

void fun(char a[100])

{

cout << sizeof(a) << endl;    // 4 (字节)

}

int main()

{

char a[] = "hello world";

char *p = a;

cout << sizeof(a) << endl;    // 12 (字节)

cout << sizeof(p) << endl;    // 4 (字节)

fun(a);

return 0;

}

用运算符sizeof可以计算出数组的容量(字节数).示例中,sizeof(a)的值是(注意别忘了'/0').指针p指向a,但是sizeof(p)的值却是.这是因为sizeof(p)得到的是一个指针变量的字节数,相当于sizeof(char*),而不是p所指的内存容量.C/C++语言没有办法知道指针所指的内存容量,除非在申请内存时记住它.

注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针.例中,不论数组a的容量是多少,sizeof(a)始终等于sizeof(char *).

3. 指针参数

如果函数的参数是一个指针,不要指望用该指针去申请动态内存.

例9: 指针作为函数的参数

#include <iostream>

using namespace std;

void getMemory(char *p, int num)

{

p = new char(num);

}

int main()

{

char *str = NULL;

getMemory(str, 100);     // str 仍然为NULL

strcpy(str, "hello");    // 运行错误

return 0;

}

例中,语句getMemory(str, 200)并没有使str获得期望的内存,str依旧是NULL,为什么?

毛病出在函数getMemory中.编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是_p,编译器使_p = p.如果函数体内的程序修改了_p的内容,就导致参数p的内容作相应的修改.这就是指针可以用作输出参数的原因.

在本例中,_p申请了新的内存,只是把_p所指的内存地址改变了,但是p丝毫未变.所以函数GetMemory并不能输出任何东西.事实上,每执行一次getMemory就会泄露一块内存,因为没有用delete释放内存.

如果非得要用指针参数去申请内存,有两种方法: 1.改用"指向指针的指针"; 2.用函数返回值来传递动态内存.

例10: 指针参数与生命期

#include <iostream>

using namespace std;

int *g;

void fun(int *q)

{

q = new int(10);

g = q;

cout << "in function" << endl;

cout << "&p: " << &q << endl;

cout << "p : " << q << endl;

cout << "*p: " << *q << endl;

cout << endl;

}

int main()

{

int *p;

cout << "before function" << endl;

// 编译器发出警告:

// local variable 'p' used without having been initialized

cout << "&p: "  << &p << endl;

cout << "p : " << p << endl;

//cout << "*p: " << *p << endl;

cout << endl;

fun(p);

cout << "after function" << endl;

cout << "&p: "  << &p << endl;

cout << "p : " << p << endl;

//cout << "*p: " << *p << endl;

cout << endl;

cout << "globe_pointer" << endl;

cout << "&g: " << &g << endl;

cout << "g : " << g << endl;

cout << "*g: " << *g << endl;

cout << endl;

return 0;

}

/*

运行结果

before function

&p: 0012FF7C

p : CCCCCCCC

in function

&p: 0012FF2C

p : 00371D48

*p: 10

after function

&p: 0012FF7C

p : CCCCCCCC

globe_pointer

&g: 0047CDE8

g : 00371D48

*g: 10

*/

对于指针p, &p 输出的是指针自身的地址; p 输出的是指针指向的地址; *p 输出的是指针指向的地址的值.

在例中,无论是否执行了函数fun(),指针p都没有任何改变.可见函数fun()并没有起到预期的作用.

另外,在函数fun()中,q位于栈内存中,而其申请的内存位于堆内存中.所以,当程序推出函数fun()中时,指针q随着栈内存的销毁而销亡,但其指向的堆内存并未销毁.被全局指针g记录下来.所以g, *g 的值与q, *q的值相等(但两个指针各自的地址&g, &q是不同的).

四.malloc/free 与new/delete

1. malloc/free 的使用要点

函数malloc的原型如下"

void * malloc(size_t size);

用malloc申请一块长度为length的整数类型的内存,程序如下:

int  *p = (int *) malloc(sizeof(int) * length);

我们应当把注意力集中在两个要素上,"类型转换"和"sizeof".

malloc返回值的类型是void *,所以在调用malloc时要显式地进行类型转换,将void * 转换成所需要的指针类型.

malloc函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数.在malloc的"()"中使用sizeof运算符是良好的风格.

函数free的原型如下:

void free( void * memblock );

为什么free函数不象malloc函数那样复杂呢?这是因为指针p的类型以及它所指的内存的容量事先都是知道的,语句free(p)能正确地释放内存。如果p是NULL指针,那么free对p无论操作多少次都不会出问题。如果p不是NULL指针,那么free对p连续操作两次就会导致程序运行错误。

2. new/delete 的使用要点

运算符new使用起来要比函数malloc简单得多.如:

int  *p1 = (int *)malloc(sizeof(int) * length);

int  *p2 = new int[length];

这是因为new内置了sizeof、类型转换和类型安全检查功能.对于非内部数据类型的对象而言,new在创建动态对象的同时完成了初始化工作.如果对象有多个构造函数,那么new的语句也可以有多种形式.

例11: new/delete 的使用

#include <iostream>

using namespace std;

class Obj

{

private:

int a;

public:

Obj(void) { a= 10; }     // 无参数的构造函数

Obj(int x) : a(x) {}     // 带一个参数的构造函数

};

int main()

{

Obj *a = new Obj;

Obj *b = new Obj(1);     // 初值为

Obj *c = new Obj[100];   // 调用对象的无参数构造函数, 初值为

// 这种写法是不对的

//Obj  *d = new Obj[100](1);  // 创建个动态对象的同时赋初值

delete a;

delete b;

delete []c;    // 留意符号[] 的使用

return 0;

}

需要注意的是,和malloc/free 不同,new/delete 是运算符.既然是运算符,就可以被重载.

局部重载new和delete. 重载一个与类相关的new和delete函数,只需重载运算符函数成为该类的成员函数.此时重载的new和delete仅用于该特定的类,在其他数据类型上仍然使用原始版本的new和delete.即遇到new和delete时,编译程序首先检查正在使用对象所在的类是否重载了new和delete,如果重载了,则使用这个重载版本;否则,使用全局定义的new和delete.

全局重载new和delete. 在任何类外重载new和delete,使它成为全局的.此时,C++中原来的new和delete被忽略,程序中使用重载的new和delete.

例12: new/delete 的重载

#include <iostream>

using namespace std;

class A

{

private:

int x, y;

public:

A(int x1, int y1) : x(x1), y(y1) {}

~A() {}

void * operator new (unsigned int size);

void operator delete (void *p);

};

/** 局部重载运算符new **/

void * A::operator new (unsigned int size)

{

return malloc(size);

}

/** 局部重载运算符delete **/

void A::operator delete (void *p)

{

free(p);

}

int main()

{

A *a;

a = new A(3, 10);   // 调用类A的重载运算符new

delete a;           // 调用类A的重载运算符delete

int *i = new int (100);  // 调用系统运算符new

delete []i;              // 调用系统运算符delete

return 0;

}

3. malloc/free 与new/delete 的比较

malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符.它们都可用于申请动态内存和释放内存.

对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求.对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数.由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free.

因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete.注意new/delete不是库函数.

new Obj (Obj是一个类名)实际上做了件事: 调用opeator new,在*存储区分配一个sizeof(Obj)大小的内存空间;然后调用构造函数Obj(),在这块内存空间上类砖砌瓦,建造起我们的对象.同样对于delete,则做了相反的两件事:调用析构函数~Obj(),销毁对象,调用operator delete,释放内存.

既然new/delete的功能完全覆盖了malloc/free,为什么C++不把malloc/free淘汰出局呢?这是因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存.

总的来说,运算符new和delete提供了存储的动态分配和释放功能.它的作用相当于C语言的函数malloc()和free(),但是性能更为优越.使用new比使用malloc()有以下的几个优点

1、new自动计算要分配类型的大小,不使用sizeof运算符,比较省事,可以避免错误.

2、它自动地返回正确的指针类型,不用进行强制指针类型转换.

3、可以用new对分配的对象进行初始化.

五.常见内存错误

1.内存泄露

在堆上分配的内存,如果不再使用了,应该把它释放掉,以便后面其它地方可以重用.在C/C++中,内存管理器不会帮你自动回收不再使用的内存.如果你忘了释放不再使用的内存,这些内存就不能被重用,就造成了所谓的内存泄露.

含有这种错误的函数每被调用一次就丢失一块内存.刚开始时系统的内存充足,你看不到错误.终有一次程序突然死掉,系统出现提示:内存耗尽.

动态内存的申请与释放必须配对,程序中new与delete的使用次数一定要相同,否则肯定有错误(malloc/free同理).

2.内存越界访问

内存越界访问有两种:一种是读越界,即读了不属于自己的数据,如果所读的内存地址是无效的,程度立刻就崩溃了.如果所读内存地址是有效的,在读的时候不会出问题,但由于读到的数据是随机的,它会产生不可预料的后果.另外一种是写越界,又叫缓冲区溢出.所写入的数据对别人来说是随机的,它也会产生不可预料的后果.

内存越界访问造成的后果非常严重,是程序稳定性的致命威胁之一.更麻烦的是,它造成的后果是随机的,表现出来的症状和时机也是随机的,让BUG的现象和本质看似没有什么联系,这给BUG的定位带来极大的困难.

3.野指针

"野指针"不是NULL指针,是指向“垃圾”内存的指针.人们一般不会错用NULL指针,因为用if语句很容易判断.但是野指针是很危险的.if语句对它不起作用/

当你调用free(p)时,你真正清楚这个动作背后的内容吗?你会说p指向的内存被释放了.没错,p本身有变化吗?答案是p本身没有变化.它指向的内存仍然是有效的,你继续读写p指向的内存,没有人能拦得住你.

释放掉的内存会被内存管理器重新分配,此时,野指针指向的内存已经被赋予新的意义.对野指针指向内存的访问,无论是有意还是无意的,都为此会付出巨大代价,因为它造成的后果,如同越界访问一样是不可预料的.

释放内存后立即把对应指针置为空值,这是避免野指针常用的方法.这个方法简单有效,只是要注意,当然指针是从函数外层传入的时,在函数内把指针置为空值,对外层的指针没有影响.比如,你在析构函数里把this指针置为空值,没有任何效果,这时应该在函数外层把指针置为空值.

4.内存分配未成功,却使用了它

编程新手常犯这种错误,因为他们没有意识到内存分配会不成功.常用解决办法是,在使用内存之前检查指针是否为NULL.如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查.如果是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理.

5.内存分配虽然成功,但是尚未初始化就引用它

犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组).

内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有.所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦.

5.返回指向临时变量的指针

大家都知道,栈里面的变量都是临时的.当前函数执行完成时,相关的临时变量和参数都被清除了.不能把指向这些临时变量的指针返回给调用者,这样的指针指向的数据是随机的,会给程序造成不可预料的后果.

参见例3: 栈内存的生命周期

6.试图修改常量

在函数参数前加上const修饰符,只是给编译器做类型检查用的,编译器禁止修改这样的变量.但这并不是强制的,你完全可以用强制类型转换绕过去,一般也不会出什么错.

而全局常量和字符串,用强制类型转换绕过去,运行时仍然会出错.原因在于它们是是放在.rodata里面的,而.rodata内存页面是不能修改的.试图对它们修改,会引发内存错误.

参见例6: 指针与数组的比较-修改内容

7.误解传值与传引用

在C/C++中,参数默认传递方式是传值的,即在参数入栈时被拷贝一份.在函数里修改这些参数,不会影响外面的调用者.

参见例9: 指针作为函数的参数

http://blog.csdn.net/procedure1984/article/details/3476812