wind2412的部落格✨~


  • 首页

  • 归档

  • 标签

Design Pattern 2

发表于 2017-10-15

之前的笔记实在是有些简陋,如今要正式地重来一遍咯!

  1. Observer
    IMAGE
    如 UML 图所见,Subject 和 Object 都是接口,仅仅定义了对于模式实现的方法,而 ConcreteSubject 和 ConcreteObject 才是实现类。
  • 对象模型:Subject 中含有一个 Object 的 List,代表它要通知的 Objects。而每一个 Object 中含有一个对自己 Subject 的引用。
  • pull模式:Subject 中的 Notify() 方法以及 Object 中的 Update() 方法才是重点。Subject::Notify() 使用轮询的方式来调用它内部每个 Object 的 Object::Update() 方法。而 Object::Update() 方法往往又调用 Subject 的 Subject::getState() 方法来拉取状态。其实这就是 Observer 的 pull方法,即由 Observer 自身去拉取的手段。
  • push模式:当然对应地,还有种实现手段是 push方法。即 Subject 调用 Subject::Notify() 开启 Object::Update() 的时候。传入自身的 State 进去。这样 Object 在实现 Object::Update() 的时候,就不去要从 Subject 中 pull 来,因为 Subject 已经 push 过去了。但是这样实现,弹性比较小,正常的标准还是 pull模式。
  1. Strategy
    IMAGE
    如 UML 图所见,Strategy 是接口类,仅仅定义了 strategy 接口。而同一层继承体系下,分化出来多个 ConcreteStrategy 类。
  • 对象模型:Context 的内部内含一个 Strategy 对象的引用。而 Strategy 是一个接口体系,可以在同一层继承下分化出来多个 SubStrategy。
  • 过程:举 awt 的栗子来说:
    IMAGE
    “Java JFrame jframe(…); // JFrame 是 Container 的一个子对象,即 Context jFrame.setLayout(new FlowLayout()); // 添加一个 Strategy 到 Context 中。 jframe.add(new JButton()); // JFrame::add 方法会在内部调用 FlowLayout 的 Strategy 方法,产生一种布局模式。 “
  • 更多的栗子:同样的栗子还有:Sorter 内部存放一个 SorterStrategy 的引用,可以赋值为 SorterStrategy 体系下的 SubStrategy,必须 BinarySortStrategy 对象,BubbleSortStrategy 对象,HeapSortStrategy 对象等等。
    IMAGE
  • 还有游戏一进去就有的选择困难等级:容易、中等、难、困难、修罗模式这种,也是策略模式的体现!
  1. Factory
    IMAGE
    如 UML 图所见,Product 是一个抽象产品类,下方有继承的 ConcreteProduct 实体的产品类。Creator 是接口工厂,内含多态的 Creator::FactoryMethod 方法,返回一个集成体系下的顶层抽象产品类 Product。ConcreteCreator 是实体工厂,用于生产 ConcreteProduct。
  • 对象模型:Creator 工厂内部没有任何成员变量,只有工厂方法。但是 Creator 工厂内部的方法可以全是 static 的。这就是一种变种,叫做静态工厂。好处在于,不用创建工厂的实体对象了~
  • 变种1:简单的工厂方法:不需要写抽象的 Creator,因为体系比较简单,直接上来就是一个简单的实体+静态工厂。
    IMAGE
  • 变种2:对于生产同一个继承体系的多种产品,可以在工厂中只写一个方法,内部使用 if…elif…else 的手段,最后产生的对象挂接到高层产品接口的句柄上返回。
    IMAGE
    如图,此 Concrete Factory 的 produce() 方法签名返回高层的 Product。
  • 变种3:Product 接口自己就是一个工厂,可以使用多态(子Product 中分别实现)产生继承体系下的多种类型的 ConcreteProduct。当然,这个多态工厂方法必然返回 Product* 的句柄就是,而且可以使用 static 静态方法。
    IMAGE
  • 变种4:ConcreteProduct 本身是一个工厂,可以产生自己。(我们只有一种产品)
    IMAGE
  • 用途:Java 框架中的依赖注入(IoC中) 等等。
  1. Abstract Factory
    IMAGE
    和 Factory 的不同之处仅仅在于,
  • Abstract Factory 是把工厂自身当做产品的。即,生产工厂的工厂。
  • Abstract Factory 由于有上边的这种性质,因此它不再局限于只能像 Factory 一样仅仅能够生产一个体系中的 Product。通过生产出不同类别的工厂,它能够自扩展而生产别的体系的 Product。我们从 UML 图即可看出。
  • 缺点:支持新种类的 Product 变得很麻烦了。
  1. Decorator
    IMAGE
    如 UML 图可见,最高层是一个抽象类 Component。它下方一定有一个直属的子类 ConcreteComponent,要不 Decorator 无法使用了。因为 Decorator 虽然也继承 Component,但是它的内部也有一个 Component 的句柄引用!那个句柄引用的正体显而易见就是那个直属的 ConcreteComponent。因为装饰者模式的策略就是:在同一继承体系中,使用已经实现好的子类,在新的子类中扩展它的功能,变成新的功能。
  • 对象模型:ConcreteComponent 成员变量不能有同类的 Component 句柄,但是 Decorator 的内部必须有一个同类的 Component 句柄!因为 Decorator 要对 ConcreteComponent 的功能进行扩展,从而衍生出自己的新功能,而不用完全从头来写。Decorator 的构造函数要传递一个被扩展的同体系的对象,UML 图中即是 ConcreteComponent。
  • 经典用途:Java 的 IO 系统。比如 BufferedInputStream 啥的都是继承自上层的 InputStream,但是自己构造的时候又要接收一个 InputStream 对象。
    IMAGE
  • 这也就给出了:为什么不在类上用继承进行扩展呢 这个问题的真正答案。很简单,因为装饰者模式比继承要灵活,弹性更大。像我们的 BufferedInputStream,既可以接收别的 FileInputStream,也可以满足比如来自 Socket 的 socket.getInputStream() 的接收这样的场景。所以,如果上层有其他的 InputStream 体系的实现,BufferedInputStream 这个装饰者可以对他们进行统一扩展,而不再仅仅局限于继承的仅仅扩展一个InputStream。这样弹性会变得非常巨大~
  1. Singleton
    IMAGE
    这个实在是基础中的基础,不讲了。但是在多线程之下,Java 中 Singleton 模式也是有很多大坑的,尤其是 LazyEvaluation。
    单例模式的博客
    不过要换成 C++,就连 Eager Singleton 也有大坑!!!
    一开始,我认为只有这样,用 shared_ptr 智能指针才行:
    “`cpp
    // 错误的 Singleton 范例!!
    class Singleton{
    private:
    static shared_ptr singleton;
    Singleton() {}
    public:
    static shared_ptr getSingleton() {
    return singleton;
    }
    };

shared_ptr Singleton::singleton = shared_ptr(new Singleton); // 用原生指针 new 一个 static 变量,最后会造成内存无法释放而泄漏。
“上边的想法确实在我自己看来是还中规中矩的……不过这两天正好读完了 Scott Meyers 的 Effective C++……其中的第四条款指出,static 这种全局变量,虽然会在使用时初始化,但是和 Java 也有本质的不同。Java 的话,在虚拟机 JVM 进行类加载的时候,会有一个“类的初始化”期,这期间会直接初始化所有 static 变量和 static 块。因此,Eager Singleton 在 Java 中可谓是天然适配,上来就默认多线程友好;但是 C++ 不一样……确实在调用之前,static 也会初始化;但是 C++ 标准并没有规定 **多个编译单元中 static 的初始化顺序**!!也就是,在多个模块中,non-local static 变量的初始化顺序是未知的!具体原因请直接看 Meyers 的 Effective C++ Item 04。因此,Meyers 指出,在 C++ 想要 Singleton 安全,必须把 non-local static 变成一个 local static,其实表现形式上,变成了一个**工厂函数**!更改后的程序见下: “cpp
// 正确的 Eager Singleton,而且不再使用恶心的 static new!
class Singleton{
private:
Singleton();
Singleton(const Singleton &);
Singleton& operator= (const Singleton &);
~Singleton();
public:
static Singleton& getSingleton() {
static Singleton singleton;
return singleton;
}
};// 注意一定要注销掉 copy constructor 以及 constructor 还有 assignment operator!!要不,最后会发生可以拷贝的愚蠢现象……比如 Singleton s = getSingleton(),因为调用的是拷贝构造函数……所以必须 private 化。因为 C++ 可以分配在栈上,因此和 Java 全是堆引用不同~
“`
参考了一个博客:ZKT的笔记本,在我迷惑的时候它帮助了我。当然,虽然还没有看,但是《Modern C++ Design》中用 TMP 来 hack 各种设计模式也非常令人憧憬~
至此单例模式完成……没想到竟然引申出了这么多东西……颇有收获~
多例模式的话,就 new 一个 ArrayList 就可以。布局和 FlyWeight 模式有点像,不过那个的内部是一个 HashMap。

  1. Command
    IMAGE
    如 UML 图可见,Client 客户端先要创建一个具有各种动作的 Receiver,虽然 GOF 只画了一个 Action()。其实内部可以有 ActionEat(),ActionDrink(),ActionPlay() 等。而 Command 的分化也可以有 ConcreteEatCommand,ConcreteDrinkCommand 以及 ConcretePlayCommand 等分化类,用于持久化各种 Action 命令。
  • 对象模型:Command 是一个抽象类,内部有一个引用的成员 Receiver 和一个 Execute 方法,Execute 旨在调用 多个 receiver.action(),从而把 receiver.action() 延后化。即,把 receiver.action() 这个瞬间的调用给封装成了一个 Command 对象藉以持久化,达到推迟 receiver.action() 执行的最终目的。ConcreteCommand 是一个实现类。Invoker 其实就是一个 List 的封装,把所有的动作全都使用 Command 延时化,然后集结到一块,通过 Invoker::invoke() 来进行一块执行所有 Command 达到最终的延时操作 actions 的目的,即 MacroCommand。
  • 变种:Undo and Redo:
    可以通过和 Command::Execute() 方法并列编写一个 Command::Undo() 方法。比如 Command::Execute() { receiver.turnOnLightAction(); },那么 Command::Undo() { receiver.turnOffLightAction(); } 就可以被如此实现。
  • 用途:执行一系列延时操作;打 Log。
  1. Adapter
  • 类配接器:
    IMAGE
    如 UML 图可见,Adaptee 是一个我们原本拥有的、但是和正规接口不符的要被配接的对象。我们使用多继承,继承自 Target 和 Adaptee,产生一个新的 Adapter,实现 Target 的正规接口,内部只要调用 Adaptee::SpecificRequest() 即可。其实十分简单。
  • 对象配接器:
    IMAGE
    如 UML 图可见,Adapter 使用了聚合的方式代替了继承的方式。STL 中的容器配接器 stack 也是这么实现的。同样,在 Adapter::Request() 中直接调用 Adaptee::SpecificRequest() 即可。
  1. Facade
    IMAGE
    IMAGE
    外观模式更像一种软件的架构模式,因此没有 UML 图。外观模式其实在我的理解类似于模块化,通过模块化的方式来定制多种模块接口,来呈现出美观的外观。子系统内部的任何变化不会影响到 Facade 接口的变化!
  2. Template Method
    IMAGE
    如 UML 图可见,我们在一个抽象类 AbstractClass 中写下 TemplateMethod() 方法,内部指定代码逻辑的实现,分别按顺序调用 PrimitiveOperation1 和 PrimitiveOperation2 这两个方法。然后后二者使用多态,可以在子类中自由变更。但是 TemplateMethod() 方法永远保持不变。
  • 应用:HttpServlet 中的 doGet(),doPost() 等多种方法。注意它们的命名用 do- 前缀开头。而且在 Servlet 的大逻辑的实现中,这些 do- 方法是按照 TemplateMethod 模式,按照一定的逻辑进行调用的。我们所更改的只是 doGet() 和 doPost() 的内部逻辑,但是外部的大框架逻辑并没有改变!
  1. Iterator
    IMAGE
    迭代器分有内部、外部、静态、动态多种。不过大多数都是内部的“标准”迭代器。C++ STL 完全基于迭代器,让容器的遍历和容器自身完全分离开来,而且可以让外部无法知道容器内部的细节。几乎是最常用的设计模式之一,不过多解释了。
  2. Compose
    IMAGE
    组合模式最常见的就是树形结构。其实它的结构和 Decorator 有些像,但是作用是天壤之别。
  • 例子:这个模式用例子来解释是最好的,就是文件系统。抽象的文件类就是 Component 这个抽象组件节点类,而 File 就是 Leaf,Folder 就是 Composite:它的内部含有一个 List,即 Folder 中可以含有 File,更可以含有其他 Folder。那么这个 UML 图就非常明了了。注意 Compose 模式的重点是树形结构就好。
  1. State
    IMAGE
    状态模式和策略模式比较像。只不过,Strategy 模式中,一个 state 对应多个 Strategy;而 State 模式中,多个 State 都有自己的不同的行为。
    IMAGE
    如上图,每个 Tool 子类都重写了 Tool 接口的四个方法。其实这和 Strategy 真的差不多。只不过 State 模式不同对象代表不同的状态,而 Strategy 总体是在一个大状态之下,而选择了不同的 Strategy 而已。
  2. Proxy
    IMAGE
    由此 UML 图可见,Proxy 和 RealSubject 都继承自 Subject 这个抽象类。都有 Request 方法。而 Proxy 的内部具有一个 RealSubject 的句柄,而 Proxy::Request() 的内部则是调用了 realSubject->Request()。即,Proxy 代理 RealSubject 来进行 RealSubject 的职务。
  • 用途:比如网络的代理软件、操作系统的软链接、智能指针等等。
  1. FlyWeight
    IMAGE
    FlyWeight 模式一般都自带一个工厂,工厂一般都可以是 Singleton Factory,内部一般存放一个 HashMap。其实在个人看来,C++ 的 map 就非常符合 FlyWeight 的观念,尤其是它重载的 operator []。FlyWeight 模式在向内调用 FlyWeightFactory::GetKey(Key key) 方法的时候,如果 key 在内部的 HashMap 中已经存放过,那么就直接取出来它的句柄,把句柄返回去。如果没有存放过,那么就把此 Key 的句柄放到 HashMap 中存放。
  2. Builder
    IMAGE
  • 对象模型:Builder 模式中,有一个 Director 占据主导地位。Director::construct() 是拼装组件的入口函数。Director 内部有一个 Builder 的 reference。Director 通过调用 builder.buildPartA, builder.buildPartB, builder.buildPartC 在 builder 内部产生多个组件。而这个 Builder::retrieveInstance 内部会由 Builder 内部所维护的成员变量 partA, partB, partC 来进行 construct 出一个 Product。注意:在 Builder 内部维护的成员变量仅仅是成员组件 partA/B/C,只有在 retrieveInstance 的时候才会真的生成一个 Product。
  • 如果把 Director 省略掉,那么就和 Factory 差不多了。不过 construct 函数要被从 Director 中迁移到 Builder 中,并且必须在 retrieveInstance 之前调用。
  1. Chain of Responsiblity
    IMAGE
  • 对象模型:Handler 类本质上是一个单向链表,next 即是自己。内部含有 HandleRequest() 方法,用来处理这个 Handler。当然,Handler 可以派生出多种类型,然后共同组成一个链。HandleRequest() 是会对此节点及之后的所有节点做处理的。所以只要在第一个节点处调用 HandleRequest() 即可。
  1. Mediator
    IMAGE
    中介者模式主要是为了避免过度耦合的现象发生的。比如同事,在软件工程的原理中,如果没有中介人,那么同事之间要两两互相认识,太麻烦了。因此中介人即是起到保存所有同事的作用;而且每个同事之中还要保存中介人的引用。
  • 对象模型:如上边所说,每个 College 之中要保存 Mediator;而 Mediator 中保存了所有的 Colledges。
  1. Bridge
    IMAGE
    Wikipedia:如“圆形”、“三角形”归于抽象的“形状”之下,而“画圆”、“画三角”归于实现行为的“画图”类之下,然后由“形状”调用“画图”。
    即:将类的功能层次结构和实现层次结构分离。
    IMAGE
    如图,各个方法的实现全部放在 DisplayImpl 中去,而 Display 和 CountDisplay 中拥有 DisplayImpl 实现方法对象,可以调用 Display 的各个实现方法组合成不同的实现。比如打印一次或者五次。
  2. Prototype
    IMAGE
    其实就是 Java 的 Cloneable 接口。根据深拷贝而形成的 Clone 方法。其实和 C++ 的 copy constructor 差不多。但是实现中要防止循环引用的深拷贝。
  3. Memento
    IMAGE
  • 对象模型:备忘录模式具有 Memento、Originator 以及 Caretaker 三个组件。Memento 在这里是一个 bean,内部具有一个 state 成员,标志着状态。而 Originator::SetMemento() 接收一个 Memento,拆包并且把 state 设置为自己的 state。而 Originator::CreateMemento() 会再把 state 封包成一个 Memento 返回出去。而 Caretaker 即是一个 List,存放多个 Memento。
  • 注意:这三者职责非常明确。Memento 只是一个包装类,只有包装的作用;Originator 原发器具有读取备忘录 的作用;而 Caretaker 只有保存备忘录以及转发的作用,并没有查看备忘录的权限。
  • 注意2:概念:宽接口 —— 正常的功能齐全的接口 窄接口 —— 啥功能都没有的接口。对 Originator 会提供 Memento 的宽接口;而对于其他的类则提供 Memento 的窄接口 —— MementoIF。而其实 MementoIF 是定义在全局的,而 Originator 中才定义了 Memento 继承于 MementoIF。也就是,只有 Originator 才能解开 MementoIF 的秘密~
    备忘录模式详解
  • 对所有对象都提供正常的 Memento 的,叫做白箱 Memento 模式;而只对于 Originator 才提供正常宽接口 Memento 的,对于其他对象统统提供 MementoIF 的,叫做黑箱 Memento 模式。这种模式对于封装性非常有用!!
  1. Visitor
    IMAGE
    Visitor 模式非常有用,也很复杂。写 parser 必备。各种 AST 子节点都需要 visitor 模式来进行对子节点的一一访问,配上模板的类型推导可食用。具体代码见我的上一篇博客。
  2. Interpreter
    IMAGE
    解释器模式就略了。其实就是写一个简单的 parser。非终结符和终结符,就是类似 BNF 范式。

设计模式

发表于 2017-10-10

学习设计模式的途中,看各种网上的资料和博客,也积累了一点东西,拿出来分享一下~


创建型设计模式

  1. Singleton
    很好的博客,在多线程方面讲的非常透彻。
  2. Builder

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    abstract class Builder {
    protected String name;
    protected int level;
    public void setName(String name) {
    this.name = name;
    }
    public void setLevel(int level) {
    this.level = level;
    }
    public abstract Instance build();
    }
    abstract class Director {
    protected Builder builder;
    protected String name;
    public Director(Builder builder) {
    this.builder = builder;
    }
    public Instance construct(Args...args) {
    builder.setName(args[0]);
    builder.setLevel(args[1]);
    ......
    return builder.build();
    }
    }
    // 和工厂方法的不同在于:builder 还有一个 director 来使用内部的 builder 来协助维持总体的组装。
  3. Prototype

    1
    2
    // 原型模式实际上就在于实现 Cloneable 接口。能够让对象进行深拷贝,而不用一个一个全部重新构造。
    // 不过,要注意防止循环引用的现象发生。
  4. Abstract Factory
    抽象工厂

    1
    2
    抽象工厂是 “工厂的工厂”。这类抽象的工厂通过传入的参数来返回工厂对象。因而这类抽象工厂是 “工厂的产生器”。
    工厂内部可以产生不同类型的对象。这些对象在被继承的时候,可以把没有关系但是却又不得不继承的方法设置为空方法。
  5. Factory
    工厂

    1
    工厂类顾名思义,其实就是创建一个工厂来通过传入的参数来返回创建好的产品对象。比较常用。这个工厂只生产一个借口下的多个子类。而 Abstract Factory 可以生产多个不同接口类下的子方法。

行为型设计模式

  1. Strategy

    1
    // 使用[继承体系],继承同一个接口,只不过每种子类有不同的实现。然后通过把这继承体系的对象当做参数,可以选择不同的算法。其实就像 C++ STL 中的非标准的 std::add, std::sub, std::mul 等等。每个重载 operator() 都是不同的算法。通过把这些 adaptor 传递给接受他们的函数,就可以相当于使用这些算法了。
  2. State

    1
    2
    3
    4
    5
    6
    7
    8
    // State 首先是一个接口而引发的继承体系。State 会有多种。
    // 其实吧,如果我有一个 boy 对象,他的状态发生了改变的话,明明是 boy 自身的变动,却由程序员[我]来手动调用 boy.setState(new BadState()) 来帮他上状态,确实很诡异...... 所以这里,为了符合语义也一定要反过来。也就是,状态 State 占主导,接收参数来改变 boy。即:
    class BadState extends State{
    void doAction(Boy boy) {
    boy.setState(this); // 还是这样顺眼一些......
    }
    }
  3. Chain of Responsibility (责任链模式)
    真是玄奥……并不明白它会被应用在哪里……

    1
    2
    // 其实本质上是一个链表结构。然后每个节点被封装了一个 level,节点内部有一个处理责任链的方法,这个方法是递归实现的,接收另一个 need_level 参数,会对每个接下来的节点进行处理。整个链表上 this.level > need_level 的节点都将会被忽略掉。也就是传入参数 need_level 是紧急的程度。紧急程度越高,就越会激活大部分的责任链了。
    这个还要好好看一看复习下咯。
  4. Interpreter
    解释器模式……parse 成 AST……

    1
    额呵呵呵......写一个解释器 ????? 有待考察......
  5. Command
    命令模式:把函数的执行包装成一个对象!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    // 这样把 “函数的执行动作” 变得 “实体化” 了,就可以实现数据库的事务等等了。这样,任务的执行都是被记录的,而且可以批量进行执行。
    // 统括思想:创建一个具有继承体系的 “命令对象”,本质是一个接口继承而来。这个通用的统一接口下,类似策略模式以及配接器模式,会对要被执行的函数进行一个封装,即把不同的策略封装在不同的类产生的对象中,但是接口统一地去进行【持久化】。然后,如果要执行一套连续的动作的话,只需要把保存在 list 中的这些对象统一取出来,再进行逐一遍历,然后调用统一的接口函数即可。
    【在学习设计模式的时候,如果想要学得深入的话,就要先想想如果是自己的话,会怎么做。然后再学习前人们的成果,最后还一定要多加重复才行。】
    // 比如我有一个类,它有多种动作:
    class Action{
    void buy();
    void sell();
    void eat();
    }
    // 为了把这些瞬时的 “动作” 持久化成为 “永久的”,我们创建一套 Order (命令)类组合:
    interface Order{
    void executeAction();
    }
    class BuyOrder implements Order{
    private Action a;
    public BuyOrder(Action a){
    this.a = a;
    }
    @Override
    void executeAction(){
    a.buy(); // 这样的话,一个 “过程” 就被封装成为了一个类对象了!!再包装一层永远是解决问题的策略。
    }
    }
    class SellOrder implements Order{
    private Action a;
    public SellOrder(Action a){
    this.a = a;
    }
    @Override
    void executeAction(){
    a.sell();
    }
    }
    class EatOrder implements Order{
    private Action a;
    public EatOrder(Action a){
    this.a = a;
    }
    @Override
    void executeAction(){
    a.eat();
    }
    }
  6. Observer

    1
    可以对应 Java GUI 中的 “通知” 的 action。适用于一个组件发生了改变,然后可以对多个其他的组件进行主动通知的设计模式。当然,需要这被通知的多个组件具有相同的接口才行!!比如,都实现了统一的 update() 方法!要不,如果方法签名都不统一,上哪去调用去......
  7. Memento (备忘录模式)
    Memento

    1
    2
    可以适用于游戏存档的场合。
    里边有 3 个类:Memento(包装一个 state 的结构)、Originator(产生 Memento 的工厂)、CareTaker(一个 Memento 的 list)。记住这三点,就非常简单。而 CareTaker 是通过操纵 Originator 来间接产生 Memento 对象存档的。
  8. Iterator

    1
    STL 中全部如此。略了。
  9. Template Method

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    在 abstract 基类中定义一个 public final 方法作为指定好的模板算法骨架,这个骨架会按某种顺序调用此类的其他方法。而继承此 abstract 类的子类,只需要重写这些 “其他方法” 就可以了。
    abstract class Template{
    abstract void first();
    abstract void second();
    abstract void third();
    public final void executeInOrder(){
    first(); // 执行模板已经定义好,**必须**按照顺序执行。子类只需要重写 first(), second(), third() 即可。
    second();
    third();
    }
    }
  10. Visitor
    Parser 的 Visitor 设计模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    // 轮大讲的非常好~
    class IVisitor{
    public:
    virtual ~IVisitor() = default;
    virtual void visit(ValueExpr *node) = 0;
    virtual void visit(AddExpr *node) = 0;
    virtual void visit(MulExpr *node) = 0;
    }
    class Expr{
    public:
    virtual ~Expr() = 0;
    virtual void accept(IVisitor *visitor) = 0;
    }
    class ValueExpr : public Expr{
    public: // 注意:数据全是 public 的。如果要设置 private,请设置 IVisitor 为友元。
    double value;
    virtual void accept(IVisitor *visitor) override{
    visitor->visit(this);
    }
    }
    class AddExpr : public Expr{
    public:
    shared_ptr<Expr> x;
    shared_ptr<Expr> y;
    virtual void accept(IVisitor *visitor) override{
    visitor->visit(this);
    }
    }
    class MulExpr : public Expr{
    public:
    shared_ptr<Expr> x;
    shared_ptr<Expr> y;
    virtual void accept(IVisitor *visitor) override{ // 接受参观者
    visitor->visit(this); // 参观者进行访问动作,即改写 visitor 自身的 result
    }
    }
    class EvalVisitor : public IVisitor{
    public:
    double result;
    double call(Expr *node) { // visitor 呼叫(通知,也就是按门铃)被参观者叫他接受 (accpet) 自己 (给自己开门)
    node->accept(this);
    return result;
    }
    void visit(ValueExpr *node) override{
    result = node->value;
    }
    void visit(AddExpr *node) override{ // 已经进入了 AddExpr
    result = call(node->x.get()) + call(node->y.get()); // 还想访问 AddExpr 内部的两个元素。
    }
    void visit(MulExpr *node) override{
    result = call(node->x.get()) + call(node->y.get());
    }
    }
    double Eval(shared_ptr<Expr> node) {
    EvalVisitor visitor;
    node->accept(&visitor); // accept 才是真的入口???
    return visitor.result; // 其实这两句写成一句 call 也行。不过一般 call 都是被设置成为 private 的吧。
    }
  11. Mediator (中介者模式)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 中介者模式顾名思义,其实就是提供了一个中介,来降低双方通信的复杂度。在双方要进行通信的时候,都会调用中介者来显示信息。
    class Chatroom {
    public static showMessage(String msg) { sysout...; }
    }
    class User {
    public sendMsg(String msg) {
    Chatroom.showMessage(msg); // 这样。直接调用中介者 Chatroom 显示在 Chatroom 的屏幕上。
    }
    }
    public static void main (String[] args) {
    User jack = new User();
    User tom = new User();
    jack.sendMsg("hello!");
    tom.sendMsg("hi!");
    }

结构型设计模式

  1. Decorator
    Java IO 中经常用到。BufferedInputStream 等其实就是对底层 IO 的封装。
    在继承体系当中,子类含有一个接口类的对象,虽然这个子类也是接口类的对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    interface Worker{
    void work();
    }
    class Son implements Worker{
    void work() {
    ...
    }
    }
    class Mother implements Worker{
    private Worker worker; // 内部有一个接口对象,可能是 Son。
    public Mother(Worker worker){
    this.worker = worker;
    }
    void work() {
    worker.work(); // 基于内部的 worker 写好的 work 进行 [装饰扩展]。
    ... // 扩展
    }
    }
  2. Proxy

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 套路和上边的 Decorator 很像,不过它的目的并不是用于加强,而是用于控制。
    interface Worker{
    void work();
    }
    class Son implements Worker{
    void work() {
    ...
    }
    }
    class WorkerProxy implements Worker{ // 也要继承 Worker。因为要保持接口一致。
    private Son son = null; // 对象是固定的 Son。因为是此类是针对 特定类 特化的 代理类。
    public WorkerProxy() {
    // nothing
    }
    void work() {
    if (son == null) {
    son = new Son(); // 没有的话就创建。相当于 Lazy Singleton。
    }
    son.work(); // 不增强。而是直接调用 son。这就是个代理,要尊重原作,并不像 Decorator 一样加以修改。
    // 第二次调用此 work 方法,son 就不必 new 了。这点和 Decorator 差不多。
    }
    }
  3. Composite

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    类比于文件系统。
    所有的一切,包括容器类全都继承自同一个接口。适用于树形结构。
    abstract class File {
    ...
    }
    class Folder extends File {
    File[] files;
    ...
    };
    class TXT extends File{
    ...
    }
    class PNG extends File{
    ...
    }
  4. Bridge
    Bridge

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 调用方式:(先写出调用方式可能更好一些。)
    public static void main(String[] args) {
    Shape redCircle = new Circle(100,100, 10, new RedCircle());
    Shape greenCircle = new Circle(100,100, 10, new GreenCircle());
    redCircle.draw();
    greenCircle.draw();
    }
    // 其中 Circle 是直接继承于 Shape 的。RedCircle 和 GreenCircle 是直接实现于 DrawAPI 的。
    // Circle.draw() 的时候,内部会直接调用成员变量 RedCircle.drawAPI() 和 GreenCircle.drawAPI() 来进行转发。
  5. Adapter

    1
    配接器模式,在 STL 中用得非常多。比如 vector -> stack,还是 std::bind2nd 等等。
  6. Flyweight (享元模式)

    1
    2
    其实就是使用 HashMap 来保存和提取公有对象而已。和 STL 的 map 的 operator[] 原理简直一模一样。
    当然,非常有可能和 [工厂模式] 以及 [单例模式] 联协使用。可以用一个 Singleton 的 Factory 内部包装一个 HashMap 的意思~
  7. Facade (外观模式)
    Facade

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // 用 Shape 这个接口产生 Circle,Rectangle 以及 Square 三个类型,共有 draw() 接口。
    // 为了隐藏逐个调用的复杂性,使用一个更高层次的类 ShapeMaker 来封装,client 不必知道系统内部的复杂性,整个系统只需要提供一个封装好的 “接待员” 即可。
    // 这段代码比较能够反映核心,因而拷贝过来。
    public class ShapeMaker {
    private Shape circle;
    private Shape rectangle;
    private Shape square;
    public ShapeMaker() {
    circle = new Circle();
    rectangle = new Rectangle();
    square = new Square();
    }
    public void drawCircle(){
    circle.draw();
    }
    public void drawRectangle(){
    rectangle.draw();
    }
    public void drawSquare(){
    square.draw();
    }
    }

Java 字节码 .class 文件解析:写一个 javap 工具

发表于 2017-10-08

我们的题目

最近闲着没事想要了解一下 Java .class 文件的结构。然后想要对它进行一下解析。毕竟解析二进制码不是一件特别麻烦的事情,以前也干过,其实是非常有意思的。因此打算按照 JVM 规范对 .class 进行解析。总共用 C++ 写了不到 3k 行代码,应该能够按照 JVM 规范所说的进行完美的解析了。其实一点也不难,按照 JVM 规范的 $4 第四章给定的数据结构和规范进行直接对字节码进行解析即可。
下面说一下详细的方法和实践步骤。
对 .class 文件的解析是非常轻松加愉快的,如果想要看代码的话,请移步 wind2412的github – 对 .class 文件进行解析 我的代码仓库进行查看完整的代码。其实头文件就是 JVM 规范的所有数据结构的集合,#define 的是各种类型的变量结构。当然,为了解析愉快,我在每个数据结构的内部全都塞进去了一个的 friend std::ifstream & operator >> (std::ifstream & f, TYPE & i); 结构进行结构式的从 .class 文件通过输入流进行读取字节码来填充进数据结构的内部。那我就让我们进入愉(wu)快(liao)的 .class 文件解析之旅吧~
本文的定位并不在于一步一步指导要怎么 parse .class file。而是要把踩过的坑都列出来。毕竟网上的菊苣们 hack 这个的也有不少,网上这方面文章还是有很多的,我没有必要重复造轮子(大雾。因此,本篇的主旨在于对想要进行这项工作的童鞋进行一个宏观的 “坑的解说(逃”。

需要之物

  1. JVM 的规范,最好要 SE 8 版本的规范,因为 SE 7 中有的部分和 8 不一样,改动其实也不太小(。当然为了效率我看的是中文版的,中文版的有不少错误QAQ。最后还是看的英文进行的解析。而且毕竟英文版的放在 oracle 官网上,因此数据结构可以直接进行复制粘贴(雾。
  2. C++ 基本语法的熟悉即可。当然用 Java 也不是不可以,但是据说有 Java 有内置的一个什么 xxxClassParser 在 sun 包下,可以直接经过人家的 API 进行解析的(逃。Java 毕竟全是引用,在 parse annotation 那里要好办很多QAQ,用 C++ 必须强行使用指针咯。
  3. java 环境。你必须要有 javap 和 hexdump 两样神器:一个是直接把 .class 文件反汇编,尤其是 javap -verbose 命令,你几乎可以查看到非常完整的反汇编代码,解释得非常清楚;另一个是直接暴力查看 .class 文件的字节码,两者强强联合,使用更佳!!而我的代码的定位,就是写一个 javap -verbose 工具。力争输出和 javap -verbose 一样~

BEGIN!

那么我们就开始吧!

  1. 预读。首先我们要知道,写 parser 的话,如果想要避免回溯,就一定要采取预读 peek 的策略。Java 官方在 .class 文件的制作上,也是采取了 peek 的策略。比如 LL(1) 文法,就是采取预读 1 个 token 的方法。而 LR(0) 文法就是预读 0 个 token。不过字节码毕竟是 bytecode,那么这个 token 当然就是一个 byte 啦!当然随着往下写我们就知道,因为一个 byte 最多也就能表示 256 个数字,因此可能太小了。要表示类别的数量,很有可能需要两个 bytes。所以我在里边写了 peek1() 和 peek2() 两个方法来进行预读工作。有了 C++ 的流,我们可以非常轻易地进行从流缓冲的读取。还是非常赚的~
  2. 注意你机器是大端序还是小端序。我的 mac 是小端序的。因此读入的时候,保存在变量中是反向存放的。所以,这样就会引发 “读的是反的” 的情况。因此,在 *nix 环境下,我们可以引用 POSIX 规范的 #include <arpa/inet.h> 头文件,使用其中的 htons() 和 htonl() 函数进行比特的逆转。当然,这两个函数其实真正是用在网络编程当中的。在 read2() 和 read4() 中,我用到了这两个函数。
  3. 关于 unicode。因为 Java 字节码全都是使用 Java 改进的 UTF8 编码进行存放的。如果我们要保存的话,就一定要将其转为 Unicode。Java 的 String 本身就支持 Unicode,自然不必多说,但是 C++ 的 std::string 不行啊……因为它某种意义上讲根本就不是一个 string……顶多算是一个 char[] 数组。但是我们有 std::wstring,它是按照 Unicode 进行存放的。其实 Java 毕竟是 Unicode,我们可以使用中文编程的,比如 class 蛤蛤 { public static int 膜 = 4; },这类的情况也需要我们进行考虑。当然,用通用的 Unicode 准没错就是了~

数据结构

JVM 规范的第四章把数据结构全用伪代码列给我们了。当然,这其中有很多坑。我会一一列举出来。

  1. 在 Java 的规范中,有非常多的继承关系。而如何对这些继承的结构进行识别,我们就需要用到 peek 来进行 distribute。也就是,使用 “前看一个或者两个字符” 来进行选择到底该选择哪个类进行使用。举个例子,比如常量池 constant_pool,我们就可以看代码:constant_pool 的继承关系,这个 cp_info 结构体 就是一个基类。预先读入 peek 一个一字节的 tag 之后,我们就会按照 tag 的大小,按照这几个 #define 的变量,见 constant_pool 的 tags,来进行选择子类的类别,并且按照子类内部的成员向内进行填充 bytecode。这之中比较坑的是 long 和 double,因为他们在常量池当中要占据两个位置……而正常的变量都占据一个位置……我还没有实现过简单的 JVM,并不知道这么做的深意……而且常量池的索引是从 1 开始的,而不是从 0 开始,我也并不明白这个的深意……不敢妄测不敢妄测。不过这个是比较坑的部分,必须小心谨慎,否则可能会一调试调试一个晚上(QAQ。
  2. 如果遇到类内部的数组(而且不定长度,是按照类内部的另一个成员变量的数值来当做长度的),这样,编译过程中长度不确定的数组是不被允许的。必须要等到运行时才能进行。因此,必须要使用 new 来在堆上在运行时分配。其实这里是比较好考虑到的。但是,由于 C++ 的 RAII 特性,我们就要在这样的类中写析构函数…… 这样不断的申请释放不断 copy 代码实在是累死了QAQ……
  3. 常量池 parse 完了之后,我们可以说是完成了 1/4 的工作吧。不过如果常量池 parse 完了,后边的工作难度就大大降低了。我们就可以 parse field and methods and interfaces。这三者其实都差不多,只不过 method 是最难的。因为内部含有大量的运行时字节码。这样的字节码将会是非常麻烦的,因为 java -verbose 的输出非常麻烦……其实截止到现在,我还并没有写完,因为实在是想要和它官方的 output 一模一样的话,实在是工作量奇大,说不定要到 4k 行去……而且要非常明确各个字节码的意义才行。等以后写一个 simple jvm 的时候再说吧。那么,比较坑的地方其实并不在于别的,而是在于 annotation(注解) 的解析。因为如果你仔细观察过 annotation 的数据结构实现,我们就会发现……其实 element_value 结构体的内部有一个 value_t* 的指针,当然这个 value_t 是继承体系中的根类。它可以变成 annotation 子类。而 annotation 结构体的内部又有着 element_value 的对象……你没看错这个其实是循环的。要读入必须要递归。读者看到这里,可能会以为,“没啥打不了的反正就是递归啊~”,会有这种想法不奇怪……毕竟我说得比较简单(逃,其实 java 中还有这种用法,即像这种一样,我还是列在下边吧:
    1
    2
    3
    4
    5
    6
    @IA
    @IB(name = "ha")
    @IC(name = "ok", a = @IA, b = { @IB(name = "a"), @IB(name = "b"), @IB()})
    public void haha () {
    }

Java 的 annotation 赋值 annotation 的猥琐用法……. oh 如果有的童鞋说 “我已经知道了”,那么请忽略我说的话 QAQ,我也是在写程序的过程中才发现这个奇技淫巧的用法的……QAQ,尤其是这货在 javap -verbose 下所产生的反汇编字符串是:

1
2
3
4
RuntimeInvisibleAnnotations:
0: #10() // IA,就是 #10 (是常量池编号) 的构造函数式空构造函数。
1: #11(#12=s#13) // IB,就是 #11 的构造函数是 #12,也就是 name,它的构造函数的参数是 #13 "ha",当然,"s" 代表的语义是 #13 是一个 Unicode 的 String。如下文一般的 "@" 表示参数是一个 annotation(递归的)。这些都可以在文档中查到。
2: #14(#12=s#15,#16=@#10(),#17=[@#11(#12=s#16),@#11(#12=s#17),@#11()])

对照着上边的源代码看,我们可以知道:IA, IB, IC 这三个 RuntimeInvisibleAnnotations 其实在反汇编码中代表着 #10, #11 和 #14。而后边跟随着的括号即是构造函数。注释我已经写在了后边,当然,最后一个我故意没有写,还请看官自行考虑~ 这部分的代码在这里,可以看到用了一个函数内部套着一个内部的 lambda 表达式。lambda 表达式内部有多个分支,其中如果是把 annotation 赋给 annotation 递归的情况,就会由内部的 inner lambda 呼叫外部的函数;如果要是最后一种情况,即是把数组赋给 annotation 的话,由于 .class 文件的数据结构内部表示问题,我们会让 inner lambda 递归呼叫自身。这个问题就留给读者吧~

尾声

搞定了上边这些,估计也到了尾声了。放一张图来表示内心的鸡冻(逃:
IMG_0966.JPG
那么这篇文章也要结束了~接下来两个月,我打算写一个 simple STL,然后仔细研读虚拟机规范,实现一个简单的 JVM。就这样吧。

Effective STL 笔记

发表于 2017-09-26

第零章 前言


建议:Effective STL 这本书非常有深度,在细节上尤为体现。建议读这本书之前,先仔细研读侯捷老师的《STL 源码剖析》,然后好好研究 sgi-stl 源码。对大致框架掌握仔细之后,可以阅读《Effective STL》加以查缺补漏以及更加精进。


第一章 容器


  1. [1] vector、string (其实还应该有基于 heap 的 priority_queue 和 queue)这类连续容器的插入、删除会对所插入的位置的后边的所有迭代器、指针以及引用变得无效。而且,如果插入操作导致了容器扩容,将会使整个容器的迭代器、指针和引用全部失效。因此,一定要默认“vector、string 的插入和删除一定会造成所有迭代器、指针、引用失效”,这样才不会发生错误和 UB 行为。 [2] list、map、set、multimap、multiset 等等基于节点的容器(链表、红黑树) 上的插入、删除操作从来不会使任何迭代器、指针和引用发生无效(除了你指向一个正在删除的元素)。因此可以尽情使用。 [3] deque 比较特殊,它是唯一一个 “当插入操作发生在开头/末尾的时候,迭代器会发生无效,但是指针和引用不会发生无效的 STL 标准容器”。因为它基于浅拷贝来进行内存的复制。
  2. 对 vector 等的使用封装化。typedef vector CCList; 这样以后 vector 可以换成 deque。必要的使用使用 template class 来进行封装细节。
  3. STL 容器全是复制来实现的。但是这对于继承和多态比较麻烦,因为传入 vector\ 的 Rect 对象的特有信息会被 slice(剥离),而且无法找回。这样的话就必须【传入指针】,即使用 vector\ 来进行操作,这样才能够一劳永逸。当然,指针的释放是头痛的问题。我们可以使用智能指针~~
  4. 调用 empty() 来代替 size() == 0,因为 empty() 只是比较 begin 和 end 迭代器,但是 size() 需要现算。size() 是 O(n) 的本质是由于 STL 牺牲了 size() 的时间复杂度,把 O(1) 的时间复杂度给了 splice()。这两个的时间复杂度必然有一个是 O(1) 一个是 O(n)。因为 splice 的参数是两个迭代器,他们之间元素的数目是不知道的。如果要更新 size,就必须遍历。如果想要保证 splice 的 O(1),就不能去遍历,而牺牲掉 size。这种矛盾构成了 size 可能是 O(n) 实现的格局。
  5. 很重要的 v.assign(iter1, iter2) 区间分配函数。意思就是把 v 变成 iter1 ~ iter2 之间的东西。
  6. list<int> l(istream_iterator<int>(f), istream_iterator<int>()); 这样写并不可行。应该这样写:list<int> l((istream_iterator<int>(f)), istream_iterator<int>());。这是 C++ 编译器的问题。
  7. 如果 使用 vector 的话,注意 v.push_back(new T) 在 v 析构的时候,T 是不会被析构的。这样就会内存泄漏。于是,我们【一定要】vector> !!
  8. 不要使用 vector>。因为这个智能指针是会【转移】对象所有权,当 v.push_back(new T) 的时候没啥问题,但是当 T temp = v[3] 的时候,会造成 v[3] 的所有权被转移到了 temp,而造成 v[3] 本身 UB。
  9. 很重要!!选择迭代器删除的方法,还要保证迭代器不失效——可以结合 clause 1 一起看!![1] 对于连续内存的容器 vector、string、deque(多个连续内存的容器),可以使用 erase-remove 的手段清除。即,v.erase(remove(v.begin(), v.end(), 3), v.end())。[2] 对于标准关联容器 set、multiset、map、multimap 的时候,使用任何名为 remove 的方法都是错误的。因为他们内部没有 remove 方法,只有 erase 方法。且使用 std::remove 可能会覆盖容器的值甚至是破坏容器。因为 std::remove 会把被删除的值通过迭代器交换到容器后边,而这 4 个容器全基于红黑树,任何的移动都会导致树结构的崩溃。可以见第 32 条。所以应该使用 c.erase(3)。[3] 对于 list 来说,两种方式都是可行的。但是一般来说都会使用 vector 和 string 的 erase-remove 方法。不造为啥……。[4] 如果是条件删除 remove-if 的话,对 [1] 而言只要改成 erase-remove_if 即可,很简单。对于 [3] 的话却变成了使用类似关联容器的 l.remove_if() 更加简单了。不过对于 [2] 的话,就要使用一些技巧。因为虽然关联容器的迭代器几乎不会失效,但是在删除元素的时候指向该元素的迭代器是肯定会失效的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    set<int> c;
    // ...
    for (auto i = c.begin(); i != c.end(); i ++) { // 使用 auto i。该拷贝就要拷贝。使用 auto && 因为 iterator 本身并没有 new 内存,因此效率是一样的,也是浅拷贝全部东西。
    if(...) c.erase(i); // 但是很不幸这是错的。因为 i 被删掉的时候已经失效。这时 i 有可能是未知值,也有可能指向其他地方。而且树也会发生旋转,结构发生改变。所以我们要修改如下:
    }
    // 改成:
    for (auto i = c.begin(); i != c.end(); ) { // 需要自己写遍历循环了。因为 vector 的 erase 一次能删一堆(因为 remove 把该删的都调到最后了),而 set 的 erase 一次只能删除迭代器指向的那个(因为 set 不能 remove),但是其实这两者的 erase 接口都是一样的,都可以删除一个 iter 或者删除一整个 iter1~iter2 的区间。区别仅仅在于有没有 remove 来扶一下。且 vector::erase() 有返回值 iter,返回被 remove 的元素的下一个;而 set::erase() 返回 void。
    if(...) c.erase(i++); // 这样的变换是非常正确的。不过乍一看好像是错的而已。容易误解成 “这不是 c.erase(i) 然后 i 再 ++ 么?不是和原先一样吗” 的感觉。不过这是错觉。其实 i 是一个 iterator class。当 i++ 的时候,其实是先调用了 operator ++ (int n)。而后内部迭代器先发生了改变,然后返回一份 copy 的发生变化前的 iterator。然后 c.erase(i),这时树的结构会发生变化。这时我们参照 clause [1],对于关联容器的插入和删除不会让迭代器、引用和指针发生失效。而且增加过的 i 也确实牢牢地指向下一个节点,纵使树发生了旋转。(因为树发生旋转仅仅是因为树节点的内部指针变化了,而节点自身并没有发生内存位置的移动)。对数据结构掌握深入的话,就会明白这一切。
    else i ++; // 我写得太多了......别忘了这还有一句(
    // 且,其实我们对 i++ 这个操作一直具有着误解。c.erase(i++) 并不等于 c.erase(i) 然后 i++ 啊。而是 i 的迭代器正体已经变化了,而又返回一个过去的 temp_i 而已。其实这是一共两份副本。其实原先学 C 语言的时候,理解的 int i = 0; func(i++) 以为是先 func(i) 然后 i++ 呢......虽然结果上来讲是对的,但是语义却是完全错的......因为【当时 i 就变了,只不过是返回副本而已,造成了看起来好像是 i 在做完 func(i) 之后而在下一句之前才 ++ 的假象。】
    }

    [5] 如果要打 log 记录被删除元素的话,由于关联容器是一个一个删,反而好打 log。而 vector、string 是批量删除,反而需要改成一个一个删除的形式才能打 log。

    1
    2
    3
    4
    5
    6
    7
    // 既然要一个一个删,那就只能使用删除单个迭代器的 erase 了。但是要注意 vector 删除元素会造成迭代器失效!因此,我们需要使用 vector<T>::erase 的返回值,该返回值是被删除元素的下一个元素的有效迭代器。
    for(auto i = c.begin(); i != c.end(); ) {
    if (...) {
    Log << .... << *i << endl;
    i = c.erase(i); // 重点!!
    } else ++i; // 记住正常使用迭代器的时候,尽量用 ++i 而不是 i++。因为 i++ 要创建一个副本!!太浪费了!!
    }
  10. 最重要!!这涉及到如何去编写一个 Allocator!!见第 10 条末尾!!记住提供 nested-rebind 模板,然后 allocate() 接收参数为要分配的对象个数,而不是字节总数!!和 sgi 的 simple_allocator 的外层包装(STL规范)一样!!但是底层实现随你便。而且,必须返回 T* 指针!!规范的allocator写法

  11. 自己编写的多个 allocator 要等价。allocate/construct/destruct/deallocate,而且不能有 non-static 成员变量,一定要实现“共享”。
  12. STL 标准规定:[1] 多个线程对同一个容器的“读”操作是安全的。但是之中不能写操作。[2] 多个线程对不同的容器做写入操作时安全的。?????????????
    【重要:】我们可以把锁的 lock(container) 封装到一个 Lock 类的 constructor 中,然后把 unlock(container) 封装到 Lock 类的 destructor 中。这样的话,Lock 的构造函数就需要接收一个 Container,内部存放一个 private 的 Container & 引用。然后这样,就不用每次都手动调用 lock 和 unlock 了。而且可以自动析构,还是异常安全的。因为即便抛出异常,只要 catch,Lock 类就会执行析构的。

第二章 vector 和 string


  1. string 一般使用引用计数来进行优化。而 vector 强制不能使用引用计数。但是,引用计数本身又会对多线程安全造成问题,因此还要手动进行线程安全处理,这就会让 string 在多线程的环境下的效率还不及不加引用计数的版本。想要避免的话,[1] 看 string 有没有手动关闭引用计数的接口 [2] 使用 vector 来代替 string 也是可以的。
  2. [1] 在 for(int i = 0; i < 1000; i ++) v.push_back(...); 的过程中,vector 会扩容高至 10 次。这样会对效率有巨大影响。在已经知道大致 size 的状况下,请先使用 v.reserve(1000);。这样能够极大提高效率。[2] 牢记:vector 和 string 的插入和删除总会使得迭代器失效。见第 1 条。 [3] 我们其实可以先 v.reserve() 一个超大的空间,然后在 push_back 或者 insert 一堆元素之后使用 17 条的 swap 缩容技法。这样能够避免频繁的堆分配和堆释放。
  3. 这一条非常棒!!也很有难度。揭示了不同的 4 种 string 实现的优劣势,同时也深化了传入指针进行构造和传入对象进行复制构造之间的共享能力的异同。联想到了 java 如果所有对象都需要接收一个参数构造的话,所有对象都传入同一个句柄构造,那么这些对象之间全都是“共享”的。也就是如果我在外部修改这个句柄,那么所有的这些对象都会“变化”。这是不安全的。这样我们需要调用 clone 方法来进行效率较低的复制才行。这样的“共享”能力是这些 string 之间的不同,也是 C++ 和 Java 之间的部分区别。详情请见书中条款 15 才好。阅读原著才是坠吼的。
  4. 在把 vector 和 string 的指针传递给一个 C API 类似于 void haha(const int *begin, int size) 这种的时候,需要谨慎再谨慎:[1] vector 尽量使用 haha(&*v.begin(), v.size()) 进行调用,而不是 haha(v.begin(), v.size())。因为前者符合语义是一个指针,而后者是一个 iterator。虽然在 vector 中确实是一样的。但是尽量写后者。[2] 如果有 haha(const char *ptr) 这种,需要使用 haha(s.c_str()) 来进行调用。不过要注意:string 的最后可能没有 ‘\0’。因为 string 其实就是类似于 vector 的定位,就是一个容器而已,只不过装的全是 char 字符。因此,在 haha 内部进行字符串遍历的时候,说不准中间会遇到 ‘\0’ 的情况。虽然 c_str() 会在返回的 const char * 后边自动加上一个 ‘\0’,但是并不保证字符串中间没有 ‘\0’。所以这里要注意一下。[3] 在 [1] 中,haha 有可能修改 vector 内部的值。要注意。如果我们传进来的 vector 是有序的,那么只能祈祷 haha 结束之后 vector 还是有序的了。[4] 在 C API 对 vector 初始化是没什么问题的。因为 vector 和 array 有着内存布局的兼容性。对 string 初始化可能要麻烦些,需要先初始化一个 vector,然后再 copy 到 string 中去。 ==> 扩展:可以用 C API 初始化 vector 啥的,再复制到 list/deque 也是可以的。但是,最好不要用 C API 初始化数组。因为我们未必知道它的大小,会比较危险。而且其实 13 条的本意是:尽量用 vector 来代替数组。
  5. 使用 swap 技巧来对无法缩容的 vector 进行缩容。vector<T>(target_vec).swap(target_vec) 即可做到。这里假设 target_vec 的 capacity 远远大于 size,有可能是一开始 reserve 时计算不正确引起,也有可能是本来我有 10001 个元素,在加到 10000 个的时候 vector 扩容 2 倍变成 20000,但是接下来只有最后一个元素被 push_back。于是有 9999 个空余的空间。这样,先复制构造 target_vec,就可以有 size == capacity 的新的匿名 vector。然后我们再把它跟有特别多空闲空间的 target_vec 进行交换即可。交换完之后匿名 vector 就会被自动析构。所以我们原先 capacity == 20000 的就消失掉了。留下的是 capacity == 10001 正好的。
  6. 避免使用 vector<bool>。vector<bool> 是针对 vector 特化的产物,不是标准的 STL 容器。而且,vector<bool> 其实并不储存 bool。bool 一个才占用 1 byte,但是指针和引用要占用 8 bytes。指针 T 和引用 T& 竟然比 T 本身占得还多。而且存储 bool 本身也太消耗内存。于是 vector<bool> 被特化成了 bitset 一样的用 位域 来实现的方式。因而 `bool pb = &v[0]是编译不过的。但是 v[0] 使用了 More Effective C++ 的代理类的方式,重载 vector<bool>::operator [] 返回一个嵌套 (nested) 代理类 (proxy class) 对象来模拟 vector<T>::operator []。但是这却是不完整的,比如我们没法做bool *pb = &v[0]`。不过我们可以使用 deque 或者 bitset 来进行代替就是了。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include <iostream>
    #include <vector>
    using namespace std;
    int main(int argc, char *argv[])
    {
    vector<bool> v;
    v.push_back(true);
    v.push_back(false);
    v.push_back(true);
    cout << v[2] << endl; // 调试进去就知道,在 llvm 下,v[2] 返回一个代理类的对象 class __bit_reference {}。而且它的内部重载了 operator bool() 强转符号。
    bool b = v[2]; // 可以通过。因为重载了 operator bool()
    // bool *bb = &v[2]; // 但是指针无法通过!!因为 error: no viable conversion from '__bit_iterator<std::__1::vector<bool, std::__1::allocator<bool> >, false>' to 'bool *'!!
    }

第三章 关联容器


  1. 理解相等 (operator == ) 以及等价 (operator < or >) 之间的差异。std::find() 函数一般都以 operator == 作为筹码,但是 set<T>::find() 和 set<T>::insert() 会以 operator < 作为筹码(因为是红黑树)。所以,对一个关联容器 set,要优先调用 set.find(xxx) 而不是 find(set.begin(), set.end(), xxx)。因为在自己配置既定比较规则 Comp 的情况下,很有可能 std::find 会失败,而 set.find() 会找到想要的结果。这也是为什么要优先调用容器内方法的原因。[注] hash_set 和 hash_map 也有相等和等价的两种设计。虽然不是标准容器。
  2. 很好的比较器标准写法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // operator() 两边全是一样的类型且已经确定的话,就可以使用 binary_function<T1,T2,T3> 这种了。
    struct StringPtrLess : public binary_function<const string*, const string*, bool>{
    bool operator() (const string *ps1, const string *ps2) {
    return *ps1 < *ps2;
    }
    };
    typedef set<string*, StringPtrLess> StringPtrSet;
    StringPtrSet s;
    // 或者:
    // 如果 operator() 两边的类型没有确定的话,在类上边加泛型做泛型类还太恶心,索性就不继承 binary_function了。
    struct StringPtrLess {
    template <typename Ptr1, typename Ptr2>
    bool operator() (const Ptr1 *pt1, const Ptr2 *pt2) { // 可以比较任意两个指针
    return *pt1 < *pt2;
    }
    };
    typedef set<string*, StringPtrLess> StringPtrSet;
    StringPtrSet s;
  3. 对于关联容器,总是让比较函数在等值情况下返回 false。这一条非常重要!!因为比如说 set<int, less_equal<int>> s 这种,由于钦定比较器是 return l <= r;,因此在 set 的 insert 10 的时候,如果原先已经有了一个 10,那么内部会做 !(10 <= 10) && !(10 <= 10),然后当然会返回 false && false,于是说明 10 和 10 不是等价的。于是第二个 10 会被插入。所以这肯定是错的!根本不能指定 less_equal 作为比较器,这回破坏 set 容器!而对于 multiset,虽然插入不会有问题,但是取 equal_range 的时候,我们本来想要得到两个 10 的迭代器作为 equal_range,但是由于 10 和 10 不等价,我们最多能得到一个 10。因此这是不对的!根本不可以指定 less_equal 作为比较器,即,我们应该 总是让比较函数在等值情况下返回 false。

  4. 不要直接改变 set/mulitset 中的元素。你需要先 erase,再 insert。学完这个 clause,会对 cast 的强转方式的理解上升一个层次。

    1
    2
    3
    4
    5
    6
    7
    set<T>::iterator i = s.find(x); // 1. 找到元素迭代器
    if(i != s.end()) {
    T temp(*i); // 2. 复制一份出来。
    // ... // 3. do some change on temp
    s.erase(i++); // 4. remove i // 递增迭代器保证 i 是有效的。因为下一步将要用到。
    s.insert(i, temp); // 5. use hint to insert the modified `temp`. O(1).
    }
  5. 考虑使用 sorted vector 来代替 set、map,如果容器的插删以及查找是分为两个部分集中进行的话,那么请使用 sorted vector 代替 set、map。而且缓存命中率会明显 up。
    不过……if (i != vw.end() && *i == w) ??? 这里不太明白…???????说是见 19 条??? 然而我并没有看懂 QAQ

  6. 当效率至关重要的时候,请在 map::operator[] 与 map::insert 之间谨慎做出选择。这一条也很有难度。因为在m[1] = obj;作为添加操作的时候,其实相当于调用了 pair<map..::iterator, bool> it = m.insert(make_pair(1, Obj())); it.first->second = obj; 相当于在 insert 的基础上,又多构造了一个临时的 Obj(),最后还 copy 了一个对象进去。多调用了 3 次没必要的函数,即 Obj() 的 constructor,Obj 的 destructor,以及 Obj 的 operator = 的 copy assignment。在需要效率的时候这个是比较浪费的。但是,在m[1] = obj作为更新操作的时候,情况正好相反了过来。因为 insert 的时候需要构造临时 pair 才行,pair 之中还要构造临时 obj 才行。这样将会引发比较大的开销 —— 因为是原先已经有过,只更新 value 即可,没有必要连同 key 一起包装成一个 pair。而 operator[] 不需要使用 pair,因此 operator [] 的效率更高。而且语法更优雅。但是后边的代码又涉及到了 23 条没看懂 QAQ 的问题……为何要额外判断一次 i == w 呢???? 见 45 条。因为 lower_bound 如果查找成功就返回查找到的第一个符合要求的元素;但是如果没有成功的话,那么就返回第一个可以插入的位置。因此需要使用 i == w 这个 “相等性判断” 来判断是否查找成功了。注意:虽然 lower_bound 使用了 “等价性” 来作为判断原则,但是最后需要用 “相等性” 来判断是否查找成功。因而,如果 lower_bound 指定的比较器和判断原则不同步,就会出现问题。所以这里需要谨慎下。或者可以使用 equal_range 进行判断则更加轻松,但是比 lower_bound 要更加昂贵。
  7. 散列容器。并不是 STL 标准。但是各种 STL 中均有提供。此条款仅仅是介绍,略。

第四章 迭代器


  1. 使用 iterator 代替 const_iterator。最好全用 iterator。当然,iterator 可以隐式转换为 const_iterator。
  2. 使用 distance() 和 advance() 将容器的 const_iterator 转换成 iterator。
  3. 使用 reverse_iterator 删除的话,需要转为 iterator 才行。但是注意它们的关系:

    1
    2
    3
    4
    5
    6
    vector<int> v{1,2,3,4,5}; // 想要删除 3:
    v.erase((++find(v.rbegin(), v.rend(), 3)).base()); // 要将返回的 reverse_iterator 先 ++ 再取 base,iterator 就会指向 3 了。否则,iterator 会变成 4。
    // 但是,下边的情况是编译不过的!
    // v.erase(-- find(v.rbegin(), v.rend(), 3).base() );
    // 这样对 vector、string 是不行的。对于其他如 list、map、set 等都是可以的。因为 vector 的 reverse_iterator 取 base 之后返回 iterator,是 T* 的指针类型。而 C 和 C++ 规范规定【函数返回的指针不可以修改】,因此必然会编译错误。所以,只能在 find 结束时候的 reverse_iterator 取 base 之前就预先判断并调整调用 base 之后产生 iterator 的位置才行。
  4. 如果我们想要性能的提升,而且不需要格式化(比如对于文件中的空格和 tab 也全部读取的话),我们可以使用 istreambuf_iterator。直接从缓冲区读取是非常快的。而 istream_iterator 是通过 operator >> 从输入流中读取的。


第五章 算法


  1. 确保目标区间足够大。[1] 一定要小心插入到容器的最后。不要用 transform(v.begin(), v.end(), result.end(), func),而是要用 transform(v.begin(), v.end(), back_inserter(result), func)!!没 malloc 空间且没有 constructor 初始化就复制上去是 UB! [2] 但是,如果我们要倒着在 result 中插入 v 的话(即 reverse),就不好弄了。因为 vector 并没有 push_front(),因此没有 front_inserter。因而只能使用另一个技巧:不能倒着插进 result,何不倒着遍历 v 呢?于是可以:transform(v.rbegin(), v.rend(), back_inserter(result), func)。 [3] 如果是在 result 的中间插入整个区间 v 的话,可以使用 inserter。比如在中间插入:transform(v.begin(), v.end(), inserter(result, result.begin()+result.size()/2), func)。但是这样效率对于 vector、string、deque 这样的连续容器肯定是 O(n) + O(重新 allocate 内存)。虽然不能避免 O(n),但是内存分配还是可以避免。我们可以使用 result.reserve(v.size() + result.size()) 预分配内存。 [4] 如果在 [3] reserve 之后再执行 [1] 这种情况,也是不可以的!Effective STL 中说的不明晰。我来补充一下:如果 v 中的元素所在的类中有 const 成员的话,这样的 copy 是会失败的!因为常成员是不允许 copy 的!!只能通过 malloc + copy constructor 进行构造!如今 malloc 有了,但是没有 copy constructor,直接使用 operator = 进行强制复制,也会在成员有 const 的时候编译失败!!/ 而且即便没有失败,也是错的。因为并没有通过 result 的接口来进行 push_back,因此 result 的 size 和 iterator end 并没有发生改变。只是拷贝了内存过去。因而也一定是 UB 行为。 [5] 如果想要直接覆盖掉 result 的话,那么可以写 transform(v.begin(), v.end(), result.begin(), func) 注意 $3 参数不是 front_inserter(result)!!那是插入。而且在 vector 也会编译错误,由于 vector 没有 push_front。这样的话,v 会直接覆盖 result 的空间。但是!这样也是错的。因为没保证 result 的 size 大小大于 v 的 size 大小(注意是 size,不是 capacity),很可能导致 UB。所以之前要写:if(result.size() < v.size()) { result.resize(v.size()); } 才行。(注意是 resize 不是 reserve!resize 是会 malloc + construct 的,面向容器的 size 属性;而 reserve 是只会 malloc 但是不会 construct 的,面向容器的 capacity 属性。) [6] 或者 result.clear(); result.reserve(v.size()); transform(v.begin(), v.end(), back_inserter(result), func); 也好!!
  2. 了解各种与排序有关的选择。a. 把排名在前 20 个 数组元素取出:[1] paritial_sort 基于堆排序。有序。partial_sort(v.begin(), v.begin()+20, v.end(), Compare) [2] nth_element 能把高于 20 和低于 20 的 element 分开,但是除了第 20 位,其他顺序都不确定。nth_element(v.begin(), v.begin()+19, v.end(), Compare)。注:nth_element 还可以寻找区间的中间值/找到特定百分比的值 b. 按照一等品和二等品的类别进行筛选:[1] paritition:vector<T>::iterator goodEnd = partition(v.begin(), v.end(), hasAcceptableQuality);。[2] 如果对于相同质量级别的 T,要保持他们的相对顺序关系(稳定排序),可以使用 stable_sort。 [注] partition / stable_sort 只需要 bidirectory_iterator 就可以工作。所以所有标准容器都可以使用 partition / stable sort。若要强行对 list 使用需要 RandomAccessIterator 才能用的 nth_element / partial sort 的话,可以把 list copy 到 vector 中去。而且 list 内置有 sort 函数。
  3. 和 clause 9 一样!是 erase-remove 以及 list.remove(),额外的还有 erase-unique! 以及 list.unique()。
  4. 重要!!千万不要贸然在 vector 的容器上使用 remove!!因为 remove 做完之后,极有可能很多指针句柄就被覆盖,从而使堆内存不会被释放!!非常非常重要!!!所以,只要遇到可能要释放或者 remove 的话,就用 vector> 就好了!!这样才能够完美!!否则极有可能出现内存泄漏!!
  5. 了解哪些算法要求使用排序的区间作为参数:binary_search、lower_bound、upper_bound、equal_range、set_union、set_intersection、set_difference、set_symmetric_difference、merge、inplace_merge、includes。还有虽然不一定要求 sorted,但是经常和 sorted 一起使用:unique、unique_copy。

    1
    2
    3
    sort(v.begin(), v.end(), greater<int>());
    // bool result = binary_search(v.begin(), v.end(), 5); // 错误!result 是 false!因为比较器有问题!默认是 less<int>(),所以根本找不到!
    bool result = binary_search(v.begin(), v.end(), 5, greater<int>()); // 正确~
  6. 通过 mismatch 或者 lexicographical_compare 实现简单的忽略大小写的字符串比较。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 只是代码记录:
    bool ciCharLess(char c1, char c2){
    return tolower(static_cast<unsigned char>(c1)) < tolower(static_cast<unsigned char>(c2)); // 为什么要强转为 unsigned char ??? 好像因为 tolower 只接受 unsigned char???
    }
    bool ciStringCompare(const string &s1, const string &s2){
    return lexicographical_compare(s1.begin(), s1.end(), s2.begin(), s2.end(), ciCharLess);
    }
    // 取巧:
    int ciStringCompare(const string &s1, const string &s2){
    return stricmp(s1.c_str(), s2.c_str()); // 非标准库函数
    }
  7. C++11 中已经有了 copy_if 了。一开始不完美的 copy_if 使用 remove_copy_if 和 not1 配接器实现的。但是实现的 copy_if 的最后一个断言参数必须接受 ptr_fun(func) 这样丑恶的配接器。因此还是要手动一个一个遍历实现才行啊。

  8. accumulate 这个函数在函数式编程中是相当强大的。这里也是一样。不过很值的在意的是,Mayers 在最后提到的,accumulate 不允许副作用,比如修改外部的全局变量等等;像书上的栗子中,把 $4 的 operator () 的函数对象所在的类中设置成员变量并且修改也算是“副作用”。而 for_each 允许。这是为什么呢?

第六章 函数子、函数子类及其他


  1. 使用 Bridge Pattern 来创建一个桥接类,来给算法传递小型的比较器来代替巨大且多态的函数对象。(非常有用!) 因为 algorithm 所接收的 Comp 统统都是按值传递的!虽然可以强行改变为引用,但是极其有可能会发生别的地方的编译错误。比如 for_each<int, DoSomething &>(v.begin(), v.end(), dosth);。所以要尽量使用桥接的方式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    template <typename T>
    class BPFC : public unary_function<T, void> { // BPFC: Big Polymorphic Functor class
    private:
    Widget w; // 大量的数据
    int x;
    ...
    public:
    virtual void operator () (const T & val) const; // 还是多态的。传值的话容易引发剥离问题。
    };
    // 可以把 BPFC 类修改为 BPFCImpl,而把桥接类命名为 BPFC:
    template <typename T>
    class BPFCImpl : public unary_function<T, void> { // 和上边一样,除了多了个虚析构函数和一个友元。
    private:
    Widget w;
    int x;
    ...
    public:
    virtual void operator () (const T & val) const;
    virtual ~BPFCImpl(); // 由于是多态的,必须用虚析构函数!!
    friend class BPFC<T>; // 多了个友元!让 BPFC 能够访问!
    };
    template <typename T>
    class BPFC : public unary_function<T, void> { // 也要继承 unary_function。因为同样实现了 operator (),虽然只不过是转调用 BPFCImpl 的 operator (),但是也要实现 unary_function.
    private:
    BPFCImpl<T>* pImpl; // 其实书中建议改成 shared_ptr<BPFCImpl<T>> pImpl;
    public:
    void operator() (const T & val) const { pImpl -> operator()(val); } // 转调用!
    // 这样,就把原先一个超多数据且是多态的大对象变成了一个单态的小对象!
    // 唯一需要注意的是,需要谨慎处理 BPFC 的复制动作 (没看懂) ????? 书中的建议是最好成员变量使用引用计数 shared_ptr。为啥 ?????
    };
  2. 要保证一个 functor 是纯函数(这一章节很棒,描述了 remove_if 中由于要把 带有计数器副作用,想要删除第三个元素的 functor 传递给 find 和 remove_copy_if 两次而导致产生 UB 现象:传递给 find 的时候计数器为 0,find 结束之后计数器为 3;但是到 remove_copy_if 的时候语义应为计数器为 3,但是由于是复制 remove_if 的 functor,造成计数器还是 0,因而第 3、6 位置的两个元素全被删除。)。一个限制方法就是:在 operator()(…) 后边加上 const 限制一下!!

    1
    2
    3
    4
    class ... {
    public:
    bool operator() (...) const; // 注意这个 const !!确实应该写这个。
    };
  3. 如果一个 class 是一个 functor (重载了 operator()),那么应该让它可配接。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // ptr_fun 可以对 函数指针使用。让它暂时升级成为一个函数对象(class)。
    bool isInteresting(int v); // 一个 predicate 函数
    auto it = find_if(v.begin(), v.end(), isInteresting); // 找到符合的第一个 elem
    auto itt = find_if(v.begin(), v.end(), not1(ptr_fun(isInteresting))); // 如果要找到不符合的第一个,我们需要套上 not1 和 ptr_fun。仅仅套上一层 not1 是不够的。这是因为 not1 需要 unary_function::argument_type 的 type 泛型定义,但是一个函数指针是不可能有的。所以用继承 unary_function 的 ptr_fun 把 isInteresting 函数指针包上一层,让它具有 unary_function::argument_type 而已。
    // 注意:在自己编写可以继承自 unary_function 和 binary_function 的 函数对象(class) 的时候:
    // 一般而言,传递给 unary_function 和 binary_function 的非指针类型都需要去掉 const 和 & 部分(没试验过) 作为模板参数。但是如果有指针的话,可以用 const T* 作为模板参数。
    // 对 非指针的类型:
    struct WidgetCompare : public binary_function<Widget, Widget, bool> { // 注意只写了 Widget 作为模板参数!
    bool operator()(const Widget & lhs, const Widget & rhs) const; // 注意这里的类型却又变成了 const Widget & 了!!
    };
    // 对 指针的类型:
    struct WidgetCompare : public binary_function<const Widget *, const Widget *, bool> { // 这里用了完全体的 const Widget *...... 然而并不知道为什么。
    bool operator()(const Widget *lhs, const Widget *rhs) const; // 这里和模板参数的声明一模一样!! WHY ?????
    };
  4. 理解 ptr_fun、mem_fun、mem_fun_ref 的来由。

    1
    2
    3
    4
    5
    6
    7
    /*
    几个要注意的地方:(以下全用 for_each 举例)
    1. 对于普通的函数指针而言,使用 ptr_fun 进行封装的地点仅仅在于函数指针前边有没有包裹配接器 not1、not2、bind1st、bind2nd。有的话因为要调用 unary_function::argument_type,就必须要使用 ptr_fun。也可以见上一个条款。
    2. 对于容器中存放指针,且 for_each 要接收一个成员函数指针而言,无论有没有配接器 not1 等,由于他们需要使用 obj->(*ptr)() 的形式调用,因此必须使用 mem_fun 来进行配接。
    3. 对于容器中存放对象,且 for_each 要接收一个成员函数指针而言,无论有没有配接器 not1 等,由于他们需要使用 obj.(*ptr)() 的形式调用,因此必须使用 mem_fun_ref 来进行配接。
    4. 重要:指针容器支持多态,而对象容器由于剥离现象,不会支持多态。而指针容器可以使用 vector<shared_ptr<T>> 来进行封装。
    */
  5. 确保 less 和 operator < 具有相同的语义。也就是,如果要自定义比较器,最好选择自己定义一个类,而不是特化 std::less 的标准模板库。仅此而已:保证 less 的完整性,不要乱修改它,需要的时候自己定义一个新的 class,让 std::less 的实现是默认的即可。


第七章 在程序中使用 STL


  1. 算法调用优先于手写循环。

    1
    2
    3
    4
    // 很好的栗子:想要对数组中的每个元素都加上 41,然后把整体插入到一个 vector 的前边:
    transform(arr, arr + n, inserter(v, v.begin()), bind2nd(plus<double>(), 41));
    // 注意几点:inserter 虽然指定 v.begin() 是插入位置,但是 inserter 内部会随着插入,会把内部的 iter + 1。也就是,最终插入的结果并不是和原来的顺序相反的。但是,如果不使用内置算法,而是手动写循环的话,有可能写成这样:insert(d.begin(), arr[i]),这样就是彻头彻尾地每次都插在 begin 处,因为 insert 函数不会对迭代器进行自增。因而,最终插入的结果是和原来的顺序正好相反,不是我们想要的结果了。
    // 记住:inserter 内部会随着每插入一个元素,会把内部的 iter ++。
  2. 容器的成员函数优先于同名的算法。[1] 无论是效率还是正确性(见 19 条。std::find 的条件是相等,即 operator ==;而 set<T>::find 的条件是等价性,即 operator < / > )。[2] 且,map 本质使用 pair 储存,如果用 std::count,你肯定需要自定义比较器。而 map 内部的 map<K,V>::count 则是只比较 key 不比较 value。如何使用它们,需要我们的权衡。[3] 在 list 中,同名的 remove、remove_if、unique、sort、merge、reverse 全都在效率上大大提高,因为它们被设定成了仅仅改变 list 的指针而不用重新 allocate 并复制对象。但是 std 算法的处理则是需要大量复制来实现这些算法。因此 list 的成员算法性能肯定更高。且,list 的 remove、remove_if、unique 的语义也不同 —— 不必使用 erase-remove/remove_if/unique 的手段进行删除,而是设定成了直接删除。而 std::sort(需要 RandomAccessIterator) 和 std::merge 根本就不能用在 list 上。

  3. 正确区分 count、find、binary_search、lower_bound、upper_bound 以及 equal_range。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // count 某种程度可以代替 find:只不过它会遍历全部区间,但是 find 找到就会返回。
    if (count(v.begin(), v.end(), x)) {...} // count 返回 int。不等于0 的话就是找到了。
    if (find(v.begin(), v.end(), x) != v.end()) {...} // 和上边一样。只不过 find 效率高些。因为不用遍历全部区间。
    // 在 list 中插入的时候,保持所有元素的顺序:
    l.insert(upper_bound(l.begin(), l.end(), newX, Comp()), newX); // 插入到原先有的 newX 之后,如果没有,就找 upper_bound 返回的合适的位置进行插入。
    // vector 中删除比某个值小的所有元素:
    v.erase(v.begin(), lower_bound(v.begin(), v.end(), Predicate()));
    // vector 中删除这个值以及比这个值小的所有元素:
    v.erase(v.begin(), upper_bound(v.begin(), v.end(), Predicate()));
    // vector 中 equal_range 代替 find 和 count:
    auto & iter_pair = equal_range(v.begin(), v.end(), x);
    if (iter_pair.first != iter_pair.second) { // 代替 find
    ... // 存在 x 这样的值
    } else {
    ... // 不存在 x 这样的值。因为返回的两个区间迭代器指向同一个地方
    }
    int count = distance(iter_pair.first, iter_pair.second); // 代替 count。
    // 对于关联容器:set、map 等,不必使用 std::count、std::find、std::equal_range,直接使用 m.count、m.find、m.equal_range 即可!

    搬运:
    IMG_0895.JPG

  4. 考虑使用函数对象而不是函数作为 STL 算法的参数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 1. 函数指针会拒绝 inline 优化。如果传入函数指针,有可能本来 inline 的函数就无效了。
    // 比如 sort() 要快于 qsort()。
    // 2. 有些情况可能会产生编译不通过的问题......
    set<string> s{"haha", "hehe"};
    transform(s.begin(), s.end(), ostream_iterator<string::size_type>(cout, " "), mem_fun_ref(&string::size)); // g++ 编译通过但是 clang++ 编译不通过。
    // 3. Mayers 给定另一个例子我使用 LLVM 和 GCC 都测试过,完全没有问题啊......还是把代码列上来把:
    template <typename T>
    T average(T val1, T val2) { // 例子 2
    return (val1 + val2)/2;
    }
    template <typename InputIter1, typename InputIter2>
    void writeAverage(InputIter1 begin1, InputIter1 end1, InputIter2 begin2) {
    transform(begin1, end1, begin2, ostream_iterator<typename iterator_traits<InputIter1>::value_type>(cout, " "), average<typename iterator_traits<InputIter1>::value_type>);
    }
    set<int> s1{1,2,3,4,5,6};
    set<int> s2{2,3,4,5,6,7};
    transform(s1.begin(), s1.end(), s2.begin(), ostream_iterator<typename iterator_traits<decltype(s1.begin())>::value_type>(cout, " "), average<typename iterator_traits<decltype(s1.begin())>::value_type>); // 这第二个例子函数指针倒是没有问题的啊。
    writeAverage(s1.begin(), s1.end(), s2.begin());
  5. 避免产生 “直写型” (write-only) 的代码。“直写型” 就是指通过灵感直接产生的代码(逃,写出来一大长串顺风顺水,但是阅读起来或者回忆起来根本看不懂。23333

  6. 略。并不是特别重要。
  7. 略。
  8. 略。

如果不想让你的mac在访问next主题的博客的时候烫烫烫

发表于 2017-09-25

请关掉 canvas_next 效果!!!
IMAGE
否则你的 mac 的耗热量会飙升!活动监视器内对能耗的影响会飙到 200+ !!正常也就 0.几…… 我还以为我博客出问题了呢(
珍爱 mac,请远离这些酷炫的效果QAQ

读者写者优先问题

发表于 2017-07-09

参阅博客: 輝夜の永遠亭

读者优先:

1
2
3
semaphore read_cnt_mutex = 1; //对临界区int readcount进行互斥
semaphore fmutex = 1; //对读者--写者,写者--写者进行互斥。(双重作用)
int read_cnt = 0;

READER:

1
2
3
4
5
6
7
8
9
10
11
P(read_cnt_mutex); //获取对read_cnt的互斥锁
if(read_cnt == 0) P(fmutex); //注意这条语句必须放在P(read_cnt_mutex)中,因为if语句和P语句之间不是原子的!!必须要防止其他的读者进程对read_cnt进行修改!!
read_cnt ++;
V(read_cnt_mutex);
read();
P(read_cnt_mutex);
read_cnt --;
if(read_cnt == 0) V(fmutex); //同上。因为if和V之间不是原子的,必须防止其他读者进行修改。 且,注意每次到0的时候释放fmutex,让写者可以进入。
V(read_cnt_mutex);

WRITER:

1
2
3
4
5
P(fmutex); //只要拿到fmutex互斥锁,就可以进行“写”操作了。
write();
V(fmutex);

从上边我们可以看出:如果在writer正在等待fmutex的时候,这时一定是已经有多个读者在读取。倘若读者不断地到来,那么writer必然一直处于饥饿的状态。

因此,这种情况下,我们必须想一个措施,让写者的优先级提高。也就是,让写者在“到来”的时候,就要让不断到来的读者停止,让正在读的读者们全部读完之后,写者就开始写。

换句话说,就是要“当写者到的时候,屏蔽源源不断新来的读者,等待已有的读者读完,然后自己进去写”。

因此,輝夜の永遠亭提供了一个思路,就是使用一个queue变量,来限制源源不断的后来读者。

在计算机网络中,我们都知道各种“层”。大体思想无非是,如果有问题解决不了,就加一层;如果还解决不了,就加两层。

这里用了同样的思想。不过用“门”来比喻更好。就是写者在fmutex这个互斥的“门”外边又套了一层门进行把关,一旦写者到来,他就把外边这层门(semaphore queue)封死,因此源源不断的后来读者就全部封死在外边了。接下来就是写者在门里等着,等屋子(fmutex)里的已有的所有读者都读完,然后自己进去写。

思想很简单吧!我们来实现一下。

公平竞争

1
2
3
4
semaphore read_cnt_mutex = 1;
semaphore fmutex = 1;
semaphore queue = 1;
int read_cnt = 0;

READER:

1
2
3
4
5
6
7
8
9
10
11
12
13
P(queue); //套上了一个“门”,READER必须得经过这个”门“,才能进”屋”读取。
P(read_cnt_mutex);
if(read_cnt == 0) P(fmutex);
read_cnt ++;
V(read_cnt_mutex);
V(queue); //过门了,就释放~
read();
P(read_cnt_mutex);
read_cnt --;
if(read_cnt == 0) V(fmutex);
V(read_cnt_mutex);

WRITER:

1
2
3
4
5
6
7
P(queue); //套上了一个“门”。
P(fmutex);
V(queue); //注意放到这里才是符合语义的! 进门之后如果进了屋,就把门开放了。如果没进屋,就在门前守着,不让其他人进来。
write();
V(fmutex);

哈哈哈。其实我想写成“写者优先”的。结果写完了之后发现时公平竞争TAT。遂把标题从“写者优先”变成了“公平竞争”。

这里我们看到,每个读者和写者都要过门,没过门就不能进。这显然是非常公平的呀!一次只有一个可以进门,这不是公平的是什么?

想要对写者偏心的话,就要改成:只对第一个写者需要过门,然后只要有这第一个写者在这里把关,那么后来源源不断的写者也可以涌入。只不过给后边的写者开小灶,直接免费过门,不需要等了。

回想第一个读者优先也是这样,只要第一个读者进了屋,那么接下来到来的读者不用参与竞争就可以免费进屋了。这里的写者也一样,只要第一个写者进了门,那么接下来到来的写者就可以不用竞争也可以免费过门了。

看下实现:

写者优先

1
2
3
4
5
6
semaphore read_cnt_mutex = 1;
semaphore write_cnt_mutex = 1;
semaphore fmutex = 1;
semaphore queue = 1;
int read_cnt = 0;
int write_cnt = 0;

READER:

1
2
3
4
5
6
7
8
9
10
11
12
13
P(queue);
P(read_cnt_mutex);
if(read_cnt == 0) P(fmutex);
read_cnt ++;
V(read_cnt_mutex);
V(queue);
read();
P(read_cnt_mutex);
read_cnt --;
if(read_cnt == 0) V(fmutex);
V(read_cnt_mutex);

WRITER:

1
2
3
4
5
6
7
8
9
10
11
12
13
P(write_cnt_mutex);
if(write_cnt == 0) P(queue); //你懂的。因为if不是原子,因此前边必须有互斥量~ 这个互斥量还对write_cnt进行了写互斥~ 双重作用。
write_cnt ++;
P(fmutex); //等着屋里的原有读者读完
V(write_cnt_mutex);
write();
P(write_cnt_mutex);
write_cnt --;
if(write_cnt == 0) V(queue);
V(fmutex); //释放锁,如果write_cnt!=0还有写者,因为queue没释放,因此源源不断的读者过不了门,因而fmutex肯定花落别的writer家。如果是0,那么之前一句queue就被释放了。这样的话,就是读者和写者竞争咯,这里会公平地争夺过门的机会。但是如果writer抢到了门,那么之后的过程就对reader不公平了。
V(write_cnt_mutex);

就完成啦~~

信号量和管程笔记

发表于 2017-06-21

最后一篇啦。其实内存、进程和这篇都是今天一天写的。明天要开始复习了TAT

信号量和管程是很有意思的,因为我只是进行了理解,然而实现因为操作系统OS concept上都有伪代码……而且几乎和真实代码差不多少,所以就可以愉快地写(chao)了!哈哈哈哈哈哈!最后还顺便模拟了一个生产者消费者问题。

  • Extra Reference:
  • https://wenku.baidu.com/view/3cf2e85c195f312b3069a57d.html 管程的概念及实现 这个非常好!!!
  • https://www.zhihu.com/question/30641734 《应该如何理解管程》
  • http://blog.forec.cn/2016/11/24/os-concepts-6/ 管程的实现

这是在monitor.h中我记录的reference,推荐一看!写的非常好,无论是哪篇文章。

重点内容其实在于关闭中断形成原子操作。对于这个只使用了一个CPU的,使用汇编cli和sti就可以关闭和开启中断了。这样,就可以保证了原子执行函数了~

具体实现请自行查看代码咯~信号量——P,V操作,管程:enter, wait, signal, 以及leave操作。

嗯,懒得写啦!就写到这里吧~纪念下我这个5k行的小kernel~

接下来我要入FP坑啦~haskell等着我!还有编译,lua等着我!哈哈哈!

进程笔记

发表于 2017-06-21

针对于神奇而又让人心驰神往的fork,在研读并且实现一个之后,我有必要讨论下自己的理解。这个fork实在是神奇地很。

fork()在linux下顾名思义是用来产生一个子进程的。而且事实上,针对于linux 0.26而言,除了fork之外,还有一个do_fork函数。虽然名字好像,而前者其实“调用”了后者,但是其实二者有截然的差别~其实在我的观察下,do_fork只是进行了copy并[设置]成和current的各种进程上下文一模一样而已,并没有真正地进行启动新的进程。而fork又仅仅只直接调用了do_fork,而其他什么也没干!那么为什么fork()会一下子变成两个进程呢?还有两个返回值呢?

其实乍一看好像仅仅在“设置”,而没有进行“调度”,但是其实在暗中已经进行调度了。秘密就在于,fork()函数虽然只调用了do_fork()函数,但是用的是0x80系统调用。也就是说,整个调用链是这样的:fork->(int 0x80中断)sys_fork->do_fork。由于fork就调用了一个sys_fork,而sys_fork就调用了一个do_fork,因而后边在不必要的地方,我们只说fork,而不说sys_fork了。

而且,虽然只有一层调用关系,但是这两个函数的意义却是截然不同的。fork()函数是在用户空间调用的,而do_fork()函数却是在内核空间调用的。而联系用户空间和内核空间的纽带,就是0x80系统调用。因此,在fork()的asm volatile ("int $0x80;"::....)函数中,父进程,也就是current进程所做的事情只有一个:系统调用中断发生,自己切换到了内核态,栈由用户栈变成了内核栈;然后用do_fork设置了一波子进程的所有东西,(仅仅是设置,并没有调度),然后就原路中断返回,之后继续执行,没有半点和单进程的不同之处啊。

看一段代码:user_main
仅仅是普普通通的用户态fork调用而已。对于父进程current而言,其实只是正常的返回了。是的!就是普普通通的正常返回了!返回了设置的子进程的pid。为何我要强调设置这么多次呢?因为它确确实实只是设置,其他什么也没有干,没有任何的调度!

那为毛fork竟然有俩返回值呢????

答案就是:因为本来只有一个fork,在进行父进程设置do_fork之后,是fork变成了两个!

啥?

确实没错~C语言仅仅允许一个返回值。返回两个值是根本不可能的。所以,不要听别人“fork返回了两个值!”来忽悠你,那就是在扯淡了。这种言论会让新人越听越懵逼的。

其实在设置do_fork的时候,开辟了子进程的pcb块,然后重点的就是,此父进程的eip会被复制一份。也就是,调度到子进程之后,子进程就会直接在父进程设置好的eip处苏醒过来。

那么在int $0x80中断的时候,其实idtframe中断帧已经由CPU自动压入了下一条指令,也就是把当前的eip+4变成了中断返回应该恢复的eip了。那么子进程将不会执行fork,而是跳过了int中断来直接从int的下一条语句执行!!这里一度困扰了我好久啊。更详细的分析见这里:sys_fork。

上边那一段说的是子进程的苏醒。那么我们可以知道,子进程苏醒之后,也是差不多从和父进程一样的地点苏醒的。也就是:又执行了一遍fork的后半部分。这个后半部分,具体指的是“fork的返回部分”。因为fork中其实只有一条int 0x80指令,那么子进程还跳过了这个指令,从下一条开始执行,那么这所谓的下一条,自然就是return语句了。

是了~~现在我们可以看出,子进程直接就是从父进程所执行的fork跳过了设置函数do_fork而开始执行的,也就是只进行了一个返回。那么这个返回值就必然是子进程fork的返回值了。由于整个fork过程中,eax寄存器的值并没有改变!因此,只要在一开始系统调用0x80之前,把eax强制清空一次就好了~所以子进程因为直接从fork的返回开始执行,因此直接返回了eax~~所以这就是返回0的秘密啦!!而刚才也说过了,父进程是一直稳稳地执行并返回,非常正常,所以其实他返回的是do_fork的值,也就是子进程的pid咯!

总结一下,因此,fork返回两个值的真正秘密,其实就是因为子进程是直接从fork的返回处开始执行,所以其实父子加起来,共计执行了两次fork。而不是fork有两个返回值啊。

完~~

内存笔记

发表于 2017-06-21

有关于mm方面写完的心得

最近一直没空写blog,但是现在我这小kernel终于是完工了(再不看学校的课就要挂了)。基于清华大学的ucore,其内所有的思想绝大多数是来自这里,我自己也diy了不少。一共写了5k行,虽然并不多,但是一个os kernel的infrastructure是已经完工了的。中断,分页管理,虚拟内存,交换区间,还有内核线程,用户进程以及信号量还有管程。其实是对应着ucore的lab1~lab7的。文件系统实在是没有时间了。如今已经是第17周,再不看数值分析数学就要挂了(唉)人生艰辛哪。我个人的话,毕竟才刚刚大二年级,水平有很多不足。写这个小kernel的时候,也是边看边学边写的。但是对于蒟蒻来讲,这种方法既傻瓜化,又能学到东西,所以还是非常适合我的。本系列的文章仅仅针对于同样写过kernel(菊苣们看看就好)的同好还有自己(其实只有自己哈哈),并不适合作为教程。踩过的许多坑全放在代码里边了,想要看的童鞋可以去翻一翻。当然,不要嘲笑我辣鸡的代码水平就好(笑)。

当时写完了内存之后,有很多心得,如今在最后,想要在这里笔录一下,做个留念。(毕竟以后不打算走这个方向TAT)写这个小kernel的原本的目的就是学习下kernel,不要一知半解地学习,这样实在是没什么意义的。从作为一个学生的角度来看,同是学生在同一屋檐下,对于所学的东西他们全都一知半解,而只有我自己明明白白的这种优越感实在是让人欲罢不能(哈哈)。不扯犊子了。进入正题好了。

  1. 在这个module中,让我耗费无数青春年华的是狗日的页目录表项(pde)和页目录项(pte)的设置。简直神特么气人的是一开始上来搞明白了怎么映射就开始写,然而并不知道无论是pte还是pde的表项内部必须放置pa(物理地址)而不是va(虚拟地址)。因为如果你TM在里边放置va,那么本来CPU就要查你给定的一个va所对应的pa,然后你pde、pte那里边还放置va虚拟地址,那CPU是根本找不到的TAT。必须在pde和pte里边全放置pa才行!!有整整半周就卡在这狗日的地方,眼睁睁看着qemu给我狂报0x14中断但是就是不知道为啥,简直绝望啊那几天。

  2. 虚拟内存蛮有意思。ucore通过IDE端口读外部磁盘swap.img,这个外部磁盘通过dd指令建立就可以了,好玩的很。还有paging这个比较烦人的问题,ucore的方式处理的很有意思,如果一个页被swap out到了swap.img中,那么就把swap.img的扇区编号放到pte中,当然,这时这个pte肯定就不合法了。因为它根本不对应一个page了。然后如果检测到这个不合法的pte,我们就执行解析扇区号,调用swap_in()来读取扇区,并且把page拿进来。of course,以上所有全是基于0x14缺页异常的。这个0x14异常简直是帮了大忙了。

  3. 自己实现一个alloc_page(),并且基于此实现一个自己的malloc()也是非常有趣的一环~自己实现的malloc啊!想想都鸡冻~只不过我的算法非常简单,这里是借用了hurley_os kernel的FIFO malloc实现的思想,然后重写了一个。哈哈,还记的那个在信息楼A311的晚上~实在是爽得不行。然而A311一到大夏天,我那最角落的位置竟然就全是飞虫,简直无法安心工作TAT(逃 我不会说这第3条明明应该和第2条换一下的

  4. 上边说的都貌似是相当简单的内容啊。但是实际上写起来是比较艰难的,尤其对于我这种水平般般的大二学生。不过人是在成长的嘛。艰辛的地方我全都用注释写到代码里边了。欢迎朋友们来star~只不过我的blog也其实就是写着玩玩给自己看的呐(就当是个小笔记好了~

  5. 说说gdb调试的问题。gdb调试当中,会遇到各种内存字节码。虽然调试一遍非常费时,但是调试出来bug的位置并加以改正之后,那心情是好的不行的~其中用的最多就是x(examine命令)。查看字节码简直是利器,x/16xw,x/32xw啥的,查起来简直酸爽。尤其是如果发现某个位置显示查不到内存了,而且这个位置在你感觉中还是合法的,那就说明:肯定TMD是页表分错了!唉(全是泪

多了不写了。其实要说还有好多好多好多。很遗憾没有在当时写完mm就写blog,而是到了现在才写,有些东西的印象也不是那么深刻了。最后附两张张图吧~~分别是页表和gdb调试内存,证明下内核的有趣!

IMG_3137.JPG

IMG_9572.JPG

中断笔记

发表于 2017-05-16

有关于中断的流程

对于中断的产生,有4步流程。

操作系统的中断一共有256个。分别从0~255。

【0-31】故障,陷阱亦或是外设中断中的非屏蔽中断(NMI)。

【32-47】这期间的16个门描述符,对应着我们外设I/O设备的中断信号IDTR。这16个中断是靠8259A芯片来控制的。毕竟I/O设备一定会产生字符请求,CPU不可能随时都盯着他们来看,如果CPU疯狂地轮询I/O设备,那还了得。所以这16个中断主要是8259A芯片来承接,产生中断来对应I/O设备的。

【48-255】剩下的所有都是软件中断了。像是Linux内部,就设置了INT 80为系统中断调用。

  1. 微机8259A芯片的编程接口初始化。正常情况下,8259A芯片一共有主从两片,是为级联结构。在ucore初始化为pic_init()函数。

  2. 当8259A芯片初始化之后,我们就需要创建中断门描述符表(IDT)了。中断门描述符表中一共有256个门描述符(GD),门描述符可以分为四种,分别是调用门描述符(CGD),中断门描述符(IGD),陷阱门描述符(TGD)以及任务门描述符(也是TGD,但是ucore不涉及)。这个时候准备工作已然充分,这个时候我们应该设置中断处理例程(ISR)了。当然之前要对ICW1进行设置,把mask设为FF即全1,也就是屏蔽所有中断。顾名思义,就是触发中断之时所执行的函数。最后,使用IDTR汇编指令对其进行设置。

    2.5 有关于CPU对某一中断号进行寻址并且调用ISR的手段:

    CPU在每执行一条指令的时候,对于【32-47】号中断向量来说,CPU执行完毕一条指令,就会查看8259A芯片的I/O缓冲当中是否有保存的中断信号。如果没有,CPU会执行下一条指令。如果有,那么CPU就会得到此中断信号,然后把此中断信号*8,这时得到的就是中断向量的索引(因为每一个中断门描述符是8字节64bit存放,而IDT和GDT一个明显的区别就是,GDT的第一个表项只能是8字节的全0,而IDT的第一个表项可以是正常的IDT表项)因此,每个中断向量在IDT的索引(偏移值)就是【中断信号量*8】。因而,藉由为IDT专门设计的寄存器IDTR,就可以查到IDT的位置;从中断向量的偏移值,我们就可以查到那个中断向量对应的门描述符。由这个门描述符,从16-31位CPU就会得知此中断向量对应的ISR所在的段选择子,也就是在GDT的偏移量,这样我们就可以定位到那个ISR所在的段了。由此,CS寄存器的值会被确定。由0-31位和48-63位CPU可以确定ISR在这个段中的OFFSET,这样EIP的值也被确定,这样,我们很完美地索引到了ISR的真正位置!!

    但是这其实是后边的步骤了。其实这个寻找之前,我们需要先判断权限。细心的童鞋会发现我们门描述符的32-47位并没有被使用。而这16位中就蕴含着很重要的“属性”。比如判断这个门描述符的真正分类到底是上述四种中的哪种?IGD?TGD?还是CGD?这个信息由这其中的位能够说明。更重要的是这16位中的后2-3位,即第45-46位。这两位代表“DPL”值,也就是此门描述符的特权优先级。这个优先级非常有意思。CPU回先比对CS段寄存器内部的“CPL”位(同样应该是两位,因为权限等级从ring0-ring3嘛。0二进制是00b,3二进制是11b。因此势必需要两位),然后会把这个位和DPL进行比较,来进行判断是否“有权限”来执行这个中断。=>打一个比方来说,如果此时进行的是外设中断,即硬件中断,那么这个优先级是非常高的。如果这时目前CPU所执行的进程位于用户态ring3(目前进程的运行权限级别由TSS段来获得,该信息在内存起始位置在TR寄存器中 ——原话),那么这个时候,CPU会强制把用户栈升级成为内核栈,然后来进行ISR的执行。但是在此之前,必须要先保存用户态的信息。也即是,CPU会保护现场,依次压入ss,esp(前二者如果原先就在内核态则不会压入),eflags,cs,eip以及errorCode(可选)。这样的话,当弹栈的时候可不要忘记倒着弹出来啊。之后刚才我们已经得到了ISR的真·地址了。这样我们就把CS和EIP设置为要设置的值就ok啦。然后就执行ISR咯。当然我们不要忘记那个errorCode。如果errorCode有值的话,那么一定在编程的时候把errorCode先弹出来进行处理咯。

    在ISR调用结束之后,CPU要进行恢复现场。这样,会倒着弹出来之前eip,cs,eflags。如果原先是用户态,那么就会继续弹出来esp和ss,并且切换回用户栈。如此一来,用户栈和内核栈对于外部中断的切换就算完成~

    =>如果是软件中断,因为CPU会禁止用户瞎玩弄中断(可能是怕把中断玩坏吧,大雾),因此和硬件外设中断恰恰相反,只有在当前进程的CPL比DPL的等级要不低于的时候(当然数值要不高于,正好相反),这样ISR才会执行。这样,就是软件中断的执行情况了。

  3. 有关于外设的中断。
    有许多外设需要中断。比如scanf的实现。而printf并不需要(如果是直接往显存中写的话)而如果是向串口和并口中写入的话,也是需要的。scanf是需要与外部的串口设备tty相连的,而且需要键盘keyboard产生中断。(键盘在外设中断的偏移值是1,即中断号33,tty的外设中断偏移值是4,即中断号36)。还有时钟中断,偏移值是0,中断号32。对于他们,我们就需要进行对相关的设备进行端口设置之后,把前文所说的mask进行“打开”,这样就可以接收中断了。这样的话,我们就可以通过打开相应的PIC的mask位,然后允许相应中断的执行了。

123
wind2412

wind2412

22 日志
8 标签
Github
友情链接o(*////▽////*)q
  • Google~
© 2017 - 2019 wind2412
由 Hexo 强力驱动
主题 - NexT.Pisces