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


工作以后已经很久没有在家里过元宵节了,最喜欢的还是黑芝麻馅的。今天早起看到一句话「可能你觉得百无聊赖的日子,正式别人梦寐以求的」,觉得跟肺炎肆意横行下,被守护着的我们很像。

《C++ 沉思录》这本书很长,所以分为上、中、下三篇记录更好。中篇对应书中的第三篇模板(12-22)。从某种意义上讲模板只不过是语法宏的一种受限形式,可以将它们当做编译期函数——以类型作为参数并产生代码。

1. 题外话

以下内容均为个人意见,不喜勿喷,可讨论。

C++ 的出现源自于对 C 的不满,Java 的出现又是源自于对 C++ 的不满,而这几种语言中 C++ 追求的 freedom,C++ freedom 的体现,他能让你用任何的形式去写代码,你不喜欢你可以拒绝,但是你不能替别人拒绝。这也是「一千个人,有一千个 C++ 的原因」。C++ 的设计理念就是我允许你做任何事情,但是做,你就得自己承担代价。比如转移构造函数(ps 这东西我也是第一次见……

struct abc {
  int a;
  int b;
};

struct abc o;
if (o) { // o 是个 struct 类型,此处却可以当做 bool 值进行判断
   ...
}

备注:

  • C++ 类型转换函数

  • 从使用场景上来讲,上面的例子使用的非常少,但 C++ 不会因为,使用场景特别少,就拒绝它

2. 容器相关(12-14)

2.1 设计容器(12)

2.1.1 问题:

问:容器中应该包含什么?

答: 对象,具体的,对象本身 or 副本?副本

问:复制容器意味着什么?

答:复制容器就是复制容器的内容,但上述答案非绝对定义,仍需视具体场景而定,比如 C 和 C++ 的内建集合实现了两种方法:

​ a. 对于结构体实现值语义,复制完成后,两个变量都有这个值的独立的副本;

​ b. 对于数组实现引用语义,复制完成后,两个变量都引用同一个底层对象。

问:怎么获取容器的元素?(ps 其实想问的是获取元素返回的类型是什么

答:以 Container 为例子,返回 T or &T 视使用场景而定

问:怎么区分读和写?

答: 假设上面读返回的 T 类型,那如何写 Container ,Container 提供一个 update 方法用于变更容器的内容。(ps 此方法有坑,仍需补充 p139

问:怎么处理容器的增长?

答:考虑要点什么时候将内存交还给系统,每次增加减少是以单个元素为单位还是以区块为单位

问:容器见是否需要继承关系?

答:不需要

2.1.2 设计一个类似数组的类

实现很简单,不详细说明,有两个问题值得思考:

  • 指针和下标的区别
    • 效率是一个关键点,
    • 指针携带比下标更多的信息,程序通过下表访问的时候,需要知道正在访问的数组,但是通过指针访问的时候则不需要
  • 在类似数组的类中使用指针会带来一个缺陷,类的 resize 会使得原来指向正确位置的指针失效。

2.2 访问容器中的元素(13)

2.2.1 模拟指针

C++ 最基本的设计原则就是用类来表示概念。指针把数组的标识和内部空间结合在一起。

注意:

  • 和使用内建数组时一样,用户还能够轻易得到一个指向 Array 内部的指针,即使 Array 本身不存在了,这个指针仍保留在那里。
  • Array 和指针之间的这种关系迫使我们不得不暴露类的内部机制。因为用户的指针可以指向 Array 内部,所以一旦 Array 占用的内存发生变化肯定会导致用户错误。

如何设计类:

  • 设计者先考虑怎么样的操作或者特性是有用的,然后才考虑实现的问题。
  • 考虑类要包含哪些信息对象,然后考虑人们可能会对信息执行什么操作。
  • 根据以上要点,此处实现的 Point 需要封装一个指向 Array 的指针和这个 Array 中的位置。
template<class T> class Pointer {
  public:
    Pointer(Array<T>& a, unsigned n = 0):ap(&a),sub(n) {}
    Pointer():ap(0),sub(0){}
    T operator*() const{
      if(ap == 0) 
        throw "* of unbound pointer"
      return (*ap)[sub];
    }
    void update(const T& t) {
      if (ap == 0)
          throw "update of unbound Pointer"
      (*ap)[sub] = t
    }
    //……
  private:
    Array<T>* ap
    unsigned sub
}

注意:

  • 如果类 Array 还能让用户获得元素地址,那么在类 Pointer 中使用 update 函数就没有太大意义了,所以此处类 Array 也需要增加 update 函数
  • 此处面临了一个难缠的方便性和安全性之间的权衡。通过使用 update ,可以隐藏实现的方法,并且能够确保用户不遭受由乱用指针而造成的错误。但是这样,会失去构造类似 Array<Array> 这种很有用处的类型的机会。所以应当选用什么方法取决于类会被怎么样使用。
  • 此处仍遗留了一个问题,如果 Array 不存在了,还可能存在一个指向它的某个元素的悬空指针 Pointer。解决该问题很好的办法就是通过引入一个合适的中间层来解决。

2.2.2 中间层 Array_data

此处需要三个类而不是两个类,Array、Pointer 和一个称之为 Array_data 的类。

  • Array 对象指向一个 Array_data 对象
  • Pointer 对象指向一个 Array_data 对象
  • Array_data 对象通过引用计数的方式记录指向的对象个数,当没有对象执行 Array_data 时可以真正销毁该对象。
  • 具体实现见 P157,此处可以考虑执行的对象如果是 const Array 的话,Pointer 需要如何处理?提示继承

2.3 迭代器(14)

一旦开始努力设计一个你确实想要的抽象,就像在词典中增加一个新单词。谨慎小心定义类的回报就是能在实际中理解和预测这些类的对象会做些什么。一旦学会了一个词,就可以在任何合适的情况下使用它们。 比如:如果你不想考虑复制构造函数和赋值操作符应该做些什么,你至少应该花些力气将它们私有化

2.3.1 什么是迭代器

迭代器能够使我们在不暴露容器内部结构的情况下访问容器的元素。设计迭代器的要点;

  • 删除容器中的元素是否会导致迭代器的访问混乱?-> (容器内保存有效的迭代器列表 or 针对容器中的元素采用引用计数)
  • 删除容器的行为如何定义?(将删除容器的操作延后到最后一个迭代器也消失以后
  • 应不应该创建一个没有绑定特定容器的迭代器(应该
  • 应该不应该允许创建一个绑定到了特定容器,但是没有绑定到特定元素的迭代器(不应该

2.3.2 Pointer 类的迭代器如何实现

重写前置++,后置++,前置—, 后置— ,+= 和 -= 的操作符号即可。

3. 序列(15)

C++ 的 USL 标准组件库列表实现源代码超过 900 行,包括 70 个成员和一个友元函数。但是真的需要那么的多方法嘛?

精简指令集容器类,它模仿了 1960 年提出的纯 Lisp 中的列表(list)。最初定义的列表只有 5 个基本概念:

  • nil:没有元素的列表
  • cons(a,b):在列表中,第一个元素为 a,其后的元素为列表 b 中的元素。
  • car(s):s 的第一个元素,而 s 必须是至少有一个元素的列表。
  • cdr(s):s 的第一个元素之外的其他所有元素,其中 s 是至少有一个元素的列表。
  • null(s):如果 s 没有元素则为真,反之则为假;s 必须是个列表。

注意:这些操作中没有一个会改变列表或者列表中的元素,但是可以用它们创建列表和提取出值来。

参考 Lisp 中列表的概念,可以在 C++ 中实现对等 list 类,即可通过以上方法的组合出以下的方法:P184

  • sort 排序
  • flip 逆序
  • == 两个链表是否相等

抽象到底是为了解决什么问题呢?

通用性,有的时候接口真的不必提供很多,提供底层原子接口,通过排列组合的方式,即可实现你需要的功能

4. 模板、泛型、迭代器(16-20)

4.1 作为接口的模板(16)

成功建立一个大规模的系统的关键是在于将它划分成独立处理的小模块,而关键的关键是需要在这些小模块之间定义清晰的接口。本章以 sum 函数为例,用 C++ 的类定义用作接口,以减少系统各部分之间的耦合。

int sum(int *p, int n ) {
  int result = 0
  for (int i = 0; i < n; i++)
    result += p[i];
  return result
}

注意:

  • sum 函数用于一组数相加
  • sum 函数所加的数是整数,并且,它所加整数以一种的特殊的方式存储了起来

4.1.1 迭代方式的分离

我们用迭代器封装遍历整个整数集合的概念。

class Int_iterator {
public:
  Int_iterator(int*, int);
  ~Int_iterator();
  int valid() const;
  int next();
  
  Int_iterator(const Int_iterator&);
  Int_iterator& operator=(const Int_iterator&);
}

注意:上面是第一层抽象手段,仅仅对遍历集合的概念做了抽象

  • 通过将 Int_iterator 抽象为模板类支持任意类型的数组 sum 的方法
  • 通过将 int_iterator 抽象为基类,可以实现表示许多不同类型的迭代器中
  • C++ 的自由之处体现在,任何满足以下条件的类都可以调用 sum 方法:
    • 可以把把 0 转换成该类的对象
    • 对该类的对象定义了+= 操作符
    • 对象具有类似值的语义,这样 sum 函数可以把对象作为值返回

抽象为基类后的 sum 的实现方式:

template<class T> T sum(Iterator<T>& ir) {
  T result = 0;
  while(ir.valid())
    result += ir.next();
  return result
}

缺点:此处由于使用了动态绑定,所以 sum 函数内部循环的时候需要调用虚函数,开销较高 (ps 可以想象怎么优化,参考见 p198

4.2模板和泛型算法(17)

STL (标准模板库)提供了一种概念框架,使用户能够轻松地添加他们自己的算法和数据结构。STL 的一个很大的功劳就是它的关于创建类和迭代类的智能框架。使用这种框架的类可以迅速地使用 STL 所提供的任何泛型算法。

该章节以 find 的查找函数为例一步步泛化该函数。

const int *
find1(const int* array, int n, int x) {
  const int* = array;
  for(int i = 0; i < n; i++){
    if (*p == x)
      return p;
    ++p
  }
  retur 0    
}

注意:可泛化的进阶步骤

  • 泛化元素类型
  • 通过将参数改变为指针,消除对元素个数的依赖
  • 将指针类型泛型化,剔除函数中关于类型的信息
template<class P, class T>
P find6(P start, p beyond, const T& x){
  while(start != beyond && *start != x)
      ++start
   return start
}

思考:查找一个链表类型的结构如何实现?能够直接调用上述泛型好的函数

4.3 泛型迭代器(18)

所谓泛型算法,就是对于所操作的数据结构的细节信息,只加入最低限度的了解。使用某种特定算法本身就确定了一些行为模式,一些我们期望模板参数应该具有的行为模式。

以逆序函数为例:

template<class P, class T>
void reverse<P start, P beyond> {
  while (start < beyond) {
    T t = *start;
    --beyond;
    *start = *beyond;
    *beyond = t;
    ++start;   
  }
}

真的能判断出指针的先后顺序嘛?判断指针的值是否相等与判断两者的先后顺序间存在着很大的区别。

template<class P, class T>
void reverse<P start, P beyond> {
  while (start != beyond) {
    T t = *start;
    --beyond;
    *start = *beyond;
    *beyond = t;
    ++start;   
  }
}

思考下:为什么用 != 的操作符号判断是更加合适的

4.3.1 迭代器的类型

  • 输入迭代器
  • 输出迭代器
  • 前向迭代器
  • 双向迭代器
  • 随机存取迭代器

思考:为什么一个迭代器都要划分出来这么多种类型,难道真的是使用场景就这么多嘛?暂时还没有想通

4.4 使用泛型迭代器(19)

使用看上去像指针的结构时,很容易把它设想成仅仅是一个指针。然而,深入研究某个特定算法使用到的指针操作,就会发现可以针对其他的数据结构模拟指针。

该章节中提及:

  • Constant_iterator 在模拟一个无限值序列
  • ostream_iterator 为输出流迭代器
  • istream_iterator 为输入流迭代器

注意:其中 ostream_iterator 和 istream_iterator 为标准模板库的组成部分

4.4 迭代器配接器(20)

在前边的章节中,我们学习了 find 函数用于查找序列中等于某个值的序列未知,那如果现在要查找序列结构中等于 x 的最后一个元素需要怎么做?
迭代器通常确保了逾尾值有效,但是对于超过头部的值没有相应的保障。所以从尾部开始向头部遍历的方案会存在缺陷。设计一个能够自动反向的迭代器配接器。

实现参考 p238

迭代器配接器除了能够用于反转迭代器方向外,还可以用于边界检查配接器。

5 函数对象和函数配接器(21-22)

5.1 函数对象(21)

函数对象提供了一种方法,将要调用的函数与准备传递给这个函数的隐形参数捆绑起来。这将允许我们使用相对简单的语法来建立复杂的表达式。

函数对象就是把函数当做值来处理,从而带来了很大的灵活性。

不用函数对象和函数配接器的写法:

bool greater1000(int n) {
  return n > 1000;
}

find_id(v.begin(), v.end, greater1000)

借助于函数对象和函数配接器的写法:

bool greater1000(int n) {
  grater<int> gt;
  return (bind2nd(gt, 1000))(n);
}

注意:gt 为函数对象,bind2nd 为函数配接器。

函数对象的更多实现参考 p246

5.2函数配接器(22)

现在还没想清楚具体使用场景和要点,后面来补充吧,参考 p255

6. 碎碎念

写了一天终于写好了,为了防止困还吃了宵夜……,哎,胖了的话一定不是我的错,都是宵夜的错。

Supongo que te gusta

Origin blog.csdn.net/phantom_111/article/details/104230343
Recomendado
Clasificación