Chapter 1 of Effective C++

文章介绍了C++中将C++视作语言联邦的重要性,强调了使用const、enum和inline代替宏的优点,以及正确初始化对象、构造函数和预处理器的最佳实践。同时提到了跨编译单元初始化顺序的注意事项。
摘要由CSDN通过智能技术生成

条款01:视C++为一个语言联邦

View c++ as a federation of languages

c++并不是一个带有一组守则的一体语言,它是由四个次语言组成的联邦政府(C, Object-Oriented C++, Template C++, STL),每个次语言都有自己的规约。

当你从某个次语言切换到另一个,导致高效编程守则要求你改变策略时,不要感到惊讶。例如对内置类型而言pass-by-value通常比pass-by-reference更高效。但你从C part of C++迁往Object-Oriented C++,pass-by-reference-to-const往往更好。

请记住:C++高效编程守则视状况而变化,取决于你使用C++的哪一部分。

条款02:尽量以const, enum, inline替换#define

Prefer consts, enums, and inlines to #defines

另一种解释是“用编译器替换预处理器比较好”,因为或许#define不被视为语言的一部分。

当你 #define ASPECT_PATIO 1.653。也许ASPECT_PATIO从未被编译器看见(可能在编译器处理源码之前就被预处理器替换了),从而没有进入记号表(symbol table)。这样可能的问题会有:

  • 如果ASPECT_PATIO被定义在一个非自己写的头文件中,你运用此常量得到了一个编译错误信息时,你可能会疑惑1.653是什么,从而浪费时间。
  • 记号器调试器可能也会出现相同问题(ASPECT_PATIO没有进入记号表)

解决方法是用一个常量替换上述宏,并有以下好处:

const double AspectRatio = 1.653
  • AspectRatio作为一个常量肯定被编译器看到,从而进入记号表。
  • 对于浮点常量而言,使用常量可能比#define导致较小量的码。预处理器盲目的将多处宏都替换为1.653。

这里有个需要注意的点是:class专属常量

class GamePlayer{
private:
    static const int NumTurns = 5; //常量声明
    int scores[NumTurns];          //使用常量
}

通常C++要求对所使用的任何东西提供一个定义式,但class专属常量且为static和整型(int, char, bool)则可以特殊处理。你可以声明并使用它们而无需提供定义式。

const int GamePlayer::NumTurns;  //定义式,由于class常量在声明式获得初值,定义时就不可再设初值

Note:无法用#define创建一个class专属常量,因为#define不重视作用域。一旦宏被定义,它在其后的编译过程中均有效。

如果你的编译器不允许“static整数型class常量”完成“in class初值设定”,而你的scores数组声明式又需要。可以改用"the enum hack"补偿做法,“一个属于枚举烈性的数值可权当int使用”。

class GamePlayer{
private:
    enum {NumTurns = 5};
    int scores[NumTurns];
}

enum hack优点:

  • enum hack的行为比较像#define,而不是const。如取const的地址是合法的,但取enum地址不合法(取#define通常也不合法)
  • 许多代码都用到了它,例如模板元编程。

把焦点拉回预处理器。另一个常见的#define误用情况是以它实现宏。宏看起来像函数,但不会招致函数调用带来的额外开销。下面这个宏夹带着宏实参,调用函数f:

#define CALL_WITH_MAX(a,b) f((a)>(b)?(a):(b))

无论合适写出这样的宏,都必须记得给所有实参加上小括号,否则在调用时可能会遇到麻烦。但纵使这样,也会遇到问题:

int a = 5, b = 0;
CALL_WITH_MAX(++a,b);      //a被累加二次
CALL_WITH_MAX(++a,b+10);   //a被累加一次

因此,最好使用模板内联函数来替代它:

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

有了const,enum和inline,我们对于预处理器(特别是#define)的需求降低了,但并非完全消除。#include仍然是必需品,而#ifdef/#ifndef也继续扮演者重要角色。目前还达不到预处理器全面隐退的时候,但应该给与它更长的假期。

请记住:

  • 对于单纯常量,最好以const对象或enums替换#define。
  • 对于形似函数的宏,最好改用inline函数替换#define。

条款03:尽可能使用const

Use const whenever possible

  • const在*左边,内容是常量。(*xx解引用了)
  • const在*右边,指针(地址)是常量。

对于STL迭代器来说,其系以指针为根据塑模出来,因此:

std::vector<int> vec;

const std::vector<int>::iterator iter = vec.begain();   //和T* const类似
std::vector<int>::const_iterator iter = vec.begain();   //和const T*类似

const成员函数

将const实施于成员函数的目的是,为了确认该成员函数可作用域const对象身上。

  • 使得class接口容易被理解,知道哪个函数可以改动对象,哪个不可以。
  • 使“操作const对象”成为可能。

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

class TextBlock{
public:
    const char& operator[](std::size_t position) const //operator for const object
          char& operator[](std::size_t position)       //operator for non-const object
}

TextBlock tb("Hello");
std::cout<< tb[0];      //call non-const operator
const TextBlock tb("Hello");
std::cout<< tb[0];      //call const operator

mutable使得const函数可以修改成员变量.

class CTextBlock{
public:
    void length(size_t size) const;
private:
    mutable size_t length = 1;
}

void CTextBlock::length(size_t size) const
{
    length = size;
}

如果operator[]中有大量代码,那么实现两个具有冗长重复代码的operator[]可能不是个好事情。因此,真正应该做的是实现operator[]的机能一次,并使用两次(用其中一个调另一个)。

这促使我们将常量性转除(casting away constness)。然而就一般守则而言,转型是一个糟糕的想法(条款27),但重复代码也不是什么令人愉快的事情。

class TextBlock{
public:
    const char& operator[](std::size_t position) const //operator for const object
          {
              //a lot of things here
              return text[position];
          }

    char& operator[](std::size_t position)       //operator for non-const object
    {
        return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
    }
}

但请注意,令const版本调用non-const版本以避免重复不是应该发生的事情。

请记住:

  • 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
  • 编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”。
  • 当const和non-const成员函数有实质等价的实现时,令non-const版本调用const版本。

条款04:确认对象被使用前已被初始化

Make sure that objects are initialized before they'er used

如果使用C part of C++,而且初始化可能招致运行期成本,那么就不保证发生初始化。non-C parts of C++规则有些变化。这就很好的解释了array不保证其内容被初始化,而vector却有此保证。

Best practice:永远在使用对象之前将它初始化。

  • 对于内置类型,你必须手工完成此事。 int x = 0;
  • 除内置类型外,初始化责任落在构造函数上。规则很简单:确保每一个构造函数都将对象的每一个成员初始化

这个规则很轻易,但不要混淆了赋值和初始化。

class PhoneNumber { ... }

class ABEntry
{
public:
    ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones)
    {
        theName = name;           //这些都是赋值,而不是初始化
        theAddress = address;
        thePhones = phones;
        numTimesConsulted = 0;
    }
private:
    string theName;
    string theAddress;
    list<PhoneNumber> thePhones;
    int numTimesConsulted;
}

C++规定,对象的成员变量的初始化动作发生在进入构造函数本体前(但对numTimesConsulted这个内置类型不一定为真,有可能他就是在这个时候初始化,且内置类型初始化与赋值的成本相同)。

ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones)
:theName(name), theAddress(address), thePhones(phones),numTimesConsulted(0)
    { }

//当你要默认构造一个成员变量,不传入参数即可
ABEntry():theName(), theAddress(), thePhones(),numTimesConsulted()
    { }

以上构造函数的效率较高,其只调用成员变量的一次copy构造函数。而前者会先调用default构造函数再调用copy assignment操作符。

Note:如果成员变量是const或references,它们一定需要初值,不能被赋值。

如果类有多个构造函数,则成员初始化列表会存在多份,这种情况下可以合理在初始化列表遗漏内置类型成员变量,改用赋值操作,并将这些赋值操作移至某个函数(通常private),供所有构造函数调用。

Note:类的成员变量总是以其声明次序被初始化,不受初始化成员列表的影响。

一旦上述知识你都掌握,那只剩一件事需要操心。“不同编译单元内定义non-local static对象”的初始化顺序。

  • 在函数内的static对象称为local static对象,其他的static对象称为non-static对象。
  • 编译单元是指产出单一目标文件的那些源码,基本上就是单一源码文件加上其所含的头文件。

C++对于定义在不同编译单元内的non-local static对象的初始化次序并无明确定义。因此,若某编译单元的non-local static对象初始化动作使用了另一个non-local static对象,它所使用的这个对象可能尚未被初始化。

可以通过一个小设计来避免这个问题,将每个non-local static对象搬到自己的专属函数内,函数返回一个reference指向它所含的对象,用户调用函数。换句话说,non-local static被local static对象替换了(Singleton模式)。这个手法的保证在于:函数内的local static对象会在“该函数被调用期间” “首次遇上该对象之定义式”时被初始化。

这样做还有一个好处:如果你从未调用这个函数,就不会引发non-local static对象的构造和析构成本。但请注意,在多线程情况下的不确定性(感觉和单例模式类似)。

  • 为内置型对象进行手工初始化,因为C++不保证初始化它们。
  • 构造函数最好使用初始化成员列表,且其排列顺序应该和它们在类中的声明次序相同。
  • 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。
  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值