1. 让自己习惯 C++
条款 01:视 C++ 为一个语言联邦
C++ 是一个多重泛型编程语言(multiparadigm programming language)。
C++ 同时支持过程(procedural)形式、面向对象(object-oriented)形式、函数(functional)形式、泛型(generic)形式、元编程(metaprogramming)形式的语言。
C++ 的次语言(sublanguage):
- C
- Object-Oriented C++
- Template C++
- STL
每个次语言有自己的规约。
条款 02:尽量以 const,enum,inline 替换 #define
因为 #define 是预处理器处理的部分,编译器不会认识对应的符号,则对应的符号不会进入符号表(symbol table),这样在 debug 时就会变得难以追踪。
字符串常量
用 const 定义 char-based 字符串常量的指针需要写两遍 const
const char* const str = "str";
通常这种情况下 string 会好一些
const std::string str("str");
类的常量成员可以定义为
class C {
static const int M = 5;
int arr[M];
};
上面是 M 的声明式,而非定义式。对于整数类型(integral type)的类的常成员,如果只使用值则不需要给出定义式,否则需要提供如下的定义式。
const int C::M;
将这个式子放入实现文件,而非头文件,因为在声明时已经有初值,一次此处不能再设值。
#define 无法定义 class 的专属常量,因为 #define 不重视作用域(scope),也不能提供任何封装性。
旧式编译器如果不支持上述语法,那么需要如下写法
// 头文件中
class C {
static const int M;
};
// 实现文件中
const int C::M = 5;
the enum hack
一个属于枚举类型的数值可以充当 int 使用。
class C {
enum { M = 5 };
int arr[M];
};
有时需要这种手段,因为无法取到一个 enum 的地址(通常 #define 也无法取到地址,但 const 的变量是可能取到地址的)。enum 也不会产生非必要的内存。
同时这也是模板元编程的基础。
#define 实现宏
#macros 经常被用来实现宏,但对于
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
需要很多括号保证不会发生一些意外。但即便有括号还是会发生意外。
CALL_WITH_MAX(++a, b);
这会变成如下代码
f((++a) > (b) ? (++a) : (b));
a 和 b 的大小关系导致了 a 不同的自增数量。
此时可以选择使用
template<typename T>
inline void callWithMax(const T& a, const T& b) {
f(a > b ? a : b);
}
预处理器的其他重要功能
#define 的上述功能可以被替代,但其他问题中预处理器依然扮演重要的角色,不能被替换。
#include #ifdef #ifndef
条款 03:尽可能使用 const
尽量指定不想更改的东西是 const 的。
const 作用于指针
char str[] = "str";
char* p = str; // non-const pointer, non-const data
const char* p = str; // non-const pointer, const data
char* const p = str; // const pointer, non-const data
const char* const p = str; // const pointer, const data
void f1(const C* p); // 指向常量的指针
void f2(C const* p); // 指向常量的指针
const 作用于 STL 迭代器
// 迭代器不得指向别的东西,但可以更改迭代器指向对象的内容
// 例如不能 ++it
const std::vector<int>::iterator it = vec.begin();
// 迭代器所指向的内容不能更改,但可以更改指向的内容
// ++it 是可以的
std::vector<int>::const_iterator it = vec.begin();
令函数返回值是常量
令函数返回值是常量可以避免修改函数的返回值。
const A f();
// 避免本意想书写 if(f() == b) 但写成了如下形式
if(f() = b)
const 成员函数
注意两个常量性(constness)不同的函数是可以被重载的。
class C {
public:
const char& operator[](std::size_t pos) const {
return text[pos];
}
char& operator[](std::size_t pos) {
return text[pos];
}
private:
std::string text;
};
C c();
std::cout << c[0]; // 调用 char& C::operator[]()
const C cc();
std::cout << cc[0]; // 调用 const char& C::operator[]()
void print(const C& c) {
std::cout << c[0]; // 调用 const char& C::operator[]()
}
bitwise const 与 logical constness
bitwise const 又称 physical constness:成员函数不改变任何(static以外)的成员变量时才可以说是 const 的。这也是 C++ 对常量性的定义。
但是很多函数可以通过 bitwise 测试却不是真的具有 const 的特性。例如一个更改了“指针所指物”的成员函数,虽然不符合 const,但只要“所指物”不属于对象,则可以通过 bitwise 测试。
例如
#include <iostream>
using namespace std;
class C {
public:
C() {
pText = new char[1];
pText[0] = 'A';
}
// 这里虽然用了 const 但是实际上 pText 指向的内容可改
char& operator[](std::size_t pos) const {
return pText[pos];
}
private:
char* pText;
};
int main()
{
const C obj;
cout<< obj[0] << "\n";
obj[0] = 'Z';
cout<< obj[0] << "\n";
return 0;
}
logical constness:一个 const 成员函数可以修改它所处理的对象内的某些 bits,但只有在客户端监测不出的时候才行。
例如
class C {
public:
std::size_t length() const {
if (!lenIsValid) {
len = std::strlen(pText);
lenIsValid = true;
}
return len;
}
private:
char* pText;
std::size_t len;
bool lenIsValid;
};
但是上述代码显然不是 bitwise constness,因此无法使用 const 关键字。
此时可以使用 mutable 关键字释放掉 non-static 成员变量的 bitwise constness。
class C {
public:
std::size_t length() const {
if (!lenIsValid) {
len = std::strlen(pText);
lenIsValid = true;
}
return len;
}
private:
char* pText;
mutable std::size_t len;
mutable bool lenIsValid;
};
两个版本的 operator[] 的重复代码
注意只相差 const 的两个版本的函数可能包含很多重复操作。例如 operator[] 中其实可能包含边界检查(bounds checking)、日志访问(log access data)、检验数据完整性(verify data integrity)等等。此时我们需要常量性移除(casting away constness)。这样我们就可以用一个版本调用另一个版本了。
#include <iostream>
using namespace std;
class C {
public:
C(){
text = "ABC";
}
const char& operator[](std::size_t pos) const {
return text[pos];
}
char& operator[](std::size_t pos) {
return
const_cast<char&>( // 将 operator[] 返回值的 const 移除
static_cast<const C&>(*this) // 为 *this 加上 const
[pos]
);
}
private:
std::string text;
};
int main()
{
const C const_obj;
cout<< const_obj[0] << "\n";
// obj[0] = 'B'; cannot compile
C obj;
obj[0] = 'B';
cout<< obj[0] << "\n";
return 0;
}
这里注意不要用 const 调用 non-const。因为 non-const 并不保证不更改任何东西。
编写代码时尽量要使用概念上的常量性(conceptual constness)。这样我们就可以用一个版本调用另一个版本了。
条款 04:确定对象被使用前已先被初始化
永远在使用对象前进行初始化。
对于内置类型,必须手工完成,其他类型初始化由构造函数完成。
注意赋值(assignment)和初始化(initialization)的区别。
class C {
int a;
public:
C(int a_) {
a = a_; // 赋值 而非 初始化
}
};
应该使用成员初值列表(member initialization list)。
class C {
int a;
public:
C(int a_)
: a(a_)
{ }
};
虽然两个构造函数最终结果相同,但是通常后者效率较高。
编译器会对没有在成员初值列表的用户自定义类型(user-defined types)的成员变量自动调用默认构造函数。因此如果采用赋值方式,则这些变量会先被默认构造,之后再被赋值。
C++ 总有固定的初始化顺序。基类(base classes)总是先于派生类(derived classes)被初始化。成员变量按照声明次序被初始化,因此成员初值列表最好以声明顺序排列。
初始化 non-local static 对象
static 对象包括:
- global 对象
- 定义于 namespace 作用域内的对象
- 在 class 内,函数内,file 作用域内被声明为 static 的对象
其中除了函数内的 static 对象称为 local static (对函数是 local 的),其他的对象称为 non-local static。
程序结束,也就是 main 函数结束时 static 对象的析构函数将会被调用。
编译单元(translation unit):产出单一目标文件(single object file)的源码,基本上它是单一源码文件加上其所含入的头文件。
如果一个编译单元的 non-local static 对象初始化时调用了另一编译单元的 non-local static 对象,则所用的对象可能尚未初始化,C++ 对定义于不同编译单元内的 non-local static 对象的初始化相对次序无明确定义。
可以将每个 non-local static 对象搬到自己的专属函数内,这些函数返回一个 reference 指向它所包含的对象。用户使用函数而不直接使用对象。这是 Singleton 模式的常见实现手法。
C++ 保证函数内的 local static 对象会在该函数被调用期间,首次遇上该对象定义式时被初始化。
// file A
class FS { ... };
FS & tfs() {
static FS fs;
return fs;
}
// file B
class Dir { ... };
Dir::Dir() {
a = tfs().f();
}
Dir& tmpDir() {
static Dir td;
return td;
}
注意所有的 non-const static 对象,在多线程环境都会有和初始化有关的竞速形式(race condition)。一个麻烦的做法是,可以早单线程启动阶段(single-threaded startup portion)手工调用所有的 reference-returning 函数。
当然单线程中的初始化次序必须是合理的,不能存在相互依赖的初始化。
Next: 《概率、统计与随机过程》 笔记 第 1 章 概率论导论