Effective C++ 学习笔记 第一章:让自己习惯 C++

第一章见 Effective C++ 学习笔记 第一章:让自己习惯 C++
第二章见 Effective C++ 学习笔记 第二章:构造、析构、赋值运算
第三章见 Effective C++ 学习笔记 第三章:资源管理
第四章见 Effective C++ 学习笔记 第四章:设计与声明
第五章见 Effective C++ 学习笔记 第五章:实现
第六章见 Effective C++ 学习笔记 第六章:继承与面向对象设计
第七章见 Effective C++ 学习笔记 第七章:模板与泛型编程
第八章见 Effective C++ 学习笔记 第八章:定制 new 和 delete
第九章见 Effective C++ 学习笔记 第九章:杂项讨论

本来看这本书已经好几天了,没准备做笔记,但看了几个条款,发现这本书总结的太好了,不记一下,回头忘了不好,如果对其他人有帮助就更好了。每个条款的内容是我自己理解的内容,最后的总结是摘抄自书中原文。

文章目录

    • 条款 01:让自己习惯 C++
      • 总结
    • 条款 02:尽量以const, enum, inline 替换 #define
      • 话题 1:用 const 和 enum 代替 #define 常量
      • 话题 2:用 inline 取代 #define 宏
      • 话题 3:#define 不等同于预处理操作
      • 总结
    • 条款 03:尽可能使用 const
      • 话题 1:const 修饰指针
      • 话题 2:const 修饰函数
      • 话题 3:const 修饰成员函数(重要)
      • 话题 4:const_cast 应用
      • 总结
    • 条款 04:确定对象被使用前已先被初始化
      • 话题 1:不要混淆赋值和初始化
      • 话题 2:non-local static 对象
      • 总结

条款 01:让自己习惯 C++

Accustoming Yourself to C++.

C++是语言联邦,它综合了多种编程语言的特点,是多重范型编程语言(注意是范型,不是泛型),支持过程形式(procedural),面向对象形式(object-oriented),函数形式(functionnal),泛型形式(generic),元编程形式(meta programming)。

将 C++认为是多种次语言的结合,次语言有四种:

  • C 语言基础。C++最早出现时,从 C 中派生出来的那些特性。
  • Object-Oriented C++。也就是 C with classes,类,继承,多态,虚函数这些概念。
  • Template C++。C++的泛型编程,类和对象泛型化。
  • STL:最重要的模板库,提供容器、迭代器、算法和函数对象等。

在次语言之间切换工程,需要认真遵守当前次语言的规范。

总结

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

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

** Prefer consts, enums, and inlines to #defines.**

话题 1:用 const 和 enum 代替 #define 常量

#define 定义之下的标记不会经由编译器处理,记住这个道理,在编译器之前被预处理器处理了,所以 #define 引入的问题,编译器很难查出来。使用 const 代替 #define 来定义常量,编译器可以帮助检查如类型错误这一类问题。

const 作用于指针,分为作用到指针指向对象的不变性和指针本身的不变性。

以下代码定义了一个类内的类静态常量成员(类专属常量成员):

class Game {
private:
    static const int NumTurns = 5;    // 注意这是声明,因为是常量,必须这里赋值,有关于为什么声明时可赋值,接下来会介绍
    int scores[NumTurns];             // 可直接使用该常量
}

如果是类静态整形常量成员,C++ 要求如果你要取 NumTurns 的地址,或者编译器要查看,那必须为其提供一个定义式,放到实现文件中:

const int Game::NumTurns;            // 这是定义,因为声明时已指定常量值,这里不能再指定一次

现代编译器允许对类静态整形常量成员在声明时指定常量值。

对于非整数的其他类静态常量成员,无法在声明中指定常量值,这时可通过定义指定常量(前者在声明时指定常量,是为了能在后续的其他声明时用,比如 scores 数组长度)。

书中还给出了补偿做法。如果编译器不允许在声明时指定常量值,可采用 enum 方式:

class Game {
private:
    enum { NumTurns = 5 };          // 这个叫 enum hack
    int scores[NumTurns];

enum 无法取地址,所以其更像 #define 而不是 const,如果你希望能约束对 NumTurns 的取地址操作,可采用 enum hack 的方式。

enum hack 是模板元编程的基础技术。

话题 2:用 inline 取代 #define 宏

我们已经知道 #define 宏来作为函数功能会导致一些难以调试的 bug。
为了避免这些 bug,我们需要做保护性操作,比如对每个参数都加括号,但依然对一些情况无法避免。例子如下:

#define CALL_WITH_MAX(a, b) f((a) > (b)) ? (a) : (b))
int a = 5, b = 0;
CALL_WITH_MAX(++a, b);              // a 被累加 2 次
CALL_WITH_MAX(++a, b+10);           // a 被累加 1 次

如注释说明。这种问题很难调试,因为要记住,#define 不会输入编译器。

话题 3:#define 不等同于预处理操作

不要认为因为 #define 有各种问题,就否定预处理操作。除了 #define 以外,还有很多预处理指令。

  • #include 在文件包含中必不可少;
  • #ifdef 和 #ifndef 在编译控制中无法被替代;
  • #pragma 控制编译特性也会被用到;

总结

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

条款 03:尽可能使用 const

** Use const whenever possible. **

话题 1:const 修饰指针

众所周知,const 修饰指针分为修饰指针本身(指针只能指向固定的地址,但指向位置的值可变)和修饰指针指向的对象(指针指向的位置的值固定,但指针可指向不同地址),或者两者。
对于后者,有些人习惯将 const 写在类型前,有些人习惯写在 * 前,这都是对的。

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

使用迭代器时,默认迭代器 iterator 在实现上等同于 T *,对其进行 const 修饰,等同于修饰指针本身为 const,若希望实现 const 修饰迭代器指向位置的值为固定值,使用 const_iterator

const std::vector<int>::iterator iter = vec.begin();          // 等同于 T * const
*iter = 10;        // 正确
iter ++;           // 错误
std::vector<int>::const_iterator cIter = vec.begin();         // 等同于 const T *
*cIter = 10;       // 错误
cIter ++;          // 正确

话题 2:const 修饰函数

const 修饰返回值时,可以有效防止错误的自定义类型赋值操作:

class Rational { ... };
const Rational operator* (const Rational &lhs, const Ratonal &rhs);
Rational a, b, c;
(a * b) = c;      // 编译器会报错,不能给 const 类型赋值
if (a * b = c) { ... }  // 错将 == 写作 =,编译器会报错,不能给 const 类型赋值

上例中,如果operator* 没有将返回值修饰为const,则后续的赋值操作编译器不会报错,但其本身也没意义,还可能隐含有 bug 难以排查。
内置类型不存在这个问题,无法给内置类型运算结果赋值。

话题 3:const 修饰成员函数(重要)

可针对同一种功能接口(成员函数)区分两种不同返回类型(non-const 和 const),来分别处理 non-const 对象和 const 对象。

另外,重要哲学思辨:const 修饰成员函数本身有两种解释:bitwise constness(又称 physical constness)和 logical constness。
bitwise constness 认为,成员函数只有在不更改任何成员变量(static 除外)时,才是 const 的。
logical constness 说,你有漏洞,const 的成员函数中,通过指针更改了指针所指物,如果所指物本身属于类,而指针本身不会在 const 的成员函数中被修改,那么编译器时不会报错的,比如:

class CTB {
public:
	char& operator[] (std::size_t position) const       // bitwise const 声明
	{
		return pText[position];
	}
private:
	char* pText;
}

但实际操作中,operator[] 还是会修改类成员变量:

const CTB cctb("Hello");          // 常量对象
char* pc = &cctb[0];              // 使用常量成员函数 operator[] 读取数据地址
*pc = 'J';                        // 编译器正常输出,此时cctb.pText = "Jello"

logical constness 认为,const 修饰成员函数,可以修改他所处理的对象内的数据,但只有当客户端感知不到的情况下才可以。
编译器按 bitwise constness 检查代码,但程序员应该做到 logical constness。
另外,引入 mutable 关键字,用来释放掉非静态成员变量的 bitwise constness 约束:

class CTB {
public:
	std::size_t length() const;
private:
	std::size_t textLength;
	mutable std::size_t mTextLength;   // 该成员变量可以在 const 成员函数内被改变
};
std::size_t CTB::length() const
{
	textLength = 2;        // 错误,const 成员函数内无法给成员赋值
	mTextLength = 2;       // 正确
}

话题 4:const_cast 应用

const_cast 用于做与 const 相关的类型转换,移除 const 或添加 const。
当类中 non-const 和 const 版本的相同功能成员函数的内部功能完全一致的情况下,为了避免重复 copy-paste,可采用 const_cast 来协助。
请查看如下示例代码:

class TB {
public:
	const char& operator[] (std::size_t position) const
	{ ... }
	char& operator[] (std::size_t position)
	{
		return
			const_cast<char&>(						// 移除 operator[] const 中的 const
				static_cast<const TB&>(*this)       // 为 *this 加上 const 修饰,防止自递归
					[position]						// 调用 const operator[]
				);
	}
};

注意,反过来,在 const 成员函数中使用这种方法调用 non-const 成员函数,是不合理的。

总结

  • 将某些东西声明为 const 可以帮助编译器检查错误。 const 可用于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
  • 编译器强制执行 bitwise constness,但编程时应当按照 logical constness 完成。
  • 当 const 和 non-const 成员函数有完全等价的实现时,可使 non-const 版本调用 const 版本来避免代码重复。

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

** Make sure that objects are initialized before they’re used. **
C++ 在一些情况下,不会给用户未指定的对象赋初值,这种时候,对象的值是未定义的任意值。
解决方法就是,确保都被初始化,如果是非基本类型对象,确保在构造函数中对对象的所有数据对象初始化。

话题 1:不要混淆赋值和初始化

看下边的例子:

class ABE {
public:
	ABE(const std::string &name);
private:
	std::string theName;
	int num;
};
ABE::ABE(const std::string& name)
{
	theName = name;			// 这个是赋值,而不是初始化
	num = 0;				// 这个也是赋值
}

如注释述,这种方式是赋值,虽然也能达到效果,但 C++规定,对象的成员变量的初始化动作要发生在进入构造函数本体之前,所以实际上编译器会在进入构造函数之前,先给所有数据成员做初始化,然后进去后再做赋值操作。
应当使用初始化列表来完成初始化(而不是在构造函数中赋值)。初始化列表这么用:

ABE::ABE(const std::string& name)
  : theName(name),
    num(0)
{ }

这么做效率高,因为不需要先调 ABE 的 defualt 构造函数再调用 std::string 的 copy 赋值操作,而是直接调用 std::string 的 copy 操作。内置类型没有影响,但考虑到格式统一,也放到初始化列表中为宜。
但注意,如果没有手动对内置类型对象做初始化,可能编译器也不会帮你做。
建议所有数据成员都写在 default 构造函数的初始化列表中,包括内置类型。这样另外一个好处是,const 和 reference 的数据成员可以被初始化了,而赋值不行,既然初始化列表都能做,干嘛还在构造函数内赋值呢。
初始化列表中的次序与其被初始化的顺序无关,初始化顺序取决于数据成员的声明顺序,但为了便于理解,建议顺序统一。这种场景比如先初始化 array 的长度,再初始化 array,反过来就会出错。

话题 2:non-local static 对象

static 对象的生存期从其构造出开始,到程序结束为止。
位于函数中的 static 对象称为 local static 对象,否则称为 non-local static 对象。
C++对定义在不同的编译单元(不同的源文件内)中的 non-local static 对象的初始化次序是未定义的。
所以存在的问题是:如果多个编译单元内同时有non-local static 对象,而且他们有依赖,将怎么保证正确性?
答案是没办法做到。
建议是,把这些 non-local static 对象放到函数里边,变成 local static 对象,然后通过调用函数传引用的方式来使用这些对象。编译器可以保证在调用函数且遇到 local static 对象时将其初始化。
但是,在多线程应用时,依然会存在问题,解决办法是,在多线程应用的单线程运行时(启动多线程之前),将这些函数都调用一下,让编译器创建其内部的所有 local static 对象。

总结

  • 为内置类型对象进行手动初始化,C++不保证为他们初始化。
  • 构造函数最好用成员初始化列表,而不要在内部使用赋值操作。初始化的排列次序与初始化列表中顺序无关,和声明次序一致。
  • 为避免跨编译单元的初始化次序问题,将这些 non-local static 对象变成 local static 对象。

你可能感兴趣的:(学习笔记,C++,c++,编程语言,高效编程,Effective,C++)