• Effective C++ 笔记


    一、让自己习惯C++

    条款01、视C++为一个语言联邦

    条款02、尽量以const,enum,inline,替换 # define

    用编译器替换预处理器

    什么是预处理

    在C++中,预处理是指在编译过程之前的一个阶段,通过预处理器(preprocessor)对源代码进行处理的过程。预处理器会根据预处理指令(以井号#开头)对源代码进行文本替换、宏展开等操作,生成一个经过预处理的源文件。

    以下是常用的预处理指令及其作用:

    1. #define:用于定义宏常量或宏函数。#define指令会将所有出现在源代码中的宏名称都替换为指定的文本。

    2. #include:用于包含头文件内容。#include指令会将指定的头文件内容插入到该指令所在位置。

    3. 等等

    a、普通常量

    宏 预处理指令定义常量,如:

    # define ASPECCT_RATIO 1.653

    尽量用以下来代替:

    const 常量

    const double AspectRaio = 1.653;

    另外 定义一个常量的 char*-based字符创,必须写const两次

    const char* const authorName = "Acott Meyers"

    其实string对象通常比char*-based合宜

    const std::string authorName("Scott Meyers")

    b、class专属常量

    解释:

    1.常量的作用域限制在class内,必须让他成为 class 的一个成员

    2.确保此常量至多只有一份实体,必须让它成为一个 static 成员:

    1. class Game{
    2. private:
    3. static const int NumTurns= 5;
    4. int scores[NumTurns];
    5. };

    常量变量默认是不允许在类中直接初始化的。但是静态成员变量可以在类内进行初始化,所以是 static const int

    也可以使用enum来实现

    1. class Game{
    2. private:
    3. enum {NumTurns = 5};
    4. int scores[NumTurns];
    5. };

    c、看起来像函数的宏

    1. //以 a 和 b 的较大值调用f
    2. #define CALL_WITH_MAX(a, b) f((a) > (b)) ? (a) : (b)

    使用template inline 函数在编译阶段处理函数调用点的,减少函数调用的开销,也能获得宏的的效率

    1. template<typename T>
    2. inline T callWithMax(const T& a, const T& b){
    3. return (a,b) ? a: b;
    4. }

    条款03、尽可能使用const

    const与指针

    如果关键字 const 出现在星号左边,表示被指物是常量,作函数参数时,不能在函数中改变

    1. #include
    2. void add(const int* a, const int* b){
    3. *a+=1; // 错误!
    4. int c = *a + *b;
    5. std::cout << c << '\n';
    6. }
    7. int main(){
    8. int num =1;
    9. const int* a = #
    10. const int* b = #
    11. add(a, b);
    12. }

    如果关键字 const 出现在星号右边,表示指针自身是常量

    1. #include
    2. int main(){
    3. // 被指物为常量
    4. char* const p = "hello";
    5. // p = "jason"; // 此时指针p不可以被修改,
    6. std::cout << *p << std::endl;
    7. }

    如果关键字 const 出现在星号两边,表示被指物和指针两者都是常量

    1. #include
    2. int main(){
    3. char words[] = "hello";
    4. const char* const p = words;
    5. std::cout << *p << std::endl;
    6. }

    const与迭代器

    1. #include
    2. #include
    3. int main(){
    4. std::vector<int> vec{1,2,3,4,5};
    5. std::vector<int>::const_iterator iter =
    6. vec.begin(); // const 出现在迭代器右边
    7. *iter += 10; // 不能通过迭代器修改被指内容
    8. // ++iter;
    9. }

    const与函数

    const成员函数

    1. #include
    2. class TextBlock{
    3. private:
    4. std::string text;
    5. public:
    6. TextBlock(const std::string & str):text(str){}
    7. const char& operator[] (std::size_t position) const{
    8. return text[position];
    9. }
    10. char& operator [] (std::size_t position){
    11. return text[position];
    12. }
    13. };
    14. int main(){
    15. TextBlock tb("Hello");
    16. std::cout << tb[0]; // 调用 non-const TextBlock::operator[]
    17. const TextBlock ctb("world"); // 调用 const TextBlock::operator[]
    18. std::cout << ctb[0];
    19. }

    这里例子是太过造作。

    真实程序中 const 对象大多用于 passes by pointer-to-const 或 passes by reference-to-const

    应用在函数参数中

    1. void print(const TextBlock& ctb){
    2. std::cout << ctb[0];
    3. }

    应用在成员函数

    1. include
    2. class Myclass{
    3. private:
    4. bool lengthIsValid;
    5. mutable int num;
    6. public:
    7. void test() const{
    8. lengthIsValid = 0; // 错误!在const成员函数内不能赋值给lengthIsValid
    9. num=10; // 正确,mutable关键字修饰的变量可以被修改
    10. }
    11. };
    12. int main(){
    13. Myclass myclass;
    14. }

    条款04、确定对象被使用前已先被初始化

    为避免在对象初始化之前过早地使用它们,需要做三件事:

    1. 为内置型对象进行手工初始化,因为C++不保证初始化
    2. 构造函数最好使用成员初值列,而不要在构造函数体内使用赋值操作。初值列列出的成员变量,其排列次序应该和他们在class中的声明次序相同。另外,C++17支持在类中直接对私有变量进行初始化而不通过构造函数
    3. 为免除“跨编译单元之初始化次序问题”,用 local static 对象替换 non-local static 对象

    前两点很好理解,这里对第三点给出例子解释:

    1. class FileSystem{
    2. public:
    3. std::size_t numDisks(){}
    4. };
    5. FileSystem& tfs(){
    6. static FileSystem fs;
    7. return fs;
    8. }
    9. class Directory{};
    10. Directory::Directory(){
    11. std::size_t disks = tfs.numDisks();
    12. }
    13. Directory& tempDir(){
    14. static Directory td;
    15. return td;
    16. }

    二、构造/析构/赋值运算

    条款05、了解C++默默编写并调用哪些函数

    如果你声明的一个空类,如:

    class Empty{};

    那么编译器会为它声明一个copy构造函数、一个copy assignment操作符和一个析构函数。如果你没有声明任何构造函数,编译器会你声明一个default构造函数。所有这些函数都是public且inline的。所以上面你声明的空类会被编译器处理为:

    1. class Empty{
    2. public:
    3. Empty(){} // default构造函数
    4. Empty(const Empty& rhs){} // copy构造函数
    5. ~Empty(){} // 析构函数
    6. Empty& operator=(const Empty& rhs){} // copy assignment操作符
    7. };

    调用copy构造函数的例子

    1. #include
    2. template<typename T>
    3. class NameObject{
    4. public:
    5. NameObject(const char* name, const T& value);
    6. NameObject(const std::string& name, const T& value);
    7. private:
    8. std::string nameValue;
    9. T objecctValue;
    10. };
    11. int main(){
    12. NameObject<int> no1("Smallest Primer Number", 2);
    13. NameObject<int> no2(no1); // 调用copy构造函数
    14. }

    一个错误的例子:

    C++不允许“让 reference 改值向不同对象”

    更改const成员是不合法的

    基于以上两点,如下例子不正确:

    1. #include
    2. template<typename T>
    3. class NameObject{
    4. public:
    5. NameObject(const std::string& name, const T& value);
    6. private:
    7. std::string& nameValue; // C++不允许“让 reference 改值向不同对象”
    8. const T objecctValue; //更改const成员是不合法的
    9. };
    10. int main(){
    11. std::string newDog("Persephone");
    12. std::string oldDog("Satch");
    13. NameObject<int> p(newDog, 2);
    14. NameObject<int> s(oldDog, 36);
    15. p = s;// 错误;C++不允许 让reference
    16. }

    条款06、若不想使用编译器自动生成的函数,就该明确拒绝

    如果不希望 copy 构造函数 以及 copy assignment 操作符起作用,则应该声明为 private 并没有定义

    1. #include
    2. class HomeForSale{
    3. public:
    4. private:
    5. HomeForSale(const HomeForSale&);
    6. HomeForSale& operator =(const HomeForSale&);
    7. };
    8. int main(){
    9. HomeForSale home;
    10. home h1;
    11. home h2(h1);
    12. }

    如此一来,就可以阻止        编译器暗自创建他们。而拷贝HomeForSale对象的时候,编译器会阻止:

    将连接期报错移动到编译期

    1. class Uncopyable{
    2. protected:
    3. Uncopyable(){} // 允许对象构造和析构
    4. ~Uncopyable(){}
    5. private:
    6. Uncopyable(const Uncopyable&); // 但阻止copying
    7. Uncopyable& operator =(const Uncopyable&);
    8. };
    9. // class HomeForSale 不再声明 copy 构造函数 或 copy assign. 操作符
    10. class HomeForSale : private Uncopyable{};
    11. int main(){
    12. HomeForSale home1;
    13. HomeForSale home2(home1);
    14. }

    总结:

    为驳回编译器自动(暗自)提供的技能,可将相应的成员函数声明为private并且不予实现。使用   像 Uncopyable 这样的 base class 也是一种做法。

    条款07、为多态基类声明 virtual 析构函数

    C++11不存在 “ non-virtual 析构函数问题”,此条款略过

    条款08、别让异常逃出析构函数

    不要让异常逃离析构函数是指在析构函数中抛出异常时,不要让该异常传播到析构函数的调用点以外。这是因为对象的析构过程通常是在对象生命周期的最后阶段发生的,此时其他对象和资源已经被清理或释放。如果析构函数抛出异常并且该异常逃离了析构函数,将会导致以下问题:

    1. 对象的析构不完全:如果析构函数抛出异常,对象的析构可能无法完成,导致对象状态没有得到完全清理或释放。这可能会导致资源泄漏或其他未定义行为。

    2. 内存泄漏:如果在抛出异常之前执行了动态内存分配,并且在抛出异常后没有适当地释放该内存块,将会导致内存泄漏。

    3. 无法回滚操作:如果在对象构造期间进行了一些操作,并且在析构函数中发生异常,无法回滚这些操作。这可能会导致数据不一致或逻辑错误。

    为了避免异常从析构函数中逃离,可以采取以下措施:

    1. 在析构函数中捕获异常并处理:可以在析构函数中使用 try-catch 块来捕获异常,并在适当的情况下进行处理。例如,记录异常信息或执行必要的清理操作。

            析构函数抛出异常就结束程序。通常通过调用 abort 完成:

    1. #include
    2. class DBConnection{
    3. public:
    4. static DBConnection create(){
    5. static DBConnection db;
    6. return db;
    7. }
    8. void close(){
    9. int num=0;
    10. throw num; // 假装抛出异常
    11. }
    12. };
    13. //该类用于管理 DBConnection 资源,在其析构函数调用 DBConnection 的close,用以关闭数据库
    14. class DBConn
    15. {
    16. private:
    17. DBConnection db;
    18. public:
    19. ~DBConn() {
    20. try{db.close();}
    21. // 省略代码:制作运转记录,记下对close的调用失败
    22. catch (int)
    23. {
    24. // 这里最好打印信息,因为QT IDE只会给你报崩溃的信息
    25. std::abort();
    26. }
    27. }
    28. DBConn(DBConnection d):db(d){}
    29. };
    30. int main()
    31. {
    32. // 现在就可以这样写代码。
    33. // dbc 对象销毁时候,会调用其析构函数,....自动关闭 DBConnection 的数据库
    34. DBConn dbc(DBConnection::create());
    35. }

    本例的假设情况:

    如果程序遭遇一个“于析构函数期间发生的错误”后无法继续执行,“强迫结束程序”是个合理选项。毕竟它可以阻止异常从析构函数传播出去(那会导致不明确的行为)。也就是说调用 abort 可以抢险置“不明确行为”于死地。

    吞下因调用而发生的异常:

    修改上例代码中的DBConn类的析构函数如下:

    1. ~DBConn() {
    2. try{db.close();}
    3. // 省略代码:制作运转记录,记下对close的调用失败
    4. catch (int){}
    5. }

    这里析构函数将异常吞掉了,一般而言,这是和坏主意,因为压制了“某些动作失败”的重要信息!然而有时候吞下异常也比负担“草率结束程序”或“不明确行为带来的风险”好。为了让这成为一个可行方案,程序必须能够继续可靠地执行,及时在遭遇并忽略一个错误之后。

    上面两种方法没有无法对“导致 close 抛出异常”的情况做出反应,另外一个思路是重新设计 DBonn接口,使其客户有机会对可能和出现的问题作出反应。例如 DBConn 自己可以提供一个close函数,给客户一个机会得以处理“因该操作而发生的异常”:

    1. #include
    2. class DBConnection{
    3. public:
    4. static DBConnection create(){
    5. static DBConnection db;
    6. return db;
    7. }
    8. void close(){
    9. int num=0;
    10. throw num; // 假装抛出异常
    11. }
    12. };
    13. //该类用于管理 DBConnection 资源,在其析构函数调用 DBConnection 的close,用以关闭数据库
    14. class DBConn
    15. {
    16. private:
    17. DBConnection db;
    18. bool closed=false;
    19. public:
    20. void close(){ // 供客户使用的新函数
    21. db.close();
    22. closed = true;
    23. }
    24. ~DBConn() {
    25. if(!closed){ // 关闭连接(如果客户不那么做的话)
    26. try{db.close();}
    27. catch(int) {
    28. // 如果关闭动作失败,
    29. // 记录下来并结束程序或吞掉异常
    30. };
    31. }
    32. }
    33. DBConn(DBConnection d):db(d){}
    34. };
    35. int main()
    36. {
    37. // 现在就可以这样写代码。
    38. // dbc 对象销毁时候,会调用其析构函数,....自动关闭 DBConnection 的数据库
    39. DBConn dbc(DBConnection::create());
    40. }

    总结:

    • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
    • 如果客户需要对某个操作函数运行期间抛出的异常作出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行操作

    条款09、绝不在构造和析构函数中调用 virtual 函数

    如标题

    条款10、令 operator= 返回一个 reference to *this

    1. #include
    2. class Widget{
    3. public:
    4. Widget& operator =(const Widget& rhs){
    5. // 将右侧对象的值赋给左侧对象的成员变量
    6. if (this != &rhs){
    7. value = rhs.value;
    8. }
    9. // 返回左侧对象
    10. return* this;
    11. }
    12. Widget(int num):value(num){}
    13. Widget(){};
    14. void getValue(){
    15. std::cout << "value: " << value << std::endl;
    16. }
    17. private:
    18. int value;
    19. };
    20. int main(int argc, char *argv[])
    21. {
    22. Widget m1(10);
    23. Widget m2;
    24. Widget m3;
    25. // 连锁赋值
    26. m3=m2=m1;
    27. m3.getValue();
    28. m2.getValue();
    29. m1.getValue();
    30. return 0;
    31. }

    本例子以operator=操作符为例, +=, -=, *=也适用

    条款11、在 operator= 中处理 “自我赋值”

    条款12、复制对象时勿忘其每一个成分

    三、资源管理

    条款13、以对象管理资源

    中心:资源取得时机便是初始化时机

    一般情况下:

    1. #include
    2. class Investment{};
    3. Investment * creatInvestment(){
    4. Investment* i = new Investment;
    5. return i;
    6. }
    7. void f()
    8. {
    9. Investment* pInv = creatInvestment();
    10. // 中间代码省略
    11. delete pInv; // 必须释放pInv所指向对象,否则导致内存泄露
    12. }
    13. int main(){
    14. f();
    15. }

    但是有时候由于中间代码出现问题,delete pInv 语句会无法得到执行,这样便导致内存泄露!

    下面这个例子示范“以对象管理资源”的两个关键想法:

    获得资源后立即放进管理对象内

             creatInvestment() 返回的资源被当作其管理 unique_ptr的初值

    管理对象运行析构函数确保资源被释放

    1. #include
    2. #include
    3. class Investment{};
    4. Investment * creatInvestment(){
    5. Investment* i = new Investment;
    6. return i;
    7. }
    8. void f()
    9. {
    10. // 获得资源后立即放进管理对象内
    11. std::unique_ptr pInv(creatInvestment());
    12. // 中间代码省略
    13. // delete pInv; // 管理对象运行析构函数确保资源被释放,不需要手动释放内存
    14. }
    15. int main(){
    16. f();
    17. }

    不过,unique_ptr 是 C++11 引入的智能指针模板,用于管理动态分配的内存资源。它提供了一个独占所有权的智能指针,确保只有一个指针可以访问动态分配的内存块,从而避免内存泄漏和悬挂指针(dangling pointer)的风险。这样做就不行:

    1. #include
    2. #include
    3. class Investment{};
    4. Investment * creatInvestment(){
    5. Investment* i = new Investment;
    6. return i;
    7. }
    8. void f()
    9. {
    10. // 获得资源后立即放进管理对象内
    11. Investment* ptr = creatInvestment();
    12. //不能把同一块内存由两个unique_ptr管理者同时管理
    13. std::unique_ptr pInv1(ptr);
    14. std::unique_ptr pInv2(pInv1); // 不允许
    15. // 中间代码省略
    16. // delete pInv; // 管理对象运行析构函数确保资源被释放,不需要手动释放内存
    17. }
    18. int main(){
    19. f();
    20. }

    而STL容器要求其元素发挥 “正常的” 复制行为,因此容不得unique_ptr。

    但是shared_ptr就不一样了:

    1. #include
    2. #include
    3. class Investment{};
    4. Investment * creatInvestment(){
    5. Investment* i = new Investment;
    6. return i;
    7. }
    8. void f()
    9. {
    10. // 获得资源后立即放进管理对象内
    11. Investment* ptr = creatInvestment();
    12. //同一块内存可以由两个shared_ptr管理者同时管理
    13. std::shared_ptr pInv1(ptr);
    14. std::shared_ptr pInv2(pInv1);
    15. pInv2 = pInv1; // 同上,无任何变化
    16. // 中间代码省略
    17. // delete pInv; // 管理对象运行析构函数确保资源被释放,不需要手动释放内存
    18. }
    19. int main(){
    20. f();
    21. }

    条款14、在资源管理类中小心 copying 行为

    对于 heap-based 资源,可以用unique_ptr以及shared_ptr进行管理。但是别的资源就需要建立自己的资源管理类

    下述例子实现了对 mutex* 的管理,并禁止复制相当于unique_str:

    1. #include
    2. #include
    3. using namespace std;
    4. void lock(mutex* pm){};
    5. void unlock(mutex* pm){};
    6. class Lock{
    7. private:
    8. mutex* mutexPtr;
    9. public:
    10. explicit Lock(mutex* pm):mutexPtr(pm)
    11. {
    12. lock(mutexPtr); // 构造函数获取资源
    13. }
    14. ~Lock()
    15. {
    16. unlock(mutexPtr); // 析构函数释放资源
    17. }
    18. private:
    19. // 不希望 copy 构造函数 以及 copy assignment 操作符起作用
    20. Lock(const Lock&);
    21. Lock& operator =(const Lock&);
    22. };
    23. int main(){
    24. mutex m;
    25. Lock m1(&m);
    26. Lock m2(m1); // 禁止复制,条款6
    27. }

    使用shared_ptr

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. void lock(mutex* pm){};
    6. void unlock(mutex* pm){};
    7. class Lock{
    8. private:
    9. std::shared_ptr mutexPtr;
    10. public:
    11. // 以某个 mutex 初始化 shared_ptr,
    12. // 第二参数是unlock
    13. // 当shared_ptr 被销毁时会调用unlock
    14. explicit Lock(mutex* pm):mutexPtr(pm, unlock)
    15. {
    16. lock(mutexPtr.get()); // 构造函数获取资源
    17. }
    18. // 不再声明析构函数
    19. // 条款5-》class析构函数会自动调用其non-static成员变量的析构函数
    20. };
    21. int main(){
    22. mutex m;
    23. Lock m1(&m);
    24. Lock m2(m1);
    25. }

    条款15、在资源管理类中提供对原始资源的访问

    来看一个例子,就理解标题的意思:

    1. #include
    2. #include
    3. using namespace std;
    4. class Investment{};
    5. Investment * creatInvestment(){
    6. Investment* i = new Investment;
    7. return i;
    8. }
    9. int daysHeld(const Investment* pi){
    10. return 10;
    11. }
    12. int main(int argc, char *argv[])
    13. {
    14. std::shared_ptr pInv(creatInvestment());
    15. // 错误,函数需要的是Investment*指针!
    16. // 传过来的却是类型为 std::shared_ptr对象
    17. int days = daysHeld(pInv);
    18. return 0;
    19. }

    那么如何获得原始指针:

    方法一:使用shared_ptr的get成员函数,用来执行显式转换,也就是他会返回智能指针内部的原始指针(的复件):

    1. int main(int argc, char *argv[])
    2. {
    3. std::shared_ptr pInv(creatInvestment());
    4. //使用shared_ptr的get成员函数,获取智能指针内部的原始指针
    5. int days = daysHeld(pInv.get());
    6. return 0;
    7. }

    方法二:就像(几乎)所有智能指针一样,shared_ptr也重载了指针取值(pointer deference)操作符(operator->和operator*),他们允许隐式转换至底部原始指针:

    1. #include
    2. #include
    3. using namespace std;
    4. class Investment{
    5. //
    6. public:
    7. bool isTaxFree() const{
    8. return true;
    9. }
    10. };
    11. Investment * creatInvestment(){
    12. Investment* i = new Investment;
    13. return i;
    14. }
    15. int main(int argc, char *argv[])
    16. {
    17. // shared_ptr管理一笔资源
    18. std::shared_ptr pi1(creatInvestment());
    19. // 经由 operator->f访问资源
    20. bool taxable1 = !(pi1->isTaxFree());
    21. // unique_ptr 管理一批资源
    22. std::unique_ptr pi2(creatInvestment());
    23. // 经由 operator*访问资源
    24. bool taxable2 = !((*pi2).isTaxFree());
    25. return 0;
    26. }

    条款16、成对使用 new 和 delete 时要采取相同形式

    必须一一对应:

    1. #include
    2. #include
    3. using namespace std;
    4. int main(int argc, char *argv[])
    5. {
    6. std::string* stringPtr1 = new std::string;
    7. std::string* stringPtr2 = new std::string[100];
    8. delete stringPtr1; // 删除一个对象
    9. delete [ ] stringPtr2; // 删除一个由对象组成的数组
    10. return 0;
    11. }

    如果你喜欢用typedef:

    1. #include
    2. #include
    3. using namespace std;
    4. int main(int argc, char *argv[])
    5. {
    6. typedef std::string AddressLines[4]; // 每个人地址有4行
    7. // 每行是一个string
    8. // 注意,“new AddressLines” 返回一个string*,就像
    9. // “new string[4]”一样
    10. std::string* pa1 = new AddressLines;
    11. // 那必须匹配“数组形式”的delete
    12. // delete pa1;//行为未有定义
    13. delete [] pa1;
    14. return 0;
    15. }

    条款17、以独立语句将 newed 对象置入智能指针

    从int prioryty(){}阅读下述例子,即可理解标题:

    1. #include
    2. #include
    3. class Widget{
    4. public:
    5. Widget& operator =(const Widget& rhs){
    6. // 将右侧对象的值赋给左侧对象的成员变量
    7. if (this != &rhs){
    8. value = rhs.value;
    9. }
    10. // 返回左侧对象
    11. return* this;
    12. }
    13. Widget(int num):value(num){}
    14. Widget(){};
    15. void getValue(){
    16. std::cout << "value: " << value << std::endl;
    17. }
    18. private:
    19. int value;
    20. };
    21. int priority(){
    22. return 10;
    23. }
    24. void processWidget(std::shared_ptr pw, int priority){}
    25. int main(int argc, char *argv[])
    26. {
    27. // shared_ptr构造函数需要一个原始指针(raw pointer)
    28. // 但该构造函数是个explicit构造函数,无法进行隐式转换,
    29. // 将自“new Widget”的原始指针转换为processWidget所要求的
    30. // shared_ptr,所以下面这样写不能通过编译
    31. // processWidget(new Widget, priority());
    32. // 这样写才能通过编译,干了三件事:如果是这个顺序
    33. // 执行 “new Widget”、调用priority、调用new Widget构造函数
    34. // 调用priority失败会“new Widget”返回的指针遗失,引发资源泄露
    35. processWidget(std::shared_ptr (new Widget), priority());
    36. // 所以最好是使用分离语句
    37. std::shared_ptr pw(new Widget); // 在单独语句内以智能指针
    38. //存储new Widget所得对象
    39. processWidget(pw,priority()); // 这个调用动作绝不至于造成泄露
    40. return 0;
    41. }

    分离语句即可说明:

            以独立语句将 newed 对象存储于(置于)指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露

    四、设计与声明

    条款18、让接口容易被正确使用,不易被误用

    很可能传一个错误的参数:

    1. #include
    2. class Date{
    3. public:
    4. Date(int month, int day, int year){};
    5. };
    6. int main(int argc, char *argv[])
    7. {
    8. Date d(30, 3, 1995); // 月份不可能大于12!!
    9. return 0;
    10. }

    • std::shared_ptr 在构造函数中提供了一个可选的删除器(deleter)参数,用于自定义资源的释放方式。删除器是一个函数或者可调用对象,用于在shared_ptr的引用计数器变为0时释放资源

      删除器可以是普通函数、函数指针、lambda 表达式等,只要符合指定的删除器的函数签名即可。删除器接受一个指向资源的指针(被shared_ptr管理的指针)作为参数,不返回任何值。

      下面是一个示例,展示了如何给 shared_ptr 提供自定义的删除器:

      1. #include
      2. #include
      3. struct MyStruct {
      4. void operator()(int* p) {
      5. std::cout << "Deleting pointer using custom deleter...\n";
      6. delete p;
      7. }
      8. };
      9. int main() {
      10. std::shared_ptr<int> ptr(new int(42), MyStruct());
      11. // 使用默认删除器释放资源
      12. std::shared_ptr<int> ptr2(new int(100));
      13. return 0;
      14. }

      在上面的示例中,MyStruct 是一个删除器类,它重载了operator(),在函数体中使用 delete 运算符释放资源。在创建 std::shared_ptr 时,通过提供 MyStruct 的实例作为构造函数的第二个参数,我们指定了自定义删除器。

      需要注意的是,使用删除器时,它必须与被管理指针的类型兼容,否则会导致未定义的行为。此外,当存在多个 shared_ptr 共享同一个对象时,它们必须使用相同类型的删除器。

      自定义删除器为我们提供了更大的灵活性,能够管理不同类型的资源释放方式,例如,在资源释放时执行一些额外的操作。

    条款19、设计 class 犹如设计 type

    条款20、宁以 pass-by-reference-to-const 替换 pass-by-value

    首先来看下 pass-by-reference-to-const 的传递成本:

    1. #include
    2. class Person{
    3. public:
    4. Person(){}
    5. virtual ~Person(){}
    6. private:
    7. std::string name;
    8. std::string address;
    9. };
    10. class Student: public Person{
    11. private:
    12. std::string schoolName;
    13. std::string schoolAddress;
    14. public:
    15. Student(){}
    16. ~Student(){}
    17. };
    18. bool validateStudent(Student s){
    19. // 省略对Student对象的检查
    20. return 1;
    21. }
    22. int main(int argc, char *argv[])
    23. {
    24. Student plato;
    25. bool platoIsOK = validateStudent(plato);
    26. return 0;
    27. }

    这里例子中,无疑地Student的 copy 函数会被调用,以 plato 为蓝本将 s 初始化。同样明显地,当 validayeStudent 返回后, s 会被销毁。因此,对此函数而言,参数的传递成本是 “一次 Student copy 构造函数调用,加上一次 Student 析构函数调用”。

    而使用 pass by reference-to-const 可以回避上述那些构造和析构动作:

    bool validateStudent(const Student& s)

    const的作用:validateStudent 不能修改传入的Student

    除了效率之外,两种传递方式还有别的区别,来看看这里例子:

    1. #include
    2. class Window{
    3. public:
    4. std::string name() const{
    5. return "window\n";
    6. }
    7. virtual void display() const{
    8. std::cout << "display from Window!\n";
    9. }
    10. };
    11. class WindowWithScrollBars: public Window{
    12. public:
    13. // 重写了基类中的display方法
    14. virtual void display() const{
    15. std::cout << "display from WindowWithScrollBars!\n";
    16. }
    17. };
    18. void printNameAndDisplay(Window w){
    19. std::cout << w.name();
    20. w.display();
    21. }
    22. int main(int argc, char *argv[])
    23. {
    24. WindowWithScrollBars wwsb;
    25. printNameAndDisplay(wwsb);
    26. return 0;
    27. }

    printNameAndDisplay函数的本意是,调用传入对象的display成员函数。也就是说,如果传入的是Window对象,就调用其display成员函数;如果传入的是WindowWithScrollBars对象,就其display成员函数。

    但是参数传递方式选择为 pass-by-value时候,即使传入的是WindowWithScrollBars对象,也是调用基类Window的display成员函数。下图是上例子的执行结果

    而将printNameAndDisplay函数参数传递方式修改为pass-by-reference-to-const时,就能实现该函数设计的本意:

    1. void printNameAndDisplay(const Window& w){
    2. std::cout << w.name();
    3. w.display();
    4. }

    但是对于内置类型,以及 STL 的迭代器和函数对象而言,比如 int,尽量选择 pass-by-value

    条款21、必须返回对象时,别妄想返回其 reference

    返回对象时,返回其 reference,可行?

    1. #include
    2. class Rational{
    3. public:
    4. Rational(int numerator = 0, int denominator = 1)
    5. :n(numerator),d(denominator){}
    6. private:
    7. int n, d;
    8. friend const Rational&
    9. operator*(const Rational& lhs, const Rational& rhs);
    10. };
    11. const Rational& operator*(const Rational& lhs,
    12. const Rational& rhs){
    13. // local 变量,是在 stack 空间创建的
    14. // 是不能作为返回值的
    15. Rational result(lhs.n * rhs.n, lhs.d *rhs.d);
    16. return result;
    17. }
    18. int main(int argc, char *argv[])
    19. {
    20. Rational a(1,2);
    21. Rational b(3,5);
    22. Rational c = a * b;
    23. return 0;
    24. }

    考虑在 heap 内构造一个对象,看能返回 referene,还是行不通:

    1. const Rational& operator*(const Rational& lhs,
    2. const Rational& rhs){
    3. // 考虑在 heap 内构造一个对象
    4. Rational* result= new Rational(lhs.n * rhs.n, lhs.d *rhs.d);
    5. return result;
    6. }

    考虑使用 static 关键字:

    1. const Rational& operator*(const Rational& lhs,
    2. const Rational& rhs){
    3. // 定义一个在函数内部的 static Rational 对象
    4. static Rational result = Rational(lhs.n * rhs.n, lhs.d *rhs.d);
    5. return result;
    6. }

    这次代码可用通过编译

    但是在书中这种写法也会在延伸点上出现问题。所以一个“必须返回新对象”的函数的正确写法是:就让那个函数返回一个新对象:

    1. const Rational operator*(const Rational& lhs,
    2. const Rational& rhs){
    3. // local 对象可以直接被返回,而不是返回其reference
    4. Rational result = Rational(lhs.n * rhs.n, lhs.d *rhs.d);
    5. return result;
    6. }

    总结:

    绝不要返回 pointer 或 reference 指向一个 local stack 对象,或返回 reference 指向一个 heap-allocated对象,或返回 pointer 或 reference 指向一个local static 对象而有多个可能同时需要多和这样的对象。条款4已经为 “在单线程环境中合理返回 reference 指向一个 local static 对象”提供一份设计实例。

    条款22、将成员变量声明为private

    一旦将一个成员变量声明为 public 或 protected 而客户开始使用它们,就很难改变那个成员变量所涉及的一切。太多代码需要重写、重新测试、重新编写文档、重新编译。从封装的角度观之,其实只有两种访问权限:priavate(提供封装)和其他(不提供封装)。

    请记住:

    • 切记将成员变量声明为 private。这可赋予客户访问数据的一致性、可细微划分控制、允诺约束条件获得保证,并提供 class 作者以充分的实现弹性
    • protected 并不比 pulic 更具封装性

    条款23、宁以 non-member、non-friend 替换 member 函数

    1. #include
    2. class WebBrowser{
    3. public:
    4. void clearCache();
    5. void clearHistory();
    6. void removeCookies();
    7. // 如果想一整个执行所有这些动作
    8. // 方式一:
    9. // 在类中提供这样一个函数,调用上面三个函数
    10. void clearEveryThing();
    11. };
    12. // 方式二:
    13. // 也可以用一个non-member函数实现
    14. void clearBrowser(WebBrowser& wb){
    15. wb.clearCache();
    16. wb.clearHistory();
    17. wb.removeCookies();
    18. }

    1、member 函数 clearEveryThing 带来的封装性比 non-member 函数 clearBrowser 低:

    • 对象内的数据,愈多函数可访问,数据的封装性就愈低

    2、降低编译依赖性

    24、若所有参数皆需类型转换,请为此采用 non-member 函数

    先来看看何为隐式转换:

    1. #include
    2. class Rational {
    3. public:
    4. // 构造函数刻意不为 explicit,
    5. // 允许 int-to-Rational 隐式转换
    6. Rational (int numerator = 0,
    7. int denominator = 1)
    8. :n(numerator),d(denominator){}
    9. // 分子(numerator)和 分母(denominator)的的访问函数
    10. int numerator() const;
    11. int denominator() const;
    12. private:
    13. int n,d;
    14. };
    15. int main(int argc, char *argv[])
    16. {
    17. // 这便是隐式转换:
    18. // 【 将会隐式地将整数5转换为Rational类型,并通过调用构造函数
    19. // Rational(int numerator = 0, int denominator = 1)来
    20. // 创建Rational对象r。在这种情况下,numerator将被初始化为5,
    21. // denominator将被初始化为1】
    22. Rational r =5;
    23. return 0;
    24. }

    再来看看operator* 的使用会涉及到什么问题:

    1. #include
    2. class Rational {
    3. public:
    4. // 构造函数刻意不为 explicit,
    5. // 允许 int-to-Rational 隐式转换
    6. Rational (int numerator = 0,
    7. int denominator = 1)
    8. :n(numerator),d(denominator){}
    9. // 分子(numerator)和 分母(denominator)的的访问函数
    10. int numerator() const;
    11. int denominator() const;
    12. const Rational operator* (const Rational& rhs) const{}
    13. private:
    14. int n,d;
    15. };
    16. int main(int argc, char *argv[])
    17. {
    18. Rational oneEighth(1,8);
    19. Rational oneHalf(1,2);
    20. Rational result = oneHalf * oneEighth;
    21. result = result * oneEighth;
    22. // 能运行
    23. result = oneHalf * 2;
    24. // 错误!
    25. result = 2 * oneHalf;
    26. return 0;
    27. }

    为啥 result = oneHalf * 2 可以运行, 而  result = 2 * oneHalf 却会报错:

    1、result = oneHalf * 2 发生了所谓隐式转换:

    oneHalf 是一个内含 operator* 函数的 class 的对象,所以编译器调用该函数。然后编译器知道你正在传递一个 int,而函数需要的是 Rational; 但它也知道只要调用 Rational 构造函数并赋予你所提供的 int,就可以变出一个适当的 Rational 来。于是它就这样做了。换句话说此一调用动作在编译器眼中有点像这样:

    1. const Ration temp(2); // 根据2建立一个暂时的 Rational 对象
    2. result = oneHalf * temp; // 等同于 oneHalf.operator*(temp)

    当然,只因为涉及到 non-explicit 构造函数,编译器才会这样做。如果 Rational 构造函数是 explicit ,以下语句没有一个能通过编译:

    1. result = oneHalf * 2;
    2. result = 2 * oneHalf;

    2、result = 2 * oneHalf,整数2并没有相应的class,也就没有 operator* 成员函数。编译器也会尝试寻找可被以下这般调用的 non-member operator* (也就是在命名空间或在 global 作用域内):

    result = operator*(2, oneHalf); // 错误

    但本例并不存在这样一个接受 int 和 Rational 作为参数的 non-member operator*,因此查找失败

    那怎么可以支持混合式算术运算。可行之道终于拨云见日:让 operator* 成为一个 non-member 函数, 便允许编辑器在每个实参身上执行隐式类型转换:

    1. #include
    2. class Rational {
    3. public:
    4. // 构造函数刻意不为 explicit,
    5. // 允许 int-to-Rational 隐式转换
    6. Rational (int numerator = 0,
    7. int denominator = 1)
    8. :n(numerator),d(denominator){}
    9. // 分子(numerator)和 分母(denominator)的的访问函数
    10. int numerator() const;
    11. int denominator() const;
    12. private:
    13. int n,d;
    14. };
    15. // 注意:参数是两个Rational对象的引用
    16. const Rational operator* (const Rational& lhs,
    17. const Rational& rhs) {}
    18. int main(int argc, char *argv[])
    19. {
    20. Rational oneEighth(1,8);
    21. Rational oneHalf(1,2);
    22. Rational result = oneHalf * oneEighth;
    23. result = result * oneEighth;
    24. // 能运行
    25. result = oneHalf * 2;
    26. // 万岁,通过编译了!
    27. result = 2 * oneHalf;
    28. return 0;
    29. }

    最后一个问题:operator* 是否应该成为 Rational class 的一个 friend  函数呢?

    就本例而言答案是否定的,因为 operator* 可以完全由 Rational 的 public 接口完成任务。这导出一个重要的观察:member 函数的反面是 non-member 函数,不是 friend 函数。

    无论任何时候可以避免 friend 函数就该避免。

    请记住:

    如果你需要为个函数的所有参数(包括被 this 指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个 non-member.

    条款25、考虑写出一个不抛异常的 swap 函数

    先来看看std::swap的典型实现:置换 a 和 b 的值

    1. namespace std {
    2. template<typename T>
    3. void swap(T& a, T& b)
    4. {
    5. T temp(a); // a 复制到temp
    6. a = b; // b 复制到 a
    7. b = temp; // temp 复制到 b
    8. }
    9. }

    只要类型 T 支持 copying (通过 copy 构造函数和 copy assignment 操作符完成),缺省的 swap 实现代码就会帮你置换类型为 T 的对象。

    尝试将 std::swap 针对 Widget 特化:

    1. #include
    2. #include
    3. class WidgetImp1{
    4. public:
    5. private:
    6. int a,b,c;
    7. std::vector<double> v;
    8. };
    9. class Widget{
    10. private:
    11. WidgetImp1* pImp1; // 一旦要置换
    12. public:
    13. Widget(){}
    14. Widget (const Widget& rhs){}
    15. Widget& operator =(const Widget& rhs){
    16. *pImp1 = *(rhs.pImp1);
    17. }
    18. };
    19. // 一旦要置换两个 Widget 对象值,需要做的就是置换其pImp1指针。
    20. // 但缺省的 swap 算法 效率很低:
    21. // 不止复制三个 Widget, 还复制三个 WidgetWidgetImp1对象
    22. //
    23. // 所以要告诉 std::swap:
    24. // 当 Widgets 被置换时真正该做的是置换其内部的pImp1指针
    25. // 所以,将 std::swap 针对 Widget 特化
    26. namespace std {
    27. template<>
    28. void swap(Widget& a, Widget& b)
    29. {
    30. swap(a.pImp1, b.pImp1);
    31. }
    32. }
    33. using namespace std;
    34. int main(int argc, char *argv[])
    35. {
    36. Widget a;
    37. Widget b;
    38. std::swap(a,b);
    39. return 0;
    40. }

    但是发现pImp1指针是私有变量,在类外访问不了

    所以要在类中定义一个swap成员函数,并在swap特化函数中 使用该函数:

    1. #include
    2. #include
    3. class WidgetImp1{
    4. public:
    5. private:
    6. int a,b,c;
    7. std::vector<double> v;
    8. };
    9. class Widget{
    10. private:
    11. WidgetImp1* pImp1; // 一旦要置换
    12. public:
    13. Widget(){}
    14. Widget (const Widget& rhs){}
    15. Widget& operator =(const Widget& rhs){
    16. *pImp1 = *(rhs.pImp1);
    17. }
    18. // 给swap 特化 所调用
    19. void swap(Widget& other){
    20. using std::swap;
    21. swap(pImp1, other.pImp1);// 置换pImp1指针
    22. }
    23. };
    24. // 一旦要置换两个 Widget 对象值,需要做的就是置换其pImp1指针。
    25. // 但缺省的 swap 算法 效率很低:
    26. // 不止复制三个 Widget, 还复制三个 WidgetWidgetImp1对象
    27. //
    28. // 所以要告诉 std::swap:
    29. // 当 Widgets 被置换时真正该做的是置换其内部的pImp1指针
    30. // 所以,将 std::swap 针对 Widget 特化
    31. namespace std {
    32. template<>
    33. void swap(Widget& a, Widget& b)
    34. {
    35. a.swap(b);
    36. }
    37. }
    38. using namespace std;
    39. int main(int argc, char *argv[])
    40. {
    41. Widget a;
    42. Widget b;
    43. std::swap(a,b);
    44. return 0;
    45. }

    上面都是 class ,而非 class template。关于 class template 的 swap 函数暂时不学习。

    请记住:

    • 当 std::swap 对你的类型效率不高时,提供一个 swap 成员函数,并确定这个函数不抛出异常。
    • 如果你提供一个 member swap,也该提供一个non-member swap 用来调用前者。对于classes (而非 templates),也清特化 std::swap.
    • 调用 swap 时应针对 std::swap 使用 using 声明式,然后调用 swap  并且不带任何 "命名空间资格修饰符”
    • 为“用户定义类型”进行 std templates 全特化是好的,但千万不要尝试在 std 内加入某些对 std 而言全新的东西

    五、实现

    条款26、尽可能延后变量定义式的出现时间

    过早定义变量的例子:

    1. #include
    2. const int MininmumPasswordLength = 10;
    3. std::string encryptPassword(const std::string& password){
    4. using namespace std;
    5. // 过早定义变量encrypted
    6. // 如果下面抛出异常,该变量没用使用到
    7. // 但还是要承受该变量的构造和析构成本
    8. string encrypted;
    9. if (password.length() < MininmumPasswordLength){
    10. throw logic_error("Password is too short");
    11. }
    12. // 必要动作,便能将一个加密后的密码
    13. // 置入变量 encrypted 内
    14. return encrypted;
    15. }

    最好延后 encrypted 的出现,直到真正需要它。

    但循环怎么办?

    如果变量只在循环内使用,那么把它定义于循环外并在每次循环迭代时赋值给它比较好,还是该把它定义于循环内?

    1. class Widget{};
    2. const int n;
    3. //A、 定义于循环外
    4. Widget w;
    5. for (int i=0; i
    6. w = 取决于i的某个值;
    7. }
    8. //B、 定义于循环内
    9. for (int i=0; i
    10. Widget w = 取决于i的某个值;
    11. }

    两种写法的成本:

    做法A: 1个构造函数 + 1个析构函数 + n个赋值操作

    做法B:n个构造函数 + n个析构函数

    看情况而定!一般选B!

    条款27、尽量少做转型动作

    旧式转型

    1. // C风格
    2. (T)expression // 将expression 转型为T
    3. // 函数风格
    4. T(expression) //将expression转型为T

    新式转型

    1. // 用来将对象的常量性转除
    2. const_cast(expression)
    3. // 安全向下转型
    4. dynamic_cast(expression)
    5. // 低级转型,很少用
    6. reinterpret_cast(expression)
    7. // 用来强迫转换
    8. static_cast(expression)

    旧式转型合法,但一般用新式转型:

    1、容易在代码中被辨识

    2、各转型动作的目标愈窄,编译器愈可能诊断出错误的运用

    唯一使用旧式转型的的时机是,当我要调用一个 explicit 函数将一个对象传递给一个函数时。例如:

    1. #include
    2. class Widget{
    3. public:
    4. explicit Widget(int size){}
    5. };
    6. void doSomeWork(const Widget& w){}
    7. int main(int argc, char *argv[])
    8. {
    9. // 以一个int加上“函数风格”的转型动作创建一个Widget
    10. doSomeWork(Widget(15));
    11. // 以一个int加上“C++风格”的转型动作创建一个Widget
    12. // 蓄意的“对象生成”动作感觉不怎么像“转型”,所以一般
    13. // 用前者
    14. doSomeWork(static_cast(15));
    15. return 0;
    16. }

    这里避免用转型

    1. #include
    2. class Window{
    3. public:
    4. virtual void onResize(){}
    5. };
    6. class SpecialWindow: public Window{
    7. public:
    8. virtual void onResize(){
    9. // 避免这种写法
    10. static_cast(*this).onResize();
    11. // 使用这种写法
    12. Window::onResize(); // 调用Window::onResize 作用于*this身上
    13. // 这里写SpecialWindow专属行为
    14. //。。。。。。。。。。。。。。
    15. }
    16. };
    17. int main(int argc, char *argv[])
    18. {
    19. SpecialWindow sw;
    20. return 0;
    21. }

    dynamic_cast 比较耗时

    之所以需要dynamic_cast,通常时候因为你想在一个你认定为 derived class 对象身上执行 derived class 操作汉航速,但你手上却只有一个“指向base”的Pointer 或 referene, 你只能靠它们处理对象。

    省略。。。。。。。。。。

    请记住:

    如果可以,尽量避免转型,特别是在注重效率的代码中避免 dynamic_cast。如果有个涉及需要转型动作,试着发展无需转型的替代设计。

    如果转型式必要的,试着将他隐藏于某个函数背后。客户随后可以调用该函数,而不需要将转型放进他们自己的代码内。

    宁可使用C++-style(新式)转型,不要使用旧式转型。前者很容器辨识出来,而且也比较有着分门别类的职掌。

    条款28、避免返回 handles 指向对象内部成分

    1. #include
    2. #include
    3. #include
    4. class Point{
    5. public:
    6. Point(int a, int b):x(a),y(b){}
    7. void setX(int newVal){}
    8. void setY(int newVal){}
    9. int getX(){return x;}
    10. private:
    11. int x,y;
    12. };
    13. struct RectData
    14. {
    15. Point leftTop;
    16. Point rightDown;
    17. };
    18. class Rectangle{
    19. private:
    20. std::shared_ptr pData;
    21. public:
    22. // point 式自定义类型,根据条款20
    23. // by-reference 更加高效
    24. // 此时返回的式内部数据的引用,有被修改的风险
    25. // 所以要前面要加上 const
    26. const Point& upperLeft() const {return pData->leftTop;}
    27. const Point& lowerRight() const {return pData->rightDown;}
    28. Rectangle(Point p1, Point p2){
    29. pData->leftTop = p1;
    30. pData->rightDown = p2;
    31. }
    32. };

    本例中是成员函数返回 references,但如果它们返回的式指针或者迭代器也应该如此

    空悬号码牌的问题:

    1. #include
    2. #include
    3. #include
    4. class Point{
    5. public:
    6. Point(int a, int b):x(a),y(b){}
    7. void setX(int newVal){}
    8. void setY(int newVal){}
    9. int getX(){return x;}
    10. private:
    11. int x,y;
    12. };
    13. struct RectData
    14. {
    15. Point leftTop;
    16. Point rightDown;
    17. };
    18. class Rectangle{
    19. private:
    20. std::shared_ptr pData;
    21. public:
    22. // point 式自定义类型,根据条款20
    23. // by-reference 更加高效
    24. // 此时返回的式内部数据的引用,有被修改的风险
    25. // 所以要前面要加上 const
    26. const Point& upperLeft() const {return pData->leftTop;}
    27. const Point& lowerRight() const {return pData->rightDown;}
    28. Rectangle(Point p1, Point p2){
    29. pData->leftTop = p1;
    30. pData->rightDown = p2;
    31. }
    32. };
    33. class GUIObject{};
    34. const Rectangle boundingBox(const GUIObject& obj){}
    35. int main(){
    36. GUIObject* pgo;
    37. const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());
    38. }

    本例子最后一句:

    boundingBox(*pgo)

    对 boundingBox 的调用获得一个新的、暂时的Rectangle 对象。这个对象没有名称,所以暂时称它为 temp。

    &boundingBox(*pgo).upperLeft()

    随后 upperLeft 作用于 temp 身上,返回一个 reference 指向 temp 的内部成分,更具体地说指向一个用以标识 temp 的 Points。于是pUpperLeft指向那个对象。

    但是在在那个语句结束后,boundingBox 的返回值,也就是我们所说的 temp,将被销毁,而那间接导致 temp 内的 Points 析构。最终导致 pUpperLeft  指向一个不再存在的对象;

    也就是说一旦产生 pUpperLeft 的那个语句结束,pUpperLeft 也就变成空悬、虚吊(dangling)

    但是我有个疑问:

    这里的shared_ptr怎么不需要分配内存?

    请记住:

    避免返回 handles (包括 references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助 const 成员函数的行为像个 const,并将 “虚吊号码牌”(dangling handles)的可能性降至最低。

    条款29、为 " 异常安全 "而努力是值得的

    仔细分析下例ChangeBackground函数

    1. #include
    2. #include
    3. using namespace std;
    4. struct Image
    5. {
    6. char image[480*640];
    7. };
    8. class PrettyMenu{
    9. public:
    10. void ChangeBackground(Image& imgSrc);
    11. private:
    12. mutex mutex1; // 互斥器
    13. Image* bgImage; // 目前的背景图像
    14. int imageChanges; // 背景图像被改变的次数
    15. };
    16. void PrettyMenu::ChangeBackground(Image& imgSrc)
    17. {
    18. mutex1.lock(); // 取得互斥器
    19. delete bgImage; // 摆脱旧的背景图像
    20. ++imageChanges; // 修改图像变更次数
    21. bgImage = new Image(imgSrc); // 安装新的背景图像
    22. mutex1.unlock(); // 释放互斥器
    23. }
    24. int main(int argc, char *argv[])
    25. {
    26. PrettyMenu pt;
    27. return 0;
    28. }

    该函数可能会发生的问题:

    泄露资源:new Image(imgSrc) 导致异常,对 unlock 的调用就绝不会执行,于是互斥器就永远把持住了

    数据败坏:new Image(imgSrc) 抛出异常,bgImage就是指向一个已被删除的对象,imageChanges 也被累加,而其实并没有新的图像被成功安装起来

    泄露资源的问题如何解决:以对象管理资源

    1. #include
    2. #include
    3. using namespace std;
    4. struct Image
    5. {
    6. char image[480*640];
    7. };
    8. class PrettyMenu{
    9. public:
    10. void ChangeBackground(Image& imgSrc);
    11. private:
    12. mutex mutex1; // 互斥器
    13. Image* bgImage; // 目前的背景图像
    14. int imageChanges; // 背景图像被改变的次数
    15. };
    16. void lock(mutex* pm){}
    17. void unlock(mutex* pm){}
    18. class Lock{
    19. private:
    20. mutex* mutexPtr;
    21. public:
    22. explicit Lock(mutex* pm):mutexPtr(pm)
    23. {
    24. lock(mutexPtr);
    25. }
    26. ~Lock()
    27. {
    28. unlock(mutexPtr);
    29. }
    30. };
    31. void PrettyMenu::ChangeBackground(Image& imgSrc)
    32. {
    33. Lock m1(&mutex1); // 来自条款14:获得互斥器并确保它稍后被释放
    34. delete bgImage; // 摆脱旧的背景图像
    35. ++imageChanges; // 修改图像变更次数
    36. bgImage = new Image(imgSrc); // 安装新的背景图像
    37. }
    38. int main(int argc, char *argv[])
    39. {
    40. PrettyMenu pt;
    41. return 0;
    42. }

    如此,利用对象管理资源!

    再来专注解决数据败坏,在解决该问题之前,先面对异常安全函数的三个保证:

    基本承诺

    如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。举个例子,ChangeBackground 函数抛出异常后,PrettyMenu 对象仍然可以继续拥有原背景图像 ,或是令它拥有某个缺省背景图像。

    强烈保证:

    如果异常被抛出,程序状态不改变。调用这样的函数需有这样的认知:如果函数成功,就是完全成功,如果函数失败,程序会回复到 “调用之前的状态”。

    不抛掷保证:

    承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。

    异常安全码(Exception-safe code)必须提供上述三种保证之一,对于大部分函数往往在基本承诺和强烈保证中择一;

    怎么让 ChangeBackground 函数提供强烈保证:

    1. 用智能指针管理Image*
    2. 调整 ++imageChanges 到真的发生新背景图像安装之后
    1. class PrettyMenu{
    2. ...
    3. std::shared_ptr bgImage;
    4. ...
    5. }
    6. void PrettyMenu::ChangeBackground(Image& imgSrc)
    7. {
    8. Lock m1(&mutex1);
    9. bgImage.reset(new Image(imgSrc));// 以 “new Image” 的执行结果设定bgImage 内部指针
    10. ++imageChanges;
    11. }

    上述两个改变几乎足够让 ChangeBackground 提供强烈的异常安全保证。

    但是我们还想进一步。有一个一般化的设计策略很典型地会导致强烈保证,很值得熟悉。这个策略被称为--copy and swap:

    1、为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改。

    2、若有任何修改动作抛出异常,原对象仍保持未改变状态。

    3、待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)。

    实现上通常是将所有 “隶属对象的数据” 从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象(implementation object,即副本)。这种手法常被称为 pimpl idiom,条款31 详细描述了它。典型的写法如下:

    1. // 将所有 “隶属对象的数据” 从原对象放进另一个对象内
    2. // 让 PMImpl 成为一个 struct 而不是 一个 class:
    3. // 1、PrettyMenu 的数据封装性已经由于 “pImpl是private” 而获得了保证
    4. // 2、如果令 PMImpl 为一个class,不是很方便 (条款25)
    5. struct PMImpl{
    6. std::shared_ptr bgImage;
    7. int imageChanges;
    8. };
    9. class PrettyMenu{
    10. ...
    11. private:
    12. mutex mutex1
    13. // 赋予原对象一个指针
    14. std::shared_ptr pImpl;
    15. };
    16. void PrettyMenu::ChangeBackground(Image& imgSrc)
    17. {
    18. using std::swap; // 见条款25
    19. Lock m1(&mutex1);
    20. // 创建副本
    21. // 感觉应该写成下面这个:
    22. //std::shared_ptr pNew(new PMImpl(*pImpl));
    23. std::shared_ptr pNew(new PMImpl);
    24. // 修改副本
    25. pNew->bgImage.reset(new Image(imgSrc));
    26. ++pNew->imageChanges;
    27. // 置换(swap)数据
    28. swap(pImpl, pNew);
    29. }

    本例中,借以 “copy-and-swap” 策略 对对象状态实现 "全有或全无"。

    清记住:

    1、异常安全函数(Exception-safe function) 即使发生异常也不会泄露资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。

    2、“强烈保证” 往往能够以 copy-and-swap 实现出来,但 “强烈保证”并非对所有函数都可实现或具备现实意义。

    3、函数提供的 “异常安全保证” 通常最高只等于其所调用之各个函数的 “异常安全保证” 中的最弱者

    条款30、透彻了解 inlining 的里里外外

    inline 关键字的作用:

    inline 关键字用于修饰函数,它是一种对编译器的建议,用于告诉编译器在编译时将函数内联展开。

    通常情况下,函数的调用会导致程序跳转到函数的定义处执行,然后再返回到调用点。当函数较小且频繁调用时,这种跳转和返回的开销可能会成为性能瓶颈。

    使用 inline 关键字可以建议编译器将函数的定义插入调用点,而不是跳转到函数的定义处执行。这样可以减少函数调用的开销,提高程序的执行效率。

    要声明一个内联函数,只需在函数定义的前面加上 inline 关键字即可。例如:

    1. inline int add(int x, int y) {
    2. return x + y;
    3. }

    需要注意的是,inline 关键字只是对编译器的建议,编译器可以选择是否将函数内联展开。通常情况下,对于较短的函数,编译器会选择内联展开。但是对于较长的函数,编译器可能会忽略 inline 关键字的建议。

    另外,将函数定义放在头文件中时,为了避免出现多重定义错误,通常需要将函数声明为 inline。因为头文件会被多个源文件包含,如果函数定义不是 inline 的,则每个源文件中都会有一份函数定义,从而导致多重定义错误。

    直接在 class 定义式内呈现成员函数的本体,会让该成员函数暗自成了inline 。

    请记住:

    1、将大多数 inlining 限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability) 更容易,也可以使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。

    2、不要只因为 function templates 出现在头文件中,就将它们声明为 inline

    条款31、将文件间的编译依存关系将至最低

    在了解文件间的编译依存关系之前,先来看看这个例子:

    1. class Person{
    2. public:
    3. Person(const std::string& name, const Date& birthday,
    4. const Address& addr){}
    5. std::string name() const;
    6. std::string birthday() const;
    7. std::string address() const;
    8. private:
    9. std::string theName; //
    10. Date theBirthData;
    11. Address theAddress;
    12. };
    13. int main(int argc, char *argv[])
    14. {
    15. std::string name;
    16. Date birthday;
    17. Address addr;
    18. Person p(name, birthday, addr);
    19. return 0;
    20. }

    没错,这个例子根本就无法通过编译,因为编译器没有取得其实现代码所用到的 classes string, Data 和 Address的定义式

    这样的定义式通常由 #include 指示符提供,所以 Person 定义文件的最上方很可能存在这样的的东西:

    1. #include
    2. #include "data.h"
    3. #include "address.h"

    但是这样一来,Person 定义文件和其含入文件之间形成了一种编译依存关系。

    现在需要 “将对象实现细目隐藏于一个指针背后”。正对 Person 可以这样做: 把 Person 分割成两个 classes,一个只提供接口,另一个负责实现该接口。如果负责实现的那个所谓 implementation class 取名为 PersonImpl,Person 将定义如下:

    1. #include
    2. #include
    3. // Person 实现类的前置声明
    4. class PersonImpl{};
    5. //Person 接口用到的 classes 的前置声明
    6. class Date{};
    7. class Address{};
    8. class Person{
    9. public:
    10. Person(const std::string& name, const Date& birthday,
    11. const Address& addr){}
    12. std::string name() const;
    13. std::string birthday() const;
    14. std::string address() const;
    15. private:
    16. // 指针,指向实现物
    17. // 以对象管理资源,条款13
    18. std::shared_ptr pImpl;
    19. };
    20. int main(int argc, char *argv[])
    21. {
    22. std::string name;
    23. Date birthday;
    24. Address addr;
    25. Person p(name, birthday, addr);
    26. return 0;
    27. }

    在这里,main class(Person) 只内含一个指针成员(这里使用std::shared_ptr,见条款13),指向其实现类(PersonImpl)。这般设计常被称为 pimpl idiom(pimpl 是 “pointer to implementation”的缩写)。这种 classes 内的指针名称往往就是pImpl,就像上面代码那样。

    这样的设计之下,Person的客户就完全与 Dates,Addresses 以及 Persons 的实现细目分离。那些classes 的任何实现修改都不需要 Person 客户端重新编译。这样就是“接口与实现分离”

    这个分离的关键在于以“声明的依存性”替换“定义的依存性”,那正是编译依存性最小化的本质:现实中让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。其他每一件事都源自于这个简单的设计策略:

    1、如果使用 object reference 或 objecct pointers 可以完成任务,就不要使用 objects。

    2、如果能够,尽量以 class 声明式替换 class 定义式。

    3、为声明式和定义式提供不同的头文件,一个用于声明式,一个用于定义式。

    像person这样使用 pimpl idiom 的 classes,往往被成为 Handle classes。

    另一个制作 Handle class 的办法式,令 Person 成为一种特殊的 abstract base class(抽象基类),称为 Interface class。

    这个例子有问题:

    1. #include
    2. #include
    3. #include
    4. // 一个针对Person而写的 Interface class (抽象基类)
    5. // 目的:
    6. // 详细描述 derived classes 的接口(见条款34)
    7. // 所以通常不带成员变量,也没有构造函数,
    8. // 只有一个virtual 析构函数以及一组 pure virtual 函数,
    9. // 用来叙述整个接口
    10. class Date{};
    11. class Address{};
    12. class Person{
    13. public:
    14. virtual ~Person();
    15. virtual std::string name() const=0;
    16. virtual std::string birthDate() const = 0;
    17. virtual std::string address() const = 0;
    18. static std::shared_ptr create(const std::string& name,
    19. const Date& birthDate,
    20. const Address& addr);
    21. };
    22. class RealPerson: public Person{
    23. private:
    24. std::string theName;
    25. Date theBirthDate;
    26. Address theAddress;
    27. public:
    28. RealPerson(const std::string& name, const Date& birthday,
    29. const Address& addr)
    30. :theName(name),theBirthDate(birthday),theAddress(addr){}
    31. virtual ~RealPerson() {}
    32. std::string name() const{}
    33. std::string birthDate() const{}
    34. std::string address() const{}
    35. };
    36. std::shared_ptr Person::create(const std::string &name,
    37. const Date &birthDate,
    38. const Address &addr)
    39. {
    40. return
    41. std::shared_ptr(new RealPerson(name,birthDate,addr));
    42. }
    43. int main(){
    44. std::string name;
    45. Date dateOfBirth;
    46. Address address;
    47. std::shared_ptr pp(Person::create(name, dateOfBirth, address));
    48. std::cout << pp->name() << " was born on "
    49. << pp->birthDate() << " and now lives at "
    50. << pp->address();
    51. }

    请记住:

    1、支持 “编译依存性最小化” 的一般构想是: 相依于声明式,不要相依于定义式。基于此构想的两个手段式 Handle classes 和 Interface classes

    2、程序库头文件应该式以 “完全且仅有声明式”的形式存在。这种做法不论是否涉及 templates 都适用。

    六、继承与面向对象设计

    条款32、确定你的 public 继承塑模出 is-a 关系

    请记住:

    “public 继承”意味着 is-a。适用于 base classes 身上的每一件事情一定也适用于 derived classes 身上,因为每一个 derived classes 对象也都是一个 base class 对象

    条款33、避免遮挡继承而来的名称

    看看这个例子:

    1. #include
    2. int x= 1; // global 变量
    3. void someFunc()
    4. {
    5. double x=2.3; // local 变量
    6. std::cout << x << '\n';
    7. }
    8. int main(int argc, char *argv[])
    9. {
    10. someFunc();
    11. return 0;
    12. }

     

    看见没,打印的是2.3。是 local 变量 x, 而不是 global 变量 x,因为内层作用域的名称会遮掩(遮蔽)外围作用域的名称。没错即使类型不一样,也会被遮蔽,只看变量名称!

    在类继承中呢?

    1. #include
    2. class Base{
    3. private:
    4. int x;
    5. public:
    6. virtual void mf1()=0;
    7. virtual void mf1(int){}
    8. virtual void mf2(){}
    9. void mf3(){}
    10. void mf3(double){}
    11. };
    12. class Derived: public Base{
    13. public:
    14. virtual void mf1(){}
    15. void mf3(){}
    16. void mf4(){}
    17. };
    18. int main(){
    19. Derived d;
    20. int x;
    21. d.mf1(); // 调用 Derived::mf1
    22. d.mf1(x); // 错误!因为 Derived::mf1 遮掩了 Base::mf1
    23. d.mf2(); // 调用 Base::mf2
    24. d.mf3(); // 调用 Derived::mf3
    25. d.mf3(x); // 错误! 因为 Derived::mf3 遮掩了 Base::mf3
    26. }

     如本例所见,即使 base classes 和 derived classes 内的函数有不同的参数类型也适用,而且不论函数是 virtual 或 non-virtual 一体适用。只要函数名字相同,继承类就会遮掩掉基类的函数

    当然,也有办法在扩展类中调用基类被遮掩的函数:

    方法一:使用using 声明式

    1. #include
    2. class Base{
    3. private:
    4. int x;
    5. public:
    6. virtual void mf1()=0;
    7. virtual void mf1(int){}
    8. virtual void mf2(){}
    9. void mf3(){}
    10. void mf3(double){}
    11. };
    12. class Derived: public Base{
    13. public:
    14. // 让Base class 内名为mf1和mf3的所有东西在Derived作用域内都可见(并且public)
    15. using Base::mf1;
    16. using Base::mf3;
    17. virtual void mf1(){}
    18. void mf3(){}
    19. void mf4(){}
    20. };
    21. int main(){
    22. Derived d;
    23. int x=1;
    24. d.mf1(); // 调用 Derived::mf1
    25. d.mf1(x); // 现在可以了
    26. d.mf2(); // 调用 Base::mf2
    27. d.mf3(); // 调用 Derived::mf3
    28. d.mf3(x); // 现在可以了
    29. }

    方法二:使用转交函数(forwarding functions)

    请记住:

    derived classes 内的名称会遮掩 base classes 内的名称。在 public 继承下从没有人希望如此。

    为了被遮掩的名称再见天日,可使用 using 声明式或转交函数(forwarding functions)

    条款34、区分接口继承和实现继承

    1. #include
    2. class Shape{
    3. public:
    4. virtual void draw() const = 0;
    5. virtual void error(const std::string& msg);
    6. int objectID() const;
    7. };
    8. class Rectangle : public Shape{
    9. void draw() const{}
    10. };
    11. class Ellipse : public Shape{
    12. void draw() const{}
    13. };
    14. int main(){
    15. // 错误! Shape 是抽象的,不能实例化
    16. Shape* ps = new Shape;
    17. Shape* ps1 = new Rectangle;
    18. ps1->draw();// 调用 Rectangle::draw
    19. Shape* ps2 = new Ellipse;
    20. ps2->draw(); // 调用 Ellipse::draw
    21. // 调用Shape::draw
    22. // 但是没有什么意义,因为纯虚函数在基类中根本没有定义
    23. ps1->Shape::draw();
    24. ps2->Shape::draw();
    25. }

    声明简朴的(非纯)impure virtual 函数的目的,是让 derived classes 继承该函数的接口和缺省实现:

    比如

    1. class Shape{
    2. public:
    3. virtual void error(const std::string& msg);
    4. }

    你必须支持一个 error 函数,但如果你不想自己写一个,可以使用 Shape class 提供的缺省版本

    再看一个例子:

    1. class Airport{};
    2. class Airplane{
    3. public:
    4. virtual void fly(const Airport& destination);
    5. };
    6. void Airplane::fly(const Airport& destination){
    7. // 缺省代码,将飞机飞到指定的目的地
    8. }
    9. class ModelA : public Airplane{};
    10. class ModelB : public Airplane{};

    为了避免在ModelA 和 ModelB 中撰写相同代码,缺省飞行行为由 Airplane::fly 提供,它同时被 ModelA 和 ModelB 继承。

    这是个典型的面向对象设计。两个 class 共享一份相同性质(也就是它们实现 fly的方式),所以共同性质被搬到 base class 中。

    如果有一个新的类 ModelC也继承 Airplane,但是飞行动作和缺省的不一样,如果忘记重载飞机动作的话,在调用飞行动作的时候就会不可避免的使用基类的飞行动作,而这会导致错误。

    1. class Airport{};
    2. class Airplane{
    3. public:
    4. virtual void fly(const Airport& destination);
    5. };
    6. void Airplane::fly(const Airport& destination){
    7. // 缺省代码,将飞机飞到指定的目的地
    8. }
    9. class ModelA : public Airplane{};
    10. class ModelB : public Airplane{};
    11. class ModelC: public Airplane{
    12. // 未声明fly函数
    13. };
    14. int main(int argc, char *argv[])
    15. {
    16. Airport PDX;
    17. Airplane* pa = new ModelC;
    18. //
    19. pa->fly(PDX);
    20. delete pa;
    21. return 0;
    22. }

    那有什么方法,避免上述问题?

    方法一:切断 “virtual 函数接口”和其 “缺省实现之间的连接”

    1. #include
    2. class Airport{};
    3. class Airplane{
    4. public:
    5. virtual void fly(const Airport& destination) = 0;
    6. protected:
    7. // 这个不是接口,是用于接口的缺省代码
    8. // 所以是protected,
    9. // 继承类也可以访问该函数
    10. void defaultFly(const Airport& destination);
    11. };
    12. void Airplane::defaultFly(const Airport& destination){
    13. // 缺省代码,将飞机飞到指定的目的地
    14. }
    15. class ModelA : public Airplane{
    16. public:
    17. virtual void fly(const Airport &destination)
    18. {defaultFly(destination);}
    19. };
    20. class ModelB : public Airplane{
    21. public:
    22. virtual void fly(const Airport &destination)
    23. {defaultFly(destination);}
    24. };
    25. class ModelC: public Airplane{
    26. public:
    27. // 现在不可能意外继承不正确的fly实现代码了
    28. // 因为 Airplane中的pure virtual 函数迫使ModelC 必须提供自己的fly版本
    29. virtual void fly(const Airport &destination){}
    30. };
    31. int main(int argc, char *argv[])
    32. {
    33. Airport PDX;
    34. Airplane* pa = new ModelA;
    35. //
    36. pa->fly(PDX);
    37. delete pa;
    38. return 0;
    39. }

    如果你不喜欢 以不同的函数分别提供接口和缺省实现,像上述的 fly 和 defaultFly 那样。

    方法二:

    pure virtual 函数必须在 derived classes 中重新声明,但它们也可以拥有自己的实现

    1. #include
    2. class Airport{};
    3. class Airplane{
    4. public:
    5. virtual void fly(const Airport& destination)=0;
    6. };
    7. void Airplane::fly(const Airport& destination){
    8. // 缺省代码,将飞机飞到指定的目的地
    9. }
    10. class ModelA : public Airplane{
    11. public:
    12. virtual void fly(const Airport &destination)
    13. {Airplane::fly(destination);}
    14. };
    15. class ModelB : public Airplane{
    16. public:
    17. virtual void fly(const Airport &destination)
    18. {Airplane::fly(destination);}
    19. };
    20. class ModelC: public Airplane{
    21. public:
    22. virtual void fly(const Airport &destination);
    23. };
    24. void ModelC::fly(const Airport &destination){
    25. // 将C型飞机飞至指定的目的地
    26. }
    27. int main(int argc, char *argv[])
    28. {
    29. Airport PDX;
    30. Airplane* pa = new ModelA;
    31. //
    32. pa->fly(PDX);
    33. delete pa;
    34. return 0;
    35. }

    纯虚函数也可以有定义!

    声明 non-virtual 函数的目的是为了令 dedrived classes 继承函数的接口及一份强制性实现:

    每个扩展类都必须有这个non-virtual 函数

    pure virtual函数、simple(impure) virtual 函数、 non-virtual 函数之间的差异,使得你得以精确指定你想要 derived classes 继承的东西:之继承接口,或是继承接口和一份缺省实现,或是继承接口和一份强制实现

    请记住:

    1、接口继承和实现继承不同。在Public 继承之下,derived classes 总是继承 base class 的接口

    2、pure virtual 函数只具体指定接口继承

    3、简朴的(非纯)impure virtual 函数具体指定接口继承以及缺省实现继承

    4、non-virtual 函数具体指定接口继承以及强制性实现继承

    条款35、考虑 virtual 函数以外的其他选择

    healthValue 并为被声明 pure virtual,这暗示我们将会有个计算健康指数的缺省算法(见条款34)

    1. class GameCharacter{
    2. public:
    3. virtual int healthValue() const;
    4. }

    还有其他方法:

    借由 Non-Virtual Interface 手法实现 Template Method 模式:

    1. class GameCharacter{
    2. private:
    3. virtual int doHealthValue() const{
    4. // 缺省算法,计算健康指数
    5. }
    6. public:
    7. // derived classes 不重新定义它,见条款36
    8. // 在class 定义式内呈现成员函数本体,会让其暗自成为inline,条款30
    9. int healthValue() const{
    10. // 做一些事前工作,
    11. // 做真正的工作
    12. int retValue = doHealthValue();
    13. // 做一些事后的工作
    14. return retValue;
    15. }
    16. }

    借由 Function Pointers 实现 Strategy 模式:

    书上的代码表述不清楚

    省略。。。。

    条款36、绝不重新定义继承而来的 non-virtual 函数

    条款37、绝不重新定义继承而来的缺省参数值

    virtual 函数系动态绑定(dynamically bound),而缺省参数值却是静态绑定(statically bound)。

    这句话是不是看起来不好理解,先来看看动态绑定和静态绑定式什么意思:

    1. // 一个用以描述几何形状的 class
    2. class Shape{
    3. public:
    4. enum ShapeColor{Red, Green, Blue};
    5. // 所有形状都必须提供一个函数,用来绘出自己
    6. virtual void draw(ShapeColor color = Red) const =0;
    7. };
    8. class Rectangle: public Shape{
    9. public:
    10. // 赋予不同的缺省参数值。这真糟糕
    11. virtual void draw(ShapeColor color = Green) const{
    12. std::cout << "color: " << color << '\n';
    13. }
    14. };
    15. class Circle: public Shape{
    16. public:
    17. // 以对象调用此函数,一定要指定参数值
    18. // 因为静态绑定下这个函数并不从其base继承缺省参数值
    19. // 但若以指针(或reference)调用此函数,可以并不指定参数值
    20. // 因为动态绑定下这个函数会从其base继承缺省参数值
    21. virtual void draw(ShapeColor color) const{
    22. std::cout << "color: " << color << '\n';
    23. }
    24. };
    25. int main(){
    26. // ps、pc、pr 被声明为 pointer-to-Shape类型
    27. // 所以不论它们真正指向什么,它们的静态类型都是 Shape*
    28. Shape* ps;
    29. Shape* pc = new Circle;
    30. Shape* pr = new Rectangle;
    31. // 对象的所谓动态类型则是指:
    32. // “目前所指对象的类型”
    33. // 所以:
    34. // pc 的动态类型是 Circle*
    35. // pr 的动态类型是 Rectangle*
    36. // ps 没有动态类型,因为它尚未指向任何对象
    37. Circle c;
    38. c.draw(); // 错误!没有指定参数值
    39. ps = pc; // ps的动态类型如今是 Circle*
    40. ps = pr;
    41. }

    Virtual 函数系动态绑定,意思是调用一个Virtual 函数时,究竟调用哪一份函数实现代码,取决于发出调用的那个对象的动态类型

    1. pc->draw(Shape::Red); // 调用Circle::draw(Shape::Red)
    2. pr->draw(Shape::Red); // 调用 Rectangle::draw(Shape::Red)

    缺省参数值是静态绑定。意思是你可能会在“调用一个定义于dereived class 内的函数”的同时,却使用 base class 为它指定的缺省参数值

    1. // 调用Rectangel::draw(Shape::Red)
    2. // 是的,你没看错参数是Shape::Red
    3. // 而不是在Rectangle里重新定义的Shape::Green
    4. pr->draw();

        即使把指针换成references问题仍然存在。问题在于draw 是个 virtual 函数,而它有个缺省参数值 在 derived class 中被重新定义了。

    当你想令 virtual 函数表现出你所想要的行为但却遭遇麻烦,聪明的做法是考虑替代设计。条款35中,NVI(non-virtual interface)手法:令 base class 内的一个 public non-virtual 函数调用 private virtual 函数,后者可被 derived classes 重新定义。这里我们可以让 non-virtual 函数指定缺省参数,而 private virtual 函数负责真正的工作:

    1. #include
    2. // 一个用以描述几何形状的 class
    3. class Shape{
    4. public:
    5. enum ShapeColor{Red, Green, Blue};
    6. void draw(ShapeColor color = Red) const{ // 如今它是non-virtual
    7. doDraw(color); // 调用一个virtual
    8. }
    9. private:
    10. // 真正的工作在此完成
    11. virtual void doDraw(ShapeColor color) const=0;
    12. };
    13. class Rectangle : public Shape{
    14. public:
    15. private:
    16. // 注意,不须指定缺省参数值
    17. // 就算你这里重新定义了缺省参数,也没有用
    18. // 因为non-vitual 函数应该绝对不被 derived classes 覆写(条款36)
    19. // 这个设计很清楚地使得 draw 函数的color 缺省参数总是为Red
    20. virtual void doDraw(ShapeColor color) const{
    21. std::cout << "color:" << color << '\n';
    22. }
    23. };
    24. int main(){
    25. Shape* r = new Rectangle;
    26. r->draw();
    27. delete r;
    28. // 即使以对象调用此函数,也不需要指定参数值
    29. Rectangle r1;
    30. r1.draw();
    31. }

    请记住:

    绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而 virtual 函数---- 你唯一应该覆写的东西---却是动态绑定。

    条款38、通过复合塑模出 has-a 或 “根据某物实现出”

    1. #include
    2. #include
    3. template<class T>
    4. class Set{
    5. public:
    6. bool member(const T& item) const;
    7. void insert(const T& item);
    8. void remove(const T& item);
    9. std::size_t size() const;
    10. private:
    11. std::list rep;
    12. }
    13. template<typename T>
    14. bool Set::member(const T &item) const
    15. {
    16. return std::find(rep.begin(), rep.end(), item) != rep.end()
    17. }
    18. template<typename T>
    19. bool Set::insert(const T &item)
    20. {
    21. if (!member(item)) rep.push_back(item);
    22. }
    23. template<typename T>
    24. void Set::remove(const T &item)
    25. {
    26. typename std::list::iterator it =
    27. std::find(rep.begin(), rep.end(), item);
    28. if (it != rep.end()) rep.erase(it);
    29. }
    30. template<typename T>
    31. std::size_t Set::size() const
    32. {
    33. return rep.size();
    34. }

    请记住:

    复合的意思和 public 继承完全不同

    在应用域,复合意味着 has-a, 在实现域,复合意味着更具某物实现出 

    条款39、明智而审慎地使用private继承

    看完这个例子后,你是否理解是否要谨慎使用 private 继承:

    1. #include
    2. class Person{};
    3. class Student: private Person{};
    4. void eat(const Person& p);
    5. void study(const Student& s);
    6. int main(){
    7. Person p;
    8. Student s;
    9. eat(p);
    10. // 错误!
    11. // classes之间的继承关系是private
    12. // 编译器不会自动将一个derived class 对象转换为一个 base class 对象
    13. // 这确实与 public 继承不一样
    14. // 第二条规则:
    15. // 由 private base 继承而来的所有成员,在 derived class 中都会变成 private 属性,
    16. // 纵使它们在 base class 中原来是 protected 或 public 属性
    17. eat(s);
    18. }

    条款40、明智而审慎地使用多重继承

    一个简单的多重继承的例子:

    1. #include
    2. class BorrowableItem{ // 图书光允许你借某些东西
    3. public:
    4. void checkOut(){} // 离开时进行检查
    5. };
    6. class ElectronicGadget{
    7. private:
    8. bool checkOut() const{} // 执行自我检测,返回是否检测成功
    9. };
    10. class MP3Player:
    11. public BorrowableItem,
    12. public ElectronicGadget{};
    13. int main(){
    14. MP3Player mp;
    15. // 有歧义,不知道是用那个基础类的
    16. mp.checkOut();
    17. // 应该这样
    18. mp.BorrowableItem::checkOut();
    19. }

    再复杂一点,一个钻石型多重继承:

    1. #include
    2. #include
    3. class File{
    4. public:
    5. std::string fileName = "jason";
    6. };
    7. class InputFile : public File{};
    8. class OutFile : public File{};
    9. class IOFile : public InputFile,
    10. public OutFile{};
    11. int main(){
    12. IOFile io;
    13. std::cout << io.fileName << '\n';
    14. }

    File class 有个成员变量 fileName,但是 IOFile  从其每一个 base class 继承一份,所以其应该有两份 fileName 成员变量。所以导致有歧义!

    解决方法是:直接继承 File 的 classes 采用 “virtual 继承”

    1. class File{
    2. public:
    3. std::string fileName = "jason";
    4. };
    5. class InputFile : virtual public File{};
    6. class OutFile : virtual public File{};
    7. class IOFile : public InputFile,
    8. public OutFile{};

    其他省略

    七、模板与泛型编程

    条款41、 了解隐式接口和编译期多态

    条款42、了解 typename 的双重意义

    以下 template 声明式,class 和 typename 意义完全相同:

    1. template<class T> class Widget;
    2. template<typename T> class Widget;

    其实下面个例子运行不起来。

    1. #include
    2. #include
    3. // 下述 template function 接受一个STL兼容容器作为参数
    4. template<typename C>
    5. void prinr2nd(const C& container){ // 打印容器内的第二个元素
    6. if (container.size() >= 2){
    7. C::const_iterator iter(container.begin()); // 取得第一元素的迭代器
    8. ++iter; // 将 iter 移往第二元素
    9. int value = *iter; // 将该元素复制到某个int
    10. std::cout << value; // 打印那个int
    11. }
    12. }
    13. int main(int argc, char *argv[])
    14. {
    15. std::vector p{1,2,3,4};
    16. prinr2nd(p);
    17. return 0;
    18. }

    这是解决办法:

    1. #include
    2. #include
    3. // 下述 template function 接受一个STL兼容容器作为参数
    4. template<typename C>
    5. void prinr2nd(const C& container){ /
    6. if (container.size() >= 2){
    7. //iter 的类型是 C::const_iterator,实际是什么必须取决于 template 参数C
    8. // template 内出现的名称如果相依某个 template 参数,称之为从属名称(dependent names)
    9. // 如果从属名称在class内呈嵌套状,我们称为嵌套从属名称(nested dependent name)
    10. // C::const_iterator 就是这样嵌套从属类型名称,所以必须以template 为前导
    11. typename C::const_iterator iter(container.begin());
    12. ++iter;
    13. int value = *iter;
    14. std::cout << value;
    15. }
    16. }
    17. int main(int argc, char *argv[])
    18. {
    19. std::vector p{1,2,3,4};
    20. prinr2nd(p);
    21. return 0;
    22. }

    当然也有例外,但暂不学习

    在真实程序的代表性例子:

    1. template<typename IterT>
    2. void workWithIterator(IterT iter){
    3. typename std::iterator_traits::value_type temp(*iter);
    4. }
    5. // 如果你认为 std::iterator_traits::value_type 读起来不畅快
    6. // 可以考虑建立一个 typedef
    7. template<typename IterT>
    8. void workWithIterator(IterT iter){
    9. typedef typename std::iterator_traits::value_type value_type;
    10. value_type temp(*iter);
    11. }

    请记住:

    声明template 参数时,前缀关键字 class 和 typename  可互换

    请使用关键字 typename 标识嵌套从属类型名称;但不得在 base class lists(基类列)或member initialization list (成员初值列)内以它作为 base class 修饰符。

    条款43、学习处理模板化基类内的名称

    看这个不能通过编译的例子:

    1. #include
    2. #include
    3. class CompanyA{
    4. public:
    5. void sendCleartext(const std::string& msg);
    6. void sendEncrypted(const std::string& msg);
    7. };
    8. class CompanyB{
    9. public:
    10. void sendCleartext(const std::string& msg);
    11. void sendEncrypted(const std::string& msg);
    12. };
    13. class MsgInfo{};
    14. template<typename Company>
    15. class MsgSender{
    16. public:
    17. void sendClear(const MsgInfo& info)
    18. {
    19. std::string msg;
    20. // 在这里,根据 info 产生信息
    21. Company c;
    22. c.sendCleartext(msg);
    23. }
    24. void sendSecret(const MsgInfo& info)
    25. {}
    26. };
    27. template<typename Company>
    28. class LoggingMsgSender: public MsgSender{
    29. public:
    30. void sendClearMsg(const MsgInfo& info)
    31. {
    32. // 将“传送前”的信息写至log:
    33. // 调用 base class 函数; 这段无法通过编译
    34. // 问题在于:
    35. // 当编译器遭遇 class template LoggingMsgSender 定义式时,并不知道它继承什么样的class
    36. // 当然它继承的是 MsgSender,但其中的Company是个是个template参数,
    37. // 不到后来(当 LoggingMsgSender 被具体化)无法确切知道它是什么。
    38. // 而如果不知道 Company 是什么,就无法知道 class MsgSender看起来像什么
    39. // 更确切地说是没办法知道它是否有个 sendClear 函数
    40. sendClear(info);
    41. // 将“传送后”的信息写至log
    42. }
    43. };
    44. int main(){
    45. MsgSender m;
    46. }

    解决之道:针对 某个Company 产生一个MsgSender 特化版:

    条款44、将与参数无关的代码抽离 templates

    条款45、运用成员函数模板接受所有兼容类型

    条款46、需要类型转换时请为模板定义非成员函数

    条款47、请使用 traits classes 表现类型信息

    条款48、认识template 元编程

    八、定制 new 和 delete

    条款49、 了解 new-handler 的行为

    通过本例了解 new-handler:

    1. #include
    2. // 以下是 operator new 无法分配足够内存时,该被调用的函数
    3. void outOfMem(){
    4. std::cerr << "Unable to satisfy request for memory\n";
    5. std::abort();
    6. }
    7. int main(){
    8. std::set_new_handler(outOfMem);
    9. int* pBigDataArray = new int[10000000000000000000000];
    10. delete pBigDataArray;
    11. }

    看见没,在崩溃前有报错信息了!!

    一个设计良好的 new-handler 函数必须做的事情:

    1、让更多内存可被使用:

    当程序一开始执行就分配一大块内存,而后当new-handler 第一次被调用,将它们释还给程序使用

    2、安装另一个new-handler

    3、卸除 new-handler

    4、抛出 bad_alloc 的异常

    5、不返回,通常调用 abort 或 exit。

    C++ 并不支持 class 专属之 new handlers,但可以自己实现。只需令每个一个class 提供自己的 set_new_handler 和 operator new 即可。

    条款50、了解 new 和 delete 的合理替换时机

    条款51、编写 new 和 delete 时需固守常规

    条款52、写了 placement new 也要写 placement delete

    九、杂项讨论

    条款53、不要轻忽编译器的警告

    条款54、让自己熟悉包括 TR1 在内的标准程序库

    条款55、让自己熟悉 Boost

  • 相关阅读:
    LC142.环形链表II
    react18【系列实用教程】useReducer —— 升级版的 useState (2024最新版)
    何为消息队列?它的特点是什么?
    Packet Tracer - 在 OSPFv2 中传播默认路由
    HTTP+ 加密 + 认证 + 完整性保护 =HTTPS(HTTPS 安全通信机制)
    C++设计模式-创建型设计模式:抽象工厂
    【C++/嵌入式笔试面试八股】二、14.内存管理基础 | 覆盖与交换 | 连续&非连续分配管理
    【数据结构与算法】LinkedList与链表
    沉思篇-剖析Jetpack的ViewModel
    优化jenkins on kubernetes构建性能慢问题
  • 原文地址:https://blog.csdn.net/weixin_45824067/article/details/133069961