《C++ 沉思录》学习笔记——下篇

今日份的电影份额看完,那就默默的《 C++ 沉思录》的学习笔记补充完成吧,毕竟「出来混总得还不是」。下篇是对书中 0-11 章内容的总结,详情如下(ps《魔女》真的好燃,被女主圈粉的一天

1. 序幕(0)

序幕中主要通过 Trace 类逐步抽象优化的过程展示了 C++ 相比于 C 的优势之处。思考下如下 C 和 C++ 实现的 Trace 的代码

  • C

    #include<stdio.h>
    static int noisy = 1;
    void trace(char *s) {
          
          
      if(noisy)
        printf("%s\n", s);
    }
    
    void trace_on() {
          
          
      noisy = 1;
    }
    
    void trace_off(){
          
          
      noisy = 0;
    }
    
  • C++

    #include<stdio.h>
    class Trace{
      public:
        Trace(){
          noisy = 0;
          f = stdout;
        }
        Trace(FILE *ff) {
          noisy = 0;
          f = ff; 
        }
        void print(char *s) {
          if(noisy)
            fprintf(f, "%s", s);
        }
        void on() {
          noisy = 1;
        }
        void off() {
          noisy = 0;
        }
      private:
        int noisy;
        FILE* f;
    }
    

结论:隐藏你该隐藏的,暴露你需要暴露的,如此变简单很多。

思考:你可能很轻易的说服一个写 C 的人转去写 C++ ,但是你如何向没用过 C 的人解释 C++ 呢?

2. 动机(1-3)

编程就是通过建立抽象来忽略那些我们此刻并不重视的因素。C++ 的两个核心思想就是「实用」和「抽象」,比如偏重执行速度快、可移植性强、与硬件和其他软件接口简单等。请记住大多数情况下「现在的折中方案,比未来的理想方案好的多」

2.1 为什么我用 C++(1)

该章节中,作者以 uucp 的工具(软件发布的工具)的优化为例,说明使用 C++ 开发的好处。

uucp 的现状是:

  • 一次只能传输一个文件
  • 发送者无法确保传输是否成功

希望改进的点:

  • 更新完成后通知发送者
  • 允许同时在不同的位置安装一组文件

C 没有内建的可变长数组,以及对于抽象数据的概念和手动管理内存的复杂性,使作者放弃是 C 进行优化,而是选择 C++,从后来 uucp 工具的知名度和易用性也证明了作者的选择是正确的。

2.2 为什么用 C++ 工作(2)

「必须做工具的主人,而不是其他的任何角色」。本人对于这句话,表达无比认同。

2.2.1 质疑

软件工厂 vs 软件开发。

  • 软件工厂是制造大量相同产品的地方,它讲求的是规模效益。
  • 软件工厂在生产过程中充分利用了分工的优势,争取用机器代替人完成机械的工作。从而达到完全消除人力劳动的目的。毕竟相比机器,人更容易产生厌烦的情绪。
  • 软件开发主要是生产数目相对较少的,彼此完全不同的人造产品。

2.2.2 抽象

  • 抽象不是语言的一部分
    • 文件就是个抽象的概念,文件根本就不是物理存在的,文件只是组织长期存储的数据的一种方式,并由程序和数据结构的集合提供支持实现这个抽象
  • 抽象和规范
    • 编写新的抽象给其他程序员用的程序员,往往不得不依靠用户自己去遵守编程语言上的限制。
  • 抽象和内存管理
    • C++ 的构造函数和析构函数
    • 其他语言的垃圾回收机制

2.3 生活在现实世界中(3)

「脚踏实地,仰望星空」

  • 轿车如果不能发动,即使再漂亮又有什么用?同样,一种编程语言写出的程序如果不能在系统中运行起来,再优秀又有什么用。
  • 功能不是编程语言本身的一部分,所以系统中的 C++ 是否具备这种功能,得看系统有没有为它提供这样的环境。

3. 类和继承(4-11)

OOP 的不同理解,指使用继承和动态绑定的编程方式。

  • 继承是一种抽象,它允许程序员在某些时候忽略某些对象间的差异,又在其他的时候利用这些差异。
  • 动态绑定(多态):程序通过指向基类对象的指针或者基类对象的引用调用虚函数,在运行的时候会即多态。

3.1 类设计者的核查表(4)

核查表并不是任务清单。用途是帮助你回忆起可能会忘掉的事情。C++ 的哲学「只为用到的东西付出代价」。

  • 你的类需要一个构造函数吗?
  • 你的数据成员是私有的吗?
  • 你的类需要一个无参的构造函数吗?
  • 是不是每个构造函数初始化所有的数据成员?
  • 类需要析构函数吗?
  • 类需要一个虚析构函数吗?
  • 你的类需要复制构造函数吗?
  • 你的类需要一个负值操作符吗?
  • 你的赋值操作符能正确地将对象赋值给对象本身吗?(ps 是否有检查自赋值的情况
  • 你的类需要定义关系操作符吗?
  • 删除数组时你记住用 delete [] 吗?
  • 记得在复制构造函数和赋值操作符的参数类型中加上 const 了吗?
  • 如果函数有引用参数,它们应该是 const 引用吗?
  • 记得适当地声明成员函数为 const ?

3.2 代理类(5)

如何设计一个 C++ 容器,使它有能力包含类型不同而彼此相关的对象?

问:假设存在 Vehicle 基类,以及派生类 RoadVehicle 、AutoVehicle、Aircraft、Helicopter 四个派生类。如何设计一个容器类,可以同时存储以上四种类型?

解 1:

  • 定义执行基类的指针数组 Vehicle * parking_lot[1000]
  • new 派生类对象 AutoVehicle x = /*……*/
  • 将 x 放入容器中 parking_log[i] = &x

缺点:parking_lot 中存放的是指向 x 的指针,一旦 x 被释放,parking_lot 就不知道指向什么地方了

解 2:

  • 定义执行基类的指针数组 Vehicle * parking_lot[1000]
  • new 派生类对象 AutoVehicle x = /*……*/
  • 将 x 的副本放入容器中 parking_log[i] = new AutoVehicle(x)

缺点:引入了动态内存管理的负担,只有知道将要放入 parking_lot 中对象的静态类型的时候,该方法才会生效。

解 3:

可以复制编译时类型未知的对象。

  • 该造基类 Vehicle
    • 将 Vehicle 声明为纯虚函数
    • 在 Vehicle 增加 copy 的纯虚函数,该方法用于获得一个指向该对象的新建副本
  • 定义代理类 VehicleSurrogate ,该类包含一个指向 Vehicle 基类的成员对象
  • 定义代理类数组 VehicleSurrogate parking_lot[1000]
  • 定义派生类对象 Automobile x
  • 将派生类对象副本放入 parking_lot 数组,parking_log[x] = VehicleSurrogate(x)

缺点:需要为每个对象创建一个代理。具体实现见 P52

3.3 句柄:第一部分(6)

代理类能让我们在一个容器中存储类型不同但是互相关联的对象,该方法的缺点是需要每个对象创建一个代理,然后将代理存储在容器中。句柄可以在保持代理的多态行为的同时,避免不必要的复制。

以表示坐标系中某点为例:

class Point {
  public:
    Point():xval(0),yval(0){}
    Point(int x,int y):xval(x),yval(y){}
    int x() const{return  xval}
    int y() const{return yval}
    Point& x(int xv){
      xval = xv;
      return *this;
    }
    Point& y(int yv){
      yval = yv;
      return *this;
    }
  private:
    int xval, yval;
}

3.3.1 引用技术型句柄

定义引用计数型句柄允许多个句柄绑定到单个对象上,可以避免不必要的对象赋值。

定义一个新类 UPoint :

class UPoint {
  // 所有成员函数都是私有的
  friend class Handle;
  Point p;
  int u;
  
  UPoint():u(1){}
  UPoint(int x, int y):P(x, y), u(1){}
  UPoint(const Point& p0):p(p0), u(1){}
}

Handle 定义

class Handle {
  public:
    Handle();
    Handle(int, int);
    Handle(const Point&);
    Handle(const Handle&);
    Handle& operate=(const Handle&);
    ~Handle();
    int x() const;
    Handle& x(int);
    int y() const;
    Handle& y(int);
  private:
    UPoint* up;
}

注:

  • 构造函数定义直接调用 UPoint 即可
  • 析构函数、赋值构造函数、赋值操作符,需要变更 UPoint 的引用计数。

3.3.2 写实复制

对象和值之间的区别只要在要改变对象的时候才会表现出来。

思考:

Handle h(3,4);
Handle h2 = h;   // 复制 Handle
h2.x(5);         // 修改 Point
int n = h.x();   

问题:修改赋值后,n 的值应该为 3 还是 5?

答:分场景,看 Handle& x(int) 函数实现的是指语义还是指针语义:

指针语义:5

Handle& Handle::x(int x0) {
  up->p.x(x0);
  return *this
}

值语言:3,写实复制(copy on write)

Handle& Handle::x(int x0) {
  if(up->u !=1){
    --up->u;
    up = new UPoint(up->p);
  }
  up->p.x(x0);
  return *this
}

3.4 句柄:第二部分(7)

通过只控制引用计数就能高效「复制」该类的对象,有个明显的缺点:为了把句柄捆绑到类 T 的对象上,必须定义一个具有类型为 T 的成员的新类。

可以将引用计数从数据中分离出来放入它自己的对象:如下图

在这里插入图片描述

3.4.1 对引用计数进行抽象

class UseCount{
  public:
    UseCount();
    UseCount(const UseCount&);
    UseCount& operator=(const UseCount&);
    ~UseCount();
    bool only();
    bool makeOnly();
  private:
    int* p;
}

handle 类的定义如下:

class Handle{
  public:
    // 和上面第六章定义相同
  private:
    Point* p;
    UseCount u;
}

思考:基于 UseCount 的写实复制如何实现,提示 only 函数和 makeOnly 函数的作用,具体见 P73

3.5 一个面向对象程序的范例(8)

基于面向对象的三要素:数据抽象、继承以及动态绑定,创建如下的树,并且能够打印出表达式的形式(-5)*(3+4)
在这里插入图片描述

解:

我们希望在main 函数中调用函数的时候,能够正确的打印表达式:

#include<iostream.h>
int main() {
  Expr t = Expr("*", Expr("-", 5), Expr("+", 3, 5));
  cout << t <<endl;
  t = Expr("*", t, t);
  count << t << endl;
}

打印:

((-5)*(3+4))
(((-5)*(3+4))*((-5)*(3+4)))
  • Expr_node 节点完成对对图中运行符和节点的抽象
    • Expr_node 派生 Int_node 、Unary_node、Binary_node 的抽象
  • Expr 完成对图中箭头的抽象,隐藏底层 Expr_node 的信息,使用户不用关系内存和指向的关系
  • 使用 Expr 抽象后无论是增加计算的 eval 方法的支持,还是增加三元运算符的支持都会非常简单

3.6 一个课堂练习的分析(上)(9)

想要学会游泳,就得亲自下水。

本章使用非抽象的方式编写一系列用以操作「字符图像」的类和函数,所谓字符图像指的是一个可打印的矩形字符阵列。P89

这个问题本身没有什么复杂之处,所以不做详细的解释,只记录作者分析过程中的思路。

3.6.1 接口设计

  • 站在用户的角度上去审视问题,「我希望有些什么操作,以及如何表述这些操作」?
  • 从使用例子中推导出操作的定义形式要比从头苦死冥想地发明这些操作容易很多

3.6.2 实现

在其他部分设计完成之前,先让一小片程序运行起来,这可以让我们确信整体设计在正确的轨道之上。想起「先扛着,再优化」的说法。

3.6.3 缺点

本章的方式只是存储了一个图像的表象,却没有存储其结构。图像对象一旦生成,其结构信息立刻就会丢掉。

3.7 一个课堂练习的分析(下)(10)

牢记一点,不仅要看到眼前的问题,还要看到长远的变化。基于抽象和动态绑定的方式实现,可以更加容易实现重构和迭代。

基本思路是

  • 定义基类 P_Node ,及派生类 String_Pic、Frame_Pic、HCat_Pic、VCat_Pic
  • 定义 Picture 包装各种操作的接口和一个指向 P_Node 的指针

注意:抽象显示的方法,使其使用起来更通用,具体见 P110

3.8 什么时候不应当使用虚函数(11)

只关注程序的行为,同时没有继承关系,那么函数是否为虚函数根本无关紧要。写程序时必须考虑自己正在做什么,仅仅根据规则和习惯思维是不够的。

不适用虚函数的情况:

  • 虚函数的代价并不是十分的高昂,但也不是免费的午餐,在使用它们之前要认真的考虑其开销,这一点十分重要
  • 有些情况下非虚函数能够正确运行,而虚函数却不可以
  • 不是所有的类都是为了继承而设计的

4. 撒花

完结,撒花,终于把这本书笔记写好了,明天一定会是一个好天气,会有好消息的。好困,要睡觉啦!

Guess you like

Origin blog.csdn.net/phantom_111/article/details/104336551