ANSI/ISO C++ Professional Programmer's Handbook 11


ANSI/ISO C++ Professional Programmer's Handbook




Contents




11


内存管理


by Danny Kalev




简介


C++比C增加了必要的内存模型以支持对象语义。另外,它修补了原来模型的漏洞,并将内存模型变得更抽象、更自动。本章深入C++内存模型。首先从三种存储类型开始,接着讨论不同版本的newdelete运算符,最后示范一些为了更高效率和更少错误的内存使用技术和方针。


存储的类型


C++有三种基本存储类型:自动存储,静态存储和自由存储。三种内存类型都有不同的初始化语义和生存期。


自动存储


没有显式申明为staticextern的局部对象、申明为autoregister的局部对象和函数参数是自动存储。这种类型的存储也叫堆栈内存(stack memory)。自动对象在进入函数或块时自动创建,退出函数或块时销毁。因此,在每一次进入函数或块时,自动对象的新的拷贝被创建。自动变量和非类对象的默认值是不确定的。


静态存储


全局对象、类的静态数据成员命名空间变量和函数的静态变量在静态内存中。


静态对象的地址保存到程序结束。


每一个静态对象在程序生存期内只初始化一次。默认情况,静态对象初始化为0。有必要构造器(nontrivial constructor)的静态对象(参见第四章“特殊成员函数:默认构造器,拷贝构造器,销毁器和分配运算符特殊成员函数:默认构造器,拷贝构造器,销毁器和赋值运算符”)由他们的构造器初始化。下面的例子包括了静态存储对象:



int num; //全局变量
int func()
{
static int calls; //默认初始化为0
return ++calls;
}
class C
{
private:
static bool b;
};
namespace NS
{
std::string str; //str是静态存储
}

自由存储


自由存储内存,也叫堆内存(heap memory)动态内存, 包含由运算符new创建的对象和变量。在自由存储空间分配的对象和变量一致存在,直到显式的调用运算符delete来释放它。从自由存储空间分配的内存不能被在程序结束的时候被系统自动的收回。


因此,没有释放由new分配的内存将导致内存泄漏。从自由内存分配对象的地址在运行期都是确定的。由new分配空间的原始值是未指明的。


POD(无格式数据)对象和非POD对象


POD(无格式数据)对象是下列数据类型之一:基本类型,指针,联合,结构,数组或不需要构造器的类。相反的,非POD对象指存在必要构造器的对象。在对象的生存期内其属性是有效的。


POD对象的生存期


从获得存储空间开始POD对象开始了它的生存期,生存期结束于它占用的存储空间被释放。


非POD对象的生存期


非POD对象的生存期开始于调用其构造器之后;结束于开始调用销毁器。


分配和释放函数


C++定义全局分配函数 newnew[],对应的分别是全局释放函数 deletedelete[]。不需包含头文件这些函数<new>就可以在程序的每一个translation unit中使用这些函数。他们隐含申明如下:



void* operator new(std::size_t) throw(std::bad_alloc); // new
void* operator new[](std::size_t) throw(std::bad_alloc); // new []
void operator delete(void*) throw(); // delete
void operator delete[](void*) throw(); // delete[]

隐含申明仅引入函数名字运算符new、运算符new[]、运算符delete和运算符 delete[]。但是,他们不引入名字stdstd::bad_allocstd::size_t。显式的引用这些名字中的一个都需要包含适当的头文件。例如



#include <new> //std和size_t的申明
using namespace std;
char * allocate (size_t bytes);
int main
{
char * buff = allocate(sizeof (char) );
return 0;
}

分配函数的语义


分配函数的返回类型是void *,它的第一个参数是类型size_t。第一个参数的值被解释成需要的内存大小。分配函数试图在空闲内存中分配指定大小的内存。如果分配请求成功,它返回内存块的首地址,内存块大小以字节记,至少和指定的大小一样。


释放函数的语义


释放函数的返回类型是void;它的第一个参数是类型void *。释放函数可以有一个以上参数。第一个参数的值可以是NULL(这种情况下,调用释放函数没有效果)。否则,给释放函数的值必须是前面调用分配函数返回的值。分配和释放函数执行基本的从空闲内存分配和释放内存的操作。但是注意,一般情况下你不直接调用这些函数。一般,,使用new表达式delete表达式。new表达式隐含的调用分配函数再调用构造函数;同样的,delete表达式销毁对象再调用释放函数释放已销毁对象的内存。





注意:再下面的章节中, newdelete分别指 new表达式和 delete表达式,除非特别指明。



malloc()和free()VS. new和delete


C++仍然提供标准C库函数malloc()free()。于C的兼容性在三种情况下是有用的:在C++程序中使用以前用C写的代码;写在环境中也被支持的C++代码(更多信息参见第十三章“与C语言的兼容性问题”);newdelete是通过调用malloc()free()来实现的。


除此之外,在C++代码中不使用malloc()free()因为——与newdelete不一样——他们不支持对象语义。newdelete也更安全更有可扩展性。


对象语义的支持


newdelete自动调用和销毁对象。另一方面,malloc()free()仅仅从堆中分配和释放生鲜内存。使用malloc()创建一个非POD对象将导致未定义行为。例如



#include <cstdlib>
#include <string>
using namespace std;
string* func() //非常糟糕
{
string *pstr = static_cast<string*> (malloc (sizeof(string))); //灾难!
return pstr; //将pstr作为指向string的指针是未定义行为
}

安全


运算符new自动计算要构造对象的大小。而使用malloc()的话,程序员就要显式的指定要分配内存的有多少比特。另外,malloc()返回void指针,必须显式的转换成需要的类型。这不仅单调而且危险。运算符new返回指定类型的指针,所以不需要显式的转换。例如



#include <cstdlib>
using namespace std;
void func()
{
int * p = static_cast<int *> malloc(sizeof(int));
int * p2 = new int;
}


可扩展性


运算符new可以被类重载。这个特性使得特定的类可以使用不同内存管理方针,就像你将看到的。而malloc()不能被特定的类重载。


使用free()来释放由new分配的内存,或用delete来释放由malloc()分配的内存都是未定义的。标准不保证是用malloc()实现运算符new;此外有些实现中malloc()new使用不同的堆。


new和delete


分配和释放数组使用new[]和delete[]


new[]分配指定类型对象的数组。new[]的返回值是数组第一个元素的地址。例如



int main()
{
int *p = new int[10];
bool equal = (p == &p[0]); //true
delete[] p;
return 0;
}

new[]分配的对象必须用delete[]来释放。若使用delete代替导致未定义行为。这是因为当new[]执行时,运行时系统存储了分配的数组元素的个数,记录的方式依赖具体编译器。相应的delete[]表达式检索到分配元素的个数再调用同样多次销毁器。new[]是怎么在分配数组时存储数组元素个数的呢?广泛使用的技术是分配而外的sizeof(std::size_t);就是说,对于类C,表达式



C * p = new C[n];

分配一块包含sizeof(std::size_t) + n * sizeof字节的内存缓冲区。值n在第一个C对象之前写入分配的缓冲区。当调用delete[]时,他查询p之前固定偏移出的np必须指向数组的第一个元素)。delete[]再调用C的销毁器,最后释放内存块。而无格式delete不执行偏移调整——它简单的调用p 指向对象的销毁器。


另外的一种技术是在联合数组中存储n,联合数组的p作为关键字,n作为关联值。当语句



delete[] p;

执行时,delete[]在联合数组中查找p例如



std::map<void *, std::size_t>

并检索它的关联值n。其他存储数组元素个数的技术也可以使用,但是不管那种技术,使用delete代替delete[]来释放由new[]分配的对象数组都会导致未定义行为,一定不能发生。类似的,使用delete[]来释放由无格式new分配的单个元素也是一场灾难:它将导致内存泄漏,堆崩溃或程序崩溃。


与流行的观点相反,规则同样适用与基本类型——不仅仅适用对象数组。尽管delete[]不调用基本类型的销毁器,它仍然必须检索数组元素的个数来计算要释放内存块的大小。例如



#include<string>
void f()
{
char *pc = new char[100];
string *ps = new std::string[100];
//...使用pc和ps
delete[] pc; //不调用销毁器,但仍然需要delete[]来释放new[]分配的数组
delete[] ps //保证每一个元素的销毁器都被调用
}

异常和运算符new


在以前的C++标准中,当不能分配要求大小的内存时,new返回NULL指针,new
象C的malloc()。使用指针之前,程序员必须检查new返回的指针是不是NULL。例如



void f(int size) //错误的使用new
{
char *p = new char [size];
if (p == 0) //到1994年之前都是对的
{
//...安全的三使用p
delete [] p;
}
return;
}
const int BUF_SIZE = 1048576L;
int main()
{
f(BUF_SIZE);
return 0;
}

然而失败时返回NULL指针是有问题的。(注意NULL指针方针对于无格式newnew[]都是可适用的。类似的,改进行为适用于newnew[]。)它迫使程序员每一次调用运算符new之 后都要检查返回值,这即麻烦又容易出错。另外,每一次都测试返回指针增加了程序的大小,也增加了运行期性能开销(可以回想一下在第六章“异常处理”讨论的 返回值方针的缺点)。动态内存分配的失败者是稀少的而且一般表示不稳定的系统状态。异常处理就是来对付这种运行期错误的。由于这些原因,C++标准化委员 会在几年后改变规范。标准现在规定运算符new失败时抛出类型为std::bad_alloc的异常,而不是返回NULL指针。





警告:尽管编译器厂商在适应变化方面行动迟缓,大多数C++编译器现在执行标准,当 new失败时抛出类型 std::bad_alloc的异常。请检查你的编译器的手册以获得更多细节。



直接调用或间接调用(比如使用STL容器)new的程序都必须包括适当的捕获std::bad_alloc异常的处理程序。否则,当new失败时,程序将由于未捕获的异常而终止。异常处理方针也意味着测试new的返回值是完全没用的。如果new成功,多余的测试浪费系统资源。另一方面,分配失败时在异常抛出的地方中断执行线程——所以测试永远不会被执行。修改前面的程序使之符合标准:



void f(int size) //符合标准的使用new
{
char *p = new char [size];
//...安全的使用p
delete [] p;
return;
}
#include <stdexcept>
#include <iostream>
using namespace std;
const int BUF_SIZE = 1048576L;
int main()
{
try
{
f(BUF_SIZE);
}
catch(bad_alloc& ex) //f()抛出异常的处理程序
{
cout<<ex.what()<<endl;
//...其他诊断和补救
}
return -1;
}

运算符new的不抛出异常版本


尽管如此,在有些情况下不希望抛出异常。例如异常处理影响性能的提高;在有些平台上,不是完全支持异常处理。


由于这些原因标准化委员会在标准增加了new的不抛出异常版本。new的不抛出异常版本在失败时返回NULL指针而不是抛出std::bad_alloc异常。new的这个版本接受一个类型为const std::nothrow_t&的附加参数(在头文件<new>中定义)。它也有两个,一个无格式new另一个new[]



//new和new[]的不抛出异常版本在头文件<new>定义
void* operator new(std::size_t size, const std::nothrow_t&) throw();
void* operator new[](std::size_t size, const std::nothrow_t&) throw();

不抛出异常new也叫nothrow new。下面是使用方法:



#include <new>
#include <string>
using namespace std;
void f(int size) //示范nothrow new
{
char *p = new (nothrow) char [size]; //数组nothrow new
if (p == 0)
{
//...使用p
delete [] p;
}
string *pstr = new (nothrow) string; //无格式nothrow new
if (pstr == 0)
{
//...使用 pstr
delete [] pstr;
}
return;
}
const int BUF_SIZE = 1048576L;
int main()
{
f(BUF_SIZE);
return 0;
}

参数nothrow在头文件<new>中定义:



extern const nothrow_t nothrow;

nothrow_t定义如下:



struct nothrow_t {}; //空类

换句话说,类型nothrow_t是一个空类(空类在第五章“面向对象的编程和设计”中讨论),它的唯一用处是为了重载全局new


Placement new


另一个版本的运算符new使你能在指定的地方构造对象(或对象数组)。这个版本叫placement new,它有很多用途,包括创建定制的内存池或垃圾搜集器。它也可以用于任务临界区应用程序,因为那儿没有分配失败的危险(由placement new使用的内存是已经分配的)。Placement new也更快,因为构造对象的内存是已分配好的。下面是使用placement new的例子:



#include <new>
#include <iostream>
using namespace std;
void placement()
{
int *pi = new int; //无格式new
float *pf = new float[2]; //new []
int *p = new (pi) int (5); //placement new
float *p2 = new (pf) float; //placement new[]
p2[0] = 0.33f;
cout<< *p << p2[0] << endl;
//...
delete pi;
delete [] pf;
}

通过Placement new创建的对象必须显式的销毁

通过Placement new创建的对象的销毁器必须显式的调用。为了说明为什么,考虑下面的例子:



#include <new>
#include <iostream>
using namespace std;
class C
{
public:
C() { cout<< "constructed" <<endl; }
~C(){ cout<< "destroyed" <<endl; }
};
int main()
{
char * p = new char [sizeof ]; //预分配的缓冲区
C *pc = new (p) C; // placement new
//... used pc
pc->C::~C(); // 1:需要显式的调用销毁器
delete [] p; //2
return 0;
}

如果不显式的调用销毁器(1),由p指向的对象永远不会被销毁,但它创建的内存块会被delete[]语句释放(2)。


构造对象期间的异常


前面提到,new指向两个操作:它先调用分配函数从空闲内存中分配生鲜内存,再在生鲜内存上构造对象。问题是在构造对象时抛出异常可能导致内存泄漏吗?答案是不会,不是吗?在异常传递给程序之前分配的内存会还给空闲内存。因此,调用运算符new可以解释为两个连续操作。第一步仅从空闲内存中分配足够大小的内存。如果失败,系统抛出类型为std::bad_alloc的异常。如果第一个操作成功,开始第二个操作。第二步用前一步保留的指针来调用对象的构造函数。不同的是,语句



C* p = new C;

被编译器转换成类似下面的代码



#include <new>
using namespace std;
class C{/*...*/};
void __new() throw (bad_alloc)
{
C * p = reinterpret_cast<C*> (new char [sizeof ]); //第一步:分配生鲜内存
try
{
new (p) C; //第二步:在前面分配的缓冲区上构造对象
}
catch(...) //捕捉C的构造器抛出的异常
{
delete[] p; //释放分配的缓冲区
throw; //重新抛出C的构造器的异常
}
}

对齐(alignment)的考虑


new返回的指针有适当的对齐属性,所以它可以转换成指向任何类型对象的指针,再用它来访问对象或数组。因此,允许你从字符数组中分配类型到后面再确定的对象。例如



#include <new>
#include <iostream>
#include <string>
using namespace std;
class Employee
{
private:
string name;
int age;
public:
Employee();
~Employee();
};
void func() //使用预分配的字符数组来构造不同类型对象
{
char * pc = new char[sizeof(Employee)];
Employee *pemp = new (pc) Employee; //再字符数组上构造
//...使用pemp
pemp->Employee::~Employee(); //显式的销毁
delete [] pc;
}

使用堆栈上分配的缓冲来避免内存泄漏可能是诱人的:



char pbuff [sizeof(Employee)];
Employee *p = new (pbuff ) Employee;//未定义行为

但是,自动存储类型的char数组不能保证必要的对齐要求。因此,在自动存储类型的预分配缓冲中构造对象是未定义行为。此外,在有静态或自动存储类型的const对象上创建新对象也将导致未定义行为。例如



const Employee emp;
void bad_placement() //试图在局域存储const对象上构造新对象
{
emp.Employee::~Employee();
new (&emp) const Employee; //未定义行为
}

成员对齐


类或结构的大小可能比每一个数据成员的大小之和要大。这是因为允许编译器增加附加的padding(填料)字节来使成员的大小适应机器字长(参见第十三章)。例如



#include <cstring>
using namespace std;
struct Person
{
char firstName[5];
int age; // int占用4 bytes
char lastName[8];
}; //Person的实际大小很有可能大于17 bytes
void func()
{
Person person = {{"john"}, 30, {"lippman"}};
memset(&person, 0, 5+4+8 ); //可能不能正确的删除person的内容
}

在32-bit体系上,在Person的第一个和第三个元素之间可能会插入三个附加字节,Person的大小可能增加为20。


在有些编译器上,调用memset()不会清除成员lastName的最后三个字节。因此要使用sizeof运算符来计算正确的大小:



memset(&p, 0, sizeof(Person));

完全的空对象的大小也不可能是0


空类没有任何数据成员和成员函数。因此它一个实例的大小表面上是0。但是,C++保证真对象的大小永远不会是0。考虑下面的例子:



class Empty {};
Empty e; // e 至少占用1 byte的内存

如果允许对象占用0 bytes。它的地址可能和不同对象的地址重叠。最显然的情况是空对象数组中所有元素的地址都是一样的。为了保证真对象中是有不同内存地址,真对象必须至少占用一字节的空间。假对象——例如,在派生类中的基类子对象——可以占用0字节。


用户定义版本的new和delete不能在命名空间中申明


用户定义版本的newdelete可以在类中申明。但是将他们申明在命名空间中是非法的。为了说明为什么,考虑下面的例子:



char *pc;
namespace A
{
void* operator new ( size_t );
void operator delete ( void * );
void func ()
{
pc = new char ( 'a');
}
}
void f() { delete pc; } // A::delete还是::delete?

在命名空间A申明的newdelete即混淆编译器又混淆程序员。有些程序员可能期望在函数f()中选择运算符A::delete,因为它匹配在分配使用的new。相反的,另一些程序员期望调用delete,因为A::deletef()中不可见。由于这些原因,标准化委员会决定禁止在命名空间中申明newdelete


在类中重载new和delete


可以为给定的类定义newdelete的特定形式来代替全局的。因此,对于定义了这些运算符的类C,下面的语句



C* p = new C;
delete p;

分别调用了类的newdelete。当默认的内存管理模式不适应时,定义类的特定newdelete是十分有用的。这种技术也用于需要定制内存池的应用程序。在下面的例子里,类C的运算符new进行了重定义,它改变了在分配失败时的默认行为,不是抛出std::bad_alloc而是抛出 const char *。 匹配的运算符delete也进行了重定义:



#include <cstdlib> //malloc()和free()
#include <iostream>
using namespace std;
class C
{
private:
int j;
public:
C() : j(0) { cout<< "constructed"<<endl; }
~C() { cout<<"destroyed";}
void* operator new (size_t size); //隐含申明为static
void operator delete (void *p); //隐含申明为static
};
void* C::operator new (size_t size) throw (const char *)
{
void * p = malloc(size);
if (p == 0)
throw "allocation failure"; //代替std::bad_alloc
return p;
}
void C::operator delete (void *p)
{
free(p);
}
int main()
{
try
{
C *p = new C;
delete p;
}
catch (const char * err)
{
cout<<err<<endl;
}
return 0;
}

记住,重载newdelete隐含申明为类的静态成员,即使他们没有显式的申明为静态。也要注意用户定义的new隐含的调用了对象的构造器;同样的,用户定义的delete隐含调用对象销毁器。


有效使用内存的指导方针


正确选择对象的存储类型是实现的关键性决定,因为每种类型的存储类型都导致不同的程序性能,可重用性,可维护性。本节告诉你怎样正确选择对象的存储类型来避免常见的缺陷和性能损失。本节也讨论了与C++内存模型相关的一般性的主题,以及C++与其他语言的比较。


只要可行自动存储比动态分配要好


与自动存储比较,在空闲内存上创建对象将有更大的开销,主要有几个原因:




  • 运行期开销从空闲内存动态分配要调用OS的功能。当空闲内存中有很多碎片,找到一块合适的内存将消耗更多时间。另外,分配失败时的异常处理也增加运行期开销。





  • 可维护性动态分配可能失败;需要另外的代码来处理可能抛出的异常。





  • 安全性一个对象可能以外的被删除多次或更本没有删除。这两者都是bug的丰富源泉,而且在许多应用中导致运行期崩溃。





下面的代码例子示范了与动态分配对象关联的两个常见的错误:



#include <string>
using namespace std;
void f()
{
string *p = new string;
//...使用p
if (p->empty()!= false)
{
//...其他代码
return; //糟糕!内存泄漏:p没有被删除
}
else //string是空的
{
delete p;
//..其他代码
}
delete p; //糟糕!如果isEmpty == false,p被删除了两次
}

在经常动态分配对象的大型程序中,这样的bug是如此的普通。一般的,在结构单一的程序中尽可能的在堆栈上创建对象,消除发生这种bug的可能性。考虑怎样使用局域string对象来简化前面的例子:



#include <string>
using namespace std;
void f()
{
string s;
//...使用s
if (s.empty()!= false)
{
//...其他代码
return;
}
else
{
//..其他代码
}
}

作为一条规则,自动和静态存储类型总比动态分配要好。


局域对象实例化的正确语法


通过调用默认构造器实例化局域对象的正确语法是



string str; //没有圆括号

尽管在类名字后面可以使用空的圆括号,就像这样



string str(); //完全不同的含义

这时语句有完全不同的意思。它被解释成一个函数申明,函数名str,没有参数,返回值是string


统一用0初始化


字面上0是一个int。但是,它可以作为每一个基本类型的同一初始化值。这时候0是一个特殊情况,因为编译器根据它的内容决定它的类型。例如:



void *p = 0; //0隐含的转换成void *
float salary = 0; // 0 转换成float
char name[10] = {0}; // 0 转换成'/0'
bool b = 0; // 0 转换成 false
void (*pf)(int) = 0; //指向函数的指针
int (C::*pm) () = 0; //指向类成员的指针

指针必须初始化


未初始化的指针是不确定的值。这种指针常被被称作狂野指针(wild pointer)。测试狂野指针是否有效几乎是不可能的,特别是作为实参传递给函数时(这时只可能检查它是不是NULL)。例如



void func(char *p );
int main()
{
char * p; //危险:未初始化
//...许多行代码;因为错误p一直没有初始化
if (p)//错误的假定非NULL就是有效地址
{
func(p); //func没有办法知道p是否有有效的地址
}
return 0;
}

即使你的编译器会自动初始化指针,最好显式的初始化他们来保证代码可读性和可移植性。


POD对象的显式初始化


就像前面提到的,默认情况下自动存储类型的POD对象有不确定的值以避免初始化带来的性能下降。但是必要时你可以显式初始化自动POD对象。下面的章节讨论怎样做。


初始化自动结构和数组


初始化自动POD对象的一种方法是调用memset()或类似的函数。但是,有更简单的方法——不调用函数,下面例子:



struct Person
{
long ID;
int bankAccount;
bool retired;
};
int main()
{
Person person ={0}; //保证person的所有成员都初始化为二进制0
return 0;
}

这种技术适用于每一种POD结构。它依赖于第一个成员是基本类型。初始化值0自动被转换成适当的基本类型。它保证了,在初始化列表包含初始化值比成员数目少的时候,余下的成员也被初始化为二进制0。注意即使Person的定义改变了——增加附加成员或成员的顺序改变——它的所有成员仍然被初始化。同样的初始化技术也适用于基本类型的局域自动数组:



void f()
{
char name[100] = {0}; //所有数组元素初始化为'/0'
float farr[100] = {0}; //所有数组元素初始化为 0.0
int iarr[100] = {0}; //所有数组元素初始化为 0
void *pvarr[100] = {0};//void *数组的所有元素初始化为NULL
//...use the arrays
}

这种技术可以用于任何数组和结构的组合:



struct A
{
char name[20];
int age;
long ID;
};
void f()
{
A a[100] = {0};
}


共用体初始化


你可以初始化一个共用体。但是与结构初始化不同,共用体的初始化列表必须仅包含一个初始化值,它必须指共用体的第一个成员。例如



union Key
{
int num_key;
void *ptr_key;
char name_key[10];
};
void func()
{
Key key = {5}; //Key的第一个是int类型
//其他元素都被初始化为二进制0
}

检查机器的Endian


术语endian指计算机体系存储一个字的方式。当底位在底地址时(例如Intel的微处理器),它叫做little endian 顺序。相反的,big endian 顺序描述的是底位在高地址底的计算机体系。下面的程序在它执行的时候检查了机器的endian:



int main()
{
union probe
{
unsigned int num;
unsigned char bytes[sizeof(unsigned int)];
};
probe p = { 1U }; //initialize first member of p with unsigned 1
bool little_endian = (p.bytes[0] == 1U); //在big endian体系中 architecture,p.bytes[0]等于0
return 0;
}

临时对象的生存期


你可以安全的绑定一个引用到一个临时对象。临时对象的生存期持续到引用的生存期结束。例如



class C
{
private:
int j;
public:
C(int i) : j(i) {}
int getVal() const {return j;}
};
int main()
{
const C& cr = C(2); //绑定引用到一个temp;temp的销毁延迟到程序结束
C c2 = cr; //安全的越界使用引用
int val = cr.getVal();
return 0;
}//temporary连同它的引用一起销毁

多次删除一个指针


多次使用delete删除一个指针的结果是未定义的。显然,不能发生这种错误。但是,可以通过再删除指针之后指针赋为NULL来避免这种错误。删除NULL指针是无害的。例如



#include <string>
using namespace std;
void func
{
string * ps = new string;
//...使用ps
if ( ps->empty() )
{
delete ps;
ps = NULL; //安全保证:删除ps将是无害的
}
//...其他代码
delete ps; //第二次删除ps。但无害
}

数据指针VS.函数指针


C和C++对于数据指针和函数指针都有明显的区别。函数指针具体表达了好几种含义比如函数符号和返回值。另一方面,数据指针仅保存了变量第一个比特的地址。两者之间的显著区别使得C标准化委员会禁止使用void*来表示函数指针,反之亦然。在C++中,这个限制是不严格的,但是强迫函数指针转换到void*的行为是依赖于具体编译器的。反过来——转换数据指针到函数指针——是非法的。


指针的等于关系


同类型的对象指针或函数指针在三种情况下可认为他们是相等的:




  • 两者都是NULL。例如






int *p1 = NULL, p2 = NULL;
bool equal = (p1==p2); //true



  • 两者指向同一个对象。例如






char c;
char * pc1 = &c;
char * pc2 = &c;
bool equal = (pc1 == pc2); // true



  • 两者指向同一数组后面的同一位置。例如






int num[2];
int * p1 = num+2, *p2 = num+2;
bool equal = ( p1 == p2); //true

重分配


除了malloc()free(),C也提供函数realloc()来改变已存在缓冲区的大小。C++没有相应的重分配运算符。给C++增加一个运算符renew是向标准化委员会提出最频繁的建议。作为代替,有两种方法调整已分配内存块的大小。第一种非常不雅而且容易出错。分为三步,先分配一块合适大小的新缓冲,再将原来缓冲区内容拷贝到新缓冲中,最后删除原来的缓冲区。例如



void reallocate
{
char * p new char [100];
//...填充p
char p2 = new char [200]; //分配更大缓冲区r
for (int i = 0; i<100; i++) p2[i] = p[i]; //拷贝
delete [] p; //释放原来的缓冲区
}

显然,这种技术低效、冗长。对于经常改变大小的对象来说这是不可接受的。更好的方法是使用STL的容器类。STL容器在第十章“STL和泛型程序设计”中讨论。译者注:STL容器又是怎么实现的呢?


局域静态变量


默 认情况下,局域静态变量(不要和静态类成员混淆)被初始化为二进制0。理论上,他们在程序开始之前创建,在程序结束之后销毁。但是,和局域变量一样,他们 只能在其申明的范围内可被访问。这种属性使得静态变量经常用于保存函数前一次调用时的状态,因为他们保留了前一次函数调用时的值。例如



void MoveTo(int OffsetFromCurrentX, int OffsetFromCurrentY)
{
static int currX, currY; //初始化为0
currX += OffsetFromCurrentX;
currY += OffsetFromCurrentY;
PutPixel(currX, currY);
}
void DrawLine(int x, int y, int length)
{
for (int i=0; i<length; i++)
MoveTo(x++, y--);
}

但是,当需要保存函数状态时,更好的设计选择是使用类。类成员函数代替成员函数,成员函数代替全局函数。在成员函数中的局域静态对象需要特别关注:每一个继承了这个成员函数的派生类引用的是同一个局域静态变量的实例。例如



class Base
{
public:
int countCalls()
{
static int cnt = 0;
return ++cnt;
}
};
class Derived1 : public Base { /*..*/};
class Derived2 : public Base { /*..*/};
// Base::countCalls(), Derived1::countCalls() 和Derived2::countCalls
// 共享cnt的同一份拷贝
int main()
{
Derived1 d1;
int d1Calls = d1.countCalls(); //d1Calls = 1
Derived2 d2;
int d2Calls = d2.countCalls(); //d2Calls = 2, 而不是 1
return 0;
}

成员函数countCalls的局域静态变量可以用来记录调用成员函数的总次数,不管在那里的对象调用了它。但是,显然程序员的意图是记录Derived2对象调用成员函数的次数。为了达到目标,可以用静态类成员代替:



class Base
{
private:
static int i;
public:
virtual int countCalls() { return ++i; }
};
int Base::i;
class Derived1 : public Base
{
private:
static int i; //隐藏了Base::i
public:
int countCalls() { return ++i; } //覆盖了Base:: countCalls()
};
int Derived1::i;
class Derived2 : public Base
{
private:
static int i; //隐藏了Base::i也不同于Derived1::i
public:
virtual int countCalls() { return ++i; }
};
int Derived2::i;
int main()
{
Derived1 d1;
Derived2 d2;
int d1Calls = d1.countCalls(); //d1Calls = 1
int d2Calls = d2.countCalls(); //d2Calls 也 = 1
return 0;
}

在多线程环境中,静态变量是有问题的,因为他们被共享,必须通过锁来访问。


全局匿名共用体


在指定命名空间或全局命名空间中申明的匿名共用体(anonymous union)(匿名共用体在第十二章“优化你的代码”中讨论)必须显式的申明为static。例如



static union //在全局命名空间中的匿名共用体
{
int num;
char *pc;
};
namespace NS
{
static union { double d; bool b;}; //指定命名空间中的匿名共用体
}
int main()
{
NS::d = 0.0;
num = 5;
pc = "str";
return 0;
}

对象的const和volatile属性


在对象的构造期间有许多阶段,包括基类对象和子对象的构造,赋值this指针创建虚函数表,调用构造器。创建cv限制constvolatile)的对象还包括一步,将对象转换成const/volatile对象。cv限制在对象完全构造完之后才加上。


总结


C++复杂的内存模型提供了最大的灵活性。三种存储类型——自动、静态和自由存储——提供了仅有汇编语言才提供的控制层次。


动态内存分配的基础是运算符newdelete。两者都有至少六个版本;无格式和数组版本,又分为异常抛出,不抛出异常的和placement。


许多面向对象语言提供了内建的垃圾收集器。 垃圾收集器是一个自动的内存管理者,它检测无引用的对象收回他们的存储空间(参见第十四章“结束的注释和未来的方向”)。回收的存储空间又可以用于创建新 对象,因此程序员不需显式释放动态分配的内存。一个自动垃圾收集器是有用的,它将消除bug的一个丰富源头,消除运行期崩溃和内存泄漏。但是垃圾收集器不 是万能药。它带来另外的运行期开销:重复压缩、引用计数和内存初始化操作,这些在严格时间要求的应用程序中是不可接受的。此外,当使用垃圾收集器的时候, 对象生命结束时就不用隐含调用销毁器,而是在一个不确定的时候调用(当垃圾收集器调用它时)。由于这些原因,C++不提供垃圾收集器。虽然如此,有许多技 术来减少——甚至消除——手工内存管理的危险和麻烦,而且没有垃圾收集器的缺点。最简单的保证自动分配和释放内存的方法是使用自动存储。对于那些必须动态 增长和减少的对象,你可以使用STL容器来自动、最优的调整他们的大小。最后,为了创建在程序执行期间都存在的对象,你可以申明它为static。但是,有时候动态分配是不可避免的。这种时候,auto_ptr(在第六章和第十一章讨论过)简化动态内存的使用。


高效和无bug使用C++内存处理方法和概念需要高层的专家意见和丰富经验。毫不夸张的说大多数C/C++程序的错误都和内存管理有关。但是。这种多样性也使C++成为用途广泛,不危及安全的程序语言。






Contents







© Copyright 1999, Macmillan Computer Publishing. All
rights reserved.


翻译:Sttony,2002.1.29


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值