在上次结束时,我期望道:当一个新产生的对象在没有完全构造时,它所占用的内存能自动释放。很幸运,C++标准委员会将这个功能加入到了语言中(而不幸的是,这个特性加得太晚了,许多编译器还不支持它)。Visual C++ 5和6都支持这个“自动删除”特性(但,如我们将要看到的,Visual C++ 5的支持是不完全的)。
要实际验证它,在上次的例6中增加带跟踪信息的operator new和operator delete函数:
// Example 7
#include <iostream>
#include <memory>
#include <stdio.h>
#include <stdlib.h>
void *operator new(size_t const n)
{
printf(" ::operator new\n");
return malloc(n);
}
void operator delete(void *const p)
{
std::cout << " ::operator delete" << std::endl;
free(p);
}
class B
{
public:
B(int const ID) : ID_(ID)
{
std::cout << ID_ << " B::B enter" << std::endl;
if (ID_ > 2)
{
std::cout << std::endl;
std::cout << " THROW" << std::endl;
std::cout << std::endl;
throw 0;
}
std::cout << ID_ << " B::B exit" << std::endl;
}
~B()
{
std::cout << ID_ << " B::~B" << std::endl;
}
private:
int const ID_;
};
class A
{
public:
A() : b1(new B(1)), b2(new B(2)), b3(new B(3))
{
std::cout << " A::A" << std::endl;
}
~A()
{
std::cout << " A::~A" << std::endl;
}
private:
std::auto_ptr<B> const b1;
std::auto_ptr<B> const b2;
std::auto_ptr<B> const b3;
};
int main()
{
try
{
A a;
}
catch(...)
{
std::cout << std::endl;
std::cout << " CATCH" << std::endl;
std::cout << std::endl;
}
return 0;
}
程序将用我们自己的operator new和operator delete代替标准运行库提供的版本。这样,我们将能跟踪所有的动态创建对象时的分配和释放内存操作。(我同时小小修改了其它的跟踪信息,以便输出信息更容易读。)
注意,我们的operator new调用了printf而不是std::cout。本来,我确实使用了std::cout,但程序在运行库中产生了一个无效页错误。调试器显示运行库在初始化std::cout前调用了operator new,而operator new又试图调用还没有初始化的std::cout,程序于是崩溃了。
我在Visual C++ 6中运行程序,得到了头大的输出:
::operator new
::operator new
::operator new
::operator new
::operator new
::operator new
::operator delete
::operator delete
::operator new
::operator new
::operator new
::operator new
::operator new
::operator new
::operator delete
::operator delete
1 B::B enter
1 B::B exit
::operator new
2 B::B enter
2 B::B exit
::operator new
3 B::B enter
THROW
::operator delete
2 B::~B
::operator delete
1 B::~B
::operator delete
CATCH
::operator delete
::operator delete
::operator delete
::operator delete
::operator delete
Blech.
我无法从中分辨出有用的信息。原因很简单:我们的代码,标准运行库的代码,以及编译器暗中生成的代码都调用了operator new和operator delete。我们需要一些方法来隔离出我们感兴趣的调用过程,并只输出它们的跟踪信息。
C++又救了我们。不用跟踪全局的operator new和operator delete,我们可以跟踪其类属版本。既然我们感兴趣的是B对象的分配和释放过程,我们只需将operator new和operator delete移到类B中去:
// Example 8
#include <iostream>
#include <memory>
class B
{
public:
void *operator new(size_t const n)
{
std::cout << " B::operator new" << std::endl;
return ::operator new(n);
}
void operator delete(void *const p)
{
std::cout << " B::operator delete" << std::endl;
operator delete(p);
}
// ... rest of class B unchanged
};
// ... class A and main unchanged
编译器将为B的对象调用这些函数,而为其它对象的分配和释放调用标准运行库中的函数版本。
通过在你自己的类这增加这样的局部操作函数,你可以更好的管理动态创建的此类型对象。例如,嵌入式系统的程序员经常在特殊映射的设备或快速内存中分配某些对象,通过其类型特有的operator new和operator delete,可以控制如何及在哪儿分配这些对象。
对我们的例子,特殊的堆管理是没必要的。因此,我在类属operator new 和operator delete中调用了其全局版本而不再是malloc和free,并去除了对头文件<stdlib.h>的包含。这样,所有对象的分配和释放的实际语义保持了一致。
同时,因为我们的operator new不在在全局范围内,它不会被运行库在构造std::cout前调用,于是我可以在其中安全地调用std::cout了。因为不再调用printf,我也去掉了<stdio.h>。
编译并运行例8。将发现输出信息有用多了:
B::operator new
1 B::B enter
1 B::B exit
B::operator new
2 B::B enter
2 B::B exit
B::operator new
3 B::B enter
THROW
B::operator delete
2 B::~B
B::operator delete
1 B::~B
B::operator delete
CATCH
三个B::operator new的跟踪信息对应于a.b1、a.b2和a.b3的构造。其中,a.b1和a.b2被完全构造(它们的构造函数都进入并退出了),而a.b3没有(它的构造函数只是进入了而没有退出)。注意这个:
3 B::B enter
THROW
B::operator delete
它表明,调用a.b3的构造函数,在其中抛出了异常,然后编译器自动释放了a.b3占用的内存。接下来的跟踪信息:
2 B::~B
B::operator delete
1 B::~B
B::operator delete
表明被完全构造的对象a.b2和a.b1在释放其内存前先被析构了。
结论:所有完全构造的对象的析构函数被调用,所有对象的内存被释放。
例8使用了“普通的”非Placement new语句来构造三个B对象。现在考虑这个变化:
// Example 9
// ... preamble unchanged
class B
{
public:
void *operator new(size_t const n, int)
{
std::cout << " B::operator new(int)" << std::endl;
return ::operator new(n);
}
// ... rest of class B unchanged
};
class A
{
public:
A() : b1(new(0) B(1)), b2(new(0) B(2)), b3(new(0) B(3)) {
std::cout << " A::A" << std::endl;
}
// ... rest of class A unchanged
};
// ... main unchanged
这个new语句
new(0) B(1)
有一个placement参数0。因为参数的类型是int,编译器需要operator new的一个接受额外int参数的重载版本。我已经增加了一个满足要求的B::operator new函数。这个函数实际上并不使用这个额外参数,此参数只是个占位符,用来区分 placement new还是非placement new 的。
因为Visual C++ 5不完全支持 placement new和 placement delete,例9不能在其下编译。程序在Visual C++ 6下能编译,但在下面这行上生成了三个Level 4的警告:
A() : b1(new(0) B(1)), b2(new(0) B(2)), b3(new(0) B(3))
内容都是:
'void *B::operator new(unsigned int, int)':
no matching operator delete found;
memory will not be freed if initialization
throws an exception
想知道编译器为什么警告,运行程序,然后和例8比较输出:
B::operator new(int)
1 B::B enter
1 B::B exit
B::operator new(int)
2 B::B enter
2 B::B exit
B::operator new(int)
3 B::B enter
THROW
2 B::~B
B::operator delete
1 B::~B
B::operator delete
CATCH
输出是相同的,只一个关键不同:
3 B::B enter
THROW
和例8一样的是,a.b3的构造函数进入了并在其中抛出了异常;但和例8不同的是,a.b3的内存没有自动删除。我们应该留意编译器的警告的!
想要“自动删除”能工作,一个匹配抛异常的对象的operator new的operator delete的重载版本必须可用。摘自 C++标准 (subclause 5.3.4p19, "New"):
如果参数的数目相同并且除了第一个参数外其类型一致(在作了参数的自动类型转换后),一个placement的释放函数与一个placement的分配函数相匹配。所有的非palcement的释放函数匹配于一个非placement的分配函数。如果找且只找到一个匹配的释放函数,这个函数将被调用;否则,没有释放函数被调用。
因此,对每个placement分配函数
void operator new(size_t, P2, P3, ..., Pn);
都有一个对应的placement释放函数
void *operator delete(void *, P2, P3, ..., Pn);
这里
P2, P3, ..., Pn
一般是相同的参数队列。我说“一般”是因为,根据标准的说法,可以对参数进行一些转换。再引于标准(subclause 8.3.5p3, "Functions"),基于可读性稍作了修改:
在提供了参数类型列表后,将对这些类型作一些转换以决定函数的类型:
l 所有参数类型的const/volatile描述符修饰将被删除。这些cv描述符修饰只影响形参在函数体中的定义,不影响函数本身的类型。
例如:类型
void (*)(const int)
变为
void (*)(int)
l 如果一个存储类型描述符修饰了一个参数类型,此描述符被删除。这存储类型描述符修饰只影响形参在函数体中的定义,不影响函数本身的类型。
例如:
register char *
变成
char *
转换后的参数类型列表才是函数的参数类型列表。
顺便提一下,这个规则同样影响函数的重载判断,signatures和name mangling。基本上,函数参数上的cv描述符和存储类型描述符的出现不影响函数的身份。例如,这意味着下列所有申明引用的是同一个函数的定义。
l void f(int)
l void f(const int)
l void f(register int)
l void f(auto const volatile int)
增加匹配于我们的placement operator new的placement operator delete函数:
// Example 10
// ... preamble unchanged
class B
{
public:
void operator delete(void *const p, int)
{
std::cout << " B::operator delete(int)" << std::endl;
::operator delete(p);
}
// ... rest of class B unchanged
};
// ... class A and main unchanged
然后重新编译并运行。输出是:
B::operator new(int)
1 B::B enter
1 B::B exit
B::operator new(int)
2 B::B enter
2 B::B exit
B::operator new(int)
3 B::B enter
THROW
B::operator delete(int)
2 B::~B
B::operator delete
1 B::~B
B::operator delete
CATCH
和例8非常相似,每个operator new匹配一个operator delete。
一个可能奇怪的地方:所有B对象通过placement operator new分配,但不是全部通过placement operator delete释放。记住,placement operator delete只(在plcaement operator new失败时)被调用于自动摧毁部分构造的对象。完全构造的对象将通过delete语句手工摧毁,而delete语句调用非placement operator delete。(WQ注:没有办法调用placement delete语句,只能调用plcaement operator delete函数,见9.2。)
在第九部分,我将展示placement delete是多么地灵巧(远超过现在展示的),但有小小的隐瞒和简化。并示范一个新的机制来在构造函数(如A::A)中更好地容忍异常。
本文地址:http://com.8s8s.com/it/it2702.htm