《Effective C++》学习笔记(1)
1 让自己习惯 C++
条款01:视 C++ 为一个语言联邦
将C++视为一个由相关语言组成的联邦而非单一语言。在某个次语言(sublanguage)中,各种守则与通例都倾向简单、直观易懂、并且容易记住。然而当你从一个次语言移往另一个次语言,守则可能改变。
- C:说到底C++仍是以C为基础。区块,语句,预处理器,内置数据类型,数组,指针统统来自C。
- Objective-Oriented C++:C with Classes所诉求的。这一部分是面向对象设计之古典守则在C++上的最直接实施。类,封装,继承,多态,virtual函数等等...
- Template C++:C++的泛型编程部分
- STL:template程序库。容器(containers),迭代器(iterators),算法(algorithms)以及函数对象(function objects)...
** note: **
C++高效编程守则视状况而改变,取决于你使用C++的哪一部分。
条款02:尽量以 const,enum,inline 替换 #define
C++ 编译过程:预处理 --> 编译 --> 链接
预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。检查包含预处理指令的语句和宏定义,并对源代码进行相应的转换。预处理过程还会删除程序中的注释和多余的空白字符。
“宁可以编译器替换预处理器”。就是尽量少用预处理。
-
预处理器
#define ASPECT_RATIO 1.653
将所有出现ASPECT_RATIO的地方替换为1.653,ASPECT_RATIO可能并未进入记号表(symbol table)。因此,当出现错误时报的是1.653而不是ASPECT_RATIO,导致目标定位有问题,问题追踪有困难。如果使用变量,则可轻易地判断。
此外,盲目地把ASPECT_RATIO替换为1.653可能会在目标码中出现多份1.653,改用常量绝不会出现相同情况。所以尽量定义为常量,const double ASPECT_RATIO = 1.653
。 -
如果在数组初始化的时候,编译器需要知道数组的大小,且编译器(错误地)不允许使用“static整数型class常量”进行数组初始化,这时可以使用枚举类型enum来替代define。
class GamePlays{
private:
static const int NumTurns = 5; // static整数型class常量
enum { NumTurns = 5 }; // 枚举
int scores[NumTurns];
... ...
}
- 宏看起来像函数,但不会招致函数调用带来的额外开销。如果你想获得高效,建议使用inline内联函数。
有了consts 、enums 和inlines,我们对预处理器(特别是#define) 的需求降低了,但并非完全消除。#include 仍然是必需品,而 #ifdef / #ifndef 也继续扮演控制编译的重要角色。目前还不到预处理器全面引退的时候,但我们要尽量限制预处理器的使用。
** note: **
- 对于单纯常量,最好以const对象或enum替换#define。
- 对于形似函数的宏,最好改用inline函数替换#define。
条款03:尽可能使用 const
const允许你告诉编译器和其他程序员某值应保持不变,只要“某值”确实是不该被改变的,那就该确实说出来。如果const修饰变量,则表示这个变量不可变;如果const修饰指针,表示指针指向的位置不可改变。
- 和指针有关的const判断:
- 如果关键字const出现在星号左边,表示被指物事常量。
const char *p
和char const *p
两种写法意义一样,都说明所致对象为常量。 - 如果关键字const出现在星号右边,表示指针自身是常量。
const char * p = "hello"; // *p的hello不可变, 与char const * p = "hello"等价
char * const p = "hello"; // 表示p的值不可变,即p不能指向其它位置
- STL迭代器的const:
- 声明迭代器为const就像声明指针为const一样(即声明一个T* const指针),表示这个迭代器不得指向不同的东西,但它所指的东西的值可以改变。
- 如果想要迭代器所指的东西不可改变(即模拟一个const T*指针),使用const_iterator。
std::vector<int> vec;
const std::vector<int>::iterator iter = vec.begin(); //类似T* const
*iter = 10; //没问题,改变iter所指物
++iter; //错误!iter是const
std::vector<int>const_iterator cIter = vec.begin(); //类似const T*
*iter = 10; //错误,*iter是const
iter++; //没问题,可以改变iter
- 令函数返回一个常量值,可以避免意外错误。
如下代码,错把==写成=,一般程序对*号之后进行赋值会报错,但在自定义操作符面前不会(因为自定义*号后返回的是Rational对象实例的引用,可以拿来赋值,不会报错)。如果*不写成const,则下面的程序完全可以通过,但写成const之后,再对const进行赋值就出现问题了。函数的参数,如果无需改变其值,尽量使用const,这样可以避免函数中错误地将==等于符号误写为=赋值符号,而无法察觉。
class Rational { ... };
const Rational operator* (const Rational& lhs, const Rational& rhs);
...
Rational a, b, c;
if(a * b = c)... //把==错写成=,比较变成了赋值
- const作用于成员函数,有两个作用:
- 可以知道哪些函数可以改变对象内容,哪些函数不可以。
- 改善C++效率,通过pass by reference_to_const(const对象的引用)方式传递对象可改善C++效率。
下面是常量函数与非常量函数的形式:
class TextBlock{
public:
...
const char& operator[] (std:size_t position) const
{ return text[position]; }
char& operator[] (std:size_t position)
{ return text[position]; }
private:
std::string text;
};
/* 使用operator[] */
TextBlock tb("hello"); //non-const 对象
cout<<tb[0]<<endl; //调用的是non-const TextBlock::operator[]
tb[0] = 'x'; //没问题,写一个non-const对象
const TextBlock cTb("hello"); //const 对象
cout<<cTb[0]<<endl; //调用的是const TextBlock:operator[]
cTb[0] = 'x'; //错误,写一个const对象
在C++中,只有被声明为const的成员函数才能被一个const类对象调用。
- 成员函数是const意味着什么?
- bitwise const主张const成员函数不可以改变对象内任何non-static成员变量。但一个更改了“指针所指物”的成员函数虽然不能算const,但如果只有指针(而非其所指物)隶属于对象,那么称此函数为bitwise const不会引发编译器异议。
- logical const主张成员函数可以修改它所处理的对象内的某些bits,但要在客户端侦测不出的情况下才得如此。
编译器默认执行bitwise。如果想要在const函数中修改non-static变量,需将变量声明为mutable(可变的)。
class TextBlock{
private:
char* pText;
mutable std::size_t textLength; // 即使在const成员函数内,
mutable bool lengthIsValid; // 这些成员变量也可能会被更改。
public:
...
std::size_t length() const;
};
std::size_t TextBlock::length() const{
if (!lengthIsValid){
textLength = std::strlen(pText); //加上mutable修饰后,便可以修改其值
lengthIsValid = true;
}
}
- 避免const和non-const成员函数重复
如果const和non-const成员函数功能相当、代码重复,编译时间、维护等会是一个大问题,这时就用non-const函数去调用const函数,但不能反过来。这是因为non-const函数可能会改变对象,const函数承诺不改变对象,const调用non-const就不安全了
class TextBlock{
public:
const char& operator[](std:size_t position) const
...
return text[position];
}
char& operator[] (std:size_t position){
return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
}
};
上面代码进行了两次转型:
- 第一次用static_cast来为*this添加const,这使接下来调用operator[ ]时得以调用const版本;
- 第二次则是用const_cast从const operator[]的返回值转除const,以符合non-const返回值类型。
** note: **
- 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
- 编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”;
- 当cosnt和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。
条款04:确定对象被使用前已先被初始化
-
对内置类型(基本类型)手动进行初始化。
-
内置类型以外的类型,初始化要靠构造函数,要确保每一个构造函数都将对象的每一个成员初始化。
类的构造函数使用成员初值列(member initialization list),而不是在构造函数中进行赋值操作,这样通常效率更高。因为赋值的版本其实是先进行初始化再进行赋值,而成员初值列版本是直接进行初始化,这对于非内置类型(std::string等)来说显然后者效率更高。对于内置类型,其初始化和赋值的成本相同,但为了一致性最好也通过成员初值列来初始化。对于const和reference类型必须是初始化,赋值操作是不允许的。 -
base classes更早于derived classes被初始化,class的初值列成员变量的排列顺序与其声明顺序相同。
-
“不同编译单元内定义之non-local-static对象”的初始化次序。
static对象,其寿命从被构造出来直到程序结束为止,包括global对象,定义于namespace作用域内的对象,在classes内、在函数内、以及在file作用域内被声明为static的对象。函数内的static对象被称为local static对象(因为它们对函数而言是local),其他static对象称为non-local static对象。
// FileSystem源文件 class FileSystem{ public: ... std::size_t numDisks() const; };
extern FileSystem tfs;
// Directory源文件,与FileSystem处于不同的编译单元
class Directory{
public:
Directory(params);
...
};
Directory::Directory(params){
...
//调用未初始化的tfs会出现错误
std::size_t disks = tfs.numDisks();
}
C++对“定义于不同编译单元内的non-local static对象”的初始化相对次序并无明确定义,因此,为防止A的初始化需要B,但B尚未初始化的错误,将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static),然后用户调用这些函数,而不直接涉及这些对象。
class FileSystem { ... };
FileSystem& tfs(){
static FileSystem fs;
return fs;
}
class Directory { ... };
Directory::Directory(params){
std::size_t disks = tfs().numberDisks();
}
Directory& tempDir(){
static Directory td;
return td;
}
经过上面的处理,将non-local转换了local对象,这样做的原理是:函数内的local static 对象会在"该函数被调用期间","首次遇上该对象之定义式"时被初始化,这样就保证了对象被初始化。使用函数返回的“指向static对象”的reference,而不再使用static对象本身。这样做的好处是不调用函数时,不会产生对象的构造和析构。但对多线程这样的方法会有问题。
** note: **
- 为内置对象进行手工初始化,因为C++不保证初始化它们;
- 构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作。初始化列表列出的成员变量,其排列次序应该和它们在类中的声明次序相同;
- 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。