├── Effective-CPP-3rd-Edition ├── README.md ├── part-1-accustoming-yourself-to-cpp.md ├── part-2-constructors-destructors-and-assignment-operators.md ├── part-3-resource-management.md ├── part-4-designs-and-declarations.md ├── part-5-implementations.md ├── part-6-inheritance-and-objected-oriented-design.md └── resource │ └── images │ ├── effective_cpp33_global_scop.gif │ ├── effective_cpp33_scop1.gif │ ├── effective_cpp33_scop2.gif │ ├── effective_cpp33_scop3.gif │ ├── effective_cpp35_strategy_pattern.png │ ├── effective_cpp40_inheritence1.gif │ └── effective_cpp40_inheritence2.gif ├── README.md └── books ├── Effective C++ 3rd Edition(中文版).pdf ├── STL源码剖析(批注版).pdf └── 数据结构(C++语言版)第三版_邓俊辉.pdf /Effective-CPP-3rd-Edition/README.md: -------------------------------------------------------------------------------- 1 | # Effective C++中文版(第三版) 学习笔记 2 | 3 | ### part1 习惯C++ 4 | - item1 视C++为一个语言集合 5 | - item2 用const,enum,inline代替#define 6 | - item3 多用const 7 | - item4 确保对象在使用前已被初始化 8 | 9 | ### part2 构造/析构/赋值运算 10 | - item5 了解C++类中自动生成和调用的函数 11 | - item6 若不想使用编译器自动生成的函数,要显式拒绝 12 | - item7 为多态基类声明virtual析构函数 13 | - item8 不要让析构函数抛出异常 14 | - item9 不要在构造和析构函数中调用virtual函数 15 | - item10 自定义赋值操作符(operator=) 要返回*this的引用 16 | - item11 注意operator= 的“自我赋值” 17 | - item12 对象进行复制时需要完整拷贝 18 | 19 | ### part3 资源管理 20 | - item13 使用对象来管理资源 21 | - item14 注意资源管理类中的拷贝行为 22 | - item15 在资源管理类中提供对原始资源的访问git 23 | - item16 使用new/delete形式要对应 24 | - item17 用单独的语句来创建智能指针 25 | 26 | ### part4 设计与声明 27 | - item18 让接口不容易被误用 28 | - item19 把类当作类型来设计 29 | - item20 用常量引用传递代替值传递 30 | - item21 不要在需要返回对象时返回引用 31 | - item22 类的数据成员声明为private 32 | - item23 用非成员且非友元函数来替换成员函数 33 | - item24 如果参数要进行类型转换,该函数不能作为成员函数 34 | - item25 考虑写一个高效的swap函数 35 | 36 | ### part5 实现 37 | - item26 尽可能推迟变量定义 38 | - item27 减少类型转换的使用 39 | - item28 避免返回指向对象内部成员的句柄 40 | - item29 保证代码的异常安全性 41 | - item30 透彻了解inline函数 42 | - item31 最小化文件之间的编译依赖关系 43 | 44 | ### part6 45 | - item32 让public继承塑造出is-a关系 46 | - item33 避免继承中发生的名称覆盖 47 | - item34 区分接口继承和实现继承 48 | - item35 考虑virtual函数的替代方法 49 | - item36 不要重写继承来的非虚函数 50 | - item37 不要重定义通过继承得到的默认参数值 51 | - item38 通过组合塑造has-a或use-a关系 52 | - item39 慎用private继承 53 | - item40 慎用多继承 54 | 55 | ### part7 56 | 57 | ### part8 58 | 59 | ### part9 60 | 61 | ### 参考文献 & 资源链接 62 | - [Effective C++ 读书笔记](https://zhuanlan.zhihu.com/c_1104392405461315584) -------------------------------------------------------------------------------- /Effective-CPP-3rd-Edition/part-1-accustoming-yourself-to-cpp.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Effective C++——习惯C++ 3 | date: 2020-01-08 20:57:13 4 | tags: [C/C++, Effective C++] 5 | categories: 编程语言 6 | comments: true 7 | --- 8 | 原书的第一部分,主要是对C++语言的介绍以及优化`#define`的方法等比较基础的知识点。 9 | 10 | 11 | ## item1 视C++为一个语言集合 12 | 不同于C,C++已经不再是一个带有单一语言规则的计算机语言。今天的C++是一个多重泛型编程语言,它同时支持*面向过程* 、*面向对象* 、*函数式编程* 、*泛型编程* 、*元编程* 。因此,我们需要**将C+++视为一个由相关语言组成的集合,而不是单一语言**。它包含了四个语言模块: 13 | - **C**:C++是以C语言为基础的,C++的语法规则、预处理器、内置数据类型、数组、指针等都是来自于C,许多C++的语法就是较高级的C。 14 | - **Object-Oriented C++**:也就是很多时候提到的*C With Classes* ,其中包含了C++面向对象的机制:类,以及封装、继承、多态、虚函数等类的支持机制。 15 | - **Template C++**:泛型编程模块。 16 | - **STL**:STL是一个封装了容器、迭代器、算法以及函数对象的template库。 17 | 18 | **为什么要区分C++中的语言模块** 19 | 为了提高C++的效率,使用不同的语言模块需要遵循不同的编程策略,例如: 20 | - 在C语言中,一般使用值传递(pass-by-value); 21 | - 在OOC和模板C++中,使用常量引用(pass-by-reference-const)传递更加高效; 22 | - 对于STL,因为迭代器是基于指针构造而成,直接使用值传递即可。 23 | 24 | 25 | ## item2 用const,enum,inline代替#define 26 | 换言之,以**编译器替换预处理器**。预处理宏会在编译器之前被简单的替换成代码,经常会发生意料之外的错误,这种错误往往难以跟踪到。因此,除了一些特殊的指令外,如`#include`,应该尽量避免使用预处理宏。解决之道是对`#define`进行替换。 27 | ### 2.1 const替换#define定义的常量 28 | - **数值常量**:对于数值类型的常量,可以通过定义一个全局常量来对`#define`进行替换,如`const double A=3.141592654;`替换`#define A 3.141592654`。 29 | - **常量指针**:由于常量定义通常被放在头文件中以便被不同的源码包含,因此需要把指针以及指针指向的数据都定义位`const`,如`const char* const author="Scotter Meyers";`。 30 | - **类的常量**:对于类中的常量成员,为了保证其能被类的对象访问到,又不会生成多个拷贝,需要将其声明为静态常量,即 31 | ``` 32 | class Player{ 33 | private: 34 | static const int numTurns=5;//静态成员变量,属于类不会在实例化过程中产生拷贝;const成员,不能进行赋值操作 35 | } 36 | ``` 37 | 38 | ### 2.2 enum使用技巧 39 | 当我们要在一个类中声明一个常量,这个常量不允许在在声明时进行初始化,而接下来某个语句明确要用到这个变量,比如说静态数组的声明,如: 40 | ``` 41 | class Player{ 42 | private: 43 | static const int numTurns;//一些编译器不允许static成员在声明时赋值,需要在类外进行定义 44 | int scores[numTurns]; // 编译无法通过,在编译过程中必须要指定数组大小 45 | } 46 | ``` 47 | 面对上面个的问题,我们可以选择使用enum来解决,即: 48 | ``` 49 | class Player{ 50 | private: 51 | enum {NumTurns=5}; // 因为枚举类型的数值可以充当int使用, 52 | int scores[NumTurns]; // 所以,编译通过。 53 | } 54 | ``` 55 | 56 | ### 2.3 inline函数 57 | 对于一些简单又需要反复调用的程序语句,将其封装成函数是非常不划算的,因为调用函数的开销甚至超过了函数内代码运行的开销。`#define`实现类似于函数的宏定义,好处是可以减少简单函数调用造成额外开销,但是其代码非常不雅观,而且会导致一些未知的错误。C++提供了**内联函数**来帮助我们避免简单函数调用带来的不必要开销,也可以避免宏替换带来的不可预料的错误。 58 | 59 | **Note:总结** 60 | - 对于单纯的常量,最好用`const`对象或`enum`对象替换`#define` 61 | - 对于形似函数的宏,最好用`inline`函数替换`#define` 62 | 63 | ## item3 多用const 64 | const允许指定一个语义约束,告诉编译器和程序员某个值应该保持不变,而编译器会强制实施这个约束。 65 | ### 3.1 const与指针 66 | - **常量指针**:指向常量的指针,const在`*`左边 67 | - **指针常量**:指针类型的常量,const在`*`右边 68 | 69 | ``` 70 | const char* p; // 数据是常量 71 | char const* p; // 数据是常量 72 | char* const p; // 指针是常量 73 | const char* const p; // 指针和数据都是常量 74 | ``` 75 | 76 | ### 3.2 const与STL迭代器 77 | STL迭代器相当于类型T的指针,即`T*`。因此,如果想定义一个迭代器指向一个常数,需要使用const_iterator。 78 | ``` 79 | std::vector vec; 80 | std::vector::const_iterator const_itr = vec.begin(); // const_itr类似于const T*,指向一个常量 81 | ++const_itr; // 迭代器可变 82 | *const_itr = 10; // 错误,*const_itr不可变 83 | 84 | const std::vector::iterator itr = vec.begin(); // iter类似于T* const 85 | ++itr; // 错误,指针是一个常量 86 | *itr = 10; // 改变指向的值 87 | ``` 88 | 89 | ### 3.3 const与函数 90 | const最有效的用法是在函数声明时的应用。在一个函数声明式内,const可以和函数返回值、参数以及函数自身产生关联。例如,我们可以让函数返回一个常量值,可以降低因用户错误而造成的意外。 91 | ``` 92 | class Rational{...}; 93 | Rational operator*(const Rational& lhs, const Rational& rhs){...}; 94 | ``` 95 | 在某处使用乘法操作符时,误把比较操作符`===`写成了赋值操作符`=`,如 96 | ``` 97 | Rational a,b,c; 98 | if((a*b)=c){...} // 编译器不会报错,很难追踪错误 99 | ``` 100 | 解决办法是**将操作符定义为返回const**,这样对其赋值将会是非法操作。 101 | ``` 102 | const Rational operator*(const Rational& lhs, const Rational& rhs){...}; 103 | ``` 104 | 105 | ### 3.4 const与类的成员函数 106 | 用const关键字修饰类的成员函数主要有两个作用: 107 | - 一是可以直观的告诉用户函数是否会改变成员变量; 108 | - 二是用const修饰的对象只能调用const修饰的成员函数。 109 | 110 | ### 3.5 数据常量性和逻辑常量性 111 | C++标准对成员函数常量性的规定是**数据常量性**,即不允许成员变量被修改。C++编译器对此的检测标准是**检查该成员函数中有没有给成员变量进行赋值操作**。 112 | ``` 113 | class CTextBlock 114 | { 115 | public: 116 | char& operator[](std::size_t pos) const 117 | { 118 | return pText[pos]; 119 | } 120 | private: 121 | char* pText; 122 | } 123 | ``` 124 | 只有指针属于对象,指针所指向的数据不属于对象,const修饰的`operator[]`中并没有赋值操作符,编译器会通过编译,但是其存在着潜在风险。如: 125 | ``` 126 | const CTextBlock ctb("Hello"); // 声明一个常量对象 127 | char* pc = &ctb[0]; // 调用operator[]取得一个指针,指向ctb的数据 128 | *pc = "J"; // 数据被修改为"Jello" 129 | ``` 130 | 131 | 数据常量性还存在着另一个局限性,如: 132 | ``` 133 | class CTextBlock 134 | { 135 | public: 136 | std::size_t length() const 137 | { 138 | if(!lengthIsValid) 139 | { 140 | textLength = std::strlen(pText); // 有赋值操作,编译会发生错误,但事实上这种改变是允许且必要的 141 | lengthIsValid = true; 142 | } 143 | return textLength; 144 | } 145 | private: 146 | char* pText; 147 | std::size_t textLength; 148 | bool lengthIsValid; 149 | } 150 | ``` 151 | 解决办法是**逻辑常量性**,即使用`mutable`关键字来修饰成员变量,允许数据被修改,但是这些修改不反映到类外。 152 | ``` 153 | class CTextBlock 154 | { 155 | public: 156 | std::size_t length() const 157 | { 158 | if(!lengthIsValid) 159 | { 160 | textLength = std::strlen(pText); // 编译可以通过 161 | lengthIsValid = true; 162 | } 163 | return textLength; 164 | } 165 | private: 166 | char* pText; 167 | mutable std::size_t textLength; 168 | mutable bool lengthIsValid; 169 | } 170 | ``` 171 | 172 | ### 3.6 在const和non-const成员函数中避免重复 173 | 在C+++中,两个函数如果只是常量性不同,可以被重载。但是这样就存在着两个几乎完全重复的函数,虽然可以另外写一个私有函数进行调用,但是还是存在一些重复的代码,如函数调用和return等语句。我们真正应该实现的是**一次实现,两次调用**,即其中一个调用另一个,需要用到**常量性转换**。 174 | ``` 175 | class TextBlock 176 | { 177 | public: 178 | const char& operator[](std::size_t pos) const 179 | { 180 | // .... 181 | return pText[ pos ]; 182 | } 183 | char& operator[](std::size_t pos) 184 | { 185 | return 186 | const_cast( // 将[]的const移除,转换成char& 187 | static_cast(*this) // 为*this加上const,从而可以调用const操作 188 | [ position ] 189 | ); 190 | } 191 | private: 192 | char* pText; 193 | } 194 | ``` 195 | 196 | **Note:总结** 197 | - 变量,指针,迭代器以及函数都可以通过const的修饰来实现只读的目的; 198 | - 编译器强制使用的是数据常量性,但是编写程序的时候应该采用逻辑常量性,对需要修改的成员变量加上`mutalbe`关键字修饰; 199 | - const和non-const成员函数有着大量重复的实现,可以使用non-const函数来调用const函数来避免重复。 200 | 201 | ## item4 确保对象在使用前已被初始化 202 | C++不能保证每个对象在定义时都被自动初始化,最佳的办法就是永远在使用对象之前先将其初始化。 203 | 204 | ### 4.1 内置数据类型的初始化 205 | C++的内置数据类型继承自C,不能保证变量在定义式自动初始化。使用未初始化的数据可能会导致程序运行错误,因此需要手动进行初始化。 206 | 207 | ### 4.2 类的初始化 208 | - 对于用户自定义的类,需要构造函数来完成类的初始化,需要保证构造函数将对象的每一个成员初始化; 209 | - C++规定,对象的成员变量的初始化动作发生在进入构造函数体之前。因此,在构造函数体内对成员变量进行的是赋值操作,其先调用默认构造函数对成员函数进行初始化,然后对它们赋予新值; 210 | - 较优的办法是使用**初始化列表**来对成员变量进行初始化,从而不再调用默认构造函数,直接进行赋值操作; 211 | - C++有着固定的初始化顺序,基类先于派生类被初始化,成员变量是按照其声明的顺序进行初始化的 212 | 213 | - 类的初始化:构造函数与初始化列表 214 | - 引用和常量的初始化 215 | - 初始化顺序:继承关系中,先父类后子类;同一个类中,只与成员变量的声明顺序有关 216 | - 非局部静态对象的初始化 217 | 218 | ### 4.3 non-local static对象的初始化 219 | 在不同的源码文件中,分别包含了至少一个non-local static对象,当这些对象发生互动时,他们的初始化顺序是不确定的,直接使用这些对象会给程序的运行带来风险。 220 | 221 | **Note:non-local static对象** 222 | 所谓的static对象,其生命周期从被构造出来到程序结束,包括全局对象、定义在namespace作用域内的对象、在类或者函数内被声明为static的对象。其中,在函数内的static对象称为local static对象,其他的static对象我们就称为nono-local static对象。 223 | 静态对象不是基于堆或者栈的,初始化的静态对象在内存的Data段,未初始化的位于BSS段。 224 | 225 | ``` 226 | /* demo1.cpp */ 227 | class FileSyste 228 | { 229 | public: 230 | // ... 231 | std::size_t numDisks() const; 232 | }; 233 | extern FileSystem fs;// 在全局范围声明一个对象fs,供其他单元调用 234 | 235 | /* demo2.cpp */ 236 | class Directory 237 | { 238 | public: 239 | Directory(param) 240 | { 241 | std::size_t disks = fs.numDisks(); // 调用了fs对象 242 | } 243 | } 244 | ``` 245 | 在上述代码中,如果我们要创建一个`Directory`对象,构造函数就会调用`fs`对象。但是两个对象是在不同的源文件不同的时间建立起来的,无法保证fs已经被初始化。解决办法就是:**将non-local static对象转换成local static对象**,即将每一个non-local static对象放到一个函数里面去,这是单例模式的常用的手法。其依据是:在C++中,函数的local static对象会在该函数第一次被调用时进行初始化,因此我们只需要让函数返回一个指向local static对象的指针或引用,就可以得到一个在使用时保证被初始化的对象。 246 | ``` 247 | /* demo1.cpp */ 248 | class FileSyste 249 | { 250 | public: 251 | // ... 252 | std::size_t numDisks() const; 253 | } 254 | FileSystem& fs() 255 | { 256 | static FileSystem fs; // 声明了一个局部静态变量 257 | return fs; 258 | } 259 | 260 | /* demo2.cpp */ 261 | class Directory 262 | { 263 | public: 264 | Directory(param) 265 | { 266 | std::size_t disks = fs().numDisks(); // 调用了fs对象 267 | } 268 | } 269 | Directory& dir() 270 | { 271 | static Directory dir; 272 | return dir; 273 | } 274 | ``` 275 | 276 | **Note:总结** 277 | - 对于内置的数据类型,要进行手动初始化; 278 | - 构造函数对类进行初始化最好使用*初始化列表* 来替换*在构造函数中使用赋值操作* 。构造函数按照变量声明的顺序进行初始化; 279 | - 对于静态对象,用局部静态对象来替换全局静态对象来保证使用前确定被初始化。 280 | -------------------------------------------------------------------------------- /Effective-CPP-3rd-Edition/part-2-constructors-destructors-and-assignment-operators.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Effective C++——构造/析构/赋值运算 3 | tags: 4 | - C/C++ 5 | - Effective C++ 6 | categories: 编程语言 7 | comments: true 8 | date: 2020-02-13 00:08:00 9 | --- 10 | 本文对应原书第二部分,主要记录C++类中使用构造函数、析构函数以及赋值运算需要注意的事项。 11 | 12 | 13 | ## item5 了解C++类中自动生成和调用的函数 14 | 编译器会默认为类创建默认构造函数,拷贝构造函数,赋值操作符,以及析构函数,但是这些函数只要在调用时才会生成。 15 | ``` 16 | class Empty{}; 17 | 18 | // 假设调用了以下功能,此类等价于 19 | class Empty 20 | { 21 | public: 22 | Empty(){} // 默认构造函数 23 | ~Empty(){} // 析构函数 24 | Empty(const Empty& empty){} // 拷贝构造函数 25 | Empty& operator=(const Empty& empty){} // 赋值运算符 26 | } 27 | ``` 28 | 29 | **Notes:** 30 | - 默认生成的析构函数是non-virtual,除非这个类的基类声明有virtual析构函数 31 | - 自动生成的拷贝构造函数和赋值操作符只是单纯的将源对象的所有非静态成员变量拷贝到目标对象,因此当对象中包含*引用成员* 和*const成员* 时,由于C++中不允许引用被初始化后指向另一个对象,所以C++编译器会拒绝为其赋值 32 | 33 | ``` 34 | // 定义一个模板类 35 | template 36 | class NameObject 37 | { 38 | public: 39 | NameObject(std::string& name,const T& value); 40 | private: 41 | std::string& nameValue; 42 | const T objectValue; 43 | } 44 | // 对类进行以下操作 45 | std::string str1("Persephone"); 46 | std::string str2("Satch"); 47 | 48 | NameObject Dog1(str1,2); // Dog1.nameValue指向了str1 49 | NameObject Dog2(str2,10); // Dog2.nameValue指向了str2 50 | 51 | Dog2=Dog1; // 会发生Dog2.nameValue=Dog1.nameValue,C++中引用被初始化后不允许指向另一个对象,编译器无法通过 52 | ``` 53 | 54 | **总结:** 55 | - 如果没有进行声明,C++编译器会自动为类创建*默认构造函数*、*拷贝构造函数*、*赋值运算符*,以及*析构函数*。 56 | 57 | ## item6 若不想使用编译器自动生成的函数,要显式拒绝 58 | 在有些特定功能的类中,我们希望其不支持拷贝和赋值功能,比如RAII范式编程中。但是,即使不声明拷贝构造函数和赋值运算符,当调用它们时,编译器还是会自动声明。**为了屏蔽编译器自动提供的函数,可以将相应的成员函数声明为private并且不予实现**。 59 | - 所有编译器自动提供的函数都是public的 60 | - 将编译器默认生成的函数进行显式声明为private,可以避免其被外部函数调用 61 | - 对声明的函数不进行定义,可以避免其被类的成员函数或友元函数调用 62 | 63 | **总结:** 64 | - 当不想让编译器为类自动生成某些函数时,可将相应的成员函数声明为private并且不予实现。 65 | 66 | ## item7 为多态基类声明virtual析构函数 67 | - **析构函数**:析构函数是用来释放对象资源的。当对象的声明周期结束或者对象资源通过delete被显式释放时,对象的析构函数被显式调用,从而释放对象占用的资源 68 | - **多态**:多态是C++面向对象编程的思想之一。在编程中一个显著的特征就是*通过基类指针指向子类对象,实现对子类对象的操作*。 69 | 70 | C++明确指出,当派生类对象通过一个基类指针被删除,而这个基类的析构函数不是虚函数,其结果没有定义,在实际的执行中表现为只调用了基类的析构函数,派生类的资源没有被释放。 71 | 当基类析构函数被声明为虚函数,派生类的析构函数也默认为是虚函数。当派生类对象被销毁时,基类会找到派生类的虚函数表,调用派生类的析构函数,接着调用基类的析构函数,实现资源的完全释放。 72 | 73 | **总结:** 74 | - 用来实现多态的基类应该将其析构函数声明为虚函数。如果一个类中包含有虚函数,那它就是被用来实现多态的,就需要一个虚的析构函数。 75 | - 如果类的设计目的不是作为基类或者不是用来实现多态的基类,就不需要将析构函数声明为虚函数。这是因为当一个类中包含虚函数的时候,编译器会为对象生成一个虚表指针,类也会多一个虚函数表,会增加内存资源的消耗。 76 | 77 | ## item8 不要让析构函数抛出异常 78 | 析构函数被调用的情况有两种:1、对象正常结束生命周期时调用;2、在异常发生时,编译器释放对象资源时调用。在第一种情况下,析构函数抛出异常不会出现无法预料的结果,可以正常捕获。但是在后一种情况下,如果对象发生异常,异常模块为了维护系统对象数据的一致性,会去调用对象的析构函数释放资源,析构函数如果抛出异常,异常处理机制无法捕获,只能调用`terminate()`函数,系统会崩溃。 79 | 80 | **总结:** 81 | - 从语法上讲,析构函数抛出异常是可以的,但是C++中并不推荐这一做法。 82 | - 如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕获该异常,然后吞下它们或者不传播。 83 | - 如果需要对某个函数运行期间抛出的异常进行处理,那么类应该提供一个普通函数而不是利用析构函数来进行该操作。主要针对的是资源释放过程中可能出现的异常,如数据库断开连接或者文件句柄的释放等。 84 | 85 | ## item9 不要在构造和析构函数中调用virtual函数 86 | 构造函数的工作是进行初始化,派生类对象构造期间首先会调用基类的构造函数,对象的类型也变成了基类的类型。如果基类的构造函数中调用了虚函数,虚函数不会绑定到子类的版本,这样virtual函数没有任何意义。析构函数书中也是同样的道理。 87 | 88 | ``` 89 | class Transaction 90 | { 91 | public: 92 | Transaction(){init();} // 默认构造函数 93 | ~Transaction(){} // 析构函数 94 | 95 | virtual void log() const = 0; 96 | private: 97 | void init(){ 98 | log(); // 这种情况更加危险,因为其更难发现 99 | } 100 | } 101 | ``` 102 | 103 | **总结:** 104 | - 不要在构造和析构函数中调用virtual函数,因为基类中调用的虚函数调用的是其自己的而不属于派生类。 105 | 106 | ## item10 自定义赋值操作符(operator=) 要返回*this的引用 107 | C++的赋值操作符可以进行链式赋值,为了实现链式赋值,赋值操作符必须返回一个指向当前对象的引用,这是C++中自定义赋值操作符应该遵循的规则。这个规则对于`+=`、`-=`、`*=`等赋值相关的操作符同样适用。 108 | ``` 109 | 110 | int x,y,z; 111 | x=y=z=5; // 链式赋值,等效于x=(y=(z=5)) 112 | 113 | class Widget{ 114 | public: 115 | ... 116 | Widget& operator=(const Widget& rhs){ //要返回一个当前类的引用 117 | ... 118 | return *this; //返回给左边的变量 119 | } 120 | ... 121 | }; 122 | ``` 123 | 124 | **总结:** 125 | - this是用来指向当前对象的对象,只存在类的成员函数里。 126 | - 自定义赋值操作符要返回一个指向当前对象的引用。 127 | 128 | ## item11 注意operator= 的“自我赋值” 129 | 虽然在代码中我们不会有意去进行自我赋值,但是一些代码还是存在潜在自我赋值的可能性,如 130 | ``` 131 | a[i] = a[j]; // 当i=j时就是自我赋值 132 | *px = *py; // 如果px和py恰好指向的是同一个对象,这也是自我赋值 133 | 134 | class Base{...}; 135 | class Derived:public Base{...}; 136 | void doSomething(const Base& rb,Derived* pd); // rb和pd有可能指向同一个对象 137 | ``` 138 | 我们在类中对资源进行管理要特别关注可能发生的拷贝现象。当我们要手动管理资源时,赋值操作符就就可能是不安全的。在下面的代码中,如果发生了自我赋值,delete语句不仅会释放*this自身的资源,rhs的资源也会被释放,最后返回的是一个损坏的数据。 139 | ``` 140 | class Bitmap{...} 141 | Class Widget{ 142 | ... 143 | public: 144 | Widget& Widget::operator=(const Widget& rhs) 145 | { 146 | delete pb; // 删除当前版本 147 | pb = new Bitmap(*rhs.bp); 148 | return *this; 149 | } 150 | private: 151 | Bitmap *bp; 152 | } 153 | ``` 154 | 为了避免自我赋值出现的错误,可以采用参数自身验证、重新排列语句等方法。 155 | 1. **解决方法1:参数自身验证** 156 | ``` 157 | Widget& Widget::operator=(const Widget& rhs) 158 | { 159 | if(this==&rhs) 160 | return *this; 161 | 162 | delete pb; // 删除当前版本 163 | pb = new Bitmap(*rhs.bp); // 风险是:当该语句抛出异常,返回的仍然是损坏数据 164 | return *this; 165 | } 166 | ``` 167 | 168 | 2. **解决方法2:重新排列语句** 169 | ``` 170 | Widget& Widget::operator=(const Widget& rhs) 171 | { 172 | Bitmap* pOrigin = pb; // 备份原来的pb 173 | pb = new Bitmap(*rhs.bp); // 赋值rhs的pb 174 | delete pb; // 删除备份 175 | return *this; 176 | } 177 | ``` 178 | 上述代码中因为对数据进行了备份,复制,删除等操作,如果该赋值操作符频繁使用,其效率是比较低的。 179 | 180 | 3. **解决方法3:先复制后交换(copy and swap)** 181 | ``` 182 | Class Widget{ 183 | ... 184 | public: 185 | void swap(Widget& rhs); // 交换rhs和*this的数据 186 | 187 | Widget& Widget::operator=(const Widget& rhs) 188 | { 189 | Widget temp(rhs); // 为rhs数据创建备份 190 | swap(temp); // 将*this数据和rhs交换 191 | return *this; 192 | } 193 | 194 | } 195 | ``` 196 | 197 | 4. **解决方法4:用传值替换传引用** 198 | ``` 199 | Widget& Widget::operator=(Widget rhs) 200 | { 201 | swap(rhs); 202 | return *this; 203 | } 204 | ``` 205 | 上述代码中利用了C++传值会自动生成一份本地拷贝的特性,有效减少了代码长度,也增加了效率。 206 | 207 | **总结:** 208 | - 使用赋值操作符要充分保证自我赋值发生时程序的安全性。 209 | - 任何函数中操作了一个以上对象时,要保证即使多个对象是同一个对象时,其行为仍然是正确的。 210 | 211 | ## item12 对象进行复制时需要完整拷贝 212 | 设计良好的面向对象系统会将对象的内部封装起来,只保留拷贝构造函数和赋值操作符来负责对象的拷贝。当我们自己定义赋值运算符和拷贝构造函数函数时,特别注意:1、复制类中所有的局部变量;2、派生类中要调用基类中相应的函数或操作符。 213 | 214 | **总结:** 215 | - 复制函数要确保复制对象内所有的成员变量以及所有基类成员变量。 216 | - 拷贝构造函数和赋值操作符有相近的代码,两者之间不能互相调用,可以建立一个新的成员函数给两者调用。 -------------------------------------------------------------------------------- /Effective-CPP-3rd-Edition/part-3-resource-management.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Effective C++——资源管理 3 | tags: 4 | - C/C++ 5 | - Effective C++ 6 | - 资源管理 7 | categories: 编程语言 8 | comments: true 9 | date: 2020-02-13 18:03:54 10 | --- 11 | 12 | 本文对应原书第三部分,主要记录C++中资源管理需要注意的事项,包括用对象管理资源、对象的复制行为、资源的释放以及智能指针等。 13 | 14 | 15 | ## item13 使用对象来管理资源 16 | 我们使用系统资源必须遵循`申请资源`->`使用资源`->`释放资源`这样一个完成的步骤,如果资源使用后没有释放就会造成资源泄漏。在编程中我们通过手动管理资源往往会存在着因为异常或者逻辑不合理跳过了资源释放,或者忘记释放资源等问题。为了避免以上情况出现,我们可以利用C++的对象析构函数自动调用机制,把资源封装在对象里面,当对象的生命周期结束,资源会保证被释放,也就是RAII规范。其基本思想就是**在构造函数中申请资源,在析构函数中释放资源**。 17 | 18 | **Note:总结** 19 | - 为防止资源泄漏,使用RAII机制来进行资源管理,通过构造函数获取资源,利用析构函数确保资源被释放。 20 | - C++中常用的RAII实例就是智能指针。`shared_ptr`使用引用计数来管理资源。 21 | 22 | ## item14 注意资源管理类的拷贝行为 23 | 现在假设我们用RAII规范来管理一个互斥锁,保证该互斥锁最后能被解除。 24 | ``` 25 | class Lock 26 | { 27 | public: 28 | explicit Lock(Mutex* pm):mutexPtr(pm) 29 | {lock(mutexPtr);} 30 | ~Lock(){unlock(mutexPtr);} 31 | private: 32 | Mutex* mutexPtr; 33 | } 34 | 35 | /* 接下来我们使用这个类*/ 36 | Mutex m; 37 | Lock lock1(&m); // 锁定m 38 | Lock lock2(lock1); // 将lock1复制到lock2 39 | ``` 40 | 在上述代码中,发生了资源的复制。在用RAII规范来管理资源类的时候,特别要注意这个类的拷贝行为。对于资源管理类的拷贝,通常可以采取以下几种措施: 41 | 1. **禁止拷贝** 42 | 对于有些资源来说,比如上面提到的互斥锁,它的拷贝是没有意义,我们可以通过将其拷贝构造函数和赋值运算符显式声明为private来禁止外部使用其拷贝功能。 43 | ``` 44 | class Lock 45 | { 46 | public: 47 | explicit Lock(Mutex* pm):mutexPtr(pm) 48 | {lock(mutexPtr);} 49 | ~Lock(){unlock(mutexPtr);} 50 | private: 51 | /* 私有化其拷贝函数 */ 52 | Lock(const Lock& lock); 53 | Lock& operator=(const Lock& lock); 54 | Mutex* mutexPtr; 55 | } 56 | 57 | ``` 58 | 59 | 2. **使用引用计数** 60 | 有时我们希望保有资源,直到最后一个使用者被销毁。这种情况下复制RAII对象时,应该将该资源的引用计数递增。`shared_ptr`就是使用的这种机制,`shared_ptr`提供了一个特殊的可定义函数——删除器(deleter),即在其引用计数为零时调用。 61 | ``` 62 | class Lock 63 | { 64 | public: 65 | explicit Lock(Mutex* pm):mutexPtr(pm,unlock) // 将unlock函数绑定到删除器 66 | {lock(mutexPtr.get());} // 锁定原始指针 67 | // 这里不再需要定义析构函数来释放资源 ~Lock(){unlock(mutexPtr);} 68 | private: 69 | std::shared_ptr mutexPtr; // 使用shared_ptr,不再使用原始指针 70 | } 71 | ``` 72 | 73 | 3. **深度拷贝** 74 | 当我们需要的不仅仅是资源的所有权,而是要获取资源的副本的时候,我们就需要拷贝底层资源,即不仅仅是指向资源的指针,在内存上的资源也要进行复制,也就是“深拷贝”。 75 | 76 | 4. **所有权转移** 77 | 当我们只想要一个RAII对象来持有这个资源,在进行拷贝的时候就要进行所有权的转移,即释放原对象对资源的权限,将所有权转移到新的对象上,这也是`auto_ptr`的工作原理。 78 | 79 | **Note:总结** 80 | - RAII对象的拷贝要根据其管理的资源具体考虑,资源的拷贝行为决定RAII对象的拷贝行为。 81 | - 常用的RAII对象的拷贝行为有:禁止拷贝、使用引用计数、深度拷贝、所有权转移。 82 | 83 | ## item15 在资源管理类中提供对原始资源的访问 84 | 资源管理类通过对原始资源的封装可以有效避免资源泄漏,但是在很多情况下外部的API需要直接访问原始资源,因此我们需要在资源管理类中提供对原始资源的访问。提供访问的方式有两种:*显式访问* 和*隐式访问*。其中,显示访问比较安全,隐式访问便于使用。 85 | - 显式访问:在RAII类中声明一个显式的转换函数,返回原始资源类型的对象 86 | - 隐式访问:利用operator操作符声明一个隐式转换函数 87 | 88 | 1. **智能指针获取原始指针** 89 | 智能指针都有一个成员函数`get()`,来执行显式转换,返回智能指针对象包含的原始指针: 90 | ``` 91 | mutexPtr.get(); // 获取指向互斥锁的原始指针 92 | ``` 93 | 智能指针重载了指针解引操作符(operator->和operator*),它们允许隐式的将智能指针转换成底部的原始指针。 94 | ``` 95 | class Investment 96 | { 97 | public: 98 | bool isTaxFree() const; 99 | } 100 | 101 | Investment* pInv; 102 | std::shared_ptr ptr1(pInv); // 使用shared_ptr管理资源 103 | bool tax1 = ptr1->isTaxFree(); // 使用operator->访问原始资源 104 | std::auto_ptr ptr2(pInv); // 使用auto_ptr管理资源 105 | bool tax1 = (*ptr2).isTaxFree(); // 使用operator*访问原始资源 106 | ``` 107 | 108 | 2. **自定义RAII类获取原始资源** 109 | 同智能指针类似,在自定义的RAII类中可以通过定义一个`get()`函数来显式的返回原始资源,也可以通过operator操作符声明一个隐式转换函数。 110 | 111 | **Note:总结** 112 | - API往往需要访问原始资源,因此RAII类应该提供一个访问原始资源的方法。 113 | - 对原始资源的访问有显式和隐式两种。一般而言,显式比较安全,隐式便于使用。 114 | 115 | ## item16 使用new/delete形式要对应 116 | 为了保证内存的释放,我们经常会特别注意new/delete成对的出现。对于下面的代码,使用了new也有对应的delete,但还是有错误。 117 | ``` 118 | std::string* strArray = new std::string[100]; 119 | ... 120 | delete strArray; 121 | ``` 122 | 当使用new来创建一个对象时,发生了两件事:第一,通过operator new操作符分配内存;第二,针对此内存,有一个或多个构造函数被调用。同理,使用delete时也有两件事发生:针对此内存有一个或多个析构函数被调用,然后通过operator delete释放内存。delete最大的问题在于**即将被删除的内存中到底有多少个对象**?这个答案决定了有多少个析构函数被调用。 123 | 确定delete要删除多少个对象,就需要明确告诉delete被删除的指针指向的是单一对象还是一个数组。这是因为单一对象的内存布局不同于数组的内存布局。数组对象中包含了一个表明数组大小的记录,以便delete知道需要调用多少次析构函数。单一对象的内存中没有这个记录。 124 | 让delete知道内存中是否存在一个数组大小记录的方法,就是显式的声明,如果使用delete时加上`[]`,delete便认定指向的是一个数组,否则便认定指向的是一个单一对象。 125 | ``` 126 | std::string* strPtr1 = new std::string; 127 | std::string* strPtr2 = new std::string[100]; 128 | ... 129 | delete strPtr1; // 使用delete删除单个对象 130 | delete[] strPtr2;// 使用delete[]删除数组 131 | ``` 132 | 133 | **Note:总结** 134 | - 主要针对单个对象,应该使用new对应delete,对于对象数组,new[] 要对应delete[]。 135 | 136 | ## item17 用单独的语句来创建智能指针 137 | 假设有如下函数: 138 | ``` 139 | int priority(); // 返回程序的优先级 140 | void processWidget(std::shared_ptr pw, int priority); 141 | 142 | /* 通过以下语句来调用processWidget */ 143 | processWidget(std::shared_ptr(new Widget),priority()); 144 | ``` 145 | 上述调用processWidget的代码中使用了智能指针来管理资源,但是仍然存在资源泄漏的可能。 146 | 在调用processWidget函数之前,编译器需要完成三件事:调用`priority()`函数;执行`new Widget`;调用`shared_ptr构造函数`。其中,`new Widget`的执行在调用`shared_ptr构造函数`之前,但是调用`priority()`函数的次序是不一定的。这是因为在C++中,为了保证代码效率,不是以固定的顺序来解析函数参数的。假设编译器生成的代码执行顺序是: 147 | - 1、执行`new Widget`; 148 | - 2、调用`priority()`函数; 149 | - 3、调用`shared_ptr构造函数`。 150 | 如果调用`priority()`函数的过程中抛出异常,`new Widget`返回的指针就会遗失,因为它还没有被放入智能指针中,从而造成资源泄漏。 151 | 为了避免上述问题,需要用单独的语句来创建智能指针,从而保证两个动作不会被隔离,即将new的对象立即放入智能指针。 152 | ``` 153 | std::shared_ptr pw(new Widget); 154 | processWidget(pw,priority()); 155 | ``` 156 | 157 | **Note:总结** 158 | - 用单独的语句创建智能指针,否则可能导致难以察觉的资源泄漏问题。 159 | -------------------------------------------------------------------------------- /Effective-CPP-3rd-Edition/part-4-designs-and-declarations.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Effective C++——设计与声明 3 | tags: 4 | - C/C++ 5 | - Effective C++ 6 | categories: 编程语言 7 | comments: true 8 | date: 2020-02-29 16:00:08 9 | --- 10 | 11 | 本文对应原书的第四部分,主要介绍为了设计和声明良好的C++接口需要注意的事项。 12 | 13 | 14 | ## item18 让接口不容易被误用 15 | 所谓的接口就是提供给用户使用代码的途径。C++有大量关于接口的概念,如函数接口,类接口,模板接口等。理想情况下,如果用户用错了接口,这个代码不应该通过编译。如果代码通过了编译,就应该得到想要的结果。 16 | 让接口不易被误用的办法包括:建立新类型、限制类型上的操作,保证接口设计的一致性,消除用户的资源管理责任。 17 | 18 | 1. **建立新的数据类型、限制类型上的操作** 19 | 接口要设计的不易被误用,就要充分考虑用户可能犯的错误。假如设计一个表示日期的类: 20 | ``` 21 | class Date 22 | { 23 | public: 24 | Date(int month, int day, int year); // 美式日期标准 25 | } 26 | 27 | Date d1(30,3,1995); // 错误,输入的是英式日期标准 28 | Date d2(3,40,1995); // 数字输入错误 29 | ``` 30 | 像上述这样的错误,可以通过引入新的数据类型来进行预防。通过使用简单的包装类(wrapper class),让编译器来检测错误: 31 | ``` 32 | struct Day{ 33 | explicit Day(int d):val(d){} 34 | int val; 35 | }; 36 | struct Month{ 37 | explicit Month(int m):val(m){} 38 | int val; 39 | }; 40 | struct Year{ 41 | explicit Year(int y):val(y){} 42 | int val; 43 | }; 44 | class Date 45 | { 46 | public: 47 | Date(const Month& m, const Day& d, const Year& y); // 美式日期标准 48 | } 49 | 50 | Date d1(3,30,1995); // 数据类型错误 51 | Date d2(Day(30), Month(3), Year(1995)); // 格式错误 52 | Date d3(Month(3), Day(30), Year(1995)); //正确 53 | ``` 54 | 将Day,Month,Year设计成成熟的类比使用上述的结构体要好。上述代码保证了数据类型的准确性,限制其值的范围也是必要的,例如一年只有12个月,Month应该反映出这个事实。可以通过enum满足功能上的要求,但是enum不是类型安全的(type safe),item2中展示过enum可以被用来当作int类型使用。比较安全的做法是预先定义所有有效的Month。下面的代码虽然繁琐,但是保证了数据的准确性。 55 | ``` 56 | class Month{ 57 | public: 58 | static Month Jan(){return Month(1);} //用函数替换对象,避免初始化出现问题(item4) 59 | static Month Feb(){return Month(2);} 60 | ... 61 | static Month Dec(){return Month(12);} 62 | private: 63 | explicit Month(int m); //explicit禁止参数隐式转换,private禁止用户生成自定义的月份 64 | ... 65 | }; 66 | 67 | Date d(Month::Mar(), Day(30), Year(1995)); //正确 68 | ``` 69 | 70 | 预防错误的另一个办法是,限制类型内什么操作允许,什么操作不允许,常见的限制是加上`const`。item3中展示了用`const`修饰operator*返回值来避免无意义的赋值: 71 | ``` 72 | if(a * b = c)... // 应该是a * b == c 73 | ``` 74 | 75 | 2. **保证接口设计的一致性** 76 | STL容器的接口设计保证了高度的一致性,例如每个STL容器都是通过`size()`成员函数返回容器中对象的个数。而在Java中,数组使用`length`属性,List使用`size()`成员函数。这样的不一致性会给开发人员带来不便。 77 | 78 | 3. **避免要求用户必须进行某些操作** 79 | 任何要求用户记得做某些事情的接口都容易造成误用,因为用户很可能忘记完成。例如动态分配了一个资源,要求用户以特定的方式释放资源。 80 | ``` 81 | Investment* createInvestment(); 82 | ``` 83 | 对于上述接口,要求用户在资源使用完后进行释放,但是用户可能产生两种错误:忘记释放资源;多次释放资源。解决方法是用智能指针来进行管理资源,为了避免用户忘记把函数的返回值封装到智能指针内,我们最好让这个函数直接返回一个智能指针对象: 84 | ``` 85 | std::shared_ptr createInvestment(); 86 | ``` 87 | 实际上,返回一个智能指针还解决了一些列用户资源泄漏的问题,如item14中讲到的,`shared_ptr`允许在建立智能指针时为它指定一个资源释放函数(即所谓的删除器,deleter)。 88 | 假如在我们设计的接口中,通过`createInvestment()`得到一个Investment*对象,这个对象必须通过`getRidOfInvestment()`来进行资源释放。我们必须把`getRidOfInvestment()`绑定到shared_ptr的删除器,这样shared_ptr在使用完成后会自动调用指定的资源释放函数,避免使用错误的释放机制。 89 | shared_ptr还有一个好处是,它会自动使用它的每个指针专属的删除器,从而能够避免所谓的DLL交叉问题(cross-DLL problem)。这个问题发生于当对象在一个DLL中被创建,在另外一个DLL中被释放时,在许多平台上会导致运行时问题,因为不同的DLL可能会被链接到不同的代码。shared_ptr会在构造时就确定当引用计数为零时调用哪个DLL的删除器,因此不必担心DLL交叉问题。 90 | 91 | 92 | **Note:总结** 93 | - 一个良好的接口应该保证其不容易被误用。我们在设计接口时要努力实现这个目标。 94 | - 保证正确使用的方法包括保证接口的一致性,以及自定义类型的行为与内置类型的行为保持一致。 95 | - 避免无用的方法包括定义新的包装类型、限制类型的操作、限制取值范围、避免让用户负责管理资源等。 96 | - shared_ptr支持绑定自定义的删除器,实现想要的析构机制,可以有效防范DLL交叉问题。 97 | 98 | 99 | ## item19 把类当作类型来设计 100 | 在面向对象编程的语言中,当定义一个新class的时候,也就是定义了一个新的type。这就意味着重载函数和操作符、控制内存的分配和释放、定义对象的初始化和析构等等,全部都要加以考虑。因此,应该带着像语言设计者设计原始类型一样谨慎。 101 | 设计一个良好的class是一项艰巨的工作,好的type有自然的语法、直观的语义以及高效的实现。在C++中,设计一个良好的类应该时刻考虑到以下的规范: 102 | 1. **新类型的对象如何被创建和销毁?** 103 | - 这影响了类的构造函数和析构函数,以及内存分配函数和释放函数(operator new, operator new[], operator delete, operator delete[])的设计。 104 | 105 | 2. **对象的初始化和对象的赋值应该有什么区别** 106 | - 这决定了构造函数和赋值操作符的行为,以及其间的差异。初始化用于未创建的对象,赋值适用于已创建的对象。 107 | 108 | 3. **新类型的对象如果作为值进行传递有什么意义** 109 | - 拷贝构造函数决定了一个type的值传递如何实现的。 110 | 111 | 4. **新类型的合法值有什么限制** 112 | - 通常情况下,并不是所有的成员变量是有效的。为了避免函数抛出异常,我们要在成员函数中堆变量进行错误检查工作,尤其是构造函数、赋值操作符和所谓的setter函数。 113 | 114 | 5. **新的类型是否存在继承关系** 115 | - 如果新的类型继承自已有的类型,类型的设计就会受到被继承类的约束,比如说函数是否为虚函数。 116 | 117 | 6. **新类型允许进行什么样的转换** 118 | - 新类型的对象可能被隐式地转换成其他类型,需要决定是否允许类型的转换。如果希望把T1隐式转换成T2,可以在class T1中定义一个类型转换函数(operator T2),或者在class T2内写一个可被单一实参调用(non-explicit-one-argument)的构造函数。如果进行显式转换,需要定义个显式转换的函数(item15)。 119 | 120 | 7. **哪些运算符和函数对于新类型是合理的** 121 | - 这决定了新类型中需要声明哪些函数,包括成员函数,非成员函数,友元函数等。 122 | 123 | 8. **哪些标准函数是需要被禁止的** 124 | - 将不希望编译器自动生成的标准函数声明为private(item6)。 125 | 126 | 9. **谁可以访问新类型中的成员** 127 | - 这决定了成员函数的访问级别是public,protected或private。 128 | 129 | 10. **新类型中的“隐藏接口”是什么** 130 | - 新类型对于性能、异常安全性、资源管理有什么保障,需要在代码中加上相应的约束条件。 131 | 132 | 11. **新类型的通用性如何** 133 | - 如果需要新类型适用于多种类型,应该定义一个类模板(class template),而不是单个class。 134 | 135 | 12. **是否真的需要一个新类型** 136 | - 如果只是定义新的派生类以便为既有类增加功能,定义一些非成员函数或者函数模板更加划算。 137 | 138 | 139 | **Note:总结** 140 | - 设计class就是设计type,在定义一个新的type前,要充分考虑到上述问题。 141 | 142 | 143 | ## item20 用常量引用传递代替值传递 144 | 默认情况下,C++以值传递的方式传递对象给函数的。编译器会调用对象的拷贝构造函数创建实参的副本,再把副本传递到函数中,而拷贝是一个耗时的操作。 145 | ``` 146 | class Person 147 | { 148 | public: 149 | Person(); 150 | virtual ~Person(); 151 | ... 152 | private: 153 | std::string name; 154 | std::string address; 155 | } 156 | class Student:public Person 157 | { 158 | public: 159 | Student(); 160 | ~Student(); 161 | ... 162 | private: 163 | std::string schoolName; 164 | std::string schoolAddress; 165 | } 166 | 167 | bool validateStudent(Student s); 168 | // 以值传递的方式调用函数 169 | Student stu; 170 | bool isOK=validateStudent(stu); // 调用函数,以值传递的方式传递参数 171 | ``` 172 | 当上述函数被调用时,发生了以下过程: 173 | - Student类的拷贝函数被调用,用来初始化参数s 174 | - 当validateStudent函数返回时,s被销毁 175 | 176 | 因此,当函数validateStudent被调用时,参数的传递成本是**调用了一次Student的拷贝构造函数,调用了一次Student的析构函数**。Student中还有两个string对象,所以每次构造Student对象也会构造两个string对象。Student又继承自Person类,Person类中也包含两个string对象。这就意味着,Student对象的值传递会调用一次Student拷贝构造函数、一次Person拷贝构造函数、四次string拷贝构造函数,析构过程也有同样的过程。 177 | 178 | ``` 179 | bool validateStudent(const Student& s); 180 | ``` 181 | 传递常量引用的效率要高得多,没有任何构造函数或析构函数被调用,因为没有任何对象被调用。参数声明中`const`是十分重要的,这样可以避免传入的参数在函数内被修改。 182 | 183 | --- 184 | 185 | 以传引用的方式传递参数可以避免对象切割(slicing)问题。当函数的参数类型是基类,通过值传递传入派生类对象时,调用的是基类的拷贝构造函数,派生类中派生的特性就会被切割,留下的是基类对象。 186 | ``` 187 | class Window 188 | { 189 | public: 190 | ... 191 | std::string name() const; 192 | virtual void display() const; 193 | } 194 | 195 | class WindowWithScrollBars:public Window 196 | { 197 | public: 198 | ... 199 | virtual void display() const; 200 | } 201 | 202 | void printNameAndDisplay(Window w) // 值传递,会产生对象切割 203 | { 204 | std::cout<`,这允许用户只对他们使用那一小部分系统形成编译依赖。 400 | 将所有便利函数放在隶属于同一命名空间的多个头文件,意味着用户可以轻松扩展这一组便利函数。用户所需要做的就是添加更多非成员且非友元函数到命名空间内,如用户为WebBrowser添加下载功能,只需要在WebBrowserStuff命名空间中建立一个头文件,内含便利函数的声明即可,新函数就像原来的便利函数那样可用并且整合为一体。这是class无法实现的,虽然可以派生出子类来扩展功能,但是子类无法访问父类的private成员而且并非所有的类都被设计用来作为基类。 401 | 402 | 403 | **Note:总结** 404 | - 尽可能用非成员且非友元的函数替换成员函数,这样可以增加类的封装性、包装弹性和功能扩展性。 405 | - 命名空间可以分布在不同的编译单元中,减小编译依赖性。 406 | 407 | 408 | ## item24 如果参数要进行类型转换,该函数不能作为成员函数 409 | 在导论中提到,**class支持隐式类型转换会给程序带来隐患**,因为如果出现了类型错误,编译器是不会报错的。但是我们在建立数值类型时,比如设计一个类来表现有理数,允许整数隐式转换为有理数是完全合理的。此外,C++也支持int到double的隐式转换。我们设计一个有理数类: 410 | ``` 411 | class Rational 412 | { 413 | public: 414 | Rational(int numerator = 0, int denominator = 1); 415 | //构造函数专门不声明为explicit来允许从int到Rational的隐式转换 416 | int numerator() const; 417 | int denominator() const; 418 | 419 | const Rational operator*(const Rational& rhs) const; 420 | ... 421 | }; 422 | ``` 423 | 作为有理数,我们可以定义各种算术运算符,例如加减乘除。上述代码中,我们采用常规的将operator*声明为成员函数,并进行代码调用: 424 | ``` 425 | // 可以对任意两个有理数对象进行相乘 426 | Rational oneEighth(1,8); 427 | Rational oneHalf(1,2); 428 | Rational result = oneEighth * oneHalf; //编译通过 429 | result = result * oneEighth; //编译通过 430 | 431 | // 将一个对象和int进行相乘 432 | result = oneHalf * 2; //编译通过,等价于result = oneHalf.operator*(2),int进行了隐式类型转换 433 | result = 2 * oneHalf; //编译错误,等价于result = 2.operator*(oneHalf) 434 | ``` 435 | 在上述代码中,一个对象和int数据进行相乘时,我们往往会想当然的觉得两种写法都是正确的,因为乘法满足交换律。事实上,`oneHalf`是一个包含`operator*`函数的对象,编译器可以调用该函数,然而整数2并没有`operator*`成员函数。虽然编译器会在命名空间内或global作用域内尝试寻找可以调用的非成员`operator*`函数: 436 | ``` 437 | result = operator*(2, oneHalf); 438 | ``` 439 | 但是本例中并没有声明这样一个函数,所以编译器会报错。 440 | 对于运行正确的代码,其发生了隐式类型转换,编译器知道传递的是int类型的值,而函数需要的是Rational。编译器用传入的值隐式调用了Rational的构造函数,生成了一个Rational对象,其真正的过程如下: 441 | ``` 442 | const Rational tmp(2); 443 | result = onHalf*tmp; 444 | ``` 445 | 上述情况是因为构造函数是non-explicit,如果构造函数是explicit的,编译器就不会这么操作,那么以下语句都不会通过编译。 446 | ``` 447 | result = oneHalf * 2; //编译错误,构造函数声明为explicit,int便不能转换为Rational 448 | result = 2 * oneHalf; //编译错误,等价于result = 2.operator*(oneHalf) 449 | ``` 450 | 451 | 总结下来,**只有当参数位于参数表里时才可以进行隐式转换**。我们这里的`operator*`作为成员函数,只能对乘号右边的函数参数进行隐式转换,而不在参数表里的参数,即调用成员函数的对象不能进行隐式转换。 452 | 453 | 然而,我们还是希望两个语句都可以编译通过,这也满足我们对乘法交换律的认识。解决方案就是将`operator*`声明为非成员函数,即允许编译器在每个实参上进行隐式类型转换。 454 | ``` 455 | class Rational 456 | { 457 | public: 458 | Rational(int numerator = 0, int denominator = 1); 459 | //构造函数专门不声明为explicit来允许从int到Rational的隐式转换 460 | int numerator() const; 461 | int denominator() const; 462 | ... // 不声明operator* 463 | } 464 | 465 | const Rational operator*(const Rational& lhs,const Rational& rhs) // 非成员函数 466 | { 467 | return Rational(lhs.numerator()*rhs.numerator(), 468 | lhs.denominator()*rhs.denominator()); 469 | } 470 | 471 | Rational oneFourth(1,4); 472 | Rational result; 473 | result = oneFourth * 2; //可以编译 474 | result = 2 * oneFourth; //可以编译 475 | ``` 476 | 477 | 很多C++程序员有一个**误区**:如果某个函数跟某个类有关并且不能作为成员函数,那么就应该将其作为友元函数。这个想法是完全不必要的,本例表明了`operator*`完全可以由Rational的public接口完成任务,不不要额外的特殊接口。而且,能够避免使用友元函数就应该极力避免。 478 | 479 | 本条款讨论的内容只是限于面向对象编程这一条件下,当我们在Template C++条件下时,让Rational作为一个class template更为妥当,涉及的内容在以后讨论。 480 | 481 | 482 | **Note:总结** 483 | - 如果某个函数所有的参数都可能需要隐式转换,这个函数必须作为非成员函数。 484 | 485 | 486 | ## item25 考虑写一个高效的swap函数 487 | swap是一个非常有用的函数,是保证代码异常安全性重要方法(Item29),也可以用来避免自我赋值(Item11)。在标准库里面,swap的实现方式就是经典的方法: 488 | ``` 489 | namespace std 490 | { 491 | template 492 | void swap(T& a,T& b) 493 | { 494 | T temp(a); 495 | a=b; 496 | b=temp; 497 | } 498 | } 499 | ``` 500 | 只要类型T支持拷贝(拷贝构造函数和赋值操作符),swap函数就会帮助实现交换功能。但是在上述过程中进行了三次拷贝:a拷贝给temp,b拷贝给a,temp拷贝给b。如果实现的是比较大的对象的交换,其效率是非常低的。 501 | 为了解决上述问题,常用的方法叫做**pimpl**(pointer to implementation,item31),其基本的思想就是把数据和功能放到不同的类中,并通过一个指针来访问数据。通过这种方法设计Widget类: 502 | ``` 503 | class WidgetImpl 504 | { 505 | public: 506 | ... 507 | private: 508 | int a,b,c; // 可能有很多数据 509 | std::vector vec; // 意味着复制需要很长时间 510 | } 511 | class Widget 512 | { 513 | public: 514 | Widget(const Widget& rhs); 515 | Widget& operator=(const Widget& rhs) 516 | { 517 | ... 518 | *pImpl = *(rhs.pImpl); // 复制Widget时,让其复制WindgetImpl对象 519 | ... 520 | } 521 | private: 522 | WidgetImpl* pImpl; // 指针所指向的对象内包含Widget数据 523 | } 524 | ``` 525 | 这样一来,当需要交换两个对象时,直接交换指针即可,不需要交换成千上万的数据。但是std::swap并不会这么操作,它不止复制三个Widget,还会复制三个WidgetImpl对象。我们需要明确的告诉std::swap,当传入的对象是Widget对象时,需要进行**特殊化**对待。 526 | ``` 527 | namespace std 528 | { 529 | template<> // 表示这是一个std::swap函数完全特殊化的实现 530 | void swap(Widget& a,Widget& b) // 当T是Widget时,会调用这个版本的函数 531 | { 532 | swap(a.pImpl,b,pImpl); // 只需要置换Widget对象的pImpl指针即可 533 | } 534 | } 535 | ``` 536 | 通常情况下,我们不能改变std命名空间中的任何东西,但是我们可以为template设计一个特殊化的版本,让其适用于我们自己的类。但是上面的代码是无法通过编译的,因为其访问了Widget对象的私有成员。我们可以把这个函数声明为类的友元函数,但是更常见的做法是在Widget类中声明一个public的swap成员函数,然后将std::swap特殊化并调用该成员函数。 537 | ``` 538 | class Widget 539 | { 540 | public: 541 | void swap(Widget& other) 542 | { 543 | using std::swap; 544 | swap(pImpl,other.pImpl); 545 | } 546 | } 547 | 548 | namespace std 549 | { 550 | template<> 551 | void swap(Widget& a,Widget& b) 552 | { 553 | a.swap(b); 554 | } 555 | } 556 | ``` 557 | 这样就可以通过编译,而且这也是STL标准容器实现swap的实现方法,STL也是通过提供public swap成员函数和std::swap特殊化版本来实现交换功能的。 558 | 559 | --- 560 | 561 | 上面讨论的对象是类,如果WidgetImpl和Widget是类模板的话,我们可以将数据类型加以参数化: 562 | ``` 563 | template 564 | class WidgetImpl{...} 565 | template