《Effective C++》学习笔记 第一章

1 Accustoming Yourself to C++

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

C++已经是一个多重范型编程语言,同时支持过程形式(procedural)、面向对象形式(object-oriented)、函数形式(functional)、泛型形式(generic)、元编程形式(metaprogramming)的语言

可以将C++语言视为众多次语言(sublanguage)的联合体,共四个:

  • C。区块,语句,预处理器,内置数据类型,数组,指针等都来自C
  • Object-Oriented C++。面向对象的C语言,包含类的构造与析构函数,封装继承多态等特性,虚函数等等
  • Template C++。C++的泛型编程
  • STL。STL是一个template程序库,,主要包含容器,迭代器,算法以及函数对象等

正是因为C++是众多次语言的集合,所以在不同的语言中,高效编程的守则肯定是不同的。例如:在C部分中,对内置类型来说,pass-by-value往往比pass-by-reference要高效。因此,C++不是一个带有唯一一组守则的语言,它取决于你使用的是C++中的哪一部分。

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

更进一步来说,其实是“宁可以编译器替换预处理器”。

1.2.1 以const替代#define

使用#define来定义常量,预处理器会在编译器记录记号表(symbol table)之前,将所有宏定义的常量符号替换为常量值。

可能导致的问题:一旦产生有关对应宏定义符号产生的错误,报错不会明确指出是符号的错,只会提到相关的常量值。这是如果追踪错误就很难追踪到宏定义符号本身。

由于预处理机制,预处理器会把所有出现宏名称的地方进行替换

可能导致的问题:相比于使用cosnt定义常量值,盲目的替换机制可能出现更多的目标码。可以理解为替换机制是每有一处宏名称,就会产生一份常量代码,而const常量就只有一份,产生的只是调用。

const替换宏定义的特殊情况:

  • 定义常量指针

常量定义式通常位于头文件中,因此指针也需要声明为const,但如果想定义的是一个char*-based的字符串(可以理解为char字符串),必须使用两次const

const char* const name = "qqh";
  • class专属常量

定义专属常量需要做到两点:要让class专属,那就需要声明为成员变量;要让称为独一份的常量,需要声明为static成员

class palyer{
private:
    static const int NumTurns = 5;		// class专属常量声明
    int score[NumTurns];				// 使用常量
    ...
}

以上所见的是常量的声明,如果是class专属常量的话,还需要进行提供常量的定义。例外,如果专属常量且为static,类型为整数类型(ints,chars,bools),就需要分两种情况。1.如果不需要对它们取地址,那么可以只声明而不提供定义;2.如果需要取专属常量的地址,或者编译器要求提供定义,就必须额外再提供一个定义

const int player::NumTurns;				// class专属常量定义

类的声明通常是放在头文件中的,上面的定义则要放进对应的实现文件中。定义时因为声明中已经给过初值,所以不能再次赋初值。

#define则无法实现class专属常量,因为除非使用#undef,否则#define没有域的概念;同时,#define也无法提供任何封装性,其访问的没有权限限制的。

有的编译器不支持在声明时进行赋初值,可以将赋初值在实现文件中进行。

1.2.2 以enum替代#define

如果在声明类的时候,使用了class的常量值,那么在定义时才对class专属常量赋初值的做法是不可行的。

而如果编译器又不支持static整数型class常量进行in class初值设定,可以改用the enum hack来做。理论基础是:一个属于枚举类型(enumerated type)的数值可以充当ints使用,因此player可以定义为如下形式:

class player{
private:
    enum{ NumTurns = 5 };
    int score[NumTurns];
}

关于enum hack

  • enum hack更像是#define,而不像const。例如const可以取地址,而前两者取地址操作是不合法的,这就可以实现不让别人获取指向常量的指针或引用
  • enum hack是模板元编程的基础技术,很常见

1.2.3 以inline替代#define

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

#define另一个常见用法为实现宏,类似函数的行为,但不会带来函数调用的额外开销。

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

虽然可以减少开销,但宏有两个弊端:

  • 必须在宏中所有的实参加上小括号,这样才能避免出现一些难以预料的问题。比如传入错误参数导致计算顺序改变
  • 即使加上小括号,由于预编译机制问题,宏的使用时先进行实参替换。参考以下代码,预处理器会把所有a替换为++a,那么a在计算中的调用次数,就决定了a的累加次数:
int a = 5, b = 0;
CALL_WITH_MAX(a, b);			// a被累加两次
CALL_WITH_MAX(a, b+10);			// a被累加一次

这就会带来函数不会有的不可预料性和类型安全性。要避免这种问题的同时获得宏带来的效率,就可以使用template inline函数:

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

条款03 尽可能使用const

通过指定const可以为对象施加一个语义约束,而编译器也会强制实施这个约束。

const用于指针

const的用法很多,但总的来说,分为两种,出现在星号左边,表示被指物是常量;出现在星号右边,表示指针自身是常量;如果出现在星号两边,则指针和被指物都是常量。一下两种写法是等价的

void f1(const Widget* pw);
void f2(Wiget const * pw);

const用于函数

const可以用于函数的返回值,各参数,函数自身(成员函数)。

  • 用于函数返回值

可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性

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

这样的代码可以防止客户写出一下的错误代码:

Rational a, b, c;
...
(a * b) = c;
  • 用于成员函数

使用const修饰成员函数的原因:确认该成员函数可以作用于const对象:1,可以使class接口比较容易理解,明确对象是否可以被改动;2,使“操作const对象”成为可能,为提高效率,pass by value往往会被替换为pass by reference-to-const。因此就需要const成员函数来获取const对象。

const是可以对成员函数进行重载的

class TextBlock {
  public:
    ...
    const char& operator[](std::size_t position) const	// for const对象
    { return text[position]; }
    char& operator[](std::size_t position)	// for non-const对象
    { return text[position]; }
  private:
  	std::string text;  
};

常量对象会调用对应重载的const成员函数。如果const对象调用了没有const重载的成员函数,编译器会报错(因为这有可能对const对象进行修改)。不过这两个函数中的代码几乎一样,为提高代码复用性,可以通过常量性转换解决。

对于常量性constness有两种流行的概念:bitwise constnesslogical const ness

bitwise constness正是C++对常量性的定义,要求成员函数只有在不改变对象之任何成员变量(static除外)时才可以说是const。这样的特性编译器通过检查成员变量的赋值动作来判断。但是编译器的检查并不总是生效。如以下代码:

class CTextBlock {
  public:
    ...
    char oprate[](std::size_t position) const
    {
        return pText[position];
    }
  private:
    char* pText;
};

// 通过const成员函数进行内容的修改
const CTextBlock cctb("HELLO");
char *pc = &ccbt[0];

*pc = 'J';	// 通过指针进行了修改

代码中的成员函数虽然声明为const,且通过了编译器bitwise const的检查,但是函数内部是通过指针来对指向内容修改,这样行为编译器是无法检查到的,所以是可以通过编译的。

logical constness,强调一个const成员函数可以修改他所处理的对象内的某些bits,但只有在客户端侦测不出来的情况下才能如此。

但是一旦在成员函数之后写上了const关键字,C++默认进行的就是bitwise constness检查。要想实现,可以利用mutable关键字,通过mutable释放吊non-static成员变量的bitwise constness约束:

class CTextBlock {
  public:
    ...
    std::size_t length() const;
  private:
    char* pText;
    mutable std::size_t textLength;
    mutable bool lengthIsValid;
};
std::size_t CTextBlock::length() const
{
    if (!lengthIsValid) {
        textLength = std::strlen(pText);
        lengthIsValid = true;
    }
    return textLength;
}

当const和non-const成员函数有着实质等价的实现的时候,令non-const版本调用const版本的可避免重复代码

在non-const成员函数中,调用const成员函数来实现重复代码的利用。但需要进行两次类型转换:第一次,转换为const对象,这样才能调用const成员函数;第二次移除const属性,返回的结果才符合non-const成员的定义。

class TextBlock {
  public:
    ...
    const char& operator[](std::size_t position) const
    {
        ...
        return text[posiztion];
    }
    char& operator[](std::size_t position)
    {
        return const_cast<char&>(static_cast<const TextBlock>(*this)[position]);	// 进行两次常量性质的转换
    }
};

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

  • 对于无任何成员的内置类型,需要手动进行初始化。

因为编译器不一定会进行你预想的初始化,比如int类型,有可能会初始化为0,也有可能初始化为未知的某个值。

  • 对于内置类型以外的任何东西,初始化责任都交给构造函数

规则:确保每一个构造函数都将对象的每一个成员初始化。

初始化和赋值是两个步骤,一定要分清

这一点主要体现在构造函数的初值列和构造函数函数体内部的赋值上。C++规定对象的成员变量的初始化动作应该发生在进入构造函数体之前。

通常来说使用初值列进行初始化的效率是高于赋值的构造函数的。原因在于:采用赋值的构造函数初始化分为了两步,首先是调用default构造函数为成员变量设初值,然后再在函数体中对其进行赋值操作;相比之下,采用初值列的构造函数只调用了一次copy构造函数。

初值列进行初始化的好处在于:

成员变量中的无论是内置类型还是自定义类型,统一采用初值列进行初始化。这样既可以避免部分参数被遗漏,也可以确保即使是const或者引用类型的成员变量(他们本身就要求进行初始化,而不是赋值)

初值列的成员初始化次序是固定的:base classes早于其derived classes被初始化,而class的成员变量总是以其在类内部被声明的次序初始化(不是根据初值列中写出来的顺序决定的)

不同编译单元内定义的non-local static对象的初始化次序

static对象的寿命是从被构造,直到程序结束为止。这种对象包括global对象,定义域namespace作用域的对象,在classes内、在函数内、以及在file作用域内被显式声明为static的对象。而上面提到的non-local是针对函数而言,如果这个static对象位于函数内部,则称之为local static对象,否则称之为non-local static对象。

编译单元:指的是产出单一目标文件的.h文件。

现在的问题是:如果某个编译单元内的某个non-local static对象的初始化动作使用了另一个编译单元内的某个non-local static对象,它所用到的这个对象可能还没被初始化,因为C++对此初始化次序并无明确的规定

解决办法:将每个non-local static对象搬到属于自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个引用指向它所包含的对象。如此一来,non-local static对象就被local static对象替换了。这也是单例模式的一个常见实现手法。

这个解决方法的基础在于:C++保证了函数内的local static对象会在“该函数被调用期间”、“首次与上该对象的定义式”时被初始化。所以通过函数来访问一个non-local static对象的时候,返回的引用必然是一个已经被初始化过的对象。

class FileSystem{...};
FileSystem& tfs()
{
    static FileSystem fs;
    return fs;
}
class Direction{...};
Direction::Direction(param)
{
    ...
    std::size_t disks = tfs().numDisks();
    ...
}
Direction& tempDir()
{
    static Direction td;
    return td;
}

从上面代码可以看出,这类函数往往十分单纯:第一行定义并初始化一个local static对象,第二行返回他。这就使得这类函数往往可以声明为inline函数。

posted @ 2020-08-18 15:31  泥猴瓜皮  阅读(142)  评论(0)    收藏  举报