一. 让自己习惯C++
1. 视C++为一个语言联邦
C++主要包括以下四个部分:
C:C++以C为基础,并兼容C。
Object-Oriented C++:面向对象编程,主要包括:类,封装,继承,多态。
Template C++:C++泛型编程(C++ Generic Programming)。
STL:主要包括:容器,迭代器,算法,函数对象。
C++高效编程守则视情况而变化,取决于你使用哪一部分C++。
2. 尽量使用const,enum,inline替换define
(1)const定义的常量在编译期确定,因此可以作为数组定义时使用。
(2)在class中只能初始化static const常量。
在class中不能初始化static成员。只能在类中声明,然后在类的外部定义。
const成员只能在类的构造函数初始话列表中初始化。
示例代码如下:
class my_Class
{
public:
my_Class(int cnumber=0):c_number(cnumber){}
private:
static const int sc_number=10;
static int s_number;
const int c_number;
};
int my_Class::s_number=90;
(3)在vs2008中是可以取enum 类型的地址的,和书上说的不一样。
(4)对于类似函数的#define宏,最好用inline函数代替。
(5)虽然有了上述的一些#define替代品,但是#ifdef/#ifndef依然扮演控制编译的重要角色。现在还不到预处理器全面隐退的时候。
3. 尽可能使用const
(1)STL迭代器相当于T*,所以const vectot<int>::iterator iter; 相当于 int const *iter;iter指针指向不能改变,但是指针指向的内容可以改变。要想让内容不改变需要使用:vector<int>::const_iterator iter;相当于const int* iter。
(2)const成员函数的目的是为了确认该成员函数可以作用于const对象身上。const对象不能改变类成员的值,不管这个成员是const还是non-const都不能改变。如果想在const成员函数中改变non-const成员的值,这个成员前面需要加上关键字:mutable,如:mutable int count;。对const对象的操作,仅限于读,并且只有const成员函数可以操作const对象。
(3)当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码的重复。示例如下:
1 class TextBlock 2 { 3 public: 4 const char& operator[](std::size_t position) const 5 { 6 ... 7 ... 8 ... 9 return text[position]; 10 } 11 char& operator[](std::size_t position) 12 { 13 return const_cast<char&>(static_cast<const TextBlock&>(*this))[position]; 14 } 15 };
首先将非const的对象通过static_cast转换成const对象,然后调用const成员函数operator[],之后对返回的const类型通过const_cast转换成非cosnt类型。
注意这种写代码的方式。
4. 确定对象被使用前都先被初始化
(1)C++规定,对象的成员变量的初始化动作发生在进入构造函数之前。构造函数最好使用成员初始化列表的形式,而不要在构造函数内使用赋值操作。成员初始化顺序是先初始化基类的成员变量,然后再初始化派生类的成员变量,class的成员初始化顺序总是按照其声明次序被初始化。所以为了安全起见,成员初始化类表顺序应该和成员声明顺序相同。
(2)为免除“跨编译器单元之初始化顺序”,请以local static对象代替non-local static 对象。
二. 构造/析构/赋值运算
5. 了解C++默默编写并调用哪些函数
(1)如果自己没有写,编译器为类声明下面四个函数:一个copy构造函数,一个copy assignment操作符,一个默认无参数构造函数和一个析构函数。如果用户定义了任意 一个构造函数,编译器不再提供默认构造函数。
(2)函数声明形式代码如下:
1 class Empty 2 { 3 public: 4 Empty(){...} 5 Empty(const Empty&){...} 6 ~Empty(){...} 7 Empty& operator=(const Empty&){...} 8 };
6. 若不想使用编译器自动生成的函数,就该明确拒绝。
(1)如果一个对象是独一无二的,不能被拷贝和赋值,可以通过将拷贝和赋值函数声明为私有的。
(2)注意这里一定要使用private继承,代码如下:
1 class UnCopyable 2 { 3 protected: 4 UnCopyable(){} 5 ~UnCopyable(){} 6 private: 7 UnCopyable(const UnCopyable&); 8 UnCopyable& operator=(const UnCopyable&); 9 }; 10 11 class HomeForSale:private UnCopyable 12 { 13 ... 14 };
7. 为多态基类声明virtual析构函数
(1)带多态性质的基类(polymorphic base class)应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
(2)class设计的目的如果不是作为base class使用,或者不具备多态性,就不该声明virtual析构函数。因为virtual函数会占用额外的一个指针空间。
8. 别让异常逃离构造函数
(1)析构函数绝不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕获任何异常,然后吞掉它们(不传播)或终止程序。
(2)如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
9. 绝不在构造和析构函数过程中调用virtual函数。因为这类函数从不下降至derived class(比起当前执行构造函数和析构函数的那一层)。
10. 另operator=返回一个reference to*this。这样是为了实现“连锁赋值”。
11. 在operator=中处理“自我复制”
(1)确保当前对象自我赋值有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址,精心周到的语句顺序,以及copy-and-swap技术。
(2)确保任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然是正确的。
(3)实现方式的代码书写,一般有下面几种形式:
1 //下面的类将copy的公共部分代码封装在一个init函数中,然后在copy和赋值拷贝中调用公共代码 2 //但是在复制赋值语句中没有考虑到自身赋值。这里也不需要考虑自身赋值,因为这里没有存在heap 3 //分配的对象 4 class Teacher{ 5 public: 6 Teacher(string _name,int _age):name(_name),age(_age){} 7 Teacher(const Teacher& rhs){ 8 init(rhs); 9 } 10 Teacher& operator=(const Teacher& rhs){ 11 init(rhs); 12 return *this; 13 } 14 string getName(){ 15 return this->name; 16 } 17 int getAge(){ 18 return this->age; 19 } 20 private: 21 void init(const Teacher& rhs){ 22 this->name=rhs.name; 23 this->age=rhs.age; 24 } 25 string name; 26 int age; 27 }; 28 29 30 //考虑"自身赋值"的四种方法 31 class School{ 32 public: 33 School(){ 34 teacher=new Teacher("Julia",22); 35 } 36 School(Teacher* _teacher){ 37 this->teacher=_teacher; 38 } 39 //对象交换方法,只需将对象中的所有属性交换即可 40 void swap(School& rhs){ 41 School tmp(rhs); 42 tmp.teacher=rhs.teacher; 43 rhs.teacher=this->teacher; 44 this->teacher=tmp.teacher; 45 } 46 School(const School& rhs){ 47 this->teacher=rhs.teacher; 48 } 49 //方法一,通过条件判断 50 //这种方法存在异常安全性,如果new语句申请内存 51 //没有成功,那么teacher指针指向将不确定 52 School& operator=(const School& rhs){ 53 if (this==&rhs){ 54 cout<<"自身赋值"<<endl; 55 return *this; 56 } 57 delete teacher; 58 teacher=new Teacher(*rhs.teacher); 59 return *this; 60 } 61 //方法二,拷贝然后复制 62 School& operator=(const School& rhs){ 63 School *tmp=this->teacher; 64 this->teacher=new Teacher(*rhs.teacher); 65 delete tmp; 66 return *this; 67 } 68 //方法三,拷贝然后交换,先将要赋值的内容做一份拷贝(生成一个局部临时对象),注意这里是pass by reference 69 School& operator=(const School& rhs){ 70 School temp(rhs); 71 swap(temp); 72 return *this; 73 } 74 //方法四,是对方法三的进一步改进,采用pass by value方式,因为值传递会拷贝一份临时对象,就不用像三那样 75 //写一条语句进行对象拷贝了 76 School& operator=(School rhs){ 77 swap(rhs); 78 return *this; 79 } 80 void print(){ 81 cout<<"this->teacher->name:"<<this->teacher->getName()<<endl<<"this->teacher->age:"<<this->teacher->getAge()<<endl; 82 } 83 private: 84 Teacher *teacher; 85 };
12. 复制对象时勿忘其每一个部分
(1)当编写一个copying函数,请确保:a. 复制所有local成员变量;b.调用所有base class内适当的copying函数。
(2)不要用某个copying函数实现另一个copying函数。应该将共同机能放在第三个函数中,这个函数一般是private的,命名为init,如上面11的代码Teacher所示。
(3)调用基类copying函数的方式如下面代码所示:
1 //赋值勿忘其每一个部分 2 class Student{ 3 public: 4 Student(string _name,int _age):name(_name),age(_age){} 5 Student(const Student& rhs){ 6 init(rhs); 7 } 8 Student& operator=(const Student& rhs){ 9 init(rhs); 10 return *this; 11 } 12 string getName(){ 13 return this->name; 14 } 15 int getAge(){ 16 return this->age; 17 } 18 private: 19 void init(const Student& rhs){ 20 this->name=rhs.name; 21 this->age=rhs.age; 22 } 23 string name; 24 int age; 25 }; 26 27 class HighStudent:public Student{ 28 public: 29 HighStudent(string _name,int _age,bool _girlFriend):Student(_name,_age),girlFriend(_girlFriend){ 30 } 31 //如果这里将Student(rhs)注释掉会出现下面的错误 32 //error C2512: 'Student' : no appropriate default constructor available 33 //这是因为回去调用基类的默认构造函数,而基类并没有提供默认的构造函数 34 HighStudent(const HighStudent& rhs):Student(rhs),girlFriend(rhs.girlFriend){} 35 HighStudent& operator=(const HighStudent& rhs){ 36 Student::operator=(rhs);//如果将这一句注释掉,基类的属性值不会改变,改变的只有派生类的值 37 girlFriend=rhs.girlFriend; 38 return *this; 39 } 40 bool getGirlFriend(){ 41 return girlFriend; 42 } 43 void print(){ 44 cout<<"Name:"<<getName()<<"\tAge:"<<getAge()<<"\tHas GirlFriend?: "<<getGirlFriend()<<endl; 45 } 46 private: 47 bool girlFriend; 48 };
三.资源管理
13. 以对象管理资源
(1)把资源放进对象内,我们便可以依靠C++的“析构函数自身调用机制”确保资源被释放。
(2)资源取得时机便是初始化时机(RAII Resource Acquisition is Initialization)。
(3)auto_ptr,若通过copy构造函数或copy assignment操作符复制它们,被复制物便会变为null,而复制所得的指针将取得资源的唯一拥有全。
(4)使用方式,std::tr1::shared_ptr<Investment> pInv(createInvestment);
std::auto_ptr<Investment> pInv(createInvestment);
注意它们只能指向单个对象,不能给它们分配对象数组。
14. 在资源管理中心小心copying行为
(1)复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
(2)普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法。不过其它行为也都可能被实现。
15. 在资源管理类中提供对原始资源的访问
(1)RAII classes并不是为了封装某物而存在;它们的存在是为了确保一个特殊行为----资源释放----会发生。
(2)APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个“取得其所管理之资源”的办法。一般是提供一个get()函数。
(3)对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但是隐式转换对客户端比较方便。
16. 成对使用new和delete时要采用相同的形式
(1)注意下面的这种形式:
typedef std::string AddressLines[4];
std::string* pal=new AddressLines;
delete pal; //这么写是错误的
delete[] pal; //这么写才是正确的
17. 以独立语句将newed对象置入智能指针
(1)给出一个出现异常的例子:
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw,int priority);
现在考虑调用:
processWidget(std::tr1::shared_ptr<Widget>(new Widget),priority());
这里编译器必须创建代码做下面三件事:
》调用priority
》new 一个widget对象
》调用tr1::shared_ptr构造函数
但是对于C++来说这三件事执行的顺序是不确定的,确定的是new 肯定在shared_ptr构造函数之前执行。如果执行顺序如下:
》new 一个widget对象
》调用priority
》调用tr1::shared_ptr构造函数
这时如果priority出现异常,将导致widget对象得不到释放。分开写成下面的形式:
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw,priority);
就不会出现上述说的问题了。
四. 设计与声明
18. 让接口容易被使用,不易被误用。
19. 设计class犹如设计type。
20. 宁以pass-by-reference-to-const替换pass-by-value
(1)一般而言,可以合理假设“pass-by-value并不昂贵”的唯一对象就是内置类型和STL的迭代器和函数对象。对它们而言,pass-by-value比较合适。
(2)pass-by-reference-to-const可以避免切割问题(slicing problem)
21. 必须返回对象时,别妄想返回其reference
(1)绝不要还回pointer或reference指向一个local stack对象,或返回一个reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象。
(2)这一条书中提供的示例很好,充分证明了作者的论点。
22. 将成员变量声明为private。
23. 宁以non-member、non-friend替换member函数。
(1)这里个人的理解是:和类密切相关的函数要写出成员函数,如果一些复杂的功能可以通过组合几个成员函数来完成,则完成这个功能的函数最好使用non-member函数。
24. 若所有参数皆需要类型转换,请为此采用non-member函数
(1)这个个人理解:对于一些二元运算符(+-×/等运算)的重载要使用non-member函数,必要时可以是friend。
(2)只有当参数被列于参数列(parameter list)时,这个参数才是隐式类型转换的合格参与者。地位相当于“被调用之成员函数所隶属的那个对象”----即this对象----的那个隐喻参数,绝不是隐式转换的合格参与者。
25. 考虑写出一个不抛出异常的swap函数。