0%

Effective C++之让自己习惯C++

1章:让自己习惯C++

条款01:视C++为一个语言联邦
  • C++视为由4个次语言组成的联邦:
    • C:没有模板、没有异常、没有重载…
    • Object-Oriented C++:类、封装、继承、多态、虚函数、动态绑定等等;
    • Template C++:泛型编程部分;
    • STL:是一个Template程序库,容器、迭代器、算法以及函数对象。

从某个此语言切换到另一个时,高效编程守则可能会发生变化。比如,C-like类型(内置类型)pass by value更好;对于Object-Oriented C++而言,pass by reference to-const更好;再切换到STL,由于迭代器和函数对象都是在C指针之上塑造出来的,pass by value守则再次适用。

条款02:尽量以constenuminline替换#define
  • 对于单纯常量,最好以const对象或enum hack替换#define

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 记号名称未进入符号表, 难以调试
    // 在多处出现目标码, 尤其浮点常量
    // 无作用域概念, 不提供任何封装性
    #define ASPECT_RATIO 1.653

    // 常量会被编译器看到, 进入符号表
    // 导致较小量的目标码, 因为只有一份
    // 可放置在类内或 namespace 中限制其作用域
    const double AspectRatio = 1.653;

    // 可以放在类或 namespace 中
    // 令 NumTurns 成为 5 的记号名称, 一般用在类中做常量用
    // 行为像 #define, 对 const 取地址合法,对 enum 和 #define 取地址就不合法
    enum {NumTurns = 5};
  • 对于形似函数的宏,最好用inline(或模板)函数替换

    宏中的变量有可能会被运算多次。

条款03:尽可能使用const
  • const作用于迭代器

    STL迭代器是以指针为根据塑模出来的,其作用就像个T*指针。声明迭代器为const只是声明一个const指针(作用就像T* const),表明的是迭代器本身不可变,但其所指的值是可以改动的。如果希望迭代器所指的值不可改动,需要的是const_iterator

    1
    2
    3
    4
    5
    6
    7
    8
    9
    std::vector<int> vec;
    ...
    const std::vector<int>::iterator iter = vec.begin();
    *iter = 10; // 正确
    ++iter; // 错误

    std::vector<int>::const_iterator citer = vec.begin();
    *citer = 10; // 错误
    ++citer; // 正确
  • operator*的返回类型声明为const-by-value

  • const可被施加于任何作用域内的对象、函数参数(常用pass-by-reference-to-const),函数返回类型、类成员函数本体

  • const施加于成员函数

    成员函数上的const限定符意味着不能修改non-mutablenon-static类数据成员。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    class TextBook
    {
    public:
    ...
    const char& operator[](std::size_t position) const
    {
    ... // 边界检验
    ... // 日记数据访问
    ... // 检验数据完整性
    return text[position];
    }
    /*
    char& operator[](std::size_t position)
    {
    ... // 边界检验
    ... // 日记数据访问
    ... // 检验数据完整性
    return text[position];
    }
    */
    char& operator[](std::size_t position)
    {
    // 调用 const_cast 移除对象身上的 const
    // 调用 static_cast 为 *this 加上 cosnt
    return const_cast<char &>(
    static_cast<const TextBook&>(*this)[position]
    );
    }
    private:
    std::string text;
    };
  • const版本成员函数调用non-const版本不合法

条款04:确定对象被使用前已先被初始化
  • 为内置型对象进行手工初始化,因为C++并不保证初始化它们

  • 构造函数使用成员初始化列表,初始化顺序与在类中声明顺序一致

  • 为避免“跨编译单元内定义的non-local static对象的初始化问题”,以local对象替换non-local static对象

    • static对象包括global对象、定义与namespace作用域内的对象、在class内、函数内、以及在file作用域内被声明为static的对象
    • 函数内的static被称为local static对象,其它static对象被称为non-local static对象
    • 程序结束时,static对象会被自动销毁,也就是它们的析构函数在main函数结束时被自动调用
    • 编译单元是指产出单一目标文件的源码文件以及所含入的头文件

现在有两个源码文件,每个至少含入一个non-local static对象,其中一个non-local static对象用到了另一个non-local static对象,而被用到的尚未被初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 文件 FileSystem.cpp 中
class FileSystem
{
public:
...
std::size_t numDisks()const; // 众多成员函数之一
...
};
extern FileSystem tfs; // 预备给客户使用的对象, non-local static 对象

// 文件 Directory.cpp 中
class Directory
{
public:
Directory(); // 构造函数
...
};
Directory::Directory()
{
...
std::size_t disks = tfs.numDisks(); // 使用 tfs 对象
...
}

现在客户决定创建一个Directory对象,用来放置临时文件:

1
Directory tempDir; // 为临时文件而做出的目录

这个时候就会出现初始化次序带来的问题。由于tfstempDir是不同的人在不同的时间于不同的源码文件中创建出来的,因此初始化次序不一定。

解决方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 文件 FileSystem.cpp 中
class FileSystem
{
public:
...
std::size_t numDisks() const; // 众多成员函数之一
...
};
FileSystem& tfs() // 用于创建预备给客户使用的对象的函数
{
static FileSystem fs; // local static 对象
return fs;
}

// 文件 Directory.cpp 中
class Directory
{
public:
Directory(); // 构造函数
...
};
Directory::Directory()
{
...
// 这样就保证了被使用对象先被初始化
std::size_t disks = tfs().numDisks(); // 使用 tfs() 创建对象
...
}