一:让自己习惯C++
条款一:视C++为一个语言联邦
学好C++ 的第一步,就是要把C++ 视为一个由相关语言组成的联邦而不是单一的语言。在其某个次语言中,各种守则与通例都比较简单,直观易懂。但是,当你从从一个次语言到另一个次语言,守则就可能改变。我们把C++分成4个次语言:
-
C语言:
这个不用多说,C++ 就是在C基础上发展来的,它也很好地继承和保留了C的特性。 -
面向对象C++:
这部分是C with Classes所追求的,包括类(构造函数和析构函数),封装,继承,多态,虚函数(动态绑定)等等。这部分是面向对象设计在C++上的最直接实施。 -
模板C++:
这是C++的泛型编程部分,类模板、函数模板等等,威力巨大。 -
STL容器
STL其实是模板程序库,它对容、迭代器、算法以及函数对象的约定有着极佳的紧密配合与协调,当你用STL写代码时,必须遵守它的规约。
因此,我认为,C++ 并不是一个带有一组守则的一体语言;它是由4个次语言组成的语言联邦,每个次语言都有自己的规矩。记住这四个次语言你就会发现C++更容易学习。
小结:
C++ 高效编程守则视状况而变化,取决于你使用C++ 的哪部分。
条款二:尽量以const, enum, inline替换#define
这个条款或许改为“以编译器替换预处理器”比较好,因为#define可能不被视为语言的一部分,我们看看有可能会出现什么问题:
#define PI 3.14
记号名称也许从未被编译器看见,pi并不在编译器的记号表中,因为预处理器会把所有出现PI的地方换成3.14,于是如果有关于这个变量的错误,编译器会提到3.14而不是pi,而且这个define是你的一个同事写的,你就不知道这个奇怪的常数是怎么来的了。
更好的方法是用一个常量替换上述的宏(#define):
const double pi = 3.14;
当我们用常量const替换宏#define,有两种特殊情况要说明一下:
- 定义常量指针:
由于常量定义式通常被放在头文件.h中(以便被不同的源文件.cpp包含),因此有必要将指针也声明为const(指针所指的是你要定义的常量,肯定是const),你必须const两次:const char* const authorName = "SYF";
- class专属常量:
为了将常量的作用域限制在class中,该常量得成为class的一个成员;为了确保这个常量只有一份实体,我们让它成为一个static成员:
这里还有一个特殊情况:如果一个变量是class专属常量,又是static,而且还是整数类型(int,char,bool等),就需要特殊处理。上面的那个式子就成了“带有初值的声明式”,你在不取地址使用它的时候是okay的,你要是取地址的话,就要在外面加一个不带初值的定义式:class GamePlayer{ private: static const int NumTurns = 5; };
这个式子请放在.cpp文件而不是.hconst int GamePlayer::NumTurns;
这个情况还是蛮特殊的,就记住吧
那么const是不是就万能呢,基本万能,但是如果你有特殊需要,比如说你不想让别人获得一个引用或指针指向你的常量,enum可以帮助你实现约束。因为enum hack的行为更像#define而不是const,取一个const的地址是合法的,但是取enum或#define就不合法。
让我们再回到预处理器,关于#define的另一个常见误用是用它来实现函数宏:
#define CALL_WITH_MAX(a, b) f(a > b ? a : b) //以a和b的较大值调用f函数
看着是不是还挺6,既避免了函数调用的开销,又实现了功能,下面来看看问在哪:
int a = 5, b = 0;
//调用者很可能这么写:
CALL_WITH_MAX(++a, b); //a加了两次
CALL_WITH_MAX(++a, b+10); //a加了一次
在上述代码中,调用f之前,a的递增次数竟然取决于它被拿来和谁比较,这肯定不是调用者想要的行为
我们的解决方法是用模板内联函数:
template<typename T>
inline void callWithMax(const T& a, const T& b){
f(a > b ? a : b);
}
小结:
- 对于单纯常量,最好用const对象或enums替代#define
- 对于函数宏,最好改用内联函数替代
条款三:尽可能使用const
我们来举个例子说明为什么要尽量使用const
我们来实现一个有理数的乘法:
class Rational{...};
const Rational operator* (const Rational& lhs, const Rational& rhs);
很多人觉得返回一个const对象没必要,但如果没有const的话,别人就可以这样使用你的程序:
Rational a, b, c;
(a*b) = c; //这个赋值时没什么意思的,我们可以通过返回const来预防它
const成员函数
将const实施于成员函数的目的,是为了确认该成员函数可以作用于const对象上
很多人漠视这样一个事实:两个成员函数如果只是常量性不同,也算是重载,例如:
class TextBlock{
public:
const char& operator[] (std::size_t position) const
//后面的const表示该函数不修改成员变量
{
return text[position];
}
char& operator[] (std::size_t position)
//跟上一个函数是重载关系
{
return text[position];
}
};
我们来调用看看:
const TextBlock ctb("World");
ctb[0] = 'x'; //这句话对不
调用operator[]没问题,错误是因为企图对const赋值
我们了解了把成员函数声明为const的好处(确保不修改成员变量),回过头来看看这样做会有什么不好的地方呢?
class TextBlock{
public:
std::size_t length() const;
private:
char* pText;
std::size_t textLength; //最近一次文本的长度
bool lengthIsValid; //当前的长度是否有效
};
//对于length()函数,我们很自然写出下面的函数代码
std::size_t CTextBlock::length() const{
if(!LengthIsValid){
textLength = std::strlen(pText);
lengthIsValid = true;
}
return textLength;
}
很可惜,这个函数是错的。因为你把它声明了const,完了又在函数体中修改成员变量textLength和lengthIsValid,但是我们会觉得其实这个函数这么写也可以接受啊,我们就用神器mutable表明这些成员变量即使在const成员函数内,也可以被改变。
在const和non-const成员函数中避免重复
方法就是:non-const不直接实现,而是利用转型去调用const版本来实现:
class TextBlock{
public:
const char& operator[] (std::size_t position) const
//后面的const表示该函数不修改成员变量
{
...//这里的省略号表示可能还有一堆操作,边界检查啊之类的
return text[position];
}
char& operator[] (std::size_t position)
//跟上一个函数是重载关系
{
return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
}
};
小结
- 编译器强制实施const,但你应该使用概念上的const,也就是该用mutable的地方要用
- 令non-const版本调用const版本可避免代码重复
确定对象被使用前已先被初始化
永远在使用对象之前将它初始化
- 对于内置类型:就直接赋值好了,虽然编译器有时候会帮着赋值,但最好还是自己动手
- 内置类型以外的:初始化就由构造函数负责,规则很简单:++确保每一个构造函数都将对象的每一个成员初始化。++
构造函数初始化的一个比较好的做法是:使用成员初始列表,原因如下:
- 直接在构造函数体内赋值的话,相当于先调用成员变量类型的默认构造函数,再调用拷贝赋值运算符,而使用成员初始化列表只会调用一次拷贝构造函数,更高效
- 如果变量有const或者引用的,更要用初始化列表了,因为它们一定是在定义时被初始化
如果你很小心地做到了以上两点:
- 将内置类型明确地初始化
- 确保构造函数运用初始化列表初始化
现在你只需要担心一个大boss了-不同编译单元内定义之non-local static对象的初始化次序
先来理解下这句话
编译单元就是.cpp加上其所包含的头文件
non-local static就是除了定义在函数内的static
所以我们的问题就是:
如果某编译单元内的某个non-local static对象的初始化使用了另一单元的某个non-local static对象,它使用的这个对象可能尚未初始化。
解决方法是:把每个non-local static对象用返回其引用的函数替换,这样在调用函数时就能保证其初始化完成了,这就是用local static对象替换non-local static对象
小结
- 对内置类型进行手动初始化
- 使用成员初始化列表初始化所有成员变量,排列次序最好与声明次序相同
- 为避免跨编译单元的初始化次序问题,请用返回引用的函数替换non-local static对象