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

1. 让自己习惯 C++

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

为了理解 C++,必须认识其主要的次语言:

  • C:C++ 仍以 C 为基础。很多时候,C++ 对问题的解法起始是较高级的 C 的解法(如,条款 02、条款 13)。
  • Object-Oriented C++:C with Classes,classes、封装、继承、堕胎、虚函数等等。这部分是面向对象设计之古典守则在 C++ 上最直接的实施。
  • Template C++:泛型编程
  • STL:STL 是个 template 程序库,即 C++ 标准库。

C++ 并不是也给带有一组守则的一体语言:它是从四个次语言组成的语言联邦,每个次语言都有自己的规约。

请记住:

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

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

此条例可以看作,使用编译器代替预处理器,因此 #define 不被视为语言的一部分。

#define ASPECT_PATIO 1.653

记号 ASPECT_PATIO 可能不会被编译器看见,导致 ASPECT_PATIO 可能没有进入记号表中。当运用此常量发生编译错误时,这个错误信息可能提到的是 1.653 而不是 ASPECT_PATIO,此时你可能会为了追踪 1.653 的源头浪费大量的时间。解决这一问题的方法是使用一个常量代换上述的宏:

const double AspectRatio = 1.653;  // 大写名称常用于宏,因此这里改变名称的写法

作为一个语言常量,AspectRatio 可你的那个会被编译器看到,并进入记号表内。

无法利用 #define 创建一个 class 专属常量

我们无法利用 #define 创建一个 class 专属常量,因为 #define 并不重视作用域。一旦宏被定义,它就在其后编程过程中有效(除非在某处被 #undef)。这意味着 #define 不仅不能用来定义 class 专属常量,也不能提供任何封装性(private)。

利用 enum 表达常量

当编译器不允许 “static 整数型 class” 完成 “inclass 初值设定(类内初始化)”,可以使用 enum 来解决,例如:

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

enum 的这个行为类似于 #define 而不是 const,例如 取一个 const 的地址是合法的,但取一个 enum 的地址就不合法,取一个 #define 的地址通常也不合法。enum 和 #define 一样绝不会导致非必要的内存分配。

不要用形似函数的宏

#define 实现的宏看起来像函数,但不会有函数调用带来的额外开销。下面这个宏带着宏实参,调用函数 f:

// 以 a 和 b 的较大值调用 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 inline 函数(条款 30):

// 由于我们不知道 T 是什么,所以采用 pass by reference-to-const 见条款 20
template<typename T>
inline void callWithMax(const T& a, const T& b) {
	if(a > b ? a : b);
}

这里不需要在函数本体中为参数加上括号,也不需要操行参数被核算多次等待。

有了 const、enum 和 inline,我们对预处理器(尤其是 #define)的需求降低了,但并非完全消除。

请记住:

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

条款 03:尽可能使用 const

const 允许指定一个于一约束(指定不被改动的对象),而编译器会强制执行这项约束。它允许你告诉编译器和其他程序员某值应该保持不变。

如果关键字 const 出现在星号左边,表示被指物事常量(底层);如果出现在星号右边,表示指针自身事常量(顶层);如果出现在星号两边,表示被指物和指针两者都是常量。

STL 迭代器是以指针为根据得到的,所以迭代器的作用就像哥 T* 指针。生命迭代器为 const 就像声明指针为 const 一样(即,声明一个 T* const 指针),表示这个迭代器不得指向不同的东西,但它所指的东西的值可以改动。

令函数返回一个常量值,往往可以降低因客户错误而造成的以外,而又不至于放弃安全性和高效性,例如:

const Rational operator* (const Rational& lhs, const Rational& rhs);

可以阻止如下操作:

Rational a, b, c;
(a * b) = c;  // 在 a * b 的成果上调用 operator=

如果 a 和 b 都是内置类型,这样的代码直截了当就是不合法。而一个”良好的用户自定义类型“的特征是它们避免无端地与内置类型不兼容。将 operator* 的返回值声明为 const 可以预防那个没有意义的赋值动作。

至于 const 参数,除非你有需要改动参数或 local 对象,否则请将它们声明为 const。

const 成员函数

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

  • 它们使 class 接口比较容易被理解。因为,这样可以知道哪些函数可以改动对象内容,哪些不行。
  • 它们使”操作 const 对象“成为可能。

需要注意一个事实:两个成员函数如果只是常量性不同,那么它们可以被重载

bitwise constness(physical constness) 和 logical constness

成员函数如果是 const 意味着什么?有两个流行概念: bitwise constness(又称 physical constness)和 logical constness。

bitwise constness(physical constness)

bitwise const 认为,成员函数只有在不更改对象的任何成员变量(static 除外)时才可以说是 const。也就是说它不更改对象内部的任何一个 bit。这种论点的好处是很容易发现错误点:编译器只需寻找成员变量的赋值动作即可。bitwise constness 正是 C++ 对常量性的定义,因此 const 成员函数不可以更该对象内任何 non-static 成员变量

但是很多成员函数虽然没有完全具备 const 性质但是却可以通过 bitwise 测试。如果一个更改了”指针所指物“的成员函数虽然不能算是 const,但是如果只有指针(所指物不属于对象)隶属于对象,那么 bitwise const 不会引发编译错误。

参考下面的例子:

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

这个 class 不适当的将 operator[] 声明为 const 成员函数,而该函数去返回一个 reference 指向对象内布置(条款 28)。在此例中 operator[] 并不更改 pText。因为是 bitwise const,所以编译器可以通过,但是如果发生下面的操作:

const CTextBlock cctb("Hello");  // 声明一个常量对象
char *pc = &cctb[0];  // const operator[] 区的一个指针,指向 cctb
*pc = 'J';  // cctb 变成了 "Jello"

创建了一个常量对象并设以某值,而且只对它调用 const 成员函数,但是还是改变了它的值。

logical constness

这种情况产生了 logical constness。这一派主张,一个 const 成员函数可以修改它所处理的对象内的某些 bits,但只有在客户端检测不出的情况下才行:

class CTextBlock {
public:
	std::size_t length() const;
private:
	char* pText;
	std::size_t textLength;  // 最近一次计算的文本区块长度
	bool lengthIsValid;  // 目前的长度是否有效
};

std::size_t CTextBlock::length() const {
	if (!lengthIsValid) {
		textLength = std::strlen(pText);  // 错误 在 const 内不能给其他成员赋值
		lengthIsValid = true;
	}
}

由于 length 会修改 textLength 和 lengthIsValid,所以编译器会发出错误。此时就需要一个 C++ 与 const 相关的摆动场:mutable。mutable 会释放掉 non-static 成员变量的 bitwise const 约束。此时编译器就不会发出错误信息了。

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

假设 TextBlock 内的 operator[] 不但只是返回一个 reference 指向某个字符,也执行一些其他的检测。把所有这些同时放进 const 和 non-const operator[] 中,会出现代码重复、回鹘、代码膨胀等待问题:

class TextBlock {
public:
	const char& operator[](std::size_t position) const {
		...  // 边界检测
		...  // 志记数据访问
		...  // 检验数据完整性
		return pText[position];
	}
	char& operator[](std::size_t position) {
		...  // 边界检测
		...  // 志记数据访问
		...  // 检验数据完整性
		return pText[position];
	}
private:
	std::string text;
};

虽然可以将这些代码放到另一个成员函数中,并令两个版本的 operator[] 调用它,但是还是重复了一些代码,如函数调用、两次 return 语句。我们应该实现 operator[] 的功能并使用它两次。

本例中 const operator[] 完全实现了 non-const 版本该做的一切,唯一的不同是返回类型多了一个 const 修饰。这时候让 non-const 版本调用 const 版本是一个避免代码重复的安全做法:

class TextBlock {
public:
	const char& operator[](std::size_t position) const {
		...  // 边界检测
		...  // 志记数据访问
		...  // 检验数据完整性
		return pText[position];
	}
	char& operator[](std::size_t position) {
		return const_cast<char&>(
			static_cast<const TextBlock&>(*this)[position]
		);  // 将 op[] 返回值的 const 移除,为 *this 加上 const,调用 const op[]
	}
private:
	std::string text;
};

这个代码有两个类型转换操作,第一次用来为 *this 添加 const(使接下来的调用使用 const 版本,而不是 non-const 版本),第二次则是从 const operator[] 的返回值中移除 const。

第一次类型转换,通过 static_cast 强制类型转换 添加了 const;第二次类型转换,移除 const,只能通过 const_cast 来实现(static_cast 和 const_cast 见条款 27)。

不应该令 const 版本调用 non-const 版本,const 成员函数承诺不改变其对象的逻辑状态,non-const 成员函数没有这样的承诺。所以如果这样做,有可能会让你承诺不改动的对象被改动了。这也是为什么“const 成员函数调用 non-const 成员函数”是一种错误行为。

请记住:

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

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

读取未初始化的值会导致不明确的行为。读取未初始化的值可能会导致程序终止运行,但更可能的情况是读入一些“半随机”bits,污染了正在进行读取动过的那个对象,最终导致不可预测的程序行为。

通常如果你使用 C part of C++ 而且初始化可能招致运行期成本,那么就不保证发生初始化。而 non-C parts of C++,规则会有所不同。这样解释了为什么 array(来自 C part of C++)不保证其内容被初始化,而 vector(来自 non-C parts of C++)却有此保证。

表面上这是无法决定的状态,最佳处理方法是:永远在使用对象之前将它初始化。

对于内置类型以外的任何东西,初始化责任落在构造函数身上。但是不要混淆赋值和初始化。在构造函数里,应该使用成员初始值列表来标识初始化。

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

C++ 有十分固定的“成员初始化次序”。是的,次序总是相同:base classes 更早于其 berived classes 被初始化,而 class 的成员变量总是以其声明次序被初始化。

跨编译单元初始化

编译单元是指产出单一目标文件的那些源码。基本上它是单一源码文件加上其所含入的头文件。

解决问题:如果某编译单元内的某个 non-local static 对象的初始化动作使用了另一个编译单元内的某个 non-local static 对象,它所用到的这个对象可能尚未被初始化,因为 C++ 对“定义于不同比那一单元内的 non-local static 对象”的初始化次序并无明确定义。

例如:

class FIleSystem {
public:
	...
	std::size_t numDisks() const;
	...
};
extern FileSystem tfs;  // 预备给客户使用的对象

假设某些客户建立了一个 class 用以处理文件系统内的目录,此时他们的 class 会用上 FileSystem 对象:

class Directory {
public:
	Sirectory(params);
};
Directory::Directory(params) {
	std::size_t disks = tfs.numDisks();  // 使用 tfs 对象
}

此时初始化次序的重要性出现了:除非 tfs 在 tempDir 之前先被初始化,否则 tempDir 的构造函数会用到尚未初始化的 tfs。但是 tfs 和 tempDir 是不同的人在不同的时间于不同的源码文件建立起来的。换句话说,C++ 对“定义于不同编译单元内的 nonlocal static 对象”的初始化相对次序并无明确定义。

解决这个问题的方法是:将每个 non-local static 对象搬到自己的专属函数内(该对象在此函数内被声明为 static)。这些函数返回一个 reference 指向它所含的对象。然后用户调用这些函数,而不直接使用这些对象。换句话说,non-local static 对象被 local static 对象替换了。

也就是说,C++ 保证,如果以 函数调用 替换直接访问 non-local static,函数内的 local static 对象会在“函数被调用时”或“首次遇到该对象定义时”已经被初始化了。

class FileSystem { ... };
FileSystem& tfs() {
	static FileSystem fs;
	return fs;
}

class Directory { ... };
Directory::Directory(params) {
	std::size_t disks = tfs().numDisks();
}
Directory& tempDir() {
	static Directory td;
	return td;
}

这么修改后,这个系统程序完全可以像以前一样使用它,唯一不同的是他们现在使用 tfs() 和 tempDir() 而不再是 tfs 和 tempDir。也就是说他们使用函数返回的“指向 static 对象”的引用,而不再使用 static 对象自身。

请记住:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值