妖藏巨细(下)

类别:编程语言 点击:0 评论:0 推荐:
  妖藏巨细(下)

前言:可能是文章字数太长,我不知道为什么不能一起发表,所以分成上下两部分,这是第二部分。请读者耐心一点:)

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

本文地址:http://com.8s8s.com/it/it29233.htm