Effective C++第一章

为了理解C++,将其分为四个次语言:

次语言
面向对象class(析构函数,构造函数)、封装、继承、多态、virtual函数….
Template C++C++泛型编程部分
STL是一个template程序库,容器、迭代器、算法以及函数对象..
C区块(blocks)、语句、预处理器、内置数据类型、数组。指针等统统来自C。但C语言的局限:没有模板、没有异常、没有重载…

当从某次语言切换到另一个,可能会高效编程守则会要求你改变策略。例如对内置类型而言,pass by value通常比pass by reference高效,但当你从C到面向对象时,由于用户自定义构造函数和析构函数的存在,pass by reference to const往往更好,运用template C++尤其如此。但是STL中,迭代器和函数对象都是在C指针上塑造出来的,所以对STL的迭代器和函数对象而言,旧式的C pass by value守则再次适用。


对于单纯常量,最好以const对象或enums替换#defines

例如#define RATIO 1.653可以替换为const double Ratio = 1.653;因为#define不被视为语言的一部分,所以记号名称RATIO可能没进入记号表,但作为语言常量的Ratio肯定会被编译器看到,当然就会进入记号表。

以常量替换#define,有两种特殊情况

  1. 定义常量指针(指向常量的指针(变量)),由于常量定义式通常被放在头文件内,因此有必要将指针(不只是指针所指之物)声明为const。

    关键词const出现在*左边,被指物是常量;在*号右边指针是常量;如果出现在*两边则被指物和指针都是常量。

    在C/C++中,常量指针(non-const pointer,const data)这样声明:

    1const int *p;
    2int const *p;
    //两种写法意义相同

    在C/C++中,指针常量(const pointer , non-const data)这样声明:

    int a;
    int *const b = &a; //const放在指针声明操作符的右侧

    还有指物和指针都是常量:

    const char* const p;        //const pointer , const data
  2. class专属常量

    为了将常量的作用域限制于class内,你必须让它成为class的一个成员,而为确保此常量至多只有一份实体,必须让其成为一个static成员。

    class GamePlayer{
     private:
        static const int NumTurns = 5;       //常量声明式(非定义式)
        int scores[GameTurn];
        ...
    };

    使用enum hack的技巧,其思想就是把GameTurn定义为一个枚举常量。上面的代码可以写为:

    class GamePlayer {
    private:
       enum {GameTurn = 10};
       int scores[GameTurn];
    };

    enum hack的优点:

    • enum定义的常量,并不是在预编译的时候进行替换,而是在编译时,从enum定义的常量存储区取定义的常量值。因此,不会导致 “不必要的内存分配”。
    • enum hack的行为某方面比较像#define而不像const。例如,取const地址是合法的,但是取值enum的地址是不合法的,#define的地址也不合法,如果你不想让别人获得一个point或reference指向你的某个整数常量,enum实现这个约束。

对于形似函数的宏,最好改用inline函数替换#defines

#define CALL_WITH_MAX(a,b) f((a)>(b)?(a):(b))//宏中所有的实参加上小括号

可以改用inline函数

template<typename T>
inline void CallWithMax(const T& a,const T& b){
    f(a>b? a:b);
}

强大的const

  • 指针常量,常量指针

  • 迭代器(因为STL迭代器以指针为根据塑模出来的,声明迭代器为const就像声明指针为const一样)

    std::vector<int> vec1;
    ...
    const std::vector<int>::iterator iter = vec1.begin();//像T* const
    *iter = 10;                                          //ok
    ++iter;                                              //wrong ! iter is const
    ...
    std::vector<int>::const_iterator cIter = vec1.begin();//像const T*
    *cIter = 10;                                          //wrong ! *cIter is const
    ++cIter;                                              //ok
  • 函数声明

    令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而不至于放弃安全性和高效性;const参数就像local const对象一样。

    class rational{...};
    const rational operator*(const rational& lhs,const rational& rhs);

    将const实施于成员函数的目的,是为了确认该成员函数可作用于const对象身上。这类成员函数的重要性在于:

    1. 可知哪个函数可以改动对象内容,而哪个不可以
    2. 使得“操作const对象”成为可能

    两个成员函数如果只是常量性不同,可以被重载,如下:

    class TextBlock{
    public:
    ...
        const char& operator[](std::size_t position) const{return text[position];}
        //operator[] for const对象
              char& operator[](std::size_t position)      {return text[position];}
        //operator[] for non-const对象
    private:
        std::string text;
    };
    
    TextBlock tb("hello");
    std::cout << tb[0];               //调用non-const TextBlock::operator[]
    tb[0] = 'x';                       //ok
    
    const TextBlock ctb("world");     
    std::cout << ctb[0];               //调用const TextBlock::operator[]
    ctb[0] = 'x';                      //wrong

    其中,当const和non-const成员函数有实质等价的实现时,

    • 利用non-const operator[]调用const operator[]是可以避免代码重复的安全做法,过程中需要转型将常量性转除。
    • 但是const成员函数调用non-const 成员函数是一种错误行为,因为const成员函数承诺不改变其对象的逻辑状态,但non-const成员函数没有这种承诺,如果const函数内调用non-const函数可能会使得承诺不改动的对象被改动。

确定对象被使用前已先被初始化

通常如果使用C part of C++而且初始化可能招致运行期成本,那么就不保证发生初始化。一旦进入non-C parts of C++,规则有些变化。这就是为什么array(来自C part of C++)不保证其内容被初始化,但vector(STL part of C++)却有此保证。

读取未初始化的值会导致不明确的行为。

最佳处理方法:对象被使用前先初始化:

首先,对于无任何成员的内置类型,必须手工完成此事。

其次,内置类型以外的任何其它,初始化责任落在构造函数身上。确保每一个构造函数都将对象的每一个成员初始化。

  class ABEntry{
    public:
        ABEntry(const std::string& name, const std::string& address, const std::list<int>& phones);
        //声明式
    private:
        std::string theName;
        std::string theAddress;
        std::list<int> thePhones;
        int num;  //num是内置类型
  };

  //(1)利用copy赋值法(首先调用default构造函数为theName、theAddress、thePhones设初值,然后再赋予新值。)
  ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<int>& phones){
    theName = name;
    theAddress = address;
    thePhones = phones;
    num = 0;
  }

  //(2)成员初值列(初值列中针对各个成员变量而设的实参,被拿去作为各成员变量之构造变量的实参)
  ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<int>& phones){
    theName(name);//theName以name为初值进行copy构造法
    theAddress(address);//同上
    thePhones(phones);//同上
    num(0);
  }

  //对大多数类型而言,(2)是比(1)高效的。但对于内置类型对象(1)和(2)成本相同,但为了一致性,最好也通过成员初值列来初始化。如果成员变量是const或reference,它们就一定需要初值,不能被赋值。
  • 建议总是使用成员初始值列,并总是列出所有成员变量。
  • class可能拥有多个构造函数,每个构造函数有自己的成员初值列,多份成员初值的存在就会导致不受欢迎的重复。这种情况下,可以合理地在初值列中遗漏那些“赋值表现像初始化一样好”的成员变量,改用它们的赋值操作,并将那些赋值操作移往某个函数(通常是private),供所有构造函数调用。这种做法在“成员变量的初值由文件或数据库读入”时特别有用。
  • C++有十分固定的成员初始化次序:父类早于子类;class的成员变量总是以声明次序被初始化,即使在初值列中以不同于声明次序出现,也不会有影响。

最后,为免除“跨编译单元之初始化次序”问题,请以local static 对象替换non-local static对象。

所谓所谓static对象,包括

  1. global对象
  2. 定义于namespace作用域的对象
  3. 在class内、函数内、在file作用域内被声明为static的对象。

其中,函数内的static对象成为local static对象,其它static对象称为non-local static对象。static对象的寿命从被构造出来直到程序结束时被自动销毁,也就是它们的析构函数在main函数结束时被自动调用。

所谓编译单元
当一个c或cpp文件在编译时,预处理器首先递归包含头文件,形成一个含有所有必要信息的单个源文件,这个源文件就是一个编译单元。这个编译单元会被编译成为一个与cpp 文件名同名的目标文件(.o或是.obj) 。简单说,一个编译单元就是一个经过预处理的cpp文件。

  • C++对定义于不同编译单元内的non-local static 对象的初始化并无明确定义
    class FileSystem{                        //编译单元1
      public:
        ...
        std::size_t numDisks() const;
        ...
    };
    extern FileSystem tfs;                  //预备给编译单元2使用的对象
    class Directory{                         //编译单元2
      public:
        Directory(param);
        ...
    };
    Directory::Directory(param){
      ...
      std::size_t disks = tfs.numDisks();   //使用tfs对象
      ...
    }
现在编译单元2创建一个Directory对象,
    Directory tempDir(param);

tfs和tempDir是定义于不同编译单元内的non-local static 对象,因此问题在于tfs是否在tempDir之前先被初始化?

  • 解决方法:将每一个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个指向local static对象的reference。调用这些函数来替换“直接访问non-local static对象”。这是Singleton模式的一个常见实现手法。
    class FileSystem{...};
    FileSystem& tfs()
    {
      static FileSystem fs;
      return fs;
    }

    class Directory{...};
    Directory::Directory(param){
      ...
      std::size_t disks = tfs().numDisks();   
      //上面是std::size_t disks = tfs.numDisks();
      ...
    }
    Directory& tempDir()
    {
      static Directory td;
      return td;
    }

C++保证,函数内的local static对象会在“该函数被调用期间”“首次遇见该对象之定义式”时被初始化。

但另一角度看,这些函数“内含static对象”的事实使它们在多线程系统中带有不确定性。任何一种non-const static对象,不论它是local或non-local,在多线程环境下“等待某事发生”都会有麻烦。处理的一种做法就是:在程序的单线程启动阶段手工调用所有reference-return函数,这可消除与初始化有关的“竞速形势”

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值