首页 > 代码库 > C++必知必会(4)
C++必知必会(4)
条款35 placement new
直接调用构造函数是行不通的,然而可以通过使用placement new来哄骗编译器调用构造函数:
void *operatornew(size_t, void* p) throw()
{ return p;}
placement new是operator new的一个标准的重载版本,也位于全局名字空间中,但和我们通常看到的operator new不同,语言明令禁止用户替换placement new,而普通的operator new和operator delete则可以被替换掉,只不过我们往往不该那样做而已。placement new的实现忽略了表示大小的实参,直接返回其第二个实参。placement new允许我们在一个特定的位置放置对象,起到了调用一个构造函数的效果:
class SPort{…}; //表示一个串口
const int comLoc= 0x00400000; //一个串口的空间位置
//…
void *comAddr =reinterpret_cast<void*>(comLoc);
Sport* com1 =new (comAddr) Sport; //在comLoc位置创建对象
区分new操作符和命名为operatornew的函数重载很重要。new 操作符不可以被重载,所以其行为总是一样的。它调用一个名为operator new的函数,然后初始化返回的存储区。我们希望对内存分配行为进行任何改变,均需要通过operator new的不同重载版本实现,而非通过new操纵符实现。同样的道理也适用于delete操作符和operator delete操作符。
placement new是函数operatornew的一个版本,它并不实际分配任何存储区,仅仅返回一个指向已经分配好空间的指针。正因为调用placement new并没有分配空间,所以不要对其进行delete操作,记住这一点很重要。
delete com1; //哎呀!
然后,尽管没有分配任何储存区,但却是创建了一个对象,这个对象应该在其生命结束时销毁。为此,应该避免使用delete操作符,代之一直接调用该对象的析构函数:
com1->~Sport(); //调用析构函数而非delete操作符
也可以使用placement arranew在给定的空间位置创建对象数组:
const intnumComs = 4;
//…
Sport *comPorts= new (comAddr) Sport[numComs]; //创建数组
当然,这些数组元素最终必须销毁:
int i=numComs;
while(i)
comPorts[--i].~Sport();
对象数组可能出现问题的地方时:当数组被分配时,必须通过调用一个默认的构造函数而初始化每一个元素。考虑一个简单的、固定大小的缓冲区,可以向这个缓冲区append新值:
string* sbuf =new string[BUFSIZE]; //调用默认构造函数
int size = 0;
voidappend(string buf[], int& size, const string &val)
{buf[size++]=val;} //刚才的默认构造动作白做了!
如果只使用了数组的一部分元素,或者元素被立即赋值,那么以上做法效率会很低。更糟糕的是,如果数组的元素类型没有默认构造函数,将会产生一个编译错误。
placement new通常用于解决此类缓冲区问题。采用这种方式,缓冲区占用的存储区的分配,可以避免被默认的构造函数初始化:
const int size_tn = sizeof(string) * BUFSIZE;
string* sbuf =static_cast<string*>(::operator new(n));
int size = 0;
在第一次访问数组元素时,不能为其赋值,因为他还没有被初始化。然而,可以用placement new通过复制构造函数来初始化元素。
voidappend(string buf[], int &size, const string &val)
{
new (&buf[size++]) string(val); //placement new
}
通常使用placement new也需要做一些清理工作:
voidcleanBuf(string buf[], int size)
{
while(size)
buf[--size].~string(); //销毁已初始化的元素
::operator delete(buf); //释放存储区
}
条款36 特定于类的内存管理、
我们无法对new操作符和delete操作符做什么,因为他们的行为是固定的,但可以改变他们所调用的operator new和operator delete。做这件事情的最佳方式是为类声明operator new和operator delete。做这件事情的最佳方式是为类声明operator new和operator delete成员函数:
成员operator new和operatordelete是静态成员函数,我们可以回想起静态成员函数没有this指针。由于这两个函数进负责获取和释放对象的存储区,因此它们用不着this指针。像其他静态成员函数一样,它们可以被继承。
如果在基类中定义了成员operatornew和delet,要确保基类析构函数是虚拟的。否则通过一个基类指针来删除派生类对象的结果就是未定义的。
一个常见的误解是以为使用new和delete操作符就意味着使用堆内存,其实并非如此。使用new操作符唯一能表明的是名为operator new操作符将被调用,且该函数返回一个指向某块内存的指针。标准,全局的operator new和operator delete的确是从堆上分配内存,但成员函数operator new和operator delete可以做他们想做的事情,对于分配内存到底来自哪里没有任何限制。
条款37数组分配
对数组而言,new表达式不是使用operatornew来为数组分配内存,而是使用array new。类似地,delete表达式也不是调用operator delete来释放数组的存储区,而是调用array delete。可以重新声明array new和array delete,如下:
void* operatornew[](size_t ) throw(bad_alloc); //arraynew
void operatordelete[](void*) throw(); //array delete
条款38:异常安全公理
公理1:异常是同步的并且只能发生在函数调用的边界
诸如预定义类型的算术操作,预定义类型(尤其是指针)的赋值以及其他底层操作不会导致异常发生。
但操作符重载和模板使得情况变为复杂,因为很难判断一个给定的操作操作是否会导致一个函数调用并可能抛出异常。
公理2:对象的销毁时异常安全的
禁止在析构函数中和销毁对象时抛出异常
公理3:交换操作(swap)不会抛出异常
条款39异常安全的函数
String &String::operator = (const char*str)
{
if(!str)str=””;
char*tmp = strcpy(new char[strlen(str)+1], str);
delete[]s_;
s_= tmp;
return*this;
}
这个函数看上去好像装饰过度了,本可以取消临时变量而采用更少行为代码来实现它。
String&string::operator = (const char* str)
{
delete[] s_;
if(!str) str=””;
s_= strcpy(new char[strlen(str)+1], str);
return *this;
}
然而,尽管array delete有公理保证,即对象的销毁时异常安全的。但是array new没有这样的承诺。如果在不清楚新缓冲区是否被成功分配之前就delete掉原来的缓冲区,可能就会使String对象变得很糟糕。
来解决这个问题的方法:首先做任何可能会抛出异常的事情(但不会改变对象的重要状态),然后以不会抛出异常的操作作为结束。
String::operator=的第一个实现就死这么做的。
条款40 RAII
RAII表示“资源获取即初始化“。RAII的基本技术原理很简单:如果希望对某个重要资源进行跟踪,那么创建一个对象,并将资源的生命期和资源的生命期相关联。如此一来,就可以利用C++复杂老练的对象管理设施来管理资源。最简单的RAII形式是,创建这一个对象,使其构造函数获取一份资源,而析构函数则释放这份资源:
class Resource{…};
classResourceHandle{
public:
explicit ResourceHandle(Resource*aResource)
:r_(aResource){} //获取资源
~ResourceHandle(){delete r_;} //释放资源
Resource *get(){ return r_; } //访问资源
private:
ResourceHandle(constResourceHandle &);
ResourceHandle&operator = (const ResourceHandle &);
Resource*r_;
};
考虑以下未使用RAII的简单代码:
void f(){
R esource* rh = new Resource;
g(); //抛出异常?
delete rh; //我们不一定能够执行到治理,异常不安全!
}
可以使用RAII解决上面这个问题
void f(){
ResourceHandle rh( new Resource );
g(); //抛出异常?没影响
//rh析构函数执行delete操作
}
当使用RAII时,只有一种情况无法保证函数得到调用,就是当ResourceHandle对象被分配到堆上时,这么一来,只有现实的delete ResourceHandle对象,此ResourceHandle对象所包含的对象析构函数才会被调用。
条款41 new、构造函数异常
为了编写完美的异常安全代码,很有必要保持对任何已分配资源的跟踪并且时刻准备当异常发生时释放它们。主要有以下方法:
1. 我们可以将代码组织成无需回收资源的方式(见条款39 异常安全的函数)
2. 也可以使用资源句柄(RAII 见条款40)
3. 可以使用try语句块(但应该尽量少用,主要在这种地方使用它们:确实希
望检查一个传递的异常类型,为的是对它们做一些处理)
然而,关于new操作符的使用有一个明显的问题。
String* title =new String(“kicks”);
如果抛出一个异常,我们说不清楚到底是operatornew抛出来的,还是String构造函数抛出来的。区分这一点很重要,因为如果operator new成功了,而构造函数抛出异常,我们就应该调用operator delete来归还分配的存储区。如果抛出异常的函数是operator new,那么就没有任何内存得打分配,因此我们就不应该调用operator delete。
幸运的是编译器会按照上面思路帮我们解决这个问题。
条款42智能指针
智能指针是一个类类型,它乔装打扮成一个指针,但额外提供了内建指针所无法提供的能力。通常而言,一个智能指针通过使用类的构造函数、析构函数和复制操作符所提供的能力来控制对它所指向东西的方法,而内建指针在这方面无能为力。
所有智能指针都重载了->和*操作符。智能指针通常采用模板类来实现。
条款43 auto_ptr非同寻常
auto_ptr是一个类模板,用于生成具体的智能指针,他们知道在用完之后如何清理资源。auto_ptr对象的复制操作实际上是对将底层对象的控制权从一个auto_ptr转移到另一个auto_ptr。
通常有两种场合应该避免使用auto_ptr。第一种场合是,他们永远都不应该用作容器。容器中的元素通常在容器内部被拷来拷去,并且容器假定其元素遵循普通的非auto_ptr复制语意。第二章场合是,一个auto_ptr应该指向单个元素,而不应该指向一个数组。原因在于,当auto_ptr所指向的对象被删除时,它使用operator delete而非使用array delete来执行删除操作。
条款44指针算术
两个指针相减的结果类型为标准typedefptrdiff_t,它通常是int的一个别名。两个指针之间不可以执行加法、乘法或除法,因为这些操作对于地址来说没有说明符合习惯的意义。
条款45模板术语
条款46类模板显示特化
template<typename T> class Heap;
上面这个通用的模板成为主模板。主模板仅仅被声明用于特化。
template<> class Heap<const char*>{…}; //这是一个针对指向字符显示特化的版本。
其中的模板参数列表是空的,但要特化的模板实参则附加在模板名字后面。这个显式特化版本其实并不是一个模板,因为此时没有剩下任何未指定的模板参数。处于这个原因,类模板显式特化通常被称为“完全特化”,以便区分局部特化区分开,后者是一个模板。
此外,C++没有显式要求特化的接口必须和主模板的接口完全匹配,是上上,我们可以增加或减少接口。