新书推介:《语义网技术体系》
作者:瞿裕忠,胡伟,程龚
   XML论坛     W3CHINA.ORG讨论区     计算机科学论坛     SOAChina论坛     Blog     开放翻译计划     新浪微博  
 
  • 首页
  • 登录
  • 注册
  • 软件下载
  • 资料下载
  • 核心成员
  • 帮助
  •   Add to Google

    >> 本版讨论高级C/C++编程、代码重构(Refactoring)、极限编程(XP)、泛型编程等话题
    [返回] 计算机科学论坛计算机技术与应用『 C/C++编程思想 』 → Thinking again in C++ 系列 查看新帖用户列表

      发表一个新主题  发表一个新投票  回复主题  (订阅本版) 您是本帖的第 6797 个阅读者浏览上一篇主题  刷新本主题   树形显示贴子 浏览下一篇主题
     * 贴子主题: Thinking again in C++ 系列 举报  打印  推荐  IE收藏夹 
       本主题类别:     
     卷积内核 帅哥哟,离线,有人找我吗?
      
      
      威望:8
      头衔:总统
      等级:博士二年级(版主)
      文章:3942
      积分:27590
      门派:XML.ORG.CN
      注册:2004/7/21

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给卷积内核发送一个短消息 把卷积内核加入好友 查看卷积内核的个人资料 搜索卷积内核在『 C/C++编程思想 』的所有贴子 访问卷积内核的主页 引用回复这个贴子 回复这个贴子 查看卷积内核的博客楼主
    发贴心情 Thinking again in C++ 系列

    爱死Thinking in系列了,所以起了这个名字。本文的思想也部分来至于这套书,或参照对比,或深入挖掘,或补益拾慧,或有感而发,既包括Thinking in C++,甚至也包括Thinking in Java。
    Thinking again in C++(一)常量性原理
    关键字:C++,常量,const,constant,引用,指针,形参,实参,函数,返回值
    1.不能将const修饰的任何对象、引用和指针作为赋值表达式的左值。
    const int cx=100;
    const int & rcx=cx;
    const int * pcx=&cx;
    cx=200; //error
    rcx=200; //error
    *pcx=200; //error
    2.const类型的对象不能直接被non-const类型的别名所引用。
    (1)不能将const类型的对象传递给non-const类型的引用。
    const int cx=100;
    int & rx=cx; //error
    (2)不能将const类型的实参传递给形参为non-const类型引用的函数。
    void f(int a)
    {
    }
    void g(int & ra)
    {
    }
    const int cx=100;
    f(cx); //ok
    g(cx); //error
    (3)不能将const类型的对象作为non-const类型引用的函数返回值。
    int & f(const int & rca)
    {
    return rca; //error
    }
    int x=100;
    f(x);
    3.可以使用const类型别名引用non-const对象。此时通过const引用不能修改对象,但对象可以通过non-const引用被修改。
    int x=100;
    int & rx=x;
    const int & rcx=x; //ok

    x=200;
    rx=200;
    rcx=200; //error
    4.指针的属性有两个:指针的类型和指针本身的常量性。其中,指向const对象与指向non-const对象,是不同的指针类型。
    int x=100;
    const int * pcx=&x; //[1]
    int * px=&x; //[2]
    int y=100;
    int * const cpy=&y; //[3]
    int * py=&y; //[4]
    [1][2]两个指针的类型不同;[3][4]两个指针的常量性不同。
    对象与指向对象的指针的规则类似于对象与引用。即,const类型的对象不能直接被non-const类型的指针所指示(同2);可以使用const类型的指针指向non-const对象(同3)。
    5.可以将相同类型(包括常量性)的const指针值赋给non-const指针。
    int x=100;
    int * px;
    const int * pcx=&x;
    px=pcx; //error
    int * const cpx=&x;
    px=cpx; //ok
    6.若函数的返回值为内建类型或是指针,则该返回值自动成为const性质。但自定义类型则为non-const性质。
    int f() //相当于返回const int
    {
    return 100;
    }
    int * g(int & ra) //相当于返回int * const
    {
    return &ra;
    }
    class CTest
    {
    int n;
    public:
    CTest(int n){this->n=n;}
    };
    CTest h() //返回的就是CTest
    {
    return CTest(200);
    }
    f()=200; //error
    int x=100;
    int y=200;
    int * px=&x;
    g(y)=px; //error
    *g(y)=x; //ok,从这点可以看出g()返回的不是const int *
    CTest t(100);
    h()=t; //ok,但却是完全错误的、危险的做法
    //所以h()的正确写法是返回const CTest(完)


       收藏   分享  
    顶(0)
      




    ----------------------------------------------
    事业是国家的,荣誉是单位的,成绩是领导的,工资是老婆的,财产是孩子的,错误是自己的。

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2006/10/23 15:40:00
     
     卷积内核 帅哥哟,离线,有人找我吗?
      
      
      威望:8
      头衔:总统
      等级:博士二年级(版主)
      文章:3942
      积分:27590
      门派:XML.ORG.CN
      注册:2004/7/21

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给卷积内核发送一个短消息 把卷积内核加入好友 查看卷积内核的个人资料 搜索卷积内核在『 C/C++编程思想 』的所有贴子 访问卷积内核的主页 引用回复这个贴子 回复这个贴子 查看卷积内核的博客2
    发贴心情 
    Thinking again in C++(二)自赋值是非公断

        关键字:C++,自赋值,自复制,赋值,assign,assignment,复制,拷贝,copy

        1.需要考虑的自赋值。当类包含指针或引用成员时应注意检查。
          class String
          {
          private:
                char * pc_Buffer;
          public:
                String & operator=(const String & strR);
                String & operator+=(const String & strR);
                //...
          };
        (1)类内部:对称赋值运算符、接受自身类型或自身基类类型参数的成员函数,有时候还要考虑+=系列运算符。
          String & String::operator=(const String & strR)
          {
                if (this==&strR)      //[1]
                      return *this;
                delete [] pc_Buffer;      //[2]
                pc_Buffer=new char[strlen(strR.pc_Buffer)+1];//[3]
                //...
          }
        [1]中的判断是必须的。如果this==&strR,[2]将本身删除,[3]就会使用“悬挂指针”。
        下面operator+=()的实现隐藏着错误。
          String & String::operator+=(const String & strR)
          {
                int iLengthNew=strlen(pc_Buffer)+strlen(strR.pc_Buffer);
                char * pcBufferNew=new char[iLengthNew+1];
                strcpy(pcBufferNew,pc_Buffer);
                delete [] pc_Buffer;                  //[4]
                strcat(pcBufferNew,strR.pc_Buffer);      //[5]
                pc_Buffer=pcBufferNew;
                return *this;
          }
        如果this==&strR,[4]将本身删除,[5]就会使用“悬挂指针”。正确的做法不必使用判断语句,只需调换[4][5]两条语句的顺序。
        (2)类外部(包括友元):接受多个同一类型参数或多个有继承关系的类型参数的函数。
          class CDerive : public CBase{};
          void f(CBase & b1,CBase & b2);
          void g(CBase & b,CDerive & d);
          CBase bSame;
          CDerive dSame;
          f(bSame,bSame);            //[1]
          f(dSame,dSame);            //[2]
          g(dSame,dSame);            //[3]
        [1][2][3]都出现了自赋值,所以f()、g()的设计中都要有所考虑。

        2.不可能出现自赋值。
        (1)拷贝构造器:因为正在构造的对象还未完全生成,而传递给构造器的实参对象是已构造完毕的对象,这两者绝不可能是同一对象。
        (2)非对称赋值运算符:即使形参类型是自身的基类。若D是B的派生类,无论是否重载了对称赋值运算符,D类对象之间的赋值行为都不会调用D::operator=(const B & b)。
          class CDerive : public CBase
          {
          public:
                operator=(const CBase & b);      //不用考虑this和b之间的自赋值
                void f(const CBase & b);      //需要考虑this和b之间的自赋值
          };
          CDerive dSame;
          dSame=dSame;            //[1]
          dSame.f(dSame);            //[2]
        语句[1]中,编译器不会把dSame上溯造型为CBase,而是调用缺省或自定义的D::operator=(const D & d)。只有等式左边确为D,右边确为B,才调用D::operator=(const B & b),这时不可能出现自赋值。相反,语句[2]中,编译器会把dSame上溯造型为CBase,所以f()需要考虑自赋值。

        3.不是自赋值的赋值。仅仅内容相同的赋值不是自赋值。
          CTest a,b,same;
          a=same;
          b=same;
          a=b;      //[1]
        [1]不是自赋值,不会出问题,不需要检查,而且内容相同无法直接用地址来检查。

        4.不应该检查的自赋值。
        strcpy(char * strDest,const char * strSrc);中,当strDest==strSrc时,是自赋值,但并不会出错。
        发现自赋值直接返回,这种特定情况下,也许能提高函数效率10倍,但绝大多数没有出现自赋值时都多了一个条件判断,可能降低函数效率10%,最后综合计算加权平均效率可能还是降低了。这取决于自赋值出现的概率。
        设不判断自赋值,函数执行时间为1;若检查自赋值,设出现自赋值的概率为x,直接返回函数执行时间为0.1,不出现自赋值,多了一个条件判断函数执行时间为1.1,那么如果要求加权平均效率不降低:
          0.1x+1.1(1-x)<1
        解之,得:x>0.1。也就是说自赋值出现的概率必须大于10%,这在实际代码中可能吗?

    ----------------------------------------------
    事业是国家的,荣誉是单位的,成绩是领导的,工资是老婆的,财产是孩子的,错误是自己的。

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2006/10/23 15:41:00
     
     卷积内核 帅哥哟,离线,有人找我吗?
      
      
      威望:8
      头衔:总统
      等级:博士二年级(版主)
      文章:3942
      积分:27590
      门派:XML.ORG.CN
      注册:2004/7/21

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给卷积内核发送一个短消息 把卷积内核加入好友 查看卷积内核的个人资料 搜索卷积内核在『 C/C++编程思想 』的所有贴子 访问卷积内核的主页 引用回复这个贴子 回复这个贴子 查看卷积内核的博客3
    发贴心情 
    Thinking again in C++(三)缺省参数的误区

        关键字:C++,缺省参数,default argument,函数,function,构造器,constructor,误区

        使用缺省参数时应该注意避开下列几种误区。

        1.滥用缺省参数,损害代码的结构和可读性。
          void f(bool b=false)
          {
                if (b)
                {
                      file://code of open file
                }
                else
                {
                      file://code of close file
                }
          }
        打开文件和关闭文件在实现代码上没有什么共同点,把两个属于同一类别的函数误认为是实现机制相同,凭空捏造一个参数硬把它们凑在一块,没有什么好处!相反,谁能记得住f(true)代表打开,f()代表关闭呢?况且,f(false)、f()都可以关闭文件,如果调用者混合使用它们就会增加维护上的困难。这种情况下,写成两个独立的函数,非常清晰。
          void Open()
          {
                      file://code of open file
          }
          void Close()
          {
                      file://code of close file
          }
        推而广之,如下的做法也值得商榷。
          class CString
          {
          private:
                char * pcData;
          public:
                CString(char * pc=NULL);
          };
          CString::CString(char * pc)
          {
                if (pc==NULL)
                {
                      pcData=new char[1];
                      //...
                }
                else
                {
                      pcData=new char[strlen(pc)+1];
                      //...
                }
          }
        这一个更具备迷惑性,“都是构造器嘛,当然写在一块喽。”有人说。非也!应当看到,无参构造器与带char *参数的构造器使用的代码完全分离,并且缺省参数值NULL在设置数据成员时没有任何作用。CString()构造器应改写如下:
          class CString
          {
          private:
                char * pcData;
          public:
                CString();
                CString(char * pc);
          };
          CString::CString()
          {
                pcData=new char[1];
                //...
          }
          CString::CString(char * pc)
          {
                pcData=new char[strlen(pc)+1];
                //...
          }
        总结:
        (1)凡是出现利用缺省参数值作if判断,并且判断后实现代码完全不同的,都应该分拆成两个独立的函数。
        (2)只有缺省参数值在函数体中被无歧视的对待,也就是函数对于任何参数的实现机制都相同时,才可能是合理的。

        2.多个缺省参数,可能引入逻辑含混的调用方式
        设计一个类,不仅仅是提供给客户代码正确的功能,更重要的是,对不正确的使用方式作力所能及的限制。
          class CPoint
          {
          public:
                int x;
                int y;
                CPoint(int x=0,int y=0)
                {
                      this->x=x;
                      this->y=y;
                }
          };
        乍一看,没什么问题。构造CPoint对象时如果不指定x、y的初值,则设为原点坐标。让我们测试一下:
          CPoint pnt1;
          CPoint pnt2(100,100);
          CPoint pnt3(100);      file://[1]
        结果发现pnt3的值为(100,0),跑到x轴上去了。对于想绑定两个参数,让它们同时缺省,或者同时不缺省,我们无能为力。但是如果去掉缺省参数,情况就会好转。
          class CPoint
          {
          public:
                int x;
                int y;
                CPoint()
                {
                      x=0;
                      y=0;
                }
                CPoint(int x,int y)
                {
                      this->x=x;
                      this->y=y;
                }
          };
        这样,语句[1]就会引发编译错误,提醒使用者。
        抬杠的会说:“CPoint pnt3(100);初始化到x轴,本来就是我想要的。”真的吗?那么,请你在你的类文档中明确指出这种独特的调用方法,并且告诉使用者,将点初始化到y轴是CPoint pnt4(0,100);这种不对称的形式。
        至于我嘛,self document好了。

        3.重载时可能出现二义性
        这个简单,随便举个例子:
          void f(int a,int b=0)
          {
          }
          void f(int a)
          {
          }
        虽然潜在的模棱两可的状态不是一种错误,然而一旦使出现f(100);这样的代码,潜伏期可就结束了。

        4.函数调用中的精神分裂症
        Effective C++ 2nd中的条款,为了本篇的完整性加在这里。这种罕见的症状出现的条件是:派生类改写了基类虚函数的缺省参数值。
          class CBase
          {
          public:
                virtual void f(int i=0)
                {
                      cout<<"in CBase "<<i<<endl;
                }
          };
          class CDerive : public CBase
          {
          public:
                virtual void f(int i=100)
                {
                      cout<<"in CDerive "<<i<<endl;
                }
          };
          CDerive d;
          CBase * pb=&d;
          pb->f();      file://[2]
        运行后输出:
          in CDerive 0
        记住,缺省参数是静态绑定,而虚函数是动态绑定,所以[2]运行的是CDerive::f()的函数体,而使用的缺省值是CBase的0。

    ----------------------------------------------
    事业是国家的,荣誉是单位的,成绩是领导的,工资是老婆的,财产是孩子的,错误是自己的。

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2006/10/23 15:41:00
     
     卷积内核 帅哥哟,离线,有人找我吗?
      
      
      威望:8
      头衔:总统
      等级:博士二年级(版主)
      文章:3942
      积分:27590
      门派:XML.ORG.CN
      注册:2004/7/21

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给卷积内核发送一个短消息 把卷积内核加入好友 查看卷积内核的个人资料 搜索卷积内核在『 C/C++编程思想 』的所有贴子 访问卷积内核的主页 引用回复这个贴子 回复这个贴子 查看卷积内核的博客4
    发贴心情 
    Thinking again in C++(四)运算符重载形式推导

        关键字:C++,运算符,operator,重载,overload,形式推导


                              大前提

        定理:自定义类型不要毫无价值地与内建类型不兼容(Effective C++ 2e)。这个定理是本篇所有内容的理论基础,可见它的重要性。但我无法证明,只是从直觉上认为它是正确的,如果允许的话,我更乐意把它称为公理。


                              小前提

        内建类型的运算符串链规则:
         int a=1,b=2,c=3;
         a=(b+=c);     //[A]     ok
         (b+=c)=a;     //[B]     ok
         (a+b)=c;      //[C]     error
         (a=b)=c;      //[D]     ok
         (++a)=b;      //[E]     ok
         (a++)=b;      //[F]     error


                              结论

        成员函数版运算符重载的声明形式:
         class Arrow
         {
         private:
              double x;
              double y;
         public:
              Arrow & operator+=(const Arrow & r);            //[1]
              const Arrow operator+(const Arrow & r) const;   //[2]
              double operator*(const Arrow & r) const;        //[3]
              Arrow & operator=(const Arrow & r);             //[4]
              Arrow & operator++();                           //[5]
              const Arrow operator++(int);                    //[6]
         };
        Arrow的真实含义是矢量,也就是数学中有大小和方向的量,本来英文名应该是vector,但STL中已经有了同名但不同义的东西,为以示区别就取了这个不伦不类的名字。对这个类来说,++运算符没有意义,另外,赋值运算符使用编译器的默认版本即可,写它们纯粹是起示例作用。


                              推导手段

        通过合理使用const修饰符和引用,使重载运算符的行为同内建类型一致。


                              单个解析

        [1]由于[A][B]的关系,+=运算符的返回值应能够被改写,所以传回*this的引用。
         Arrow & Arrow::operator+=(const Arrow & r)
         {
              x += r.x;
              y += r.y;
              return *this;
         }

        [2]+运算符不改变左操作数,所以函数为const性质。+的结果存放在函数的local对象中,出于安全上的考虑,local对象不能作为引用返回,又因为[C]的关系,所以传回local对象的const拷贝。
         const Arrow Arrow::operator+(const Arrow & r) const
         {
              return Arrow(x+r.x,y+r.y);
         }
        Arrow的构造器省略。

        [3]内建类型的返回值被编译器约定不能作左值,此时可省略返回值的const。其他同[2]。
         double Arrow::operator*(const Arrow & r) const
         {
              return x*r.x+y*r.y;
         }
        矢量相乘得到它们的内积,内积是数而不是矢量。

        [4]由于[D]的关系,同[1]。
         Arrow & Arrow::operator=(const Arrow & r)
         {
              x = r.x;
              y = r.y;
              return *this;
         }

        [5]自增运算符的前缀版本返回改变后的对象,又由于[E]的关系,所以传回*this的引用。
         Arrow & Arrow::operator++()
         {
              //...
              return *this;
         }

        [6]自增运算符的后缀版本返回改变前的对象,所以必须建立一个local对象保存原状态,于是函数返回值同[2]。
         const Arrow Arrow::operator++(int)
         {
              Arrow arr(*this);
              //...
              return arr;
         }


                              逆推

        从[1][4][5]的实现可以看到,我们能够自然提供[A][B][D][E],同时保证足够的严谨。而对于[2],不存在安全的手段,让形如[C]的式子通过编译。还有,为什么内建类型的串链规则[E][F]有如此微妙的差异,从[5][6]可以看出来,因为函数可以安全地返回改变后的对象,却无法安全地返回改变前的对象。
        这些都增加了大前提定理的可信度,小前提也显得更加合理。


    后记:
    1.限于篇幅,这里只列出了最简单的运算符重载,省略了很多关于复杂、特种运算符的讨论。
    2.+、*这类二元运算符最好采用非成员形式,也就是友元形式。
    如果希望看到进一步的讨论,我会再写一点。

    ----------------------------------------------
    事业是国家的,荣誉是单位的,成绩是领导的,工资是老婆的,财产是孩子的,错误是自己的。

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2006/10/23 15:42:00
     
     卷积内核 帅哥哟,离线,有人找我吗?
      
      
      威望:8
      头衔:总统
      等级:博士二年级(版主)
      文章:3942
      积分:27590
      门派:XML.ORG.CN
      注册:2004/7/21

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给卷积内核发送一个短消息 把卷积内核加入好友 查看卷积内核的个人资料 搜索卷积内核在『 C/C++编程思想 』的所有贴子 访问卷积内核的主页 引用回复这个贴子 回复这个贴子 查看卷积内核的博客5
    发贴心情 
    Thinking again in C++(五)深入认识对象初始化

        关键字:C++,初始化,initialization,对象,object


        来自实际项目的一段代码,简化形式如下:
    switch (t)
    {
    case 0:
      int a = 0;
      break;
    default:
      break;
    }
        有什么问题吗?似乎没有。请用编译器编译一下……
        嗯?!一个错误“error C2361: initialization of 'a' is skipped by 'default' label”。这怎么可能?
        几番思琢,悟出解释:C++约定,在块语句中,对象的作用域从对象的声明语句开始直到块语句的结束,也就是说default标号后的语句是可以使用对象a的。如果程序执行时从switch处跳到default处,就会导致对象a没有被正确地初始化。确保对象的初始化可是C++的重要设计哲学,所以编译器会很严格地检查这种违例情况,像上述的示例代码中default语句后面并没有使用a,但考虑到以后代码的改动可能无意中使用,所以一样被封杀。
        明白了原因,解决起来就很容易了。只要明确地限制对象a的作用域就行了。
    switch (t)
    {
    case 0:
      {  //added for fix problem
      int a = 0;
      break;
      }  //added for fix problem
    default:
      break;
    }
        如果确实需要在整个switch语句中使用对象a,那就把int a = 0;移到switch语句之前即可。不过从原先的语句看,其意图似乎并不是这样的,所以推荐前面的解决方案。


        结束了吗?没有。让我们继续考究错误提示信息中“initialization”(也就是初始化)的确切含义。C++很看重初始化,所以往往会给我们造成一种错觉,似乎对象在定义处一定会经过初始化过程。真实情况如何呢?还是用实例来证明吧。
    switch (t)
    {
    case 0:
      int a;
      a = 0;
      break;
    default:
      break;
    }
        编译,这次没有报错。很明显int a;定义了对象,但没有进行初始化,否则就应该报告原先的错误。
        再看看用户自定义类型。
    class B
    {
    };

    switch (t)
    {
    case 0:
      B b;
      break;
    default:
      break;
    }
        编译结果也没有错误,所以没有提供构造器的类仍然没有初始化过程。
        如果给类加入构造器,情况就不同了。
    class B
    {
    public:  //added for initialization
      B(){} //added for initialization
    };
        这样就能重现原先的错误。证明有了构造器,编译器就将进行初始化处理并对之进行安全检查。


        从上面的实验,可以直观地体验到一些基本的C++观念和原理,并提高认识深度。
        1.int a = 0;既是声明也是定义,还包括初始化;int a;是声明还是定义依上下文而定,但如果是定义就不会包括初始化;a = 0;仅仅是赋值语句,在此句前对象已经存在了。
        2.为了避免不必要的开销,默认情况下,即程序员没有在代码中明确指示时,编译器不提供初始化过程。某些需要确保初始化的类,请提供构造器。这里透露出一个C++的设计哲学:通常你会面对多种选择,所以请精确地控制代码,其收益则是可以自由取舍调配的安全性、速度、内存开销等程序特性。
        3.严密注意程序中标号的使用情况,特别是case、default等常规标号,否则他们可能会破坏对象的正确状态。如果提供了对象初始化,则能够获得编译器的额外帮助。

    ----------------------------------------------
    事业是国家的,荣誉是单位的,成绩是领导的,工资是老婆的,财产是孩子的,错误是自己的。

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2006/10/23 15:43:00
     
     GoogleAdSense
      
      
      等级:大一新生
      文章:1
      积分:50
      门派:无门无派
      院校:未填写
      注册:2007-01-01
    给Google AdSense发送一个短消息 把Google AdSense加入好友 查看Google AdSense的个人资料 搜索Google AdSense在『 C/C++编程思想 』的所有贴子 访问Google AdSense的主页 引用回复这个贴子 回复这个贴子 查看Google AdSense的博客广告
    2024/11/22 23:54:27

    本主题贴数5,分页: [1]

    管理选项修改tag | 锁定 | 解锁 | 提升 | 删除 | 移动 | 固顶 | 总固顶 | 奖励 | 惩罚 | 发布公告
    W3C Contributing Supporter! W 3 C h i n a ( since 2003 ) 旗 下 站 点
    苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》
    171.875ms