妖藏巨细(下)
前言:可能是文章字数太长,我不知道为什么不能一起发表,所以分成上下两部分,这是第二部分。请读者耐心一点:)
3. 如何自由的调用重载的operator new和系统的operator new?
#include <iostream>
#include <vector>
#include <cstddef>
#include <new>
#include <memory>
using std::cout;
using std::endl;
using std::vector;
class memory
{
public:
memory() {}
~memory() { delete ptr; }
void *ptr;
std::size_t size;
std::size_t line;
};
vector<memory> v;
void* operator new(std::size_t size,std::size_t line=__LINE__)
throw (std::bad_alloc)
{
memory pm;
cout<<" "<<size<<endl;
pm.ptr=std::operator new(size); //希望调用系统的operator new,怎么用?
pm.size=size;
pm.line=line;
v.push_back(pm);
return pm.ptr;
}
void operator delete(void *ptr)
{
if (ptr == 0) return;
std::operator delete(ptr);
}
int main()
{
int* i=new int(3); //总是调用我写的operator new!
return 0;
}
将operator new、operator delete、array new或者array delete的标准global版本替换为自定义版本,这几乎从来都不是个好主意——即使C++标准允许你这么做。这些函数的标准版本一般都针对通用目的(general-purpose)的存储管理做了极大优化,而用户自定义的替代版本则不大会做得更好了。(然而,针对特定的类别或类别阶层体系采用(自定义的)成员函数形式的操作来定制其内存管理,则通常是合理的。)【注3】
注3:请参考Stephen C. Dewhurst写的《C++ Gotcha》Item62: Replacing Global New and Delete 。
这个程序有很多的毛病,我一一道来。
(1) 运算符(operator)应该全部在全局名字空间(global namespace)中(自定义时可以作为成员函数),也就是说+,-,*,/…都在global namespace中,operator new也是一个运算符。因此std::operator new(size);这种用法是错误的,应该换成::operator new(size);
(2) void* operator new(std::size_t size,std::size_t line=__LINE__) throw( bad_alloc )现在在global namespace中有两个operator new(自己写的一个,内建的一个)在这个函数中,有这样一句,pm.ptr=::operator new(size);你说现在的这个operator new是调用内建的那个,还是递归调用本身呢?你不知道,我不知道,编译器也不知道,因此编译时有歧义性!解决方法是让自己定义的operator new的第二个参数line不要有缺省引数(argument)。operator delete在这里直接调用内建的。
(3) memory类的设计有问题,析构函数调用delete,
{
memory pm;//定时炸弹开始计时
…
return pm.ptr;
}
//调用memory::~memory(),炸弹爆炸,伴随着魔鬼的狞笑。
解决办法是改写memory的析构函数为空!
最后这个程序如下:
// 前面不变
class memory
{
public:
memory() {}
~memory() { }//revised
void *ptr;
std::size_t size;
std::size_t line;
};
vector<memory> v;
void* operator new( std::size_t size,std::size_t line ) throw
(std::bad_alloc)//revised
{
memory pm;
cout<<" "<<size<<endl;
pm.ptr=::operator new(size); //调用系统的operator new
pm.size=size;
pm.line=line;
v.push_back(pm);
return pm.ptr;
}
void operator delete( void* p,std::size_t )
//match overloaded operator new
//应注意,当我们重载operator delete时,这些重载版本绝不会被“使用标准delete形式的表达式”唤起。
//在这个例子中,可以不重载operator delete,内建的已经够用了!后面就可以直接用delete i;不需要丑陋的operator delete(i,1);
{
::operator delete( p );//已经处理p=0
}
int main()
{
int* i= new(__LINE__) int(3); //调用我写的operator new,evised!
// delete i;//这种形式会调用global operator delete,当然在这里是完全可以的!
// i->~int();//不合法,但是如果是有nontrivial destructor的自定义类型,显示调用destructor不可避免!
operator delete(i,1);//调用自己重载的operator delete
return 0;
}
多么的丑陋,你真的需要自定义operator new and operator delete 吗?请三思而后行!!
4. 看看这个输出:cout<<(cout<<"HelloWorld"<<endl)<<endl;
Answer:<<是一个左移位运算符(shift operator),C++将其重载,变成输出流运算符,而且C++对各种基本类型都做了重载,因此我们能自然的使用<<输出int,float,double,char*,…,记住operator <<对void*也做了重载(后面会发现很重要!),所以如果你要用<<来输出自定义的类型,则必须自己重载operator <<。下面我们分两步来处理上面的问题;
(1)首先看(cout<<"HelloWorld"<<endl),输出HelloWorld,换行,刷新缓冲。在这里请记住operator <<的返回类型是std::ostream&;std::cout就是类型为std::ostream的一个对象。
(2)std::ostream是一个typedef。typedef basic_ostream<char, char_traits<char> > ostream;basic_ostream的父类是basic_ios;basic_ios有一个member function:operator void*(),转型运算符。现在已经很清楚!(cout<<"HelloWorld"<<endl)返回本身即:cout<<(cout)<<endl;(cout)调用operator void*()隐式转型为一个指针。然后在将类型为void*的指针输出,换行,刷新缓冲。(operator void*()的用途主要在于判断stream的状态是否失败,用在while(cout<<*p)等形式的连续输入中)。至于最终的指针值是多少,跟实现相关!
5. const A& A::operator=(const A& other)
{
if(this!=&other)
{
this->~A();
new(this) A(other);
}
return *this;
} 有错误吗?
Answer:这个问题的提出使我再次确认了一个事实,一句话不在于内容是什么,是否有意义,而在于说这句话的人是谁!hustli说出这句话,就会有人说他应该看看软件工程,不要炫耀奇技淫巧;但是如果是Herb Sutter说出来的,绝对不会有人要他去补软件工程的课,大部分人会认真的去看他提出的问题。(事实是Herb Sutter的确提出了类似的问题,并用pimpl手法解决的很漂亮!)
(1)const A&应该是A&,operator =应该返回一个可以改变的左值,因为我们可以这样用int a,b=2,c=4;(a=b)=c;所以我们应该也可以这样写:A a,b,c;(a=b)=c;不是吗?所以不能返回const reference。
(2)if(this!=&other)
{
this->~A();
new(this) A(other);
}
其实这一句很漂亮。就地析构,但不释放内存,然后用placement new就地定位构造对象。从技术上来看,它既不是必要的也不是充分的。在实践当中,它工作得颇好。可惜的是,有时候美丽只有一层皮厚!它不是异常安全的,异常安全有三种形式:
基本保证:元素还在,但可能状态改变!No resource leak!
强烈保证:要么成功,要么回到起点。绝对不会让你进退两难!
不抛出异常:一定成功,不会失败!
上面的这个操作连基本保证也没有达到!如果copy constructor失败,类的不变式(invariance)就可能被破坏!不过现在有一个比较好的解决方法,创建一个临时物,然后与*this交换。(copy and swap),自己创建一个成员函数swap(),并保证它的异常规范为throw(),使用pimpl手法是可以做到的。(可以参考www.gotw.ca中的Guru of the week)。
不过如果保证copy constructor成功的话,别的操作也正常。应该是不会有什么object slice出现的,即使有派生。因为this->~A()肯定是从叶子类(派生最远的类)的析构函数开始调用,一直到根类。【注4】。
注4:请参考ANSI C++标准12.4。
这个问题的答案还不够完整,为寻求彻底的解答,还需要再努力,在写这篇文章的时候我只能做到上面这些了。我最近发现Herb Sutter写的《Exceptional C++》中的Item 41. Object Lifetimes part 2有很详细的解答,大家可以去参考一下。
现在我们应该可以松一口气了。这5个问题的答案希望大家补充。
对初学者的一点小小的建议:
在我们这个浮萍的年代,只有浮萍的人生。真正能够沉下去认真学点东西,做点事情的人实在是凤毛麟角!因此首先要能够有一个正确的心态,欲速不达,千古不变的至理!有时候看到人家似乎学到很快,那是因为没有看到他以前投入的没入成本(sink cost),如果我们的基础好,一门语言学的很好,另外一门也会更快的入门。大家认真的学习过操作系统,编译原理,离散数学……吗?认真的做过一个程序吗?这条路我自己走的很难很难,现在最大的缺憾是一直就没有遇到一个能带给我巨大影响的师友,还有就是基础太差(专业跟计算机没有很大的关系)。
其次,我们也应该有宽容的心态。我们能够容忍不同的声音在耳边不断回响吗?当我们与别人争论的时候,能够在发表自己的观点之前,先在大脑中站在对方的位置想一想:为什么他会那样想,他为什么会有那样的观点。我想再说出自己的观点的时候,会更具有说服他人的能力!
最后,我想在学一门语言的时候,最开始应该学习基本的语法,以及标准库的使用,不要太早与魔鬼(细节)打交道,否则很容易进入魔道!并尽快的与实际项目,课题联系起来,没有课题自己也可以想,就是写写string、各种数据结构、基础算法也是非常有收获的。请记住,专业知识的学习非常重要,非常重要!语言有时候真的只是一门工具!学习编程有一个好办法,读程序,写程序。一定要写!
吴桐写于2003.4.26
最近修改2003.6.16
最近修改2003.6.15