汇编解析C语言函数结构体struct参数的方式

本文介绍在x86汇编中没有加优化选项时的函数体struct传参和返回的方法。

结构体返回值

测试代码

我们先讲讲较为简单的函数返回结构体的方法——

上面的代码实现了一个非常简单的返回结构体的例子,对应的汇编代码如下——

分析

在上面的程序中,内层  return_struct 函数在被调用时,有两个有意义的寄存器,分别是 %rdi 记录了在function1中定义的那个结构体的位置, %rsi 记录传入的n。

在函数进入时,两个寄存器都被保存到了内存里面,接下来按部就班实现C++中的逻辑。重点部分在第30-35行,使用了三个位移操作赋值了五个值。最初理解代码,尝试寻找拷贝内存的语句时,将这三句话直接排除在外,实际上,由于结构体里面是5个整形,而使用64位寄存器,一次可以拷贝两个整数,因此三次拷贝恰好可以用最快的时间完成拷贝。

结论

因此,结构体返回的方式是将返回的结构体的起始位置指针放入 %rdi 寄存器,并在子程序中通过该寄存器定位修改结构体的内容。

结构体传参

测试代码

首先设计样例代码如下——

反汇编出的信息如下——

分析

有了上面的铺垫,我们可以迅速找到在30-35行有一段结构体的拷贝——将位于局部变量中的结构体拷贝到了在28行分配的一段连续空间中,而拷贝操作都是由 %rax 定位的。有意思的是,当我们认为 %rax 正是一个向子程序定位结构体的指针是,发现在子程序中 %rax 的寄存器中的值并没有被使用,反而是使用了 %rsp 进行定位。从另一个方面思考其实也并不奇怪,结构体参数本身是不能通过寄存器传参的,实际上,传参使用了最简单的方式——“通过压栈方式传参”,参数按照“从右到左”的顺序压栈,倘若遇到结构体,则压栈的内容变得多一些而已。

结论

结构体作为参数传入使用压栈方式传参,参数按照“从右到左”的顺序压栈,特别的,当参数为结构体,将结构体整体压栈。

原创文章地址:【汇编解析C语言函数结构体struct参数的方式】转载时请注明出处mhy12345.xyz

rcaudio:实时录音的音频特征处理库

简介

rcaudio是一个实时的录音,并处理录制的音频的库,使用rcaudio,你可以不用自己编写任何代码,而实现——

  • 获取原始的音频数据
  • 实时监测音量
  • 实时分析录音(歌曲)的节拍,并加以预测

安装

rcaudio是我的第一个放在了pypi.org的工程,因此可以使用pip安装 pip install rcaudio .其中该工程依赖 pyaudio ,倘若出现了无法找到头文件的错误,可直接重装覆盖系统默认的

rcaudio的源码地址为:https://github.com/mhy12345/rcaudio,欢迎push&issue

使用

CoreRecorder

CoreRecorder  是提取原始音频信息的类. 其本身会额外开一个进程,当成员函数 start() 被调用后,录音开始,音频数据会出存在一个队列 CoreRecorder.buffer 中。

如下是一个以字符形式输出录音的波形图的小程序

SimpleRecorder

在多数较为复杂的情况中,我们可以使用 SimpleRecorder ,从效率角度出发,推荐只实例化一次 SimpleRecorder因为绑定了录音部分,所以他会占用掉相当一部分的运行资源。

通过调用 SimpleRecorder 的成员函数 register() ,我们可以将多个 Analyzer 类绑定。

当成员函数 start() 被调用后,录音自动开始,同时起绑定的所有 Analyzer 实例也同时开始运行。

Analyzer

所有继承于 BaseAnalyzer 的类都是一个 Analyzer ,其功能是从原始的音频数据中提取出用户希望的信息。如下例子中,一个 VolumeAnalyzer 用于实时询问音量。

下面例子中,一个 BeatAnalyzer 用于分析节拍并进行预测。(这里的预测会有1~2s的延迟)。 BeatAnalyzer 本质上是通过在当前最后可用的若干 rec_time 时间的数据,提取起节拍信息,进而校准当前的节拍器。校准的具体算法可以参考我的另一篇文章python实现音频节拍的实时识别

多个Analyzer同样可行,比如如下程序在分析节拍的同时,通过音量判断乐曲是否结束。

如果你需要自定义音频特征的提取函数,不妨试试重写 FeatureAnalyzer 的 data_process 成员函数(其传入一个一维数组表示特定时间段的音频原始数据,返回这段时见的特征)。 默认的 data_process 函数计算了过零率。

注意事项

大部分的函数都存在1-2s的延时,个人认为如果不是把所有细节都在实时环境中重写一遍,是很难避免的。在 BeatAnalyzer 中,我使用了很多技巧来让用户看起来好像是事实的,而在 FeatureAnalyzer 如果特征的处理速度太慢,可能这个延时会被无限放大。如果这种情况真的发生,你可以考虑减少采样率sr,或者自己重写一个Analyzer来取舍一些不太重要的步骤。

最后

如果这个工程真的帮助你了的话,不妨给我一个Star哦,你的支持是我的动力!要是有pull request那我就幸福死了ovo

原创文章地址:【rcaudio:实时录音的音频特征处理库】转载时请注明出处mhy12345.xyz

Java多线程实现归并排序

Java中,一个class可以继承Thread类以实现多线程调用

 

原创文章地址:【Java多线程实现归并排序】转载时请注明出处mhy12345.xyz

Java使用正则表达式实现重叠匹配

问题:在一篇文章中一个匹配日期 yyyy-MM-dd 的程序。

显然一个日期对应Java中的正则表达式

然而,这样的正则表达式实现方式在处理如下输入数据时,无法提取出重叠的匹配

结果

这是由于第一个匹配消耗了其所在的所有字符的缘故,后续匹配将无法使用这些字符。这是我们需要使用 (?=((\\d{4})-(\\d{2})-(\\d{2}))). 正则表达式。

其中 (?=X) 这个语法匹配一个“位置”,这个位置向后可以完全匹配正则表达式X。 (\\d{4})-(\\d{2})-(\\d{2}) 是之前用于匹配日期的正则表达式, . 用于匹配单个字符。整个正则表达式语义为“匹配一个可以匹配日期字符串的位置及其后的一个字符”。

而原来的年月日信息可以通过 groups() 函数得到——

对应的整个程序为——

 

原创文章地址:【Java使用正则表达式实现重叠匹配】转载时请注明出处mhy12345.xyz

一句话解析汇编中的ret和call指令

这篇文章有点标题党了,因为实际上这ret和call还真没法一句话说完,向下读之前,读者需要知道——

  • 什么是,栈的pop和push指的是什么
  • 初步了解了栈在函数调用中的作用

汇编中,函数调用是通过栈维护的,栈顶指针称为%rsp。通常有两类汇编命令能修改%rsp:

  • 一类是直接使用add和sub汇编指令,这种情况多用于函数在栈中分配零时空间直接通过对%rsp加减指定数值得到。
  • 另一类是pop和push汇编指令,该指令隐含了一个%rsp加一或减一的操作,多用于试图保存一个寄存器的值供后续恢复。

另一方面,有个程序运行指令的计数器%rip,时时刻刻指向下一条指令的内存位置。一般情况下,%rip都是一个一个加下去的,特例是使用了jmp指令,则%rip直接被赋值为jmp的目标地址。

当汇编指令call被调用的时候,首先会隐式调用一个push命令,将储存有“之后本应该执行的语句的地址<CALLBACK>”的寄存器%rip的值放到栈顶,然后一个jmp指令通过修改%rip,将程序指令流跳转到目标位置。

当汇编指令ret被调用的时候,首先会有一个pop命令,将栈顶的值<CALLBACK>弹出,并放在%rip中作为下一个需要执行的代码。

关于汇编中的函数调用,我在另一篇文章解析汇编中实现函数调用的若干方式中有更详细的阐述。

原创文章地址:【一句话解析汇编中的ret和call指令】转载时请注明出处mhy12345.xyz

解析汇编中实现函数调用的若干方式

相同的c/c++语言的函数调用,在编译成汇编代码之后,会存在不同的解析方式,这些解析方式一方面依赖于编译器本身(比如gcc和g++编译出的代码就不尽相同),另一方面也会受到编译选项的影响(比如是否打开-O2 -fno-stack-protector这些编译开关)。网上很多其他的资料无视了实现函数调用的汇编代码的多样性,使得读者会在对照理解时出现一些困难。

在这篇文章中,我将通过几个小例子,介绍不同情况汇编实现C/C++语言函数调用的方式。具体的代码可以在 我的github 上面看到。


下图是一个简单的读入字符串的程序(注意,这个程序故意引入了不安全的gets函数)的C语言版本和C++版本——

 

这个程序中的getbuf函数就是我们今天讨论的主题

通过栈帧实现的函数调用

简单的例子

这是C语言代码版本通过编译命令 gcc c.c -o c -fno-stack-protector 生成的汇编代码——

理解%rsp和%rbp

其中两个重要的寄存器分别是 %rbp 和 %rsp , %rbp 称作帧指针,而 %rsp 称作栈指针。首先需要记住如下事实:

  • 函数的栈空间是从大地址空间向小地址空间生长。也就是说,在栈空间中的push操作会使 %rsp 越来越小。(对应右图中上面为高地址,下面为低地址)
  • 当前程序的局部变量储存在 %rsp 和 %rbp 两个指针所夹的那一段区域内(对应右图中紫色部分)

现在,在形式化定义 %rsp 和 %rbp :

%rsp 表示栈顶指针,永远指向当前使用栈空间中最下面的位置的地址。因此在我们的程序中,只用于定位栈顶的位置,并支持栈的插入弹出,而不用于地址索引。一般需要改变 %rsp 的地方有——

  • push指令:push指令将 %rsp 的值减一,并在新的 %rsp 位置放上需要push的值
  • pop指令:pop指令将 %rsp 的值加一,并将原来的 %rsp 位置上的值放在pop的寄存器中
  • 进入函数中,函数需要一定的空间,直接使用 sub $0x10,%rsp 分配整块的空间。
  • call指令和ret指令:其中内含了对于寄存器 %rip 的push和pop操作。关于call指令和ret指令更加详细的说明,在我的另一篇文章一句话解析汇编中的ret和call指令有更详细的解答。

%rbp 表示帧指针,一般在函数中用于定位,由于 %rsp 身兼多职,可能在一个函数块内修改,因此,给予 %rsp 的栈内信息定位是可行但不方便的。帧指针 %rbp 指向的位置是函数调用后,使用push操作保存上一个函数的帧指针后, %rsp 的位置,换句话说,这个位置就是没有进行任何局部变量分配是,栈顶 %rsp 的位置,之后的任何函数内的 %rsp 修改就都与 %rbp 无关了。从构造上来说, %rbp 还能用来实现函数的退出以及上一层函数的恢复。需要修改 %rbp 的地方有——

  • 调用函数时, %rbp 仍然指向的是上一层函数的帧,需要更新。为了之后可以恢复,需要将当前的 %rbp 的值使用push指令,放在栈顶,然后将 %rbp 赋值为 %rsp ,标记为“当前帧指针就是刚进入函数,没有分配任何内容的栈顶指针”。同时,新的 %rbp 指向的位置存有 %rbp 在函数退出时需要恢复的值。
  • 退出函数时,需要将%rbp更新回去。

汇编代码解读

回到之前的汇编代码,我们看看调用getbuf之后,究竟干了些啥:

整个getbuf的调用依次是——

  • 【位于main中】call指令,将程序结束下一条指令地址 %rip push至栈顶,并将call的函数入口地址赋值给 %rip ,实现指令流的跳转。
  • 保存旧的 %rbp 至栈顶,此时push使得 %rsp 加了1
  • 将栈顶指针 %rsp 赋值到 %rbp 中,此时新的 %rbp 可以理解成main函数内部变量和getbuf函数内部变量的分界线,而这个位置之后会作为getbuf函数内部变量定位的基准。
  • 函数内部变量需要使用接下来0x10大小的空间,在栈中预先分配
  • 我们将分配的那一块空间用来存buf数组,那么buf指针的值就应该是 %rbp-0x10 了
  • 将存有buf数组的寄存器 %rax 赋值给 %rdi
  • 将零时储存器 %rax 清零
  • 调用gets函数
  • 将储存器 %rax 赋值为1作为返回值
  • 使用leaveq指令恢复栈帧 %rbp
  • 使用ret指令将栈顶储存的“函数退出后下一条语句地址”弹出,并重定向为下一句执行位置。

C语言与C++语言

C++语言的getbuf函数汇编代码如下,可以发现,除了函数符号名有些区别外,没有其他的区别。

只使用栈指针实现函数调用

倘若在编译时,添加-O2优化,则C语言代码的getbuf部分变成了:

由于在之前的实现中, %rbp 的作用实际上是简化由于 %rsp 的变化导致定位偏移量不容易计算而设计出来的,如果我们成功追踪了 %rsp 在每个函数调用过程中的变化量,就很容易实现函数调用的恢复操作。

而在-O2优化下,编译器会以效率优先,如果我们完全知道了代码长什么样的,也知道每一次程序 %rsp 的变化量,实际上 %rbp 就不是那么有必要了。也就是说,这个程序是单纯使用 %rsp 实现函数调用的例子。

在上面程序中,getbuf调用步骤为:

  • 【main函数中】call指令调用getbuf,getbuf返回后下一条指令的地址被压栈,处理器指令运行的指针被置于getbuf其实位置
  • 移动 %rsp 分配足够空间
  • 清零 %eax
  • %rsp (即buf数组的地址),作为gets的参数,放在 %rdi 中
  • 调用gets函数
  • 将返回值放在 %eax 中
  • 将之前 %rsp 分配空间部分撤销
  • 退出

函数调用中的参数传递

尝试将原来的程序中,添加参数传入,得到下面的getbuf函数——

函数中传入了一大堆参数,这时因为,C++在传入参数少和传入参数多的时候,处理方式有所区别,因此,只有当 getbuf 函数传入足够多的参数时,才能成功涵盖两种方式。

将上述代码编译之后,得到汇编代码如下——

以及调用者main函数的反汇编代码。

 

可以发现,对于参数的传递,分为两种方式——

  • 当参数个数本身较少的时候,直接使用寄存器传参。对应了0x4005e6-0x400601的代码
  • 当参数个数比较多的话,无法用寄存器装下的参数,在调用前压到栈里面,由被调用者通过 %rbp 定位取之,在调用完成后,被调用者再将压栈的参数弹出。

在细节上,对于寄存器传参,依次使用了寄存器 %rdi , %rsi , %rdx , %rcx , %r8 , %r9 ;对于压栈传参,是按照参数位置,从右到左压栈。

包含金丝雀保护机制的函数调用

倘若编译参数中,没有 -fno-stack-protector,则C语言的getbuf编译出来如下——

多出来的一部分代码被称为金丝雀,是防止gets操作产生危险后果的,具体原理有时间再更~

环境与配置

gcc版本 (Ubuntu 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609

g++版本 (Ubuntu 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609

Ubuntu版本 16.04 64位

原创文章地址:【解析汇编中实现函数调用的若干方式】转载时请注明出处mhy12345.xyz

Latex平衡双栏引用的两栏高度

最近投论文,要求全文双栏,我使用了默认的 \bibliography

结果如图

主办方工作人员非常nice地告知需要平衡两栏长度,并且提供了详细的解决方案

  • 如果是使用普通方法手打引用的话,可以使用 \vfill\eject 命令分割需要换栏的位置。
  • 如果使用 \bibliography 生成引用的话,可以先  \usepackage{balance} ,然后在  \bibliographystyle 和 \bibliographystyle 之间加一句  \balance 。

改后引用页面如下——

原创文章地址:【Latex平衡双栏引用的两栏高度】转载时请注明出处mhy12345.xyz

浅析CapsNet的原理以及Pytorch实现

原文链接:Dynamic Routing Between Capsules

概述

在这篇文章中,Hinton提出了一种新颖的模型CapsNet来解决传统的手写数字识别MNIST问题,并取得了较好的效果。CapsNet相比于传统的卷积神经网络CNN有如下创新:

  • 对于特征向量(Channels),使用其模长表征该向量的重要程度。
  • 对于特征的层次传递,不再是使用传统方法——高层特征来源于底层特征的加权求和,而是引入一个Routing策略。

从CNN到CapsNet

与CapsNet相对应的是传统的卷积神经网络CNN,事实上,在CapsNet在构造上和CNN非常相似,其中很多部分甚至直接使用了卷积层,因此本文尝试通过对比的方式在回顾CNN的同时介绍CapsNet。

Capsule是什么?

在CNN中,一个特定的卷积核可以提取出原图中的某一个局部的特征信息。而多个卷积核的并列,等同于同时记录了多种不同的局部特征,为之后整合成一个更加复杂的特征做好了铺垫。

举一个例子,一个普通的MNIST图片(1个28*28维向量)经过一个[kernel_size=9*9,out_channels=10]的卷积核,可以得到10个20*20维的特征向量。这10个特征向量可以记录宽度、扭曲,粗细等不同细节。

再让我们看看CapsNet是怎么构造出来的——

Capsule,胶囊,在这里,可以理解为一个功能独立的小模块,他可以产生一个特征向量。比较而言,CNN中的一层,可以类比于CapsNet中的out_channels个Capsule。

与CNN相似,特征经过每一层的组合,从简单特征慢慢变得复杂,一个高层的Capsule的输入依赖于低层Capsule的输出。这种依赖关系不在是CNN中的加权求和,而是另外一种更加巧妙的方式,具体细节将在后面详述。

特征向量在Capsule间的传递已经讲解了,那么特征向量在Capsule内部干了什么呢?这就依赖于Capsule的设计者的想要解决什么问题了,至少在本文MNST识别中,Capsule内部执行了一次卷积操作。

特征向量的重要性权重

不论是CapsuleNet还是CNN,我们都通过卷积操作,提取出了特征向量。不同的特征向量重要性不尽相同。那如何处理每一个特征的重要性呢?

在CNN中,由于相邻的卷积层中,第i层的输入是第i-1层所有输出的加权和,因此,我们可以用了一个矩阵来表示第i-1层的第X个特征向量对于第i层的第Y个特征向量的重要性。这个矩阵恰好对应了一个线性全连接层,可以通过梯度下降算法学习。

而Capsule Net提出了一个全新的方法——将特征向量的重要性记录在向量的模长中!具体而言,倘若Capsule的内部计算出了一个特征向量s_j,我们希望通过一种名为squash的归一化方式,使得模长较大的s_j映射到接近1的特征向量v_j,而模长较小的s_j映射到接近0的特征向量v_j。映射方程如下:

    \[v_j = squash(s_j) =  \frac{\|s_j\|^2}{1+\|s_j\|^2} \frac{s_j}{\|s_j\|}\]

Routing策略

如果说在传统算法中,我们通常先用网络求解出一系列低级特征向量s_i,和一个表示该特征向量对于高一层特征j重要性的标量b_{i,j},则高一层特征的输入可以表示为

    \[s_j = f \left( \sum_j b_{i,j}*s_i \right)\]

实际上,相比于之后我们将介绍的Routing策略,原来的方式略显笨拙,因为b_{i,j}是一个于s_i无关的量。而我们希望的是b_{i,j}随着s_i而自适应调整。这也就是Routing策略的设计初衷。

在Routing策略中,b_{i,j}不再是通过梯度下降算法学习出来的,而是通过若干次迭代得到的——

每一次Routing初始时满足b_{i,j} = 0

对于b_{i,j}做一遍softmax归一化【注:论文是这样写的,不过我感觉softmax应该是第一维啊……不过实际上应该没有太大的差别】,

    \[c_{i,j} = \frac{exp(b_{i,j}) }{\sum_k exp(b_{i,k})}\]

归一化之后的数组c_{i,j}满足 \sum_k c_{i,k} = 1,换句话说,就是我们得到了低层s_i组合出高层s_j的加权系数c_{i,j}了。

    \[s_j = \sum_i c_{i,j} \hat{u}_{j|i} , \hat{u}_{j|i} = W_{ij} u_i\]

对于初始情况,我们的b_{i,j}=0,所有局部特征向量等权重得贡献到高一层特征中去(注意,即使是等权重,由于向量本身的模长不同,一次不同的低层次特征向量对于高层次特征向量的影响也不尽相同)。不过随着迭代的深入,b_{i,j}会发生改变,每次变化量a_{i,j}的定义如下:

    \[a_{i,j} = v_j \cdot \hat{u}_{j|i}\]

其中v_j = squash(s_j).

这个定义式的理解其实也很简单,我们相信如果一个输入向量和输出向量的方向相近,这个向量就是重要的,在下一次迭代中我们就会倾向于给这个输入向量更高的权重。

    \[b_{i,j} \rightarrow b_{i,j} + \hat{u}_{j|i} \cdot v_j\]

损失函数的设计

多层Capsule顺序链接,最后一层包含10个Capsules的特征向量v_k,这里的v_k里面包含了两个信息

  • v_k的模长包含了输入数据究竟像不像这个Capsule代表的数字。
  • v_k的向量方向包含了足以刻画原来图像的所有重要特征。

这两个信息我们分别可以设计一个Loss值来表示.

刻画相似度的Margin Loss

首先定义三个个超参数m^+ = 0.9, m^- = 0.1,以及\lambda = 0.5,并假设T_k = 1当且仅当输入图片属于第k类。

    \[L_k  = T_k max(0,m^+ -  \| v_k \| )^2 + \lambda (1-T_k) max(0, \|v_k \| - m^-)^2\]

这个式子的原理就是我们希望对于正确的那一类的向量的模长大于等于m^+,而其他分类错误的类别的向量模长小于等于m^-.

用于正则化的Reconstruction Loss

我们使用特征向量的模长能够构造出Margin Loss,这时,我们并没有使用特征向量本身,因此至少对于MNIST这个课题来说,我们可以利用最后的特征向量,来尝试恢复原来的输入数据,进而产生一个Reconstruction Loss,有利于模型的防止过拟合。

不过这里,论文中注明Reconstruction Loss仅仅用来作为一个正则项,并不能够主导模型的效果,因此在实际Loss中,Reconstruction Loss仅仅贡献一个很小的比例0.005.

使用CapsNet解决MNIST问题

整个模型包含一个CapsNet用于提取特征,和一个ReconstructNet用于尝试恢复最终提取出来的向量。其中CapsNet结构如下——

在这里我们只用了单层CapsNet,首先,对于原图进行一次卷积,得到一个256维向量,然后在进行卷积,得到32个8维向量组成的Capsules。通过之前介绍的Routing策略,我们可以对于这些低层次的Capsules合成10个代表数值的高层Capsules,每个特征向量16维。这些向量中,正确的那一个特征向量进而传到Reconstruct Net中,验证其是否真的表示了那个数字。

 

解码部分单纯由三层带激活函数的全连接层实现,最终的ReconstructionLoss决定于原图和解码后向量的距离。

 

Pytorch实现

我的实现代码参考了论文作者的实现,在这个实现中,几个常用的pytorch函数被多次用到,所以读者需要先确定你理解了这些pytorch函数。

  • view
  • unsqueeze
  • expand

其次,代码的分块清晰对应了流程图中的不同部分,包含了PrimaryCaps模块,处理处最底层的Capsule向量,以及通过一层AgreementRouting决定出的第二层Capsule向量(DigitCaps).

预处理

一个非常神奇的东西是,pytorch作为一个非常成熟的框架,竟然没有one_hot表示的内部实现,所以我们需要自己实现一个代码将标号转化为one_hot表示。

这个转换是网络上提供的经典转换方式,使用了_scatter函数。感兴趣的读者可以查阅文档了解这个函数的作用,这里就不细讲了。

Squash函数

squash函数的定义式是

    \[v_j = squash(s_j) =  \frac{\|s_j\|^2}{1+\|s_j\|^2} \frac{s_j}{\|s_j\|}\]

而函数只是无脑翻译成代码而已。

PrimaryCapsule模块

PrimaryCapsule通过卷积生成了最底层的向量,其卷积核大小都在论文中(也在流程图中)清晰指出了。

Routing模块

Routing模块本质上是通过三次迭代,得到每一个Capsule向量对于下一层的Capsule的贡献大小。

整体结构

将两个模块合并为1个模块即可得到如下结构

 

原创文章地址:【浅析CapsNet的原理以及Pytorch实现】转载时请注明出处mhy12345.xyz

vim简单教程及配置

vim是什么?

既然今天是向大家安利vim,那么先说说安利的原因吧:

通过官网链接下载vim并且安装后,我们可以看到vim的启动界面——

唔,请关注重点……

我是在安利读者为慈善事业做贡献呢!

这个逼装的,我给99分……剩下1分哪里去了呢?

——剩下一分是因为vim确实确实非常好用

言归正传,vim是一款编辑器,而编辑器是什么呢?

编辑器就是类似于notepad(记事本)这样的只能用来打字的工具,而当这种工具集成了编译运行调试等等功能后,就成了集成开发环境(Integrated Development Environment)也就是我们常说的IDE,比如DevC++和VC6.0。

IDE与编辑器的区别就在于由于IDE集成了所有功能,所以对初学者非常友好,但是开发效率通常来说比较糟糕,反之,编辑器一心只做编辑方面的优化,所以在编写代码方面效率完爆IDE。这些在之后都会提到。

Vim作为一个编辑器,作者亲身经历表示他至少能让你的代码编写速度提升一倍,而且和vim配套使用的g++,gdb还会使你的代码调试速度变成原来的三倍甚至更多。

为什么是vim

  1. 作者作为一个vim党,当然会推荐vim咯
  2. 帮助乌干达的可怜儿童
  3. 无视个人偏好,emacs,notepad++,sublime等“编辑器”都是很不错哒(换句话来说dev-c++,vc6.0,xcode等IDE就不符合这篇文章的核心思想了),当然,vim也属于这些编辑器中的一个
  4. 以后同学们编写代码不一定是在本机,有可能需要登录到其他电脑上,这时能够在终端里面使用的编辑器据我所知也就vim和emacs了,到时候即使你是一个资深IDE党也不得不去学习使用vim。
  5. 我曾在计算机系群里的调查,结果如下(大势所趋,不服不行啊……):
    1. 明确表示使用编辑器编程的同学们中,vim党最多
    2. 对于所有的被调查者vim也有相当大的优势

vim怎么用

不同平台的vim其实都是大同小异的

我们接下来就用mac下的终端vim举例了。

安装

不同平台vim的安装方式不同

  • Windows用户:在vim官网中搜索安装方法与安装文件,需要注意的是,windows下的vim是gvim,表示图形界面版本的vim。
  • Linux用户:可以使用该发行版自带的包管理器安装vim,比如Ubuntu使用 apt install vim
  • Mac用户: brew install vim

与此同时,倘若用vim写C++程序的话,我们还需要安装g++与gdb。

  • Mac用户: brew install g++ ,gdb最好就改成mac自带的lldb好了
  • Ubuntu用户: apt-get install g++ && apt-get install gdb
  • Windows用户:
    • 方法1:首先安装Dev-C++,在安装目录中找到g++,gdb所在的目录,将目录加入windows环境变量path中,至于环境变量该怎么用,有什么意义,不是本文探讨的范畴。
    • 方法2:安装MingW,然后使用mingw-get安装g++。

运行

Windows用户

windows用户在桌面打开图标就好了。

当你看到帮助乌干达的可怜儿童那句话就说明你成功打开vim了~

注意vim存在一个当前目录的概念,windows大家最好还是通过“用Vim打开”一个目录特定文件的方式进入vim以确保当前文件夹就是你想要储存文件的文件夹。否则,倘若直接打开vim的桌面快捷方式,你保存的文件会去一个奇奇怪怪的地方。

而检验gdb/g++是否安装成功,你只需要在CMD里面输入命令 g++ ,看是否报错说没有该命令。

MAC/LINUX用户

mac、linux用户在终端输入vim命令即可,在mac/linux终端里面,你在哪个文件夹下打开的vim,vim的当前目录就是哪个。

而检验gdb/g++是否安装成功,你只需要在终端输入命令 g++ ,看是否报错说没有该命令。

vim简易教程

首先,vim官方是有教程的,在开始界面输入 :help 就可以直接进去

其次,如果想要学习vim,最好拿着电脑跟着教程亲自试一试

最后,要有心理准备,你可能会经历一周左右生不如死的适应过程。

第一步:像用记事本一样用vim

先说个笑话:

问:如何生成随机字符串
答:让萌新退出vim

大家可以自己尝试一下(windows用户不允许使用窗口那个红叉叉)就会发现,进入vim后,不但不容易退出,甚至按键盘是无法直接开始编写程序的,因为vim分为不同模式,开始时进入的模式叫做“命令模式”,是专门输入命令的,而我们需要在“插入模式”中写程序。

模式切换

命令模式:输入命令的模式。之后没有特殊指名,所以命令都在命令模式下输入

插入模式:输入字符直接插入文本的模式

  • 命令模式->插入模式: i
  • 插入模式->命令模式: <esc>

基本操作

vim中,有很多操作以冒号开头,这些操作使用频率相对较低,但不影响其重要性:

退出vim:退出 :q 或强制退出 :q!

  • 查看帮助: :help
  • 运行命令: :!<command> ,例如 :!ls 列出当前文件夹下文件(命令的执行目录为当前vim打开的文件的目录)
  • 保存文件: :w 或 :w <filename>
  • 打开文件(若不存在新建): :e filename

保存与打开文件可以为绝对路径,也可以为一个文件名,此时默认在“当前目录”打开

快捷操作

在命令模式中,命令不一定以冒号开头。

  • 剪切/删除一行: dd
  • 粘贴: p
  • 撤销: u
  • 回退: <ctrl+r>
  • 光标移动:方向键或 h / j / k / l

第二步:像IDE一样用vim

先从一个特定目录进入vim,然后编写程序,并保存为a.cpp.

我们知道,倘若在终端下切换到了源文件所在目录,执行 g++ a.cpp o a 可以将当前文件下的a.cpp编译为可执行文件,同理,调用 gdb ./a 可以使用gdb调试程序(mac是lldb)。

我们还知道,vim中,我们可以使用 :!<command> 调用外部命令,所以我们在vim中输入 :!g++ % -o %< 就等同于从终端用g++命令编译源文件。(注意:命令中的 % 表示当前vim正在编辑的文件的文件名,而 %< 则表示他去了后缀的文件名。)

通过上面所述的手段,我们可以实现在vim中的编译运行:

  • 编译: :!g++ % -o %<
  • 调试: :!gdb %<
  • 运行: :!%< 
  • 查看空间: :!size %<

vim提供了键盘映射函数map,形如 :map A : B ,那么如果我们提前用了映射命令,就不用输入编译命令了。

  • 编译: :map <F9> : !g++ % -o %< <CR>
  • 运行: :map <F6> : !%< <CR>
  • 调试: :map <F5> : !gdb %< <CR>

其中, <CR> 表示回车,读者可以试试没有这个 <CR> 会变成什么。

通过了映射,我们可以直接按 <F9> 编译,按 <F5> 调试。

最后,由于这些map命令时预处理性质的,我们可以将它写入一个叫做vimrc的文件,每次打开vim的时候自动加载

  • 在mac/linux中该文件为 ~/.vimrc
  • 在windows中,可在菜单栏-编辑-启动设置中直接修改

比如我们可以在vimrc中加入

 

第三步:定义自己的vim

之前已经知道vimrc中可以定义编译运行快捷键,实际上还可以有更多的自定义属性,比如:

  • 左边显示行号: set number
  • 查找命令高亮: set hlsearch
  • 开启自动缩进:
    • set smartindent
    • filetype plugin indent on
  • 用鼠标拖拽分栏: set mouse=a
  • 设置折叠: set fdm=marker
  • 设置缩进:
    • set tabstop=2
    • set softtabstop=2
    • set shiftwidth=4
  • 设置代码高亮: syntax on
  • 令插入模式jj为退出: imap jj <esc>
  • 设置颜色: colors evening

其中,颜色我推荐大家使用

加上这些命令之后,界面美观度一下就上升了——

最后,读者可以在文末找到我的vimrc配置文件。

第四步:神一般的编辑器

了解完之前的只是,我们也只能把vim当做记事本来用,下面介绍一些快捷键,可以极大加快编写代码的速度。在开始了解快捷键前,你需要知道vim快捷键的设计初衷是让你可以不用鼠标,甚至手不离开主键盘就可以打字。

基本操作

vim中,有很多操作以冒号开头,这些操作使用频率相对较低,但不影响其重要性:

  • 退出Vim: :q
  • 强制退出Vim: :q!
  • 查看帮助: :help
  • 运行命令: :!<command> ,例如 :!ls 列出当前文件夹下文件(命令的执行目录为当前vim打开的文件的目录)
  • 保存文件: :w 或 :w <filename>
  • 强制保存: :w!
  • 保存并退出: :wq
  • 打开文件(若不存在新建): :e filename

撤销操作

  • 撤销: u
  • 回退: <ctrl+r>

分栏

  • 竖向分栏: :vsp
  • 横向分栏: :sp
  • 不同栏跨越: :<ctrl+w> <up>/<down>/<left>/<right> 或 :<ctrl+w> i/j/k/l

多文件

打开vim时用 vim a.cpp b.cpp 或 vim *.cpp 可以同时打开多个文件,不过第一个文件全屏显示,需要通过快捷键切换。

  • 下一个文件: :n
  • 上一个文件: :N

进入插入模式

  • 当前光标前: i
  • 当前光标后: a
  • 当前行首: I
  • 当前行末: A
  • 当前行后新建一行: o
  • 当前行前新建一行: O
  • 退出插入模式:
    • 默认:  <esc>
    • 在配置文件中加入 imap jj : <esc> 之后,可用 jj 退出插入模式

进入覆盖模式

  • 进入字符覆盖模式,并在覆盖后回到命令模式: r
  • 直接进入覆盖模式: R

复制粘贴与删除

  • 复制当前行: yy
  • 剪切当前行: dd
  • 删除光标位置字符: x
  • 复制之后的10行: 10yy
  • 剪切之后的10行: 10dd
  • 全文复制: :%y
  • 复制第3行到第5行: :3,5 y
  • 剪切第3行到第5行: :3,5 d
  • 粘贴: p
  • 粘贴10份: 10p

光标跳转

  • 行首: ^
  • 行末: $
  • 第5行: 5G 或 :5
  • 光标移动:方向键或 h / j / k / l
  • 文首: H
  • 文末: L

查找替换

  • 查找: /pattern
  • 全文替换: :%s/old_pattern/new_pattern/g
  • 全文替换(每行替换第一个匹配): :%s/old_pattern/new_pattern
  • 第5至10行替换: :5,10s/old_pattern/new_pattern/g
  • 跳到下一个: n
  • 跳到上一个: N
  • 替换所有int单词(正则表达式使用): :%s/\<int\>/long long/g

代码缩进

  • 自动缩进: yy=G 或 gg=G
  • 第五行到第十行向前缩进: 5,10 <
  • 第五行到第十行向后缩进: 5,10 >
  • 接下来五行向前缩进: 5<<
  • 压行: J

其他

  • 括号匹配: %
  • 宏定义(录制操作): q<name>
  • 宏定义调用: @<name> 或 @@
  • 调用make命令: :make

再论Vim的好处

效率高

  1. 使用了vim你的手甚至可以不动鼠标,不离主键盘
  2. 编程中存在的很多类似于复制特定行,粘贴若干遍的操作在vim下都是可以用命令解决的
  3. 查找替换功能强大,连不能查找替换的很多功能都可以用宏来解决,对于批量修改帮助极大
  4. 其配套的gdb让你永远告别肉眼查错,进入二分查错的行列,复杂度直接从O(N)降到了O(logN)

用途广

  1. 基本上所有平台都有vim,而其他IDE只能在特定平台用
  2. vim的操作不依赖于具体语言,不需要每学一门语言都重新学习一个新的IDE
  3. 有丰富的插件可以使用(但是插件方面Vim比起Emacs还是差远了)
  4. Vim模式在很多地方都有支持,比如Emacs和Sublime,所以你学了Vim还可以直接迁移到这些平台上

好玩

  1. 突然发现找到了除大括号换不换行(当然要换)之外新的战场:编辑器用Emacs还是Vim(当然是Vim),引战什么的最好玩了~
  2. 我这人还是非常支持慈善事业的

结语

愿世界和平,乌干达儿童幸福快乐~

 

原文为《【和平向】一波vim安利》,计62班级公众号

配置文件

总结下来,一个简单的windows版vimrc如下(其中1-33行是自带的内容,34行之后是自定义内容),这个版本适合OI党使用,可以现场手敲vimrc。

MAC版的vimrc如下(支持了更多文件的运行快捷键)

最后,可以在这里找到我的最新的vimrc

原创文章地址:【vim简单教程及配置】转载时请注明出处mhy12345.xyz