第三部分、有效的内存管理
一、使用动态内存
在程序中使用动态内存有两个主要的优点:
· 动态内存可以在不同的对象与函数之间共享。
· 动态分配的内存空间的大小可以在运行时确定。
1、 如何描述内存
句柄:用于描述一个指针,它指向的也是一个指针,后者指向某个内存单元。
2、 内存的分配与撤销
new int
;
à 孤立内存
new不仅仅会分配内存,它还会
构造对象。
int* ptr = new (nothrow) int; //new的另外一个版本,它不会抛出异常,而只是返回一个NULL。
3、 数组
· 把数组放在堆中的优点:
可以使用动态内存在运行时定义数组的长度。
· 注:尽量不要再C++中使用realloc()函数,因为用户定义的对象不能很好的进行按位复制。
· 对象数组:使用new会对每个对象
自动调用无参数的构造函数,这样一来,使用new分配对象数组就会返回一个指针,它指向一个充分构建并已初始化的对象数组。
· 删除数组:
delete [] object_ptr;
· 不要把new和delete与
new[]和
delete[]相混淆,它们是分别配对使用的。
4、
多维堆数组
基于堆的多维数组不像基于栈的多维数组那样工作,为其分配的内存不是连续的。
正确的做法:
—
〉必须先为基于堆的数组的第一维下标分配一个连续的数组,该数组的每个元素实际上是指向另一个数组指针的,这个数组存储了对应第二维下标的元素。
例:
new
:
char** array = new char*[xValue]; //Allocate first dimension.
for(int i = 0; i < xValue; i++) {
array[i] = new char[yValue]; //Allocate ith dimension.
}
delete
:
for(int i = 0; i < xValue; i++) {
delete [] arrar[i]; //delete ith..
}
delete [] array; // delete first..
5、使用指针
多层指针实际上只是访问数据过程中的各个步骤。
对指针解除引用,是告诉程序要在内存再深入一层。
1
、指针类型强制转换
既然指针仅仅是内存地址,所以指针时弱类型的。指向XML文档的指针与指向整数的指针大小也是一样的。通过使用C风格的类型强制转换,编译器可以很容易地把任何指针类型强制转换为另一个指针类型。
例:
Document* documentPtr = GetDocument();
char* charPtr = (char*)documentPtr;
static类型强制转换的安全性更高些。编译器会拒绝对指向不同数据类型的指针完成static类型强制转换。
例:
static_cast<char*> (documentPtr);//BUG...
如果要强制转换类型的两个指针实际上是
指向通过继承相关联的对象,编译器会允许完成
static类型强制转换,但是在继承层次结构中,
动态的(
dynamic_cast
)类型强制转换更为安全。
2
、
const
指针
如果
const
位于类型前面,它表示指针指向的指是受保护的,在数组中,就表示数组的各个元素是const的。
要
保护指针本身,关键字const要直接位于
变量名的前面。
例:
const int* const value; //指针和指针指向的值都是受保护的。
二、数组与指针的对应
在
堆上分配的数组由指向第一个元素的指针来引用。
1、数组即指针
使用数组语法声明的数组可以通过指针来访问,
把数组传递给函数时,总是作为指针传递的(效率考虑的可能性)。
例:
void doubleInts(int* arrary, int size); //指针传递
void doubleInts(int array[], int size); //也是指针传递
2、指针并非都是数组
指针和数组之间存在微妙但很重要的区别,指针和数组有很多共同的性质,有时候可以交替使用,但是它们并不完全相同。
例:
int* ptr = new int;
//
数组会自动引用为指针,但是并非所有的指针都是数组。
三、动态字符串
1、C风格的字符串
C语言中,字符串表示为字符数组。
2、字符串直接量
例:
cout << “hello” << endl; //字符串直接量或字符串字面量
与字符串直接量相关联的具体内存空间位于
内存的只读部分,这就是为什么他是常量字符数组的原因。这就允许编译器通过
重用指向等价字符串直接量的引用来优化内存的使用(即使程序使用了字符串直接量“hello”500次,编译器只是在内存中创建hello一个实例)。
3、C++的字符串类
string类作为C++标准库的一部分,实际上是basic_string模板类的一个实例化,这个类最好的一点,如果使用得当,
string
类会负责分配内存。
由于兼容性考虑,可以使用方法
c_str()把C++string转化为C风格的字符串。
四、低级的内存操作
1、指针运算
C++编译器使用指针的声明类型来支持完成
指针运算。
指针运算的另一个有用的应用涉及到减法:将一个指针减去同类型的另一个指针,所得到的是
两个指针之间的元素个数,而不是两个指针之间的绝对字节数。
2、自定义内存管理
基本上内置的内存分配功能就已经足够了,在后台new和delete会完成所有的这些工作:以适当大小的块分配内存,维护可用的内存列表,删除内存时再把内存块释放到可用的内存列表中。
使用new分配内存时,程序还需要保留一小块空间来记录已经分配了多少内存空间,这样,调用delete时就可以释放适当数量的内存。对于大部分对象,相对于分配的内存来说,这个开销小的多,所以并没有太大的区别。然而,
对于小对象或大量对象的程序,这种开销可能会有较大的影响。
3、垃圾回收
垃圾回收存在以下缺点:
· 垃圾回收器主动运行时,可能会使程序的运行减慢。
· 如果程序大量地分配内存,那么垃圾回收器可能跟不上这个速度。
· 如果垃圾回收器本身有bug,或者认为一个已经抛弃的对象仍然在使用,可能会造成 不可恢复的内存泄漏。
4、对象池
对象池模拟了内存的再利用,来提高性能效率的细节问题。
5、函数指针
正常情况下,不会考虑函数在内存中的位置,但是每个函数的确都位于一个特定的地址上。在C++中,可以把函数作为数据使用,换句话说,可以把
函数的地址作为参数,就像变量一样使用。
函数指针根据
参数类型和兼容函数的
返回类型来确定类型。
例:
typedef bool(*YesNoFunc)(int, int);
参考代码:

























































五、常见的内存缺陷
1、字符串空间分配不足
C风格的字符串操作函数有可能把字符串的最后部分写到固定大小之外的空间中。
2、内存泄漏
Valgrind免费工具是面向Linux的开源工具,用于内存跟踪。
3、智能指针(smart pointer)
智能指针概念源于这样一个事实:即如果把一切都放在栈中,就可以避免与内存相关的大多数问题。栈比堆更安全,因为栈变量超出作用域时,它们会自动撤销和清除。
智能指针结合了栈变量的安全性和对变量的灵活性。
智能指针基本原理:它是一个
带有关联指针的对象,当智能指针超出作用域时,会删除关联的指针,本质上讲,
就是在一个基于栈的对象内部包装一个堆对象。
C++标准模板库提供了一个智能指针的实现:
auto_ptr。可以把动态分配的对象存储在基于栈的auto_ptr实例中,而不是存储在指针中。不需要显示地释放与auto_ptr关联的内存,auto_ptr超出作用域时,与之关联的内存会得到清除。
例:
auto_ptr
<Simple> mySimple(new Simple());
智能指针也可以象标准指针一样,解除引用,调用函数等。
4、二次删除与无效指针
Valgrind会检查二次删除和使用已释放的对象的问题。
5、访问越界指针
导致越界(超过数组)写内存的bug经常称为[
缓冲区溢出错误]。