Home 从 C 到 C++
Post
Cancel

从 C 到 C++

enum hack

在 C++ 中, 大部分宏定义常量可以通过 const 关键字替换, 一个例外是在 class 编译期间需要使用一个 class 常量, 这种情况可以利用 enum 类型来实现替换. 其理论基础在于, 一个属于枚举类型的数值可以被视为 int 型来使用. 例如下面的类定义:

1
2
3
4
5
6
7
class GamePlayer {
private:
    enum { num_turns = 5 };

    int scores[num_turns];
    ...
};

#define 类似, 对 enum 取地址是不合法的. 如果 不想让别人获得一个指针或引用指向某个整数常量 , 可以通过 enum 来实现.

使用 const

const_iterator

迭代器的作用类似于 T*, 因此将迭代器声明为 const, 相当于声明一个 T* const 指针. 即指针不可变, 但指针所指的值是可变的. 如果需要一个 T const * 指针, 则需要使用 const_iterator.

const 成员函数

考虑下面这个用来表示一段文本的类:

1
2
3
4
5
6
7
8
class TextBlock {
public:
    ...
    const char& operator[](std::size_t pos) const;
    char& operator[](std::size_t pos);
private:
    std::string text;
};

通过函数重载, 可以避免下面这类错误:

1
2
const TextBlock ctb("word");
ctb[0] = 'x';	// 报错, 试图对一个 const 对象进行写操作

Bitwise const 和 Logical const

  • Bitwise const: 成员函数只有在不改变对象的任何成员变量( static 类型除外)时才可以说是 const.
  • Logical const: 一个 const 类型成员函数可以在客户端侦测不出来的情况下修改对象的某些成员变量. 这需要通过 mutable 关键字实现. 示例代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class TextBlock {
public:
    ...
    std::size_t length() const;
private:
    char * text;
    mutable std::size_t length;
    mutable bool length_valid;
};

std::size_t TextBlock::length() const
{
    if (!length_valid) {
        length = std::strlen(text);
        length_valid = true;
    }
    return length;
}

在 const 和 non-const 成员函数中避免重复

同一个函数的 const 版本和 non-const 版本中往往包含大量的重合代码, 通常的做法是使这两个函数中的其中一个调用另一个, 这就需要使用 转型(casting) 进行常量性的变化. 示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class TextBlock {
public:
    ...
    const char& operator[](std::size_t pos) const
    {
        ...
        return text[pos];
    }

    char& operator[](std::size_t pos)
    {
        return const_cast<char&>(
            static_cast<const TextBlock&>(*this)[pos]
        );
    }
};

代码中需要使用两次转型, 首先要通过 static_cast 将 non-const 对象转型为 const 对象, 否则 [] 运算将会递归的调用自己. 在得到 const 成员函数的返回结果后, 再通过转型操作为其加上 const.

在 const 和 non-const 成员函数有实质等价的实现时, 可以令 non-const 版本调用 const 版本 , 而反向的做法是 不可取 的.

确保对象在被使用前已被初始化

区分赋值(assignment)和初始化(initialization)

给出一个类 ABEntry, 代码如下:

1
2
3
4
5
6
7
class ABEntry {
public:
    ABEntry(const std::string& name, const std::string& addr);
private:
    std::string name;
    std::string addr;
};

下面的代码在其构造函数中进行 赋值 :

1
2
3
4
5
6
ABEntry::ABEntry(const std::string& name, const std::string& addr)
{
    this->name = name;
    this->addr = addr;
    ...
}

初始化 通常发生在进入构造函数本体之前, 因此在上面的代码中, nameaddr 其实发生了两次修改, 首先通过默认的构造函数进行了初始化, 之后才在函数体内被赋值. 这种做法导致了一次无效操作, 通常可以使用成员初值列来进行修改, 代码如下:

1
2
3
4
5
6
ABEntry::ABEntry(const std::string& name, const std::string& addr) :
    name(name),
    addr(addr)
{
    ...
}

C++ 有着固定的成员初始化次序, 基类 早于 子类被初始化, 而成员变量按照其 声明次序 被初始化.

non-local static 对象的初始化

static 对象包活 global 对象, 定义域 namespace 作用域内的对象, 在类内、在函数内以及在 file 作用域内被声明为 static 的对象. 函数内的 static 对象被称为 local static 对象, C++ 保证此类对象会在 函数被调用期间 被初始化. 而对于 non-local static 对象则没有这样的保证, 因此无法确定其会在何时被初始化. 例如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class FileSystem { ... };
FileSystem tfs;

class Directory {
public:
    Directory();
    ...
};

Directory::Directory()
{
    ...
    std::size_t disks = tfs.numDisks();
    ...
}

这时, 如果用户创建一个 Directory 对象, 而在构造函数中使用了尚未初始化的 tfs 对象, 就会导致错误出现. 解决此类问题的方法, 即是通过 reference-returning 函数 将 non-local static 对象替换为 local static 对象. 这也是 C++ 中单例(Singleton)模式的一个实现方法. 示例代码如下:

1
2
3
4
5
6
class FileSystem { ... };
FileSystem& tfs()
{
    static FileSystem fs;
    return fs;
}

在上面的代码中, 通过函数 tfs() 来替换原先的 tfs 对象, 在函数中初始化一个 local static 对象, 并返回一个引用指向该对象.

在多线程情况下, 这种做法仍存在不确定性, 一种可行的办法是在程序的单线程启动阶段手工调用所有 reference-returning 函数.

This post is licensed under CC BY 4.0 by the author.