Effective C++ 总结
一、让自己习惯C++
条款01:视C++为一个语言联邦
- 略
条款02:尽量以const,enum,inline替换#define
- #define或许不被视为语言的一部分,所以#define定义的宏不一定被看见,推荐用一个常量替换宏。且使用常量预处理器所需的代码量可能比#define更小。
#define VAR 1.656 // 不推荐 const double Var = 1.656; //推荐
- 若要定义常量字符串,使用char必须const两次,而string只需要一次,所以定义成string会更好一些。
const char* const p1 = "test~"; const std::string p2{"test~"};
- #define无法创建一个class专属常量,因为#define不提供任何封装性,没有所谓的private,若用static const去封装,因为static成员只有整数类型(int,char,bool)可以声明时获得值,万一编译器不允许static const整数类型声明时候获取初始值,且class的static成员在class创建对象时就必须用到,就会使程序出错或者退出,这时候可以使用enum定义变量代替static const声明整数类型。并且获取enum地址是不合法的,可以有效避免别人修改数值。
private: //static const int Num = 5; // 假设编译器不允许这样做 enum {Num = 5, Test}; // 枚举类型默认为static const int类型 int scores[Num]; // 该类创建时要求知道Num值
- 把看起来“像函数的宏”用inline函数替换,因为该宏使用容易出错,这里使用inline template
#define CALL_WITH_MAX(a,b) f((a)>(b))?(a):(b)) // 替换为 template
inline void callWithMax(const T& a, const T& b) { f(a > b ? a : b); }
条款03:尽可能使用const
- 令函数返回一个常量值,往往可以降低错误而造成的意外
- 函数返回值若为右值,无法对其赋予变量等操作
class gameplayer { public: char test1(unsigned position) // 返回值为右值 {return text[position];} char& test2(unsigned position) // 返回值为左值 {return text[position];} private: std::string text; }; int main() { gameplayer p{"abcdefg"}; p.test1(3) = 'w'; // 报错 p.test2(3) = 'w'; return 0; }
- mutable可使non-static成员变量在const成员函数内可更改
- 若const函数和non-const函数代码数量庞大且高度一致(两函数可以重载也可以不重载),为了避免代码重复,可以使用转型的方法
class gameplayer { public: gameplayer(const std::string& str) :text(str) {} void const print() const {cout << this->text << endl;} const char& test1(unsigned position) const { /*******重复性代码*******/ return text[position]; } char& test1(unsigned position) { /*******重复性代码*******/ return const_cast<char&>( static_cast<const gameplayer&>(*this).test1(position)); } private: std::string text; }; int main() { const gameplayer p1{"abcdefg"}; //p1.test1(0) = 'x'; // 报错,表达式必须使可修改的左值 p1.print(); gameplayer p2{ "abcdefg" }; p2.test1(0) = 'x'; p2.print(); return 0; }
输出:
abcdefg
xbcdefg
二、构造/析构/赋值运算
条款04:确定对象被使用前已先被初始化
- 构造函数最后使用成员初始化列表,而不要在构造函数体内使用赋值操作,因为该操作会调用赋值函数。初始化列表列出的成员变量最好与声明顺序相同。若有多个构造函数和多个成员变量,可以将某些“赋值表现和初始化一样好”的成员变量放入某个函数中,然后在构造函数中调用。
- 不同编译单元内的non-local static对象初始化次序使不确定的,为避免“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象(以下代码来自书)
//////////你的程序库////////// class FileSystem{ public: ... std::size_t numDisks() const; // 成员函数之一 ... }; extern FileSystem tfs; // 预备给客户使用的对象 //////////你的程序库////////// //////////客户的程序库////////// class Directory{ public: Directory(params) { ... std::size_t disks = tfs.numDisks(); // 使用tfs对象 ... } }; //////////客户的程序库////////// //////当调用Directory构造函数时: Directory tempDir(params); // 无法确定tfs是否在temDir之前初始化
////////改变方法//////////// class FileSystem{...}; FileSystem& tfs() // 该函数替换tfs对象 { static FileSystem fs; return fs; } class Directory{...}; Directory::Directory(params) { ... std::size_t disks = tfs().numDisks(); // 原本tfs改成tfs() ... } Directory& tempDir() // 该函数替换tempDir对象 { static Directory td; return td; }
条款05:了解C++默默编写并调用哪些函数
- 一个empty class,编译器会为它声明默认构造函数,拷贝构造函数,析构函数,赋值函数。
- 若class内有引用或const成员变量,那么调用默认赋值函数是不合法的。第一,const成员变量值无法更改;第二,看代码
class A { A(string& str):nameValue(str){} private: std::string& nameValue; // 引用成员变量 }; int main() { std::string str1{"str1} ; std::string str2{"str2} ; A a1{str1}; A a2{str2}; a1 = a2; // 报错,调用默认拷贝赋值函数,将a1.nameValue = a2.nameValue return 0; } // C++不允许修改引用的对象(地址) // 编译器也不知道拷贝赋值函数里面应该做些什么 // 是修改引用的地址还是值? // 所以编译器会直接删除默认拷贝赋值函数
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
- 若不想使用就直接“ = delete”
class A { public: A(); A(const A&) = delete; // 不使用就delete A& operator = (const A&) = delete; };
条款07:多态基类声明virtual析构函数
- 带有多态性质的基类应该声明一个virtual析构函数。如果class带由任何的virtual函数,就应该拥有一个virtual析构函数。特别是父类指针指向子类对象,当父类析构函数不是virtual时,是静态绑定(编译器绑定),只会调用父类析构函数,当父类析构函数是virtual时,会发生动态绑定(运行期绑定),先调用所指向的子类对象析构函数,再调用自身析构函数。
class father { private: int* p1; public: father(int* num) :p1(num) {} ~father() // 未加virtual { delete p1; p1 = nullptr; cout << "father的构造函数" << endl; } }; class son : public father { public: son(int* num1, int* num2) : father(num1), p2(num2){} ~son() { delete p2; // num1让父类自己释放 p2 = nullptr; cout << "son的析构函数" << endl; } private: int* p2; }; int main() { int* a1 = new int{ 1 }; int* a2 = new int{ 2 }; int* a3 = new int{ 3 }; int* a4 = new int{ 4 }; son* s1 = new son(a1, a2); father* f1 = new son(a3, a4); // 父类指向子类对象 cout << "s1调用构造函数" << endl; delete s1; cout << "\nf1调用构造函数" << endl; delete f1; return 0; } 输出: s1调用构造函数 son的析构函数 father的构造函数 f1调用构造函数 father的构造函数
加了virtual后,输出
- Classes的设计目的如果不是作为基类使用,或不是为了具备多态性,就不该声明virtual析构函数。
条款08:别让异常逃离析构函数
- 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
条款09:绝不在构造和析构过程中调用virtual函数
- 在构造和析构期间调用virtual函数,因为这类调用从不下降至派生类
条款10:令 operator = 返回一个reference to * this
- 略
条款11:在 operator = 中处理“自我赋值”
- 确保对象自我赋值时 operator = 有良好行为。包括比较“来源对象”和“目标对象”的地址、周到的语句顺序、以及copy-and-swap。(注意“ = ”号两边可能是同一个对象)
- 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为任然正确。
条款12:复制对象时勿忘其每一个成分
- 拷贝构造函数和赋值函数应该确保赋值“对象内的所有成员变量”以及“所有基类成分。(延伸:Qt中的QObject类以及继承QObject的子类都不能使用拷贝构造和赋值函数,因为信号槽机制,要求把每一个QObject对象看成是唯一的,如果通过拷贝或者赋值操作出一个一模一样的复制体,那么新的复制体名字应该叫什么,应该和老对象一样吗?那么发送或者接收信号的时候,该由哪个对象操作呢?为了省事,Qt直接在拷贝构造函数和赋值函数末尾添加了“ = delete”)
- 不要尝试以某个copy函数(拷贝构造和赋值函数)实现另一个copy函数。应该将共同技能放入第三个函数(私有的)中,并又两个拷贝函数共同调用。
三、资源管理
条款13:以RAII对象管理资源
- 为防止资源泄漏,请使用RAII对象(Resource Acquisition Is Initialization),它充分利用了C++语言局部对象自动销毁的特性来控制资源的生命周期。
- 可以通过std::shared_ptr和std::unique_ptr管理指针,它们都是RAII对象,std::auto_ptr已经弃用。但并不代表所有指针都推荐使用智能指针,只有ownership/resource acquisition这个前提才使用smart ptr,其他情况应使用reference/raw ptr。比如Qt的QObject类以及子类,若它贯穿整个程序生命周期,意味着资源回收时期在程序关闭时,那么用智能指针来管理是没有必要且繁琐的。
- 扩展:std::lock_guard和std::unique_lock是锁的RAII对象。
条款14:在资源管理类中小心coping行为
- RAII对象被复制,一般选择以下行为作为可能:
1)禁止复制
2)对底层资源使用“引用计数法”(类似std::shared_ptr)
3)复制底部资源
4)转移底部资源的所有权
条款15:在资源管理类中提供对原始资源的访问
- APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个“取得其所管理之资源”的办法。
- 对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,隐式转换对客户比较方便。
条款16:成对使用new和delete时要采取相同形式
- new表达式使用[],delete表达式也使用[]。
条款17:以独立语句将newed对象置入智能指针
- 以独立语句将newed对象置入智能指针。否则,一旦异常被抛出,有可能导致难以察觉的资源泄漏。
processWidget(std::shared_ptr
(new Widget), priority()); // 书上说该代码不应该一行完成,理由是new Widget之后 // 不知道编译器是先调用priority还是shared_ptr构造函数 // 若先调用priority后发生异常,就会产生内存泄漏
// 所以改成如下 std::shared_ptrpw(new Widget) processWidget(pw, priority()); // 但是现在不推荐用上面的方式分配内存 // 推荐使用std::make_shared方式 // 这样就不会出现调用顺序问题,因为new Widget在 // td::make_shared内部完成 // 就不需要分开写了 processWidget(std::make_shared , priority());
四、设计与声明
条款18:让接口容易被正确使用,不易被误用
- 好的接口容易被正确使用,不容易被误用。
- “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
- “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
- std::shared_ptr和std::unique_ptr支持定制删除器(custom delete)。这可以方法DLL问题,可被用来自动解除互斥锁等等。
条款19:设计class犹如设计type
- 详情看书
条款20:宁以pass-by-reference-to-const替换pass-by-value
- 尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题。
- 切割问题:通过pass-by-value方式将子类传入接收父类的函数中,拷贝出来的临时对象是以父类的拷贝构造函数构建的,无法调用子类的对应函数。引用可以解决该问题。
- 内置类型,以及STL的迭代器和函数对象,往往使用pass-by-value比较适当
条款21:必须返回对象时,别妄想返回其reference
- 函数返回对象时,绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。
条款22:将成员变量声明为private
条款23:宁以non-member、non-friend替换member函数
- 这样做可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性。以下为namespace例子:
#pragma once #include
apple.husing namespace std; namespace eat_fruits { class Apple { public: void first() { cout << "拿起苹果" << endl; } void second() { cout << "给苹果削皮" << endl; } void third() { cout << "吃苹果" << endl; } }; void eatApple(Apple& a) { a.first(); a.second(); a.third(); } } #pragma once #include
orange.husing namespace std; namespace eat_fruits { class Orange { public: void first() { cout << "拿起橘子" << endl; } void second() { cout << "拨开橘子皮" << endl; } void third() { cout << "吃橘子" << endl; } }; void eatOrange(Orange& o) { o.first(); o.second(); o.third(); } } #include"orange.h" #include"apple.h" int main() { eat_fruits::Orange orange; eat_fruits::eatOrange(orange); cout << "/************/" << endl; eat_fruits::Apple apple; eat_fruits::eatApple(apple); return 0; }
main.cpp
条款24:若所有参数皆需要类型转换,请为此采用non-menber函数
- 如果构造函数是explicit,那么就不支持隐式类型转换(implicit type conversion)。
条款25:考虑写出一个不抛出异常的swap函数
- 当std::swap(内部是拷贝构造和赋值函数)对你的类型效率不高时,提供一个该类型swap成员函数,并确定这个函数不抛出异常。
- 提供一个public swap成员函数,并在你的class或template所在的命名空间内提供一个non-menber swap,并令它调用上述swpa成员函数,对于classes(而非templates),也请特化std::swap
- 调用swap时应针对std::swap使用using声明式,然后调用swap并不带任何“命名和空间资格修饰”,让编译器自己查找适当的swap并调用。
- 在std里头添加新东西,程序可以编译和运行,但是,如果希望你的软件有可预期行为,请不要这样做。
五、实现
条款26:尽可能延后变量定义式的出现时间
- 这样做可以增加程序的清晰度并改善程序效率。
- 清晰度:过早定义,可能忘记使用变量或多申请变量。
- 改善效率:过早定义,至少会多调用一次默认构造函数,若该变量最终没有使用,还多调用一次析构函数。
条款27:尽量少做转型动作
- 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。
- 如果转型是必要的,试着将它隐藏于某个函数背后。
- 宁可使用C++风格转型,不要使用旧式转型。前者不仅容易辨识,也有各自的职能。
条款28:避免返回handles指向对象内部成分
- 避免返回handles(包括reference、指针、迭代器)指向对象内部,遵守这个条款可增加封装性。
- 使const成员函数的行为像个const(函数返回值添加const修饰符)
- 将产生悬空指针(dangling handles)的可能性降至最低。
条款29:为“异常安全”而努力是值得的
- 异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能保证:基本型、强烈型、不抛异常型。
条款30:透彻了解inlining的里里外外
- 将大多数inline限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
- 不要只因为function templates 出现在头文件,就将它们声明为inline。
- 扩展:其实现代编译器非常聪明,它会自己分析是否需要将函数变为内联的,就算你在函数前面加了inline,也不会成为它判断是否内联的依据。结论,inline已经没用了,如果你还继续使用,说明你学的是上个世纪的教材。。。。
条款31:将文件间的编译依存关系降至最低
- 一种方法是在头文件中前置声明该类,并只在指针、引用或者函数返回值中使用。然后在cpp文件中用#include添加
// h.文件
class Test; // 前置声明 class Use { private: test t1; // 报错,没有导入Test头文件,编译器不知道其大小 test* t2; // 正确,因为指针大小是固定的,32bit4字节,64bit8字节 // 就算修改Test文件,编译器也认为test* t2没有变化 // 所以对于编译器来说不需要重新编译 test& t3也一样 test& t3; public: Test useTest(); // 没问题,这里只是声明返回值是Test,并没有调用 void showTest(Test t); // 没问题,理由同上 // 上面返回值使用引用,以及形参使用指针或引用都是可以的 }; - 另一种方法是使用interface class,即类中都是纯虚函数,这样的类使用的时候只能以指针方式出现,同样可以减少编译依赖。
- 扩展,继承类被前置声明无效。
class Father; // 前置声明 class Son:public Father{}; // 报错
六、继承与面向对象设计
条款32:确定你的public继承塑模出is-a关系
- “public 继承” 意味is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上(反过来不一定成立),因为每一个derived class 对象也都是一个base class对象。
- 扩展,子类调用父类虚函数方法:son->father::func();
条款33:避免遮掩继承而来的名称
- derived classes 内的名称会遮掩base classes 内的名称。为了让子类调用父类同名函数,可以可以使用。
条款34:区分接口继承和实现继承
- pure virtual 函数只具体指定接口继承(但是书上例子可以指定默认实现继承→_→)。
class base { virtual void show() = 0 { cout<<"this is base."<<endl; } }; class derived { virtual void show() { base::show(); // 父类默认实现 } };
- impure virtual 函数具体指定接口继承及默认实现继承。
- non-virtual 函数具体指定接口继承以及强制性实现继承(non-virtual 代表子类不该 overwrite 这个函数)。
条款35:考虑virtual函数以外的其他选择
本条款地根本忠告是:当你为解决问题而寻找某个设计方案时,不妨考虑 virtual 函数的替代方案。
- 使用 non-virtual interface(NVI)手法,那是 Template Method 设计模式(与c++templates并无关联)的一种特殊形式。它以 public non-virtual 成员函数包裹较低访问性(private 或 protect)的virtual。
1 class gamecharacter 2 { 3 public: 4 gamecharacter(int physique = 10, int power = 10, int agility = 10) : 5 m_physique(physique), 6 m_power(power), 7 m_agility(agility) 8 {} 9 int healthvalue() const { return dohealvalue(); } // 生命值 10 int getphysique() const { return m_physique; } 11 int getpower() const { return m_power; } 12 int getagility() const { return m_agility; } 13 private: 14 virtual int dohealvalue() const { return m_physique * 10; } // 生命值计算方法 15 private: 16 int m_physique; // 体格 17 int m_power; // 力量 18 int m_agility; // 敏捷 19 }; 20 21 class Soldier : public gamecharacter 22 { 23 public: 24 Soldier() :gamecharacter() {} 25 private: 26 virtual int dohealvalue() const { return (getphysique() * 10 + getpower() * 20); } 27 }; 28 29 int main() 30 { 31 Soldier soldier{}; 32 cout << soldier.healthvalue() << endl; // 输出150 33 return 0; 34 }
- 将 virtual 函数替换为“函数指针成员变量”,这是Strategy设计模式的一种分解表现形式。
1 class GameCharacter; 2 int defaultHealthCalc(const GameCharacter& gc); 3 class GameCharacter 4 { 5 public: 6 // 声明一个HealthCalcFunc指针类型, 7 // 用于接受生命值计算方法函数指针 8 typedef int (*HealthCalcFunc)(const GameCharacter&); 9 explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc, 10 int physique = 10, int power = 10, int agility = 10) : 11 healthFunc(hcf), 12 m_physique(physique), 13 m_power(power), 14 m_agility(agility){} 15 16 int healthValue() const { return healthFunc(*this); } // 生命值 17 int getPhysique() const { return m_physique; } 18 int getPower() const { return m_power; } 19 int getAgility() const { return m_agility; } 20 private: 21 int m_physique; // 体格 22 int m_power; // 力量 23 int m_agility; // 敏捷 24 HealthCalcFunc healthFunc; // 生命值计算方法函数指针 25 }; 26 27 int defaultHealthCalc(const GameCharacter& gc) 28 { return (gc.getPhysique() * 10); } // 默认生命计算方法 29 30 // 坦克生命计算方法 31 int tankHealthCalc(const GameCharacter& gc) 32 { return (gc.getPhysique()*10 + gc.getPower()*20); }; 33 34 // 刺客生命计算方法 35 int assassinHealthCalc(const GameCharacter& gc) 36 { return (gc.getPhysique() * 10 + gc.getAgility() * 5); }; 37 38 class Soldier : public GameCharacter 39 { 40 public: 41 Soldier(HealthCalcFunc hcf = defaultHealthCalc) :GameCharacter(hcf) {} 42 }; 43 44 int main() 45 { 46 Soldier soldier1(tankHealthCalc); 47 cout<
// 输出300 48 49 Soldier soldier2(assassinHealthCalc); 50 cout< // 输出150 51 return 0; 52 } - 以 std::function 成员变量替换 virtual 函数,因而允许使用任何可调用物(callable entity)搭配一个兼容于需求的签名式。这也是 Strategy 设计模式的某种形式。
1 class GameCharacter; 2 int defaultHealthCalc(const GameCharacter& gc); 3 class GameCharacter 4 { 5 public: 6 // 使用函数封装器std::function,用于存储生命值计算方法函数 7 typedef std::function <int(const GameCharacter&)> HealthCalcFunc; 8 explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc, 9 int physique = 10, int power = 10, int agility = 10) : 10 healthFunc(hcf), 11 m_physique(physique), 12 m_power(power), 13 m_agility(agility) {} 14 15 16 int healthValue() const { return healthFunc(*this); } // 生命值 17 int getPhysique() const { return m_physique; } 18 int getPower() const { return m_power; } 19 int getAgility() const { return m_agility; } 20 private: 21 int m_physique; // 体格 22 int m_power; // 力量 23 int m_agility; // 敏捷 24 HealthCalcFunc healthFunc; // 生命值计算方法函数指针 25 }; 26 27 int defaultHealthCalc(const GameCharacter& gc) 28 { return (gc.getPhysique() * 10); } // 默认生命计算方法 29 30 int testHealthCalc(const GameCharacter& gc) 31 { return gc.getPhysique(); } // 测试生命计算方法 32 33 // 老弱血量结构体 34 struct OldHealth 35 { 36 int operator() (const GameCharacter& gc) const 37 { return (gc.getPhysique()*5); } 38 }; 39 40 // 精英血量类 41 class EliteHealth 42 { 43 public: 44 int eliteHealthCalc(const GameCharacter& gc) const 45 { return (gc.getPhysique() * 10 + 46 gc.getPower() * 20 + 47 gc.getAgility() * 5); } // 精英生命值计算方法 48 }; 49 50 class Soldier : public GameCharacter 51 { 52 public: 53 Soldier(HealthCalcFunc hcf = defaultHealthCalc) :GameCharacter(hcf) {} 54 }; 55 56 int main() 57 { 58 Soldier soldier0{testHealthCalc}; // 传函数 59 cout<
endl; 60 61 OldHealth oh; 62 // 传结构体成员函数 63 Soldier soldier1{ std::bind(&OldHealth::operator(), 64 oh, 65 std::placeholders::_1) }; 66 cout< endl; 67 68 EliteHealth eh; 69 // 传类成员函数 70 Soldier soldier2{ std::bind(&EliteHealth::eliteHealthCalc, 71 eh, 72 std::placeholders::_1) }; 73 cout< endl; 74 return 0; 75 } 76 77 // 输出: 78 // 10 79 // 50 80 // 350 - 将继承体系内的 virtual 函数替换为另一个继承体系内的 virtual 函数。这是 Strategy 设计模式的传统实现手法(代码略)。
条款36:绝不重新定义继承而来的non-virtual函数
条款37:绝不重新定义继承而来的默认参数值(默认形参)
- 因为默认参数值都是静态绑定,而 virtual 函数--才是你唯一应该重写的东西,因为它是动态绑定。
class base { virtual void show(int i = 10){ cout<<"base = "<< i << endl;} }; class derived: public base { virtual void show(int i = 20) override { cout<<"derived = "<< i << endl; } }; int main() { // base* b 是静态绑定,其默认形参是静态绑定 // new derived 是动态绑定, 其默认形参是静态绑定,虚函数是动态绑定 base* b = new derived; b->show(); // 输出"derived = 10" return 0; }
- 但如果你试着遵守这条规则,并且同时提供默认参数值给 base 和 derived ,将会导致代码重复和相依性(with dependencies),如果 base 虚函数默认参数值变了,那么 derived 默认参数值也必须要改变,这样及其不方便。解决办法之一是NVI(non-virtual interface)手法,参考条款35。
- 扩展:关键字 override 很重要,一定要在 derived 虚函数后面添加。作用两个,第一,表示该虚函数是从 base 继承,非 derived 自己定义;第二,强制编译器检查虚函数是否 override ,例如形参改变,返回值改变,const是否添加等等,这些会导致 override 行为都会报错。
- overload(重载):重载的概念最好理解,在同一个类声明范围中,定义了多个名称完全相同、参数(类型或者个数)不相同的函数,就称之为overload(重载)。重载的特征如下:
(1)相同的范围(在同一个类中); (2)函数名字相同; (3)参数不同; (4)overload 并不是关键字。它只是C++编译器的一种行为方式。
- override(重写):覆盖的概念其实是用来实现C++多态性的,即子类重新改写父类声明为virtual的函数。Override(覆盖)的特征如下(PS:我一直以为IDE里面翻译说的“重写”是overwrite,其实是不对的):
(1)不同的范围(分别位于派生类与基类); (2)函数名字相同; (3)参数列表完全相同; (4)基类函数必须有 virride 关键字。
- overwrite:重写是指派生类的函数屏蔽(或者称之为“隐藏”)了与其同名的基类函数。正是这个C++的隐藏规则使得问题的复杂性陡然增加,这里面分为两种情况讨论:
(1)如果派生类的函数与基类的函数同名,但是参数不同。那么此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。 (2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。那么此时,基类的函数被隐藏(注意别与覆盖混淆)。
(3)overwrite 并不是关键字。它只是C++编译器的一种行为方式。
条款38:通过复合塑模出has-a或“根据某物实现出”
- 根据迪米特法则,一个对象应该对其他对象保持最少的了解。类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大,也就越容易出错,所以尽量使用组合、聚合来代替继承。
条款39:明智而审慎地使用private继承
- private 继承意味着is-implemented-in-terms of(根据某物实现出)。它通常比复合的级别低。但是当 derived class 需要访问 protect base class 的成员,或者需要重新定义继承而来的 virtual 函数时,这么设计是合理的。
- 和复合不同,private 继承可以造成 empty base 最优化。这对致力于 “对象尺寸最小化” 的程序库开发者而言,可能很重要。
条款40:明智而审慎地使用多重继承
- 多重继承比单一继承复杂,它可能导致新的歧义性。
- virtual 继承会增加大小、速度,初始化(及赋值)复杂度等等成本。如果 virtual base classes 不带任何数据,将是最具有实用价值的情况。
- 多重继承的确有正当用途。其中一个情节涉及“public 继承某个 Interface class” 和 “ private 继承某个协助实现的class ” 的两相组合。