Effective C++之旅 之 const

来源:互联网 发布:阿里云redis 外网访问 编辑:程序博客网 时间:2024/05/02 02:25

 

    很多人肯定都知道const,但真正用的应该不会很多,还没有这个意识。所以这次简单介绍复习一下,希望尽量使用const。

 

POINT 5, const修饰指针变量。

    char szName[] = "Nicolas“;

    char *psz = szName;             //non-const pointer, non-const data

    const char* psz = szName;    //non-const pointer, const data

    char const *psz = szName;    //non-const pointer, const data

    char* const psz = szName;    //const pointer, non-const data

    const char* const psz = szName;  //const pointer, const data

 

从上面的例子就会知道,const在*左边,表示const data,在*右边,表示const pointer。

 

POINT 6, const修饰函数参数,返回值  

    现在介绍修饰函数参数和返回值的用处。

    修饰参数,很好明白,就是保证输入参数不要被该函数内部改变,所以尽量使用const,以避免被无意的改变,这种问题很难debug查到原因,比如 strcmp(const char* pSrc, const char* pDest);

    修饰返回值,这个很少用到,比如:

    const Area operator * (const Area& ra1, Area& ra2);

这样做的目的就是使返回值不能成为左值,以保证避免下面的事情发生。另外顺便提一下,如果返回值是内部类型,应该也没有必要用const修饰,因为试图改变一个返回值,编译器也是不允许的。

    Area a, b, c;

    a*b = c;  //实际上是避免==和=误写  if( a*b == c)

 

POINT 7, const成员函数

    这个实在class里面非常常用的修饰方式,一般情况下,成员函数都要声明成const的,除非是要更改成员变量。这样做有个好处:

从函数声明上,很容易就看出,哪些函数可以改变成员变量,哪些函数不能改变成员变量。

    另外一点,const还可以作为函数是否重载的标示,以前说道函数重载,会想到,不同的参数类型,不同的参数数量可以作为重载的条件(注意,函数返回值不能作为重载的条件,因为编译器分不出int和unsigned int),现在const也能作为这个重载的条件了,举例:

    Area::rectangle(); 和 Area::rectangle() const; 可以是两个不同的函数。

 

const对于函数的修饰是为了保护成员变量,但有时却适得其反,参考p21,比如:

    class A

    {

     public:

         char& operator [] (std::size_t position) const

         { return pText[position]; }       //这样做,虽然pText的值不会被改变,但是*pText的值是可以被改变的

     private:

          char* pText;

     };

 

     const A a("hello");      //声明一个常量对象

     char* psz = &a[0];     //调用const operator []取得一个指针,指向a的数据

     *psz = "w";     //现在可以更改内容为"wello"了

这样的操作不会有任何错误,虽然我们调用了const函数,但还是改变了它里面成员变量指针所指的值,这样虽然不能说对象成员变量的值被改了(因为pText会是const的),但是却改变了它所指的值也不是太合理的。

如果我们用下面的方法,就可以避免这个问题了:

    class A

    {

     public:

         char& operator [] (std::size_t position) const

         { return text[position]; }       //这样做,虽然pText的值不会被改变,但是*pText的值是可以被改变的

     private:

          std::string text;

     };

 

     const A a("hello");      //声明一个常量对象

     char sz = a[0];     //调用const operator []取得一个值

     这样的话,是无论如何也改变不了对象a里面的text的值的。

另外需要提出的是:无论是const类型的对象还是non-const类型的对象,都是可以调用const类型的函数的。但是如果两个函数是靠const来重载的,那么const类型的对象是调用const类型的函数,non-const对象是调用non-const类型函数的。

 

     状态不错,所以把对象初始化的部分也讲了。首先澄清的是赋值assignment和初始化initialization是不一样的,比如:

POINT 8, 初始化和赋值

     int x; x = 5; //assignment

     int y = 5;    //initialization

这里两个是不一样的,初始化会更高效一点,因为赋值是先经历了一个初始化操作,然后又经历了一个赋值操作。

上面是内置类型的,class里也是一样的, 比如:

     class A

     {

      public:

          A(std::string& name, std::address, int number)

          : _name(name), _address(address), _number(number)      //这里就是初始化,在class称为initialization list

          {

                _name = name;                 //这里就都是赋值

                _address = address;

                _number = number;

          }

 

      private:

          std::string _name;

          std::string _address 

          int _number;

      }

 

下面讲几点需要注意的:

      1. 初始化列表比赋值高效

      2. 初始化列表里的变量初始化(实际也是call对应对象变量的构造函数,比如string),是早于构造函数的开始执行的

      3. 即使我们没有用初始化列表来初始化,那个std::string对象的构造函数也还是会执行的,所以顺便在这初始化取代赋值是值得的

      4. 对于内置类型的变量,初始化和赋值操作是一样的,不存在效率和先后问题,但为了统一,最好也是放在初始化里做

      5. 初始化列表里的初始化顺序,并不是按照我们写的顺序来初始化的,而是按照class里变量定义的顺序来初始化的,所以这里很容易出问题,需要注意。比如初始化array时需要指定大小,因此代表大小的那个成员变量必须得先被初始化。

      6. 另外一个顺序是,如果有父类,那么先初始化base class的成员变量,然后是derived class的。

 

下面讲一个比较特殊的类型的成员变量: non-local static

POINT 9, non-local static intialization

       我们把函数内声明的static类型的变量成为local staic,之外的为non-local static。对于在这种类型所带来的问题就是,这个对象是可以被extern的,就是不同的类之间是可以使用的,所以产生的问题就是,很有可能其中一个类A使用了B类里的static变量b,而这个时候B类里的b还没有被初始化。

       那么解决这种问题的方式就是design pattern里的Singleton了,熟悉的人一定就知道其中的原因了,原理就是用local static对象替代non-local static 对象,同时带来的好处就是,

1. 如果你没有使用它的时候,是不需要被创建的

2. 使用前肯定是会被初始化的

      还需要注意的就是多线程环境,因为在这种环境下,无论是local还是non local的都会有麻烦,处理这个麻烦的做法就是在程序还在单线程的时候就手工初始化它们,这样可以消除race conditions,另外我觉得也可以考虑interlock的原子操作,或double checking机制。

 

先到这里,待续... ...