C++学习之六、有效的内存管理

来源:互联网 发布:矩阵的秩的物理意义 编辑:程序博客网 时间:2024/06/11 00:13
 

有效的内存管理

 

在程序中使用动态内存优点:

1. 动态内存可以在不同的对象与函数之间共享。

2. 动态分配的内存空间的大小可以在运行时确定。

 

预备知识:

int i = 7;

i实在栈上分配的。

int *ptr;

ptr = new int;

指针ptr在栈上,而ptr指向的内存在堆上。

int **handle;

handle = new int*;

*handle = new int;

handle指针在栈上,*handle指针在堆上,*handle指向的内存单元也在堆上。

使用new分配内存时是在堆上分配的,它会返回一个指向分配好的内存块的指针。需要使用delete显示释放。如果忽略了new的返回值,或者指针变量超出了作用域,那么内存就会变成孤立单元,因为你再也无法访问这块内存了。

 

malloc与new的区别:

new不仅仅是分配内存,而且还调用对象的构造函数。malloc只是预留一块固定大小的内存,它并不关心对象是什么。

 

free()与delete的区别:

使用free()时,并不会调用对象的析构函数。使用delete会调用对象的析构函数,并且会正确清除该对象。

 

数组:

 

 

作为经验:不要使用realloc(),这个很危险,因为用户定义的对象不能很好地尽享按位复制。

 

删除数组:

使用用于数组的new(new[])分配内存时,就必须用用于数组的delete(delete[])来释放内存。这个delete除了释放相关内存之外,还会自动撤销数组中的对象。

Simple *mySimple = new Simple[4];

//use mySimple

delete[] mySimple;

当然,只有当数组中的元素是纯对象是才会调用析构函数。如果是一个指针数组,则仍然需要单独地删除各个元素,就向单独分配每个元素一样。

Simple **mySimple = new Simple*[4];

for(int i = 0;i<4;i++)

mySimple[i] = new Simple();

//use mySimple

 

for(int i = 0;i<4;i++)

delete mySimple[i];

 

delete[] mySimple;

 

 

 多维栈数组:

 多维堆数组:

基于堆的多维数组就想基于堆的一维数组一样,可以通过指针对其进行访问。区别在于,对于N维数组,需要N层指针。

下面代码编译不会通过,

char **board = new char[i][j]; //error

因为基于堆的多维数组不像基于栈的多维数组那样工作。为其分配的内存不是连续的,所以此做法是不正确的。正确的做法是:必须建基于堆的数组的一维下标分配一个连续的数组。该数组的每个元素实际上是指向另一个数组的指针,这个数组存储了对应第二维下标的元素。

 

                      

遗憾的是,编译器不会自动给你分配子数组的内存。这就需要你明确分配了。释放时,delete不会自动删除子数组,也需要手动释放。

// new

char ** myArray = new char*[xSize];

for(int i = 0;i<xSize;i++)

myArray[i] = new char[ySize];

//delete

for(int i = 0;i<xSize;i++)

   delete[] myArray[i];

delete[] myArray;

 

使用指针:

由于指针使用相对容易,所以很容易遭到滥用。因为指针仅仅是一个内存地址,所以在理论上可以手动修改。甚至可以做下面不该做的事:

char *p = 7;

上面代码建立了一个指向内存地址为7的指针,它可能是一个随机垃圾,也可能是应用中其他地方正在使用的一个内存。如果使用了不是用new分配的内存区域,最终会破坏与对象关联的内存,这样程序很容易崩溃。

用*对指针进行解除引用。

用&对变量取地址。

 

指针类型那个强制转换:

既然指针仅仅是内存地址,所以指针是弱类型的。指向XML文档的指针和指向整数的指针大小也是一样的。通常使用c风格的类型强制转换,编译器可以很容易地把任何类型的指针强制转换成另一种指针类型。

Document *docPtr = getDocument();

char *charPtr = (char*)docPtr;

 

static类型强制转换的安全性更高一些。编译器会拒绝对指向不同数据类型的指针完成static类型强制转换。

Document *docPtr = getDocument();

char *charPtr = static_cast<char*>(docPtr); //error

如果要强制转换类型的二个指针实际上是指向通过继承相关联的对象,编译器允许完成static_cast类型强制转换。

 

const指针:见const关键字的说明。

在实际中,很少需要保护指针。如果函数能够改变所传递的指针值,这也无关大碍。其作用仅限于这个函数内部,对于调用者而言,指针仍然指向它原来指向的地址。把指针设置为const,这对于说明指针用途更有意义,其实提供不了多少真正的保护。但是保护指针指向的值则是很常见,从而防止重写共享数据。

 

数组与指针的对应:

指针和数组之间存在某种重叠。在堆上分配的数组由指向第一个元素的指针引用。基于栈的数组使用数组语法([])来引用。

基于栈的数组的地址其实就是第0个元素的地址,数组名就是指向第0个元素的指针。只是这个指针不能改变。

可以函数传递基于栈或者基于堆的数组。在传递基于栈的数组时,编译器会自动把数组变量退化为指向数组的指针来处理。在传递基于堆的数组时,因为指向数组的指针已经存在,只是简单地按值传递给函数即可。如果函数取数组作为实参,并改变了数组中的值,这个函数实际上是修改了原始数组。,而不是数组的副本。其实这样是因为C++考虑效率问题,而没有采用复制数组,而是采用将其退化为指针来处理,因为复制数组需要花较多的时间,还可能消耗大量的内存空间。

 

字符串:

C风格的字符串:

千万记住C风格字符串后面还有一个占空间的’\0’;

字符串直接量:与字符串直接量相关联的内存位于内存的只读部分。

char *ptr = “hello”; //字符串直接量赋给变量,ptr指向了这个只读内存

ptr[1] = ‘a’; //不能这样做, 这是个字符串常量,不能修改

安全的做法就是:

const char *ptr = “hello”; //字符串直接量赋给const 变量

ptr[1] = ‘a’;

也可以使用字符串直接量作为基于栈的字符数组的初始值,因为基于栈的变量在任何情况之下都不可能引用其他地方的内存,所以编译器会负责把字符串直接量复制到基于栈的数组内存中。

char stackArray[] = ‘hello’;

char stackArray[1] = ‘o’; //ok

C风格的字符串的优缺点:

优点:1.比较简单,利用了底层的基本字符类型和数据结构。

      2.占用空间小,如果正确使用,它们只需占用真正需要的内存空间。

      3.更底层,所以可以作为原始内存很容易地进行处理和复制。

      4.程序员能够更好地理解。

缺点:

      1.不能忍受内存bug的存在,而且很受其影响。

      2.没用充分利用C++面向对象特性。

      3.提供的辅助函数命名很糟糕,有时还会把人搞糊涂。

      4.要求程序员了解字符串的底层表示。

 

C++的字符串类string:

基于操作符重载的魔力,string使用+连接二个字符串,=进行赋值(会进行字符串复制),==进行比较,[]进行访问单个字符。

可以使用c_str()把C++string转化为C风格的字符串

 

 

低级的内存操作:

C++的主要优点之一就是不需要特别担心关于内存的问题。如果代码用到了对象,只需要确保各个类能适当地管理它自己的内存即可。通过构造和撤销对象,编译器会告诉你什么时候做什么,从而帮助你管理内存。但是出于某些应用,可能会遇到这种情况,即需要在低层次上使用内存。

 

指针运算:指针加1是指针向前移动一个单位。同类型指针减是二指针之间的元素的个数。

如果编一个把字符串转换为大写,char *toCaps(const char * inString);

如果只想想把字符串myStr的后面转换为大写,则可以这样调用  toCaps(myStr+5);

 

自定义内存管理:大多数情况之下,内置的内存分配功能就已经足够了,但是如果资源要求紧张,就完全可以自己来管理内存。自己管理内存可能会减少资源开销。使用new分配内存时,程序还需要保留一小块空间来记录已经分配了多少内存空间。这样,调用delete时,就可以释放适当数量的内存。对于大部分对象,相对于分配的内存来说,这个开销要小得多,所以并没有太大的区别。然而,对于小的对象或者有大量对象的程序,这种开销可能会有较大的影响。自己管理内存时,你事先已经知道每个对象的大小,所以可以避免这个开销。对于大量的小对象来说,与使用new和delete的方法相比,这样就会带来很大的差别。

 

垃圾回收:在支持垃圾回收的环境中,程序员很少需要显式地释放与对象关联的内存。取而代之的是,有一个低优先级的后台任务负责监视内存状态,清理它认为不需要的内存。

不同于java语言,在C++中,没有把垃圾回收作为内置功能。大部分的C++程序通过new和delete在对象层次上管理内存。在C++中实现垃圾回收也不是不可能,但是要想从释放内存的任务中解脱出来,可能又会带来新的问题。

垃圾回收的一种方法称为标记和清扫。使用这种方法,垃圾回收器会周期性地检查程序中得每个指针,并标记所引用的内存仍在使用。在循环结束时,没有标记的内存就认为未在使用,可以释放。

需要完成的步骤:

1.   向垃圾回收器注册所有的指针,这样就可以很容易地扫描整个指针列表。

2.   让所有对象派生一个混合类(如GarbageCollectible),它允许垃圾回收器把对象标记正在使用。

3.   确保垃圾回收器运行时不会对指针做修改,以此来保护对对象的并发访问。

这个简单的垃圾回收方法要求程序员很仔细才行。与delete相比,这种方法可能更容易带来错误。在C++中已经试图建立一种安全而容易的机制来完成垃圾回收,但是即使在C++确实提供了一个理想的垃圾回收实现,也不一定适用于所有的应用。

垃圾回收存在以下缺点:

1.   垃圾回收器主动运行时,可能会使程序的运行减慢。

2.   如果程序大量地分配内存,那么垃圾回收器可能跟不上这个速度。

3.   如果垃圾回收器本身有bug,或者认为一个已经抛弃的对象仍然在使用,可能会造成不可恢复的内存泄露。

 

对象池:后面再分析。

 

函数指针:每个函数的确都位于一个特定的地址。在C++中,可以把函数作为数据使用,换句话说,可以把函数的地址作为参数,就想变量一样使用。

函数指针根据参数类型和兼容函数的返回类型来确定函数类型。使用函数指针最容易的方法就是使用typedef机制来为一组有给定特征的函数赋一个函数名。下面声明了一个类型YesNoFcn,它表示一个指针,该指针指向有二个int参数且返回bool类型的任意函数。

 

typedef bool (*YesNoFcn)(int,int);

既然有了新类型,就可以编写一个取YesNoFcn作为参数的函数了。

void findMatches(int values1[],int values2[],int numValues,YesNoFcn inFunction)

{

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

      if(inFunction(values1[i],values[2]))

          cout<<”match! “;

      else cout<<”not match! “;

   cout<<endl;

}

 

bool intEqual(int inItem1,int inItem2)

{

  return inItem1==inItem2;

}

//调用

int a[2]={1,2},b[2]={1,3};

findMatches(a,b,2,&intEqual);

 

 

常见的内存陷阱:

1.   字符串空间分配不足

char str[3] = “yes”; //error 还有一个’\0’

 

2.   内存泄露

如果分配了内存,但是忘记了释放,此时就容易产生内存泄露。(跟踪内存使用情况可以使用免费的valgrind工具)。

也可以使用智能指针避免内存泄露。即如果把一切都放在栈中,这就可以避免与内存相关的大多数问题。栈比堆更安全,因为栈变量一旦超出作用域时就会自动撤销和清除。智能指针结合了栈变量的安全性和堆变量的灵活性。它是一个带有关联指针的对象。当智能指针超出作用域时,会删除关联的指针。本质上讲就是在一个基于栈的对象内包装一个堆对象。

C++标准模板库包含了一个智能指针的基本实现。叫做auto_ptr。可以把动态分配的对象存储在基于栈的auto_ptr实例中,而不是存储在指针中。不需要显式地释放与auto_ptr关联的内存-auto_ptr超出作用域时,与之关联的内存会得到清除。

void leaky()

{

   Simple * mySimple = new Simple();

   mySimple->go();

}//没有显式地释放内存,删除对象。

void leaky()

{

   auto_ptr<Simple> mySimple(new Simple);

   mySimple->go();

}

智能指针也和标准指针一样可以使用* ->来解除引用。

 

3.二次删除与无效指针

一旦使用delete释放了与指针关联的内存,程序中得其他部分就可以使用这段内存了。但是,没有什么能阻止你试图继续使用这个指针。二次删除也是一个问题。如果在指针上第二次使用delete,程序可能会释放已经指派给另一个对象的内存。

许多内存泄露检查程序(如valgrind)也会检查二次删除和使用已释放的对象等问题。

 

4.访问越界指针

导致越界写内存的bug经常称为缓冲区溢出错误。这样的bug已经被一些能力极强的病毒和蠕虫所利用,狡猾的黑客也可以利用只一点重写内存的某一部分。从而在正在运行的程序中注入代码。

幸运的是,许多内存检测工具也可以检测出缓冲区溢出错误。而且,尽管C风格的字符串和数组写入内容时存在大量相关的bug,但是如果使用像C++字符串和向量这样一些高级构造的话,有助于防止这样一些bug。

 

原创粉丝点击