Effective C++
浏览 1315 · 点赞 1 · 1年前 (2023-09-30)
目录第一章、让自己习惯C++1、视C++为一个语言联邦2、尽量以const,enum,inline替换#define3、尽可能使用const4、确定对象被使用前已先被初始化第二章、构造/析构/赋值运算5、了解C++默默编写并调用哪些函数6、若不想使用编译器自动生成的函数,就该明确拒绝7、为多态基类声明virtual析构函数8、别让异常逃离析构函数9、绝不在构造和析构过程中调用虚函数10、令operator=返回一个reference to *this11、在operator=中处理“自我赋值”12、复制对象时勿忘其每一个成员第三章、资源管理13、以对象管理资源14、在资源管理类中小心copying行为15、在资源管理类中提供对原始资源的访问16、成对使用new和delete时要采用相同形式第四章、设计与声明18、让接口容易被正确使用,不易被误用19、设计class犹如设计type20、宁以pass-by-reference-to-const替换pass-by-value21、必须返回对象时,别妄想返回其reference22、将成员变量声明为private23、宁以non-member、non-friend替换member函数24、若所有参数皆需类型转换,请为此采用non-member函数25、考虑写出一个不抛异常的swap函数第五章、实现26、尽可能延后变量定义式的出现时间27、尽量少做转型动作28、避免返回handles指向对象内部成分
第一章、让自己习惯C++
1、视C++为一个语言联邦
总共4个次语言区,为满足高效编程,4个区对值传递和引用传递时的效率不一样。
2、尽量以const,enum,inline替换#define
类内如果要用const成员变量,最好再加一个static,因const成员在定义时初始化了就不能更改值了,加个static后就不用类的每一个实体都去建一个该成员变量。 静态常量成员变量需要注意:(1)、有些编译器需要在.cpp文件(即实现文件而非头文件)中专门加一个定义式; (2)、如果要该成员变量的地址也需要在.cpp文件(即实现文件而非头文件)中专门加一个定义式, 如:
1const int classA::NumTurns;//不用再赋值了,因在类内定义时已初始化了,但如果类内没有赋初值,即类内只进行了成员的声明,则在此处需要赋一个初值。如
2class classA{
3private:
4static const double dDouble;//常量声明,位于头文件内
5…};
记住:
- 对于单纯常量,最好以const对象或enums替换#defines
- 对于形似函数的宏(macros),最好改用inline函数替换#defines
- const double classA::dDouble = 1.35;//常量定义,位于实现文件内
3、尽可能使用const
如果const出现在星号左边,表示被指物是常量,如果出现在星号右边,表示指针自身是常量,如是出在星号的两边,表示被指物和指针两者都是常量。 记住:
- 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
- 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。
4、确定对象被使用前已先被初始化
这里先抛弃C++编译器对内置类型和类内成员变量进行默认初始化的规则,对于无任何成员的内置类型,你必须手工完成此事;对于内置类型以外的任何其他东西,初始化责任落在构造函数身上,规则很简单:确保每一个构造函数都将对象的每一个成员初始化。 一个难题:在不同的实现文件之中,如果有多个非局部静态对象,且其中某些非局部静态比如A对象需要用另外一些非局部静态对象比始B初始化,那怎么保证编译器先编译了含对象B的实现文件,再去编译含对象A的实现文件?答案是不可能的,即无法保证。解决这个问题的方法就是将非局部静态对象转变成局部静态对象,在类B的头文件中实现,如:
xxxxxxxxxx
221//以下是类B的头文件
2class classB{
3public:
4int numDisks() const;
5};
6classB& tfs()
7{
8static classB fs;
9return fs;
10}
11//以下是类A的头文件
12#include “classB.hpp”
13class classA{
14private:
15int disk = tfs().numDisks();
16};
17//如果其它地方要用到类A的静态成员对象,则在头文件中也可加上如下:
18classA& tempDir()
19{
20static classA fs;
21return fs;
22}
记住:
- 为内置型对象进行手工初始化,因为C++不保证初始化它们
- 构造函数最好使用成员初值列表,而不要在构造函数本体内使用赋值操作。初值列列出的成员变量,其排列次序应该和它们在类中的声明次序相同,实事上类初始化成员的顺序是按成员在类中声明的顺序来的,且如果初值列中没有的成员对象,会在初值列初始化前先判断在初值列中没有的成员对象有没有类内初始化,有的话先执行内类初始,如果也没有类内初始化,则先执行默认初始化,因内置类型都有默认初始化,但自定义的类不一定有,所以初值列中必须包含没有默认构造函数的类的构造函数。
- 为免除“跨编译单元之初始化次序“问题,请以local static对象替换non-local static对象。
第二章、构造/析构/赋值运算
5、了解C++默默编写并调用哪些函数
如果你写下一个空类,则编译器会自动为你加上几个函数
xxxxxxxxxx
81class Empty{};这就好像你写下如下这样的代码:
2class Empty{
3public:
4Empty(){…};//default构造函数
5Empty(const Empty& rhs){…};//copy构造函数
6~Empty(){…};//析构函数,是否该是virtual见稍后说明
7Empty& operator= (const Empty& rhs){…};//copy assignment操作符
8};
惟有当这些函数被需要,它们才会被编译器创建出来,程序中需要它们是很平常的事,下面代码造成上述每一个函数被编译器产出:
xxxxxxxxxx
31Empty e1;//产出默认构造和析构函数
2Empty e2(e1);//产出复制构造函数
3e2 = e1;//产出赋值操作符
注意:编译器产出的析构函数是个non-virtual,除非这个类的基类自身声明有virtual析构函数(这种情况下这个函数的虚属性,主要来自基类) 至于copy构造函数和copy assignment操作符,编译器创建的版本只是单纯地将来源对象的每一个非静态成员变量拷贝到目标对象。 例外:如果类内有引用,常量成员,或者基类将赋值操作符声明成了privat,有这三者之一,编译器将拒绝为类生成默认赋值操作符。即只有自己手动写一个赋值操作符。
6、若不想使用编译器自动生成的函数,就该明确拒绝
明确拒绝的方法有三个: (1)、比如将复制构造函数和赋值操作符声明成private,并且不定义函数体,因如果定义了函数体,则成员函数和友元函数还是可以调用它们。 (2)、创建一个不被允许复制的基类并继承,创建方法如(1)。 (3)、在函数声明后面加上“=delete”; 记住:
- 为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class也是一种做法。
xxxxxxxxxx
281#include <iostream>
2using namespace std;
3class classA
4{
5public:
6classA(int i);
7classA(const classA&) = delete;//明确拒绝copy构造函数,则再去定义函数会报错,最好写成私有的
8~classA(){}
9classA& operator=(const classA&) = delete;//明确拒绝赋值操作运算符,则再去定义函数会报错,最好写成私有的
10private:
11int m_i;
12};
13classA::classA(int i):m_i(i){ }
14// classA::classA(const classA& cA)
15// {
16// this->m_i = cA.m_i;
17// }
18// classA& classA::operator=(const classA& cA)
19// {
20// this->m_i = cA.m_i;
21// }
22int main()
23{
24classA cA(78);
25classA cB(89);
26//cA = cB;//报错
27system("pause");
28}
7、为多态基类声明virtual析构函数
多态:基类有虚构函数,等派生类根据自己的情况去实现。 当继承了非虚虚构函数的基类时,如果有基类的指针指向了派生类的指针,当删除基类指针时,会出现内存泄漏。 虚函数的目的是允许派生类的实现得以客制化,任何类只要带有虚函数都几乎确定应该也有一个虚析构函数。如果类不含虚函数,通常表示它并不意图被用做一个基类。当类不企图被当作基类时,令其析构函数为虚析构函数往往是个馊主意。许多人的心得是:只有当类内至少一个虚函数,才为它声明虚析构函数。 记住:
- 带多态性质的基类应该声明一个虚析构函数,如果类带有任何虚函数,它就应该拥有一个虚析构函数。(2)、类的设计目的如果不是作为基类使用,或不是为了具备多态性,就不该声明虚析构函数。
8、别让异常逃离析构函数
使用std::abort();函数可以抢先制“不明确行为”于死地。 记住:
- 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么类应该提供一个普通函数(而非在析构函数中)执行该操作。
9、绝不在构造和析构过程中调用虚函数
记住:
- 在构造和析构期间不要调用虚函数,因为这类调用从不下降至派生类(比起当前执行构造函数和析构函数的那层)。
10、令operator=返回一个reference to *this
和其它的教程不一样,这里是返回一个this指针的引用
xxxxxxxxxx
141class Widget{
2public:
3…
4Widget& operator+=(const Widget& rhs)//这个协议适用于+=,-=,*=,等等
5{
6…
7return *this;
8}
9Widget& operator=(int rhs)//此函数也适用,即使此一操作符的参数类型不符协定
10{
11…
12return *this;
13}
14}
记住:
- 令赋值操作符返回一下reference to this
11、在operator=中处理“自我赋值”
xxxxxxxxxx
131class Bitmap{};
2class Widget{
3private:
4Bitmap* pb;//指针,指向一个从头文件分配而得的对象
5};
6Widget& Widget::operator=(const Widget& rhs)
7{
8//不管是不是给自己赋值,都取一个中间变量先存储成员指针,只有成员有指针的才这样做
9Bitmap* pOrig = pb;//记住原先的pb地址,一会儿好释放内存空间
10pb = new Bitmap(*(rhs.pb));//令pb指向*pb的一个复件,以完成赋值操作运算
11delete pOrig;//删除原先的pb,以释放原来的内存空间
12return *this;
13};
记住:
- 确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap.
- 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
12、复制对象时勿忘其每一个成员
设计良好之面向对象系统会将对象的内部封装起来,只留两个函数负责对象拷贝,那便是带着适切名称的copy构造函数和copy assignment操作符,我称它们为copying函数。如果你声明自己的copying函数,意思就是告诉编译器你并不喜欢缺省实现中的某些行为。 记住:
- Copyin函数应该确保复制“对象内的所有成员变量”及“所有基类成分”。
- 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。
第三章、资源管理
所谓资源就是,一旦用了它,将来必须还给系统。如果不这样,糟糕的事情就会发生。C++程序中最常使用的资源就是动态分配内存(如果你分配内存却从来不曾归还它,会导致内存泄漏),但是内存只是你必须管理的从多资源之一。其他常见的资源包括文件描述器、互斥锁、图形界面中的字型和笔刷、数据库连接、以及网络sockets。不论哪一种资源,重要的是,当你不再使用它时,必须将它还给系统。
13、以对象管理资源
以对象管理资源的两个关键想法: (1)、获得资源后立刻放进管理对象内; (2)、管理对象运用析构函数确保资源被释放。 记住: 为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。两个常使用的RAII classes分别是tr1::shared_ptr和auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null。
xxxxxxxxxx
291//effective C++条款13 以对象管理资源
2#include <string>
3#include <windows.h> //Sleep
4using namespace std;
5class ResourcesClass//只有new的就放入这个类中
6{
7public:
8ResourcesClass(string *str)
9:m_str(str)
10{};
11~ResourcesClass()
12{
13delete[] m_str;
14};
15private:
16string* m_str;
17};
18int main()
19{
20while(true)
21{
22string *str = new string[5000];
23//马上放进资源管理类
24ResourcesClass resourcesClass1(str);//不要这一行和要这一行在任务管理器中查看内存使用情况
25Sleep(100);
26}
27system("pause");
28return 0;
29}
14、在资源管理类中小心copying行为
RAII概念:资源取得时机便是初始化时机 条款5说过,class析构函数(无论是编译器生成的,或用户自定的)会自动调用其non-static成员变量的析构函数。 记住:
- 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法。不过其他行为也都可能被实现。
xxxxxxxxxx
521//effective C++条款14源码
2#include <mutex>
3//#include <memory>
4#include <thread>
5#include <windows.h> //Sleep
6using namespace std;
7class Lock{
8public:
9explicit Lock(mutex* pm)
10:mutexPtr(pm)
11{
12lock(mutexPtr);
13}
14~Lock(){unlock(mutexPtr);}
15private:
16mutex* mutexPtr;
17void lock(mutex* pm){pm->lock();}
18void unlock(mutex* pm){pm->unlock();}
19};
20void t1(int& i,mutex& m)
21{
22while(true)
23{
24Lock m1(&m);//注意不能再去复制m1,智能指针的用法没写出来;
25++i;
26printf("1:%d\n",i);
27Sleep(1);//不能睡眠时间长了,要不然只会跑一个线程
28}
29}
30void t2(int& i,mutex& m)
31{
32while(true)
33{
34Lock m1(&m);//注意不能再去复制m1,智能指针的用法没写出来;
35++i;
36printf("2:%d\n",i);
37Sleep(1);//不能睡眠时间长了,要不然只会跑一个线程
38}
39}
40int main()
41{
42mutex m;
43//Lock m1(&m);//注意不能再去复制m1,智能指针的用法没写出来;
44int i = 0;
45
46thread th2(t2,ref(i),ref(m));
47thread th1(t1,ref(i),ref(m));
48
49th2.join();
50th1.join();
51system("pause");
52}
15、在资源管理类中提供对原始资源的访问
16、成对使用new和delete时要采用相同形式
记住:
- 如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。typedef时注意,如果typedef中使用了[],那同样要在delete中使用[]。
第四章、设计与声明
18、让接口容易被正确使用,不易被误用
记住:
- 好的接口很容易被正确使用,不容易被误用,你应该在你的所有接口中努力达成这些性质。
- 促使正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
- 阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
- tr1::shared_ptr支持定制型删除器。这可防范dll问题,可被用来自动解除互斥锁等等。
19、设计class犹如设计type
设计高效的class,需要考虑如下:
- 新type的对象应该如何被创建和销毁
- 对象的初始化和对象的赋值该有什么样的差别
- 新type的对象如果被passed by value(以值传递),意味着什么
- 什么是新type的“合法值”?
- 你的新type需要配合某个继承图系吗?
- 你的新type需要什么样的转换?
- 什么样的操作符和函数对此新type而言是合理的?
- 什么样的标准函数应该驳回?
- 谁该取用新type的成员?
- 什么是新type的“未声明接口”?
- 你的新type有多么一般化?
- 你真的需要一个新type吗?
20、宁以pass-by-reference-to-const替换pass-by-value
记住:
- 成员函数形参尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题。高效是因为用引用,就没有新的拷贝对象创建,则没有构造函数和析构函数产生,注意,是const,说明不能更改其值。
- 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当。
21、必须返回对象时,别妄想返回其reference
记住:
- 绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。
22、将成员变量声明为private
记住:
- 切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允许约束条件获得保证,并提供class作者以充分的实现弹性。protected并不比public更具封装性。
23、宁以non-member、non-friend替换member函数
24、若所有参数皆需类型转换,请为此采用non-member函数
25、考虑写出一个不抛异常的swap函数
记住:
- 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
- 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classs(而非templates),也请特化std::swap.(3)、调用swap时就针对std:swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。
- 为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。
第五章、实现
26、尽可能延后变量定义式的出现时间
27、尽量少做转型动作
C++提供四种新式转型:
- const_cast
() - dynamic_cast
() - reinterpret_cast
() - staitc_cast
() 各有不同目的: - const_cast通常被用来将对象的常量性转除
- dynamic_cast主要用来执行“安全向下转型”,它是唯一可能耗费重大运行成本的转型动作
- reinterpret_cast意图执行低级转型,很少用,例如将一个整型指针转型为一个整型
- staitc_cast用来强迫隐式转换,例如将non-const对象转为const对象,或将int转为double等等
28、避免返回handles指向对象内部成分
- references、指针和迭代器统统都是所谓的“handles”(号码牌,用来取得某个对象)。
- 成员变量的封装性最多只等于返回其reference的函数的访问级别
- 如果const成员函数传出一个reference的成员变量,那么这个函数调用者可以修改这个成员变量,不管该成员变量是不是私有的。
- 上一点即时给传回的reference加上const也存在风险
记住:
- 避免返回handles(包括reference、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数行为像个const,并将发生“虚吊号码牌”的可能性降至最低。
后面的28个规则只看了标题和结论