首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
241 阅读
2
nvim番外之将配置的插件管理器更新为lazy
133 阅读
3
2018总结与2019规划
133 阅读
4
从零开始配置 vim(15)——状态栏配置
122 阅读
5
PDF标准详解(五)——图形状态
103 阅读
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
emacs
VimScript
读书笔记
linux
elisp
文本编辑器
Java
反汇编
OLEDB
数据库编程
投资理财
Masimaro
累计撰写
363
篇文章
累计收到
32
条评论
首页
栏目
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
页面
归档
友情链接
关于
搜索到
363
篇与
的结果
2023-03-14
从0开始自制解释器——实现多个整数的加减法
在上一篇我们实现了一个可以计算两个多位整数加减法的计算器。本章我们继续来给这个计算器添加功能,这次要给它添加可以连续计算多个整数相加减的功能。例如我们可以计算 1 + 2 + 3 这样的表达式。语法图在正式写代码之前让我们先来学习一下一些基本的理论知识。这次要介绍的理论是语法图。什么是语法图呢?语法图是编程语言语法语法规则的图形表示。它体现了词法分析的运行规则。语法图直观的展示了在编程语言中哪些语句是符合语法的,哪些是不符合语法规范的。语法图的阅读非常容易,它类似于程序的流程图,只要顺着箭头指向的路径来读即可。与程序流程图类似,语法图中有些路径表示选择,有些表示循环。我们试着来读一下下面的语法图这张语法图表示的含义是,一个术语(term) 可选的跟上一个加号或者减号,而后面又需要跟上另一个术语。接着又可以有选择的跟上另一个加号或者减号。但是加号或者减号后面必须跟上另一个术语。这里又提到另一个单词,term 它的中文意思是术语。似乎很难用其他文字来解释何为术语。你只需要知道在这里它代表的是一个整数,它并不影响我们阅读这个语法图代码展示在上一篇中我们提到,将Token流识别为对应结构的过程被称之为词法分析,我们代码中的词法分析的实现主要在函数 expr 中。在这个函数中我们主要实现了词法分析以及最后的解释执行。我们按照语法图修改一下词法分析的代码我们先给出下面的伪代码获取第一个整数作为计算结果保存 while(解析到最后一个字符) { 获取操作符(+/-) switch(操作符) { case +: 获取下一个整数,如果不是整数则退出并报错 与结果相加 break; case -: 获取下一个整数,如果不是整数则退出并报错 与结果相减 break; } } 最终打印计算结果或者打印语法错误基于这个思路我们给出具体的实现代码int expr() { bool bRet = false; int result = get_term(&bRet); int bEOF = false; do { ETokenType oper = get_oper(&bRet); switch (oper) { case PLUS: { int num = get_term(&bRet); if(bRet) result += num; } break; case MINUS: { int num = get_term(&bRet); if(bRet) result -= num; } break; case END_OF_FILE: printf("%d\n", result); bEOF = true; break; default: bRet = false; break; } } while (bRet && !bEOF); if (!bRet) { printf("Syntax Error!\n"); } }这里为了便于理解,我将获取整数和操作符的模块又进行了一次封装,提供了两个函数分别是 get_term() 和 get_oper()。它们的代码如下int get_term(bool *pRet) { Token token = { 0 }; dyncstring_init(&token.value, DEFAULT_BUFFER_SIZE); int value = 0; if (get_next_token(&token) && token.type == CINT) { value = atoi(token.value.pszBuf); if (pRet) *pRet = true; } else { if (pRet) *pRet = false; } dyncstring_free(&token.value); return value; }ETokenType get_oper(bool* pRet) { Token token = { 0 }; dyncstring_init(&token.value, DEFAULT_BUFFER_SIZE); int oper = 0; if (get_next_token(&token) && (token.type == PLUS || token.type == MINUS)) { oper = token.type; if (pRet) *pRet = true; } else if (token.type == END_OF_FILE) { oper = END_OF_FILE; if (pRet) *pRet = true; } else { oper = -1; if (pRet) *pRet = false; } dyncstring_free(&token.value); return oper; }到此为止,就实现了多个整数的算术运算。整个实现过程的代码我都放到该位置。有兴趣的小伙伴可以自己对照着代码跟着我一起来实现属于自己的解释器。
2023年03月14日
22 阅读
0 评论
0 点赞
2023-03-08
从0开始自制解释器——实现多位整数的加减法计算器
上一篇我们实现了一个简单的加法计算器,并且了解了基本的词法分析、词法分析器的概念。本篇我们将要对之前实现的加法计算器进行扩展,我们为它添加以下几个功能计算减法能自动识别并跳过空白字符不再局限于单个整数,而是能计算多位整数提供一些工具函数首先为了支持减法,我们需要重新定义一下TokenType这个类型,也就是需要给 - 定义一个标志。现在我们的TokenType的定义如下typedef enum e_TokenType { CINT = 0, PLUS, MINUS, END_OF_FILE }ETokenType;由于需要支持多个整数,所以我们也不知道最终会有多少个字符,因此我们提供一个END_OF_FILE 表示我们访问到了最后一个字符,此时应该退出词法分析的过程。另外因为整数个数不再确定,我们也就不能按照之前的提供一个固定大小的数组。虽然可以提供一个足够大的空间来作为存储数字的缓冲,但是数字少了会浪费空间。而且考虑到之后要支持自定义变量和函数,采用固定长度缓冲的方式就很难找到合适的大小,太大显得浪费空间,太小有时候无法容纳得下用户定义的变量和函数名。因此这里我们采用动态长度的字符缓冲来保存。我们提供一个DyncString 的结构来保存这些内容#define DEFAULT_BUFFER_SIZE 16 // 动态字符串结构,用于保存任意长度的字符串 typedef struct DyncString { int nLength; // 字符长度 int capacity; //实际分配的空间大小 char* pszBuf; //保存字符串的缓冲 }DyncString, *LPDyncString; // 动态字符串初始化 // str: 被初始化的字符串 // size: 初始化字符串缓冲的大小,如果给0则按照默认大小分配空间 void dyncstring_init(LPDyncString str, int size); // 动态字符串空间释放 void dyncstring_free(LPDyncString str); //重分配动态字符串大小 void dyncstring_resize(LPDyncString str, int newSize); //往动态字符串中添加字符 void dyncstring_catch(LPDyncString str, char c); // 重置动态数组 void dyncstring_reset(LPDyncString str);它们的实现如下/*----------------------------动态数组的操作函数-------------------------------*/ void dyncstring_init(LPDyncString str, int size) { if (NULL == str) return; if (size == 0) str->capacity = DEFAULT_BUFFER_SIZE; else str->capacity = size; str->nLength = 0; str->pszBuf = (char*)malloc(sizeof(char) * str->capacity); if (NULL == str->pszBuf) { error("分配内存失败\n"); } memset(str->pszBuf, 0x00, sizeof(char) * str->capacity); } void dyncstring_free(LPDyncString str) { if (NULL == str) return; str->capacity = 0; str->nLength = 0; if (str->pszBuf == NULL) return; free(str->pszBuf); } void dyncstring_resize(LPDyncString str, int newSize) { int size = str->capacity; for (; size < newSize; size = size * 2); char* pszStr = (char*)realloc(str->pszBuf, size); str->capacity = size; str->pszBuf = pszStr; } void dyncstring_catch(LPDyncString str, char c) { if (str->capacity == str->nLength + 1) { dyncstring_resize(str, str->capacity + 1); } str->pszBuf[str->nLength] = c; str->nLength++; } void dyncstring_reset(LPDyncString str) { dyncstring_free(str); dyncstring_init(str, DEFAULT_BUFFER_SIZE); } /*----------------------------End 动态数组的操作函数-------------------------------*/另外提供一些额外的工具函数,他们的定义如下void error(char* lpszFmt, ...) { char szBuf[1024] = ""; va_list arg; va_start(arg, lpszFmt); vsnprintf(szBuf, 1024, lpszFmt, arg); va_end(arg); printf(szBuf); exit(-1); } bool is_digit(char c) { return (c >= '0' && c <= '9'); } bool is_space(char c) { return (c == ' ' || c == '\t' || c == '\r' || c == '\n'); }主要算法我们还是延续之前的算法,一个字符一个字符的解析,只是现在需要额外的将多个整数添加到一块作为一个整数处理。而且需要添加跳过空格的处理。首先我们对上次的代码进行一定程度的重构。我们添加一个函数专门用来获取下一个字符char get_next_char() { // 如果到达字符串尾部,索引不再增加 if (g_pPosition == '\0') { return '\0'; } else { char c = *g_pPosition; g_pPosition++; return c; } }expr() 函数里面大部分结构不变,主要算法仍然是按次序获取第一个整数、获取算术运算符、获取第二个整数。只是现在的整数都变成了采用 dyncstring 结构来存储int expr() { int val1 = 0, val2 = 0; Token token = { 0 }; dyncstring_init(&token.value, DEFAULT_BUFFER_SIZE); if (get_next_token(&token) && token.type == CINT) { val1 = atoi(token.value.pszBuf); } else { printf("首个操作数必须是整数\n"); dyncstring_free(&token.value); return -1; } int oper = 0; if (get_next_token(&token) && (token.type == PLUS || token.type == MINUS)) { oper = token.type; } else { printf("第二个字符必须是操作符, 当前只支持+/-\n"); dyncstring_free(&token.value); return -1; } if (get_next_token(&token) && token.type == CINT) { val2 = atoi(token.value.pszBuf); } else { printf("操作符后需要跟一个整数\n"); dyncstring_free(&token.value); return -1; } switch (oper) { case PLUS: { printf("%d+%d=%d\n", val1, val2, val1 + val2); } break; case MINUS: { printf("%d-%d=%d\n", val1, val2, val1 - val2); } break; default: printf("未知的操作!\n"); break; } dyncstring_free(&token.value); }最后就是最终要的 get_next_token 函数了。这个函数最主要的修改就是添加了解析整数和跳过空格的功能bool get_next_token(LPTOKEN pToken) { char c = get_next_char(); dyncstring_reset(&pToken->value); if (is_digit(c)) { dyncstring_catch(&pToken->value, c); pToken->type = CINT; parser_number(&pToken->value); } else if (c == '+') { pToken->type = PLUS; dyncstring_catch(&pToken->value, '+'); } else if (c == '-') { pToken->type = MINUS; dyncstring_catch(&pToken->value, '-'); } else if(is_space(c)) { skip_whitespace(); return get_next_token(pToken); } else if ('\0' == c) { pToken->type = END_OF_FILE; } else { return false; } return true; }在这个函数中我们先获取第一个字符,如果字符是整数则获取后面的整数并直接拼接为一个完整的整数。如果是空格则跳过接下来的空格。这两个是可能要处理多个字符所以这里使用了单独的函数来处理。其余只处理单个字符可以直接返回。parser_number 和 skip_whitespace 函数比较简单,主要的过程是不断从输入中取出字符,如果是空格则直接将索引往后移动,如果是整数则像对应的整数字符串中将整数字符加入。void skip_whitespace() { char c = '\0'; do { c = get_next_char(); } while (is_space(c)); // 遇到不是空白字符的,下次要取用它,这里需要重复取用上次取出的字符 g_pPosition--; } void parser_number(LPDyncString dyncstr) { char c = get_next_char(); while(is_digit(c)) { dyncstring_catch(dyncstr, c); c = get_next_char(); } // 遇到不是数字的,下次要取用它,这里需要重复取用上次取出的字符 g_pPosition--; }唯一需要注意的是,最后都有一个 g_pPosition-- 的操作。因为当我们发现下一个字符不符合条件的时候,它已经过了最后一个数字或者空格了,此时应该已经退回到get_next_token 函数中了,这个函数第一步就是获取下一个字符,因此会产生字符串被跳过的现象。所以这里我们执行 -- 退回到上一个位置,这样再取下一个就不会有问题了。最后为了能够获取空格的输入,我们将之前的scanf 改成 gets。这样就大功告成了。我们来测试一下结果最后的总结最后来一个总结。本篇我们对上一次的加法计算器进行了简单的改造,支持加减法、能跳过空格并且能够计算多位整数。在上一篇文章中,我们提到了Token,并且说过,像 get_next_token 这样给字符串每个部分打上Token的过程就是词法分析。get_next_token 这部分代码可以被称之为词法分析器。这篇我们再来介绍一下其他的概念。词位(lexeme): 词位的中文解释是语言词汇的基本单位。例如汉语的词位是汉字,英语的词位是基本的英文字母。对于我们这个加法计算器来说基本的词位就是数字以及 +\- 这两个符号parsing(语法分析)和 parser(语法分析器) 我们所编写的expr函数主要工作流程是根据token来组织代码行为。它的本质就是从Token流中识别出对应的结构,并将结构翻译为具体的行为。例如这里找到的结构是 CINT oper CINT。并且将两个int 按照 oper 指定的运算符进行算术运算。这个将Token流中识别出对应的结构的过程我们称之为语法分析,完成语法分析的组件被称之为语法分析器。expr 函数中即实现了语法分析的功能,也实现了解释执行的功能。
2023年03月08日
22 阅读
0 评论
0 点赞
2023-03-07
从0开始自制解释器——实现简单的加法计算器
为什么要学习编译器和解释器呢?文中的作者给出的答案有下面几个:为了深入理解计算机是如何工作的:一个显而易见的道理就是,如果你不懂编译器和解释器是如何工作的那么你就不明白计算机是如何工作的编译器和解释器用到的一些原理和编程技巧以及算法在其他地方也可以用到。学习编译器和解释器能够学到并强化这些技巧的运用为了方便日后能编写自己的编程语言或者专用领域的特殊语言接下来我们就从0开始一步一步的构建自己的解释器。跟着教程先制作一个简单的加法计算器,为了保证简单,这个加法计算器能够解析的表达式需要满足下面几点:目前只支持加法运算目前只支持两个10以内的整数的计算表达式之间不能有空格只能计算一次加法举一个例子来说,它可以计算诸如"1+2"、"5+6" 这样的表达式,但是不能计算像 "11+20"(必须是10以内)、"1.1+2"(需要两个数都是整数)、"1 + 2"(中间不能有空格)、"1+2+3"(只能计算一次加法)有了这些限制,我们很容易就能实现出来。实现的算法假设我们要计算表达式 5+6。这里主要的步骤是通过字符串保存表达式,然后通过索引依次访问每个字符,分别找到两个整数和加法运算符,最后实现两个整数相加的操作。第一步,我们的索引在表达式字符串的开始位置,解析得到当前位置的字符是一个整数,我们给它打上标记,类型为整形,值为5。第二步,索引向前推进,解析当前位置的字符是一个+。还是给它打上标记,类型为plus,值为+。第三步,索引继续前进,解析到当前位置的字符是一个整数,我们给它打上标记,类型为整形,值为6最后一步,根据得到的两个整数以及要执行的算术运算,我们将两个数直接进行相加得到最终结果具体的代码首先我们定义这个标记的类型,目前支持整数以及加法的标记typedef enum e_TokenType { CINT = 0, //整型 PLUS //加法运算符 }ETokenType; // 这里因为只支持10以内的整数,所以表示计算数字的字符只有一个,加上字符串最后的结束标记,字符数组只需要两个即可 typedef struct Token { ETokenType type; //类型 char value[2]; //值 }Token, *LPTOKEN;接着定义一些全局变量来保存算术运算的表达式和当前指针的索引char* g_pszUserBuf = NULL; char* g_pPosition = NULL;接着我们定义一个函数来模拟上述说到的不断解析每一个字符的过程bool get_next_token(LPTOKEN pToken) { char* sz = g_pPosition; g_pPosition++; pToken->value[0] = '\0'; if (*sz >= '0' && *sz <= '9') { pToken->type = CINT; pToken->value[0] = *sz; return true; } else if (*sz == '+') { pToken->type = PLUS; pToken->value[0] = *sz; return true; } else { pToken->value[0] = '\0'; return false; } }最后我们定义一个函数来执行获取每个标记并最终计算结果的操作int expr() { int val1 = 0, val2 = 0; Token token = { 0 }; if (get_next_token(&token) && token.type == CINT) { val1 = atoi(token.value); } else { printf("首个字符必须是整数"); return -1; } if (get_next_token(&token) && token.type == PLUS) { } else { printf("第二个字符必须是操作符,并且当前只支持 + 运算"); return -1; } if (get_next_token(&token) && token.type == CINT) { val2 = atoi(token.value); } printf("%d+%d=%d\n", val1, val2, val1 + val2); }在main函数里面我们只需要建立一个缓冲来保存字符,并且在循环中不断等待用户输入,完成解析并输出结果即可// 重制当前解析环境 void reset() { memset(g_pszUserBuf, 0x00, 16 * sizeof(char)); scanf_s("%s", g_pszUserBuf); g_pPosition = g_pszUserBuf; } int main() { g_pszUserBuf = (char*)malloc(16 * sizeof(char)); while (1) { printf(">>>"); reset(); if (strcmp(g_pszUserBuf, "exit") == 0) { break; } expr(); } return 0; }最终执行的结果如下最后的总结程序我们已经写完了,你可能觉得这个程序太简单了,只能做这点事情。别着急,后面将会逐步的去完善这个程序。以便它能实现更加复杂的运算。最后我们来引入一些概念性的东西:我们将输入内容按照一定规则打上的标记被称之为Token上述get_next_token函数体现的将一段字符串分割并打上有意义的标签的过程被称为词法分析。解释器工作的第一步就是将输入的字符串按照一定的规则转换为一系列有意义的标记。完成这个工作的组件被称之为词法分析器,也可以被称为扫描器或者分词器
2023年03月07日
16 阅读
0 评论
0 点赞
2023-03-04
从0开始自制解释器——综述
作为一个程序员,自制自己的编译器一直是一个梦想。之前也曾为了这个梦想学习过类似龙书、虎书这种大部头的书,但是光看理论总有一些云里雾里的感觉。看完只觉得脑袋昏昏沉沉并没有觉得有多少长进。当初看过《疯狂的程序员》这本书,书里说,真正能学会编译原理并不是靠看各种书然后通过相关考试,而是有一天你的领导找到你对你说:“小X啊,你是我们公司技术能力最强的人,咱们现在用的编译器性能有点跟不上,要不你看看能不能改进一下”。所以想要学习编译原理相关的知识首先要做的还是实践——实现一个自己的编译器。之前也看过类似的教你如何自制编译器,但是他们有一个共同的问题就是在很大程度上都借助第三方工具,隐藏了一些底层的细节。我希望的是每一行代码都是自己的完成的。所以一直怀揣着这个梦想直到最近我找到了一篇教程。一起写一个简单的编译器——魔力Python。这篇教程是实用Python完成的,但是这里我不打算使用Python,我打算实用最纯粹的C 语言来完成这个任务,我考虑使用C主要基于以下几个原因:Python 有一些封装的细节,不方便全方位的展示相关算法。原教程使用的就是Python,还用一样的话思路会受到教程的影响,要真正的理解需要自己一行行的敲代码,最好的方式就是用另一种语言来实现同样的算法现在市面上大多数都是用c来实现编译器,如果后续想要更近一步学习编译原理可以考虑在我完成的这版中很方便的加入一些新学的知识点自己有使用C的能力,而且用C写编译器自带装B属性基于以上理由,我准备开始跟着教程使用C来实现自己的解释器。这并不是一篇教程什么的,更多的是作为一篇实践笔记。而且根据我之前写的Vim专栏的经验来说,将它已专栏的形式发布出来之后鸽的可能性更小,更有动力来完成它。当然如果各位能从专栏中学到什么那就更好了。总之后面让我们一起进入学习编译原理的路程吧
2023年03月04日
63 阅读
1 评论
0 点赞
2023-03-04
从零开始配置vim(32)——最后再说两句
很抱歉我决定结束这个系列的内容了。原本我打算介绍markdown、orgmode相关的配置,甚至还打算介绍如何在vim 中使用 emacs 的 org-agenda 来进行日常的任务管理。但是出于一些原因我打算放弃了。首先如果将markdown 理解为另一种类似于HTML 的标记语言的话,我们在介绍LSP 的时候已经介绍过该如何新增新的编程语言的支持,再另外介绍Markdown 的配置就显得多余了。而且本系列也并不打算事无巨细的带领大家从零开始配置一套完整的配置,我仅仅希望通过这一系列的内容介绍一下vimscript 或者lua 接口以及vim 的一些特性,让大家看完之后又能力自行动手弄出一套属于自己的配置。至于orgmode 的内容,我发现目前还没有任何插件能完美的模拟emacs 的orgmode 功能。vim 上的插件也仅仅能做到渲染样式,语法高亮而已。也就没有必要单独介绍了。如果后续我能掌握 emacs 的话,再来介绍也不迟总之就是本系列到此结束了。一些建议不知道各位小伙伴在跟着我这一系列文章尝试自己配置vim 的时候有什么感觉?我当初在整理这些配置的时候发现它越来越像vs code ,甚至最近几年新推出的LSP以及 DAP 的一些插件几乎都是原生的用于 vscode 上的或者从它上面移植过来的。有些主题也是照搬 vscode 的。我们发现自己费劲心力终于将vim 变成的 vscode 。有没有觉得在做无用功?既然要将它变成 vscode 那为何不直接使用 vscode 呢?可能有人会说, vscode 对于vim的一些模式和 ex 命令的支持并不好。我想这就是我们使用 vim 的理由,也是vim 比其他编辑器强的地方。我们仅仅是在使用工具而已,哪个工具好用,哪个工具能帮助我们快速完成工作,那就用哪个。工具本身没有高低贵贱之分,只有合适与否的差异。作为程序员要拥抱新技术,千万不要抱着某个技术某个工具不放。也不要觉得用vim 的比用 vscode 或者其他编辑器的高级,就高人一等。vim自身也在吸收其他技术不断的成长,例如它从 vscode 那边学来了LSP 和 DAP 。这就有点像武侠小说中的吸功大法,集万物所长为我所用。另外一条建议就是千万不要拿我给出的配置直接来进行使用。这一套配置仅仅是为了教学使用,很多地方没有进行深度定制,并且基本采用白话的写法,完全不考虑封装性和程序设计,另外我也没有考虑通用性,很多小伙伴评论出现了各种各样的问题,最后就是它的效率也不算高。我也不希望自己的文章仅仅给各位小伙伴提供了一套配置。我更希望小伙伴们能通过这一系列文章学到一点东西,从这套配置中衍生出一套适合自己的内容。若干年以后,各位小伙伴在对vim有更深的理解回过头来看到这套配置时可能发出这样的声音:“这是什么破烂配置,连 xxx 的支持都没有;有些功能有时候会报错,我看看把它改好;启动时间咋这么慢,我能把它优化到xx毫秒;现在还在用xx技术早就落伍了,看我把它改成用xx技术”。(我自认为本系列最有价值的是开始配置之前,vim相关特性的介绍)最后的一条建议就是,如果各位小伙伴未来将长时间使用vim 进行代码的编写和日常的开发。那么我推荐使用一些社区比较活跃的第三方通用配置,例如我最近在使用的lunarVim。使用这类的配置有一些好处:不用费力折腾配置,节约时间学习高手的配置,提升自己对编辑器的审美。就像没学习vim之前我一直觉得使用编辑器用鼠标选中文本是天经地义的事,我习惯了它,甚至习惯了用鼠标翻页等操作,完全不知道这样有多么的浪费时间。通过高手配置可能能使你重新审视自己使用编辑器的习惯,从而找到一套真正适合自己的高效的文本操作术。社区活跃的话,除了问题不用自己死磕,可能有人能帮忙解决PS: 如果各位觉得我的教程不好或者有些内容没有提到,各位可以去看看lunarVim作者的另一个项目,Neovim-from-scratch 该项目也是从0开始配置vim,并且在油管上有对应的教学视频。后面的学习通过本系列的学习相信各位小伙伴已经有能力能看懂各种第三方配置的代码,能在此基础之上衍生出一套属于自己的配置。甚至能完全抛弃第三方配置独立弄出一套自己的配置。所以后面我推荐的学习路线就是:不断阅读vim官方手册熟练使用某一个第三方配置在熟练的基础之上根据自己的习惯来定制一些只属于自己的功能形成一套只属于自己的科学的、高效的文本操作习惯根据这套习惯尝试定制自己的配置在其他编辑器中通过一定的配置尝试复刻这一套科学而又高效的操作习惯目前我正在第三部分努力。希望本系列文章能带领大家真正入门vim ,不会再出现因为觉得难而中途放弃。最后祝愿各位小伙伴在vim的使用中能收获快乐,并坚持下去!
2023年03月04日
24 阅读
0 评论
1 点赞
2023-02-18
2023年阅读清单
2022 年总的来说读的书并不多,虽然上班通勤时间变长,按道理来说这段时间是读书的大好时光,但是我自己躲不过手机的诱惑,经常刷刷B站或者知乎,时间就过去了。想到这里我感觉有点后悔,大量的时间被浪费了。但是再怎么后悔2022已经过去,还是打起精神好好度过2023年才是真的。总的来说记录自己读书的清单并且写一些评语还是很有用的,有时候书读过后忘记了,为了写点评语我又回过头来看看书中的内容,并且为了这个目的有时候也会思考并且做些笔记,总之这个办法比起囫囵吞枣的堆读书数据来看收获更多,我还是坚持这个习惯《Unix传奇》本书的作者 Brain W.Kernighan 博士退休前在贝尔实验室计算科学研究中心工作。并且与肯.汤姆森和丹尼斯.李奇是很好的同事和朋友,作者亲眼见证了Unix的诞生以及发展。本书充满了有趣的回忆。书中从贝尔实验室的诞生开始介绍,并且介绍了实验室中一些科室中的有趣的人和事物,例如他们对必须挂工牌的反感以及对同事的恶作剧。实验室没有现在很多公司提倡的KPI、KOR等等考核指标,只有轻松的工作氛围,所有员工都在自己感兴趣的领域深入研究,谁也不知道能研究出什么样子。上层高管们也没有规定必须出现成果或者要产生商业价值,可以说贝尔实验室的工作氛围是所有打工人都梦寐以求的。只不过看着新进来的卡内基梅隆大学、斯坦福大学、伯克利大学加州分校的研究生,这是我等普通打工人无法进入的领域。贝尔实验室真是一个理想主义大放异彩的时代!先后诞生了Unix、C语言、管道、Grep、vi、yacc、lex、awk 、BSD Unix(虽然这个是因为版权问题由伯克利大学重新编写代码)、以及在此之上的BSD Socket、Linux。另外书中介绍的Unix从诞生之初就有许多先进的、划时代的设计理念。比如沿用至今的Unix哲学——“一个软件只用做好一件事情,不要试图在单个程序中完成多个任务”。还有类似于开创性的将文件内容和解析交给上层应用程序,在操作系统看来文件只是一堆二进制内容,操作并不关心。同时为了安全设计了沿用至今的文件权限的标识位。后续又发明了管道、grep、vi、yacc、lex、awk等等工具。作者按照时间发展的顺序清晰的讲解了unix的发展历史。虽然最后因为种种原因Unix不得不退出历史舞台,但是它的诞生种子已经在各行各业生根发芽并成长为参天大树。比如现在还存在的BSD Unix以及在其上发展的TCP/IP协议层的实现代码、以及后续诞生的Linux。为如今的互联网发展奠定了坚实的基础。书中也能学到一些设计理念,我总结的理念如下:清晰原则:代码尽量写的清晰易懂模块原则:每个程序只用做好一件事情,不要试图在单个程序中完成多个任务组合原则:不同程序之间通过接口相连,接口之间用文本进行通信。例如为了这个发明的管道。《穷查理宝典》本书是查理芒格本人的生平以及芒格发表的各路演讲、各色媒体对芒格本人评价的大杂烩。本书号称展示了芒格的处事智慧,谦逊的为人,令人敬佩的价值观等等,似乎看过之后对自己的人生有很大的帮助,但是在我看来这是言过其实,而且也对不起它在豆瓣的高分。全书读下来我认为有这么几个问题:本书是一个大杂烩,本来用很小的篇幅就能说明的问题硬是搞出500多页的内容,恨不得把关于查理芒格的所有东西都加进去。大量重复无用的篇幅读下来就是恶心、浪费时间。本书标榜的是介绍查理芒格的智慧以及处事哲学,但是里面充斥着社会上形形色色的人对芒格的评价。此举有点像为了凑字数骗稿费。本书最大的败笔是里面包含大量无用的插图,并且大段关于插图的解说,它们与正文杂糅在一起没有很好的区分,容易干扰思路。本书中芒格教导我们要谦逊、正直、好学这些老生常谈的就不说了。里面也有一些有价值的观点:哪年你没有破坏一个你最爱的观点,那么你这些年就白过了。(就像前几年网上一句话:所谓成长就是不断觉得过去的自己就是一个傻x)如果一件事是个坏主意,你不会做过头。但是如果一件事是一个好主意,蕴含着重要的正理,那么你就没办法忽略了。然后你就很容易做过头。所以呢,如果你把它们做过头了,那些好主意是让你遭受可怕后果的好方法(一个人太过于认为自己是正确的是十分危险的,为了达到这个正确的结果往往会不择手段。就像复仇者联盟里面的灭霸为了达到使宇宙可持续发展这种正义的目的不惜牺牲宇宙一半的生命。另外罗翔老师说过我们无法用一个非正义的手段来达到正义的结果,我想说的应该就是这种情况吧)你必须知道重要学科的重要理论并经常使用它——要全部用上,而不是只用几种。大多数人都只养成一个学科。在手里拿着铁锤的人看来,世界就像钉子。我这辈子遇到的聪明人,没有不每天阅读的,一个都没有。简化任务的最佳方法一般是解决那些答案显而易见的大问题(《矛盾论》中也提到矛盾分为主要矛盾和次要矛盾,应该首先解决主要矛盾)这本书虽然拖沓、啰嗦,但是仍然一些比较好的观点和见解,值得一读,但是话说回来我只推荐前几章,后面的演讲部分反复讲一些重复的观点,完全可以跳过。最终我自己给这本书6分或者6.5分。《魔鬼数学》本书是一本关于数学的科普书。就跟它的副标题——《大数据时代数学思维的力量》一样,主要侧重与数学思维在如今这个大数据时代日常生活的相关应用。本书没有很深奥的数学知识,你只要有中学数学程度就能看懂。书中通过对我们日常生活中常见的例子进行探讨分析最终引出相关数学概念并给出一些解法。通过例子你不一定能清楚的学到很深的数学概念,但是相信通过这本书你对数学会有一个重新的认识。至少会明白这么多年经历的数学教育并没有白费,可能想要更好的买菜真的要用到数学知识。学好数学真的会让我们在往后的日子中少踩一些坑。读完之后我感觉自己至少有下面的一些收获:对彩票和博彩不再感兴趣了,及时或者博彩的期望高出了投入,在没有足够多本钱进行大量随机试验的情况下仍然可能亏损。概率高并不一定会发生,只是重复的次数够多发生的次数比不发生的次数多双轨思维,白天证明晚上证伪。就像孔子所说的,吾日三省吾身。对线性预测的准确性不太确定了,买基金或者做其他投资的时候不会说前面几年的业绩一直在增长,后面一定会增长及时官方的统计数据是准确的,但是有时候也不能轻易下结论,因为需要考虑统计的样本是否具有普遍性。这就是所谓的数据不会骗人但是统计学会对平庸有更好的容忍度,因为回归理论最终都是要回归平庸的。刻苦即勇气,在长时间毫无进展的情况下仍然坚持也是一种优秀的品质。这种品质甚至比所谓的聪明更有用数学不是在象牙塔中的高深知识,而是一种人类常识的直观体现下面摘录一些我喜欢的句子从事数学研究的人经常会问“你的假设是什么?这些假设合理吗?” 这样的问题令人厌烦,但有时却富有成效数学就是一些常识只要你认为“某个东西有价值,因此多多益善”,就是一种线性推理用一个数除以另一个数只是单纯的计算。考虑清楚用什么除以什么才是真正的数学问题贝叶斯定理不仅可以被看作一个数学方程式,还是一种偏重于数值的规则,它告诉我们如何结合新的观察结果修正我们赋予事物的置信度我的座右铭是“如果你将不可能排出在外,那么剩下的,无论可能性多么的小,都必然是事实,除非它是你没考虑到的那种假设”如果在机会对你有利时投入足够多的资金,就能抵消任何可能出现的坏运气我们用数学方法证明了一个我们已知的法则:钱越多,你所能承受的风险也就越大。有钱人有足够的资金储备,可以承受偶尔的失利造成的损失,并且通过继续投资,最终变得更有钱优秀的特质不会持续存在,随着时间的推移,平庸这位不速之客会悄然登场实际上,生活中随着时间产生起伏变化的的任何东西,几乎都会受到回归效应的影响相关关系并不意味着因果关系期望值并不代表我们期望发生的结果,而是指在多次做出该决定后的平均结果尽管看不到明显进展,却仍然全神贯注、有条不紊地反复钻研某个问题,不放过所有可能取得突破的机会。当今的哲学家把这种品质称作勇气,它是数学研究的必备条件白天证明,晚上反证的做法不仅适用于数学,还可以对我们的社会、政治、科学与哲学理念施加压力。在白天时,尽可能相信自己的理念是正确的,但是到了晚上,则认真思考自己的理念是不是错误的。不要自欺欺人!《财富自由之路》之前网上流传着这么一句话“你永远挣不到认知以外的钱,凭运气挣的钱最后也会凭实力亏掉”。与之对应的,想要财富自由首先自己要有相应的认知。这本书就是在重塑自己的认知,以便自己能配的上未来的财富自由。这本书是李笑来在微信公众号上同名专栏的一系列文章的汇总。本书阅读起来比较流畅,并没有晦涩难懂的地方,读起来一气呵成。可能是最有效的人生道理往往是最朴素、流传最广的。书中并没有什么比较新颖的地方,都是一些经常在其他书中提到的一些概念。它就是一本人生哲理的汇编,作者对一些哲理结合现代社会和自己的生活经历给出了一些新的理解,并且给出了一些切实可行的实践方法。虽然读起来可能没有醍醐灌顶的感觉,但也是一本重塑认知的好书。本书中比较重要的观点主要有下面几点:注意力>时间>金钱。 在有限的时间内集中注意力干有利于自己成长的事情,或者说是专注自己的成长想要快速学会某个知识点,行动是最重要的,在行动中学习理论。我们只需要掌握某个学科的入门级知识,剩下的在实践中学习选择比努力重要,想要在未来选择正确,需要大量不同学科知识的积累财务自由只是人生的某个里程碑,与高考结束,大学毕业一样,是人生的一个过程并不是人生的终点投资的刚需是避险,永远不要压上全部身家自己要先成为贵人,然后才能源源不断的遇到贵人改变能改变的,接受不能改变的是幸福的前提在财务自由前,出卖自己的时间。需要考虑这个工作能否让自己获得成长《刻意练习》在我阅读 《财富自由之路》的时候里面提到一个观点,“学习学习再学习”。也就是我们先要学习如何学习,然后再来进行学习。这本《刻意练习》正是教我们如何正确的学习。就像它的副标题写的那样——如何从新手到大师。采用书中刻意练习的方式可以相对快速的学习新的技能。一般读这种讨论方法的书,我总会考虑这么几个问题。是什么?为什么?怎么做?什么是刻意练习呢?刻意练习与我们常规的练习方式有什么区别呢?刻意练习的反面是天真的练习。所谓“天真的练习”,基本上只是反复地做某件事情,并指望只靠那种反复,就能提高表现和水平。与之相对的是有目的的练习,有目的的练习具有以下特点:有目的的练习具有明确的目标有目的的练习是专注的有目的的练习包含反馈有目的的练习需要走出舒适区刻意练习与有目的的练习又有区别。1。 首先刻意练习需要一个已经得到合理发展的行业或领域。也就是说,在那一行业或领域之中,最杰出的的从业者已经达到一定程度的表现水平,使他们与其他刚刚进入该行业或领域的人们明显的区分开来,例如书中经常举例的体育界、音乐界其次刻意练习需要一位能够布置练习作业的导师。这个导师必须已经达到一定的水平,并且有一些可以传授给别人的有益的练习方法。刻意练习有下面几个特点:刻意练习发展的技能,是其他人已经想出怎样提高的技能,也是已经拥有一套行之有效的训练方法的技能。刻意练习发生在人们的舒适区之外,而且要求学生持续不断地尝试那些刚好超出他当前能力范围的事物。刻意练习包含得到良好定义的特定目标,通常还包括目标表现的某种方便;它并非只想某些模糊的总体改进。刻意练习是有意而为的,也就是说,它需要人们完全的关注和有意识的行动。刻意练习包含反馈,以及为应对那些反馈而进行的调整的努力。刻意练习既产生有效的心理表征,又依靠有效的心理表征。刻意练习通过着重关注过去获得的技能的某些特定方面,致力于有针对性的提高那些方面,并且几乎总是包括构建或修改那些过去已经获取的技能。那么为什么要用刻意练习的方法呢?自然是为了有效的提升自己的某项技能。最后一个问题,该如何在生活中使用刻意练习的法则呢?看到之前关于刻意练习的描述,似乎我们生活中很多场景用不了刻意练习。比如我想学习英语,能脱离字幕看懂英文电影。或者能直接阅读英文的咨询或者英文书籍。又或者我们想学编程来提高自己的工作效率。这两个领域似乎都不算合理发展的行业,无法界定谁是最出色的,而且也没有所谓的导师来引领。如果本书仅仅到这里我想它是一本浪费时间的垃圾书。但是书中也给了我们一些建议,针对一些无法使用刻意练习的场景的一些建议。首先需要目标,也就是我们希望通过练习达到哪种水平然后尝试寻找导师。找到当前领域一些相对杰出的人,学习他们是如何掌握这项技能的。有的地方可以依葫芦画瓢。如果没有导师,需要牢牢记住3F原则,即 focus、feedback、fix。专注、反馈和修正总体来说,这本书干货确实不多。花了大量的篇幅在阐述天才是天生的还是通过后天的努力练习来获得天赋的,以此来给刻意练习背书。对我来说这些并不是我关心的。我只关心我应该如何做才能快速提升自己的能力。这方面书中并没有过多的阐述。如果想通过这本书获得通用的学习方法那你可能要失望了,你只能根据这些原则来自己制定自己的学习计划并努力付诸实施。然后通过大量的练习来达到目的。中间可能仍然会走弯路,甚至没有反馈而放弃。最后我给这本书的定位是可以读也可以不读。读完有那么一点收获但是不多。
2023年02月18日
19 阅读
0 评论
0 点赞
2023-02-01
从零开始配置vim(31)——git 配置
很抱歉又拖更了这么久了,在这个新公司我想快速度过试用期,所以大部分的精力主要花在日常工作上面。但是这个系列还是得更新下去,平时只能抽有限的业余时间来准备。这就导致我写这些文章就慢了一些。废话不多说,咱们正式开始有关git相关的配置。这些配置都是根据我自身使用习惯来定义的,不一定符合各位的习惯,各位可以根据自身的习惯来调整gitsigns第一个要推荐的插件是 gitsigns。就像它的名字一样,该插件可以将最近的更改以标签的形式展现出来方便我们查看。我们可以使用这样的代码进行安装 use {'lewis6991/gitsigns.nvim' }。需要注意最新的版本需要 neovim 的版本在0.7 以上。安装完成之后我们通过配置 require('gitsigns').setup()来使用它。这样我们就可以通过 Gitsigns toggle_signs来打开或者关闭符号显示了。除了采用最基本的符号显示以外,它还可以对改变位置的行号进行标记以及高亮显示变更的行。这两个功能可以通过 Gitsigns toggle_numl和 Gitsigns toggle_linel来打开,打开之后显示如下:从图中可以看到,更改行行号被用绿色显示了出来并且更改行进行了高亮显示另外它还有其他的显示效果:它主要的一些显示功能主要有下面几个:toggle_signs: 显示变更记录toggle_numl: 显示变更行号toggle_linel:高亮变更的行toggle_delete: 显示被删除的行,以红色背景高亮显示toggle_word_diff: 在两行分别显示修改前和修改后的内容toggle_current_line_blame: 在对应行后面显示提交记录我们将所有的这些功能都打开将得到这么一个效果是不是看着有点乱?这是我修改了一处的,一旦修改多了看着会更混乱。所以我自己的经验告诉我在这个buffer里面最好是只打开 signs、numl、linel、current_line_blame的功能,其他的都关掉。也就是在当前buffer中只显示现在的代码,然后辅助以简单的符号来显示哪行是新加的,哪行被删除了,哪行被修改了,至于修改前是什么样子的,我可以通过其他方式来查阅。所有内容都在一个buffer 中显示会比较乱,不利于阅读代码。我们可以通过配置将我们要显示的内容进行定义,也可以定义使用何种图标来表示修改记录。我们采用如下的配置gitsigns.setup({ signs = { add = {hl = 'GitSignsAdd' , text = '+', numhl='GitSignsAddNr' , linehl='GitSignsAddLn'}, change = {hl = 'GitSignsChange', text = '│', numhl='GitSignsChangeNr', linehl='GitSignsChangeLn'}, delete = {hl = 'GitSignsDelete', text = '-', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLn'}, topdelete = {hl = 'GitSignsDelete', text = '-', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLn'}, untracked = {hl = 'GitSignsAdd', text = '+', numhl='GitSignsAddNr' , linehl='GitSignsAddLn'}, changedelete = {hl = 'GitSignsChange', text = '~', numhl='GitSignsChangeNr', linehl='GitSignsChangeLn'}, }, signcolumn = true, -- Toggle with `:Gitsigns toggle_signs` numhl = true, -- Toggle with `:Gitsigns toggle_numhl` linehl = true, -- Toggle with `:Gitsigns toggle_linehl` word_diff = false, -- Toggle with `:Gitsigns toggle_word_diff` watch_gitdir = { interval = 1000, follow_files = true }, attach_to_untracked = true, current_line_blame = false, -- Toggle with `:Gitsigns toggle_current_line_blame` current_line_blame_opts = { virt_text = true, virt_text_pos = 'eol', -- 'eol' | 'overlay' | 'right_align' delay = 1000, ignore_whitespace = false, }, current_line_blame_formatter = '<author>, <author_time:%Y-%m-%d> - <summary>', sign_priority = 6, update_debounce = 100, status_formatter = nil, -- Use default max_file_length = 40000, -- Disable if file is longer than this (in lines) preview_config = { -- Options passed to nvim_open_win border = 'single', style = 'minimal', relative = 'cursor', row = 0, col = 1 }, yadm = { enable = false }, })其中大部分都是官方给出的默认配置,我只是在此基础之上改了 signs在各种状态下显示的图标,以及显示哪些内容。主要是使用 + 来表示新增、| 来表示修改、- 表示删除。除了显示以外它有一个重要的功能就是在各种修改状态之间跳转,例如调用 next_hunk来跳转到下一个更改位置。并且它也集成了一些git的操作。我们对常用的操作定义一些快捷键on_attach = function() vim.api.nvim_set_keymap("n", "<leader>gj", "<cmd>Gitsigns next_hunk<CR>", {silent = true, noremap = true}) vim.api.nvim_set_keymap("n", "<leader>gk", "<Cmd>Gitsigns prev_hhunk<CR>", {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hs', ':Gitsigns stage_hunk<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('v', '<leader>hs', ':Gitsigns stage_hunk<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hr', ':Gitsigns reset_hunk<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('v', '<leader>hr', ':Gitsigns reset_hunk<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hS', '<cmd>Gitsigns stage_buffer<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hu', '<cmd>Gitsigns undo_stage_hunk<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hR', '<cmd>Gitsigns reset_buffer<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hp', '<cmd>Gitsigns preview_hunk<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hb', '<cmd>lua require"gitsigns".blame_line{full=true}<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>tb', '<cmd>Gitsigns toggle_current_line_blame<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hd', '<cmd>Gitsigns diffthis<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hD', '<cmd>lua require"gitsigns".diffthis("~")<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>td', '<cmd>Gitsigns toggle_deleted<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('o', 'ih', ':<C-U>Gitsigns select_hunk<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('x', 'ih', ':<C-U>Gitsigns select_hunk<CR>', {silent = true, noremap = true}) end这里是我将官方给出的配置直接粘贴了过来,这堆快捷键定义的操作中我最常用的就是 next_hunk和 prev_hunk来在各种修改之间进行跳转。虽然偶尔也用用 diffthis来显示差异,但这部分我更喜欢使用我接下来介绍的插件diffview这个插件从名字上看就知道是专门用来查看版本差异的插件。与前面介绍的 gitsigns插件相比它有下面几个优点:它是专门用来显示差异的,与gitsigns相比,显示的更加明显它可以在文件树中显示有变更的文件它可以做到任意版本之间的差异对比它可以显示单个文件的版本提交记录它还有另外的功能,可以由各位小伙伴根据官方文档自行了解。它的使用方式如下::DiffviewOpen显示当前与上一个版本之间的差异:DiffviewOpen + 版本号 可以显示当期与某一个特定版本的差异,例如 :DiffviewOpen HEAD~2或者:DiffviewOpen 906ddac317来查看版本差异● :DiffviewFileHistory + 文件名 来查看某个文件的版本差异因为它比较简单,具体的用法就不在这里演示了。关于它的配置,我自己认为平时很少使用,而且默认的配置已经够用了,也不太需要花精力为它优化快捷键了。lazygitlazygit是一个非常好用的git客户端,可以方便的进行提交、回滚、查看变更等git操作。这里我不推荐什么插件,因为它本身已经很强大了,而且脱离vim它也可以很好的工作。想来想去只有配置一个快捷键来快速打开 lazygit 的终端不知道各位小伙伴是否还记得之间介绍的 toggleterm 插件,我们将要依赖它来快速启动local lazygitterm = Terminal:new({ cmd = 'lazygit', direction = 'float' }) function lazygit_toggle() lazygitterm:toggle() end vim.api.nvim_set_keymap("n", "<leader>lg", "<Cmd>lua lazygit_toggle()<CR>", {noremap = true, silent = true})通过这样简单的配置我们已经可以使用 <leader>lg来快速启动 lazygit的客户端了。至此我们关于git的配置就完成了。一般我的使用习惯是使用 gitsigns来在更改中进行跳转,用于提交前或者合并分支前的代码审查,做到提交和合并都心中有数。在发生bug要回溯代码并且查看当前与没有问题的版本之间的差异会用到 diffview插件。在进行提交、合并、回溯等git相关操作时会使用到 lazygit。各位小伙伴也可以根据自己的使用习惯来定制这一部分的配置。
2023年02月01日
32 阅读
0 评论
0 点赞
2022-12-30
从零开始配置vim(30)——DAP的其他配置
很抱歉这么久才来更新这一系列,主要是来新公司还在试用期,我希望在试用期干出点事来,所以摸鱼的时间就少了。加上前面自己阳了休息了一段时间。在想起来更新就过去一个多月了。废话不多说了,让我们开始进入正题。在前一章,我们谈论了如何在 neovim 中使用cpptools 这个DAP 的适配器对代码进行调试,目前针对编译型和解释型语言来说我们都有了对应的方法来配置调试器对其进行调试。本节将要介绍关于dap的其他一些功能,主要包括 repl窗口和 gdb的集成repl 窗口什么是 repl 呢?它的全称是 Read Eval Print Loop 中文一般翻译为交互式解析器,可能看到这你还是一脸懵逼,你可以想想 python 或者nodejs,在控制台输入python 就可以进入到它的交互式解析器中,随着我们输入python 的语句,它会实时的给出运行的结果。交互式解析器就是这么一个东西,输入命令,它给你一个实时的结果。在调试中使用交互式解析器还是很有用的,比如我想显示当前某个变量的值,当前执行到哪个语句了等等。nvim-dap已经提供了一个内置的 repl 窗口,我们每次启动调试的时候都会看到它每次都会创建一个新的名为 dap-repl的buffer。之前我们没怎么关注它主要还是因为那个时候的重点在于如何进行调试,现在我们将要来优化这部分的显示和功能。首先我们发现每次调试结束的时候这个buffer 都会被遗留,需要我们手动的进行关闭,除了针对buffer通用的 :q 或者 :bd命令进行关闭,还可以使用 :lua require('dap').repl.close()。当然也可以配套使用 :lua require('dap').repl.open()来打开一个 repl的窗口,既然每次都会自动新建,那么这里我们就不需要进行新建,主要用于想办法关闭就可以了。还记得之前介绍 nvim-dapui 插件的时候介绍的那两个监听函数吗,同样的我们要在监听调试结束的函数中添加代码来关闭repl 窗口,函数的整个代码如下dap.listeners.before.event_terminated["dapui_config"] = function() dapui.close({}) dap.repl.close() end dap.listeners.before.event_exited["dapui_config"] = function() dapui.close({}) dap.repl.close() end再启动调试的时候发现它已经可以在调试结束的时候自动关闭了。还有另外一个问题就是我不太喜欢现在这样在最下角显示 repl。我希望它能够在最下方以整行的形式显示。或者可以方便的不显示,只有在需要的时候显示。要达成这个目的我们需要修改 dapui 的配置。dapui 的配置主要以 element为基础,每个 element 代表一个提供对应功能的窗口。我们先来根据它默认的配置来讲解每部分的含义require("dapui").setup({ icons = { expanded = "", collapsed = "", current_frame = "" }, mappings = { -- Use a table to apply multiple mappings expand = { "<CR>", "<2-LeftMouse>" }, open = "o", remove = "d", edit = "e", repl = "r", toggle = "t", }, -- Use this to override mappings for specific elements element_mappings = { -- Example: -- stacks = { -- open = "<CR>", -- expand = "o", -- } }, -- Expand lines larger than the window -- Requires >= 0.7 expand_lines = vim.fn.has("nvim-0.7") == 1, -- Layouts define sections of the screen to place windows. -- The position can be "left", "right", "top" or "bottom". -- The size specifies the height/width depending on position. It can be an Int -- or a Float. Integer specifies height/width directly (i.e. 20 lines/columns) while -- Float value specifies percentage (i.e. 0.3 - 30% of available lines/columns) -- Elements are the elements shown in the layout (in order). -- Layouts are opened in order so that earlier layouts take priority in window sizing. layouts = { { elements = { -- Elements can be strings or table with id and size keys. { id = "scopes", size = 0.25 }, "breakpoints", "stacks", "watches", }, size = 40, -- 40 columns position = "left", }, { elements = { "repl", "console", }, size = 0.25, -- 25% of total lines position = "bottom", }, }, controls = { -- Requires Neovim nightly (or 0.8 when released) enabled = true, -- Display controls in this element element = "repl", icons = { pause = "", play = "", step_into = "", step_over = "", step_out = "", step_back = "", run_last = "", terminate = "", }, }, floating = { max_height = nil, -- These can be integers or a float between 0 and 1. max_width = nil, -- Floats will be treated as percentage of your screen. border = "single", -- Border style. Can be "single", "double" or "rounded" mappings = { close = { "q", "<Esc>" }, }, }, windows = { indent = 1 }, render = { max_type_length = nil, -- Can be integer or nil. max_value_lines = 100, -- Can be integer or nil. } })首先最上面的 icons 表示,各个部分显示的图标,这里分别定义了展开的,合并的以及当前位置的图标信息,我们可以观察一下变量栏或者调用栈显示信息的左侧就可以看到这里定义的图标。mappings 代表的是部分窗口动作定义的快捷键。例如上面定义的 expand = { "\<CR>", "" }表示可以在待展开项上按下回车或者鼠标左键双击来展开。element_mappings表示的是我们为某些窗口特意定制的一些快捷键。例如上面注释的是针对调用栈定义的快捷键。layouts代表布局,每个布局都有一个子table,而每个子table主要由 elements、size、position组成,它们分别代表采取该种布局的元素(也可以说是窗口),窗口大小以及窗口的位置。窗口一般通过id来描述,每种窗口都有固定的ID,根据官方文档的描述,它支持这么几种窗口:scopes显示全局或者当前局部变量,它支持的操作主要是 edit编辑变量的值、expand展开结构化的变量、repl将变量拷贝到repl窗口stacks显示当前正在运行的线程以及它们对应的调用栈,它主要支持的操作是 open :运行代码到当前被选中的位置, toggle:打开或者关闭该窗口watches显示我们需要追踪的变量,它支持的主要操作是 edit: 输入想要追踪的变量或者给对应的变量赋值。expand: 展开结构化的变量,remove:删除当前监视的变量,repl:将变量拷贝到repl窗口breakpoints显示当前激活的断点。它支持的主要操作有 open:执行代码到当前选中的断点处, toggle :激活或者使当前断点无效repl显示repl窗口console显示控制台窗口这些窗口的这些操作的快捷键我们已经通过上方的 mappings做了定义了,只要保持光标在对应窗口然后按下快捷键就可以执行对应的窗口命令了。如果想要单独对窗口进行快捷键定义可以在element_mappings 中被注释的代码controls 部分配置的是在repl窗口上方显示的那一堆调试按钮。由于我们定义了一些快捷键,这些按钮没太大的作用。这里我不需要它显示调试用的按键,所以我就在 controls 项中设置 enabled = false 禁用它。floating、window、render则定义的是悬浮窗口样式和普通窗口的一些样式。这里就不深究了。下面是改造之后的配置dapui.setup({ icons = { expanded = "", collapsed = "", current_frame = "" }, mappings = { -- Use a table to apply multiple mappings expand = { "<CR>", "<2-LeftMouse>" }, open = "o", remove = "d", edit = "e", repl = "r", toggle = "t", }, layouts = { { elements = { { id = 'scopes', size = 0.35 }, {id = "stacks", size = 0.35}, {id = "watches", size = 0.15}, {id = "breakpoints", size = 0.15}, }, size = 40, position = "left", }, { elements = { "repl" }, size = 5, position = "bottom", } }, controls = {enabled = false}, floating = { max_height = nil, -- These can be integers or a float between 0 and 1. max_width = nil, -- Floats will be treated as percentage of your screen. border = "single", -- Border style. Can be "single", "double" or "rounded" mappings = { close = { "q", "<Esc>" }, }, }, windows = { indent = 1 }, })repl 窗口的主要命令如下:.exit: 退出/关闭一个 repl 窗口.c/.continue: 继续执行代码.n/.next: 执行下一行代码.into: 跳转到函数中继续执行.out: 跳出函数.scopes: 打印当前栈的一些变量信息.threads: 打印线程信息.frames: 打印当前线程的调用栈.capabilities: 打印当前适配器实现的一些功能.p 暂停当前运行的程序更多的命令可以通过在 repl窗口中输入 .help查看看了这么多无聊的文字描述,不知道小伙伴们有没有觉得头晕眼花呢?我们来通过实际的例子来看看如何应用这些内容来进行调试。示例1:调试单线程死循环假设有一段程序在不知不觉中被写成死循环了,程序无法正常执行下面的操作,我们以下面的程序为例#include <stdio.h> #include <unistd.h> void loop_forever(){ for(unsigned int i = 10; i >= 0; --i){ // do something sleep(0.1); } } int main (int argc, char *argv[]) { // do something loop_forever(); printf("do other things\n"); return 0; }本来我们想的是它执行完函数 loop_forever之后会执行接下来的操作,但是我们死活看不到它执行后面的操作,这个时候我们意识到它可能在某个地方陷入死循环,无法出来了,假设前后都有大断的代码,无法快速定位到死循环的位置,该如何处理这种情况呢?我们先通过<F5>来执行操作,然后在 repl 中输入 i 进入插入模式,然后执行.p 中断当前程序执行。此时程序已经断了下来,接着我们输入.frames 查看当前调用栈信息。我们发现此时程序停留在loop_forever 函数的 sleep 中,我们在栈中找到 sleep 的位置并按下回车,这个时候我们发现程序执行到了 sleep() 函数处了。这个时候我们在这里按下<F9>下一个断点,接着使用 <F5> 继续运行到断点位置停止,这个时候我们通过实时显示的 i值已经发现问题所在了。原来i 递减到0之后,继续递减,因为它是无符号数所以永远无法达成小于0 的条件。示例2:调试多线程死锁我们以下面一个多线程程序为例#include <cstdio> #include <thread> #include <vector> using namespace std; void func(int thread_id){ int var = thread_id; while(true); } int main (int argc, char *argv[]) { vector<thread> vec; for(int i = 0; i < 5; i++){ vec.push_back(thread(func, i)); } // join for(int i = 0; i < 5; i++){ vec[i].join(); } return 0; }因为使用了c++ 相关的内容和 thread 库,因此编译的时候需要使用 g++ 并且指定链接 pthread 库,我们采用 g++ main.cpp -g -o main -lpthread 来编译它我们还是按照之前的步骤,先按下 .p来暂停程序,这个时候我们发现它会提示我们需要暂停哪个线程,遗憾的是根据线程的id还没法判断具体哪个是子线程哪个是主线程。这里我们随便选一个暂停。然后执行 .threads查看当前线程信息,在某个线程下使用回车键可以看到调用的函数栈。我们发现子线程卡在while 这句话,我们还是一样在卡主的位置按下回车跳转到对应代码位置,在此处下一个断点。然后我们在对应线程位置按下 o 命令来继续执行之前暂停的线程。这样就完美的找到了线程卡死的位置了。后面可以使用 .c 来继续执行所有被中断的线程nvim-gdb 插件该插件提供了一种方式,可以直接在neovim中进入gdb的session。例如我们可以通过命令 :GdbStart gdb -q a.out来启动一个gdb会话,并且关联了一个 a.out 的程序。后续可以直接使用gdb相关的命令来启动调试这个程序。我们可以使用如下的代码来安装它use {"sakhnik/nvim-gdb", run="./install.sh"}我们先来试试效果,直接使用快捷键 <leader>dd 来加载一个程序进行调试。进入到gdb会话之后可以使用gdb 的命令。例如我们使用 b main来在 main函数的位置打一个断点,然后通过r来启动程序运行到断点处。接着可以使用 n来执行下一步或者使用 c来直接运行到下一个断点。最后可以使用 q退出基础配置我们发现使用 nvim-gdb 插件的时候会在对应代码位置显示断点或者当前执行行。这里我们对它做一些配置,先统一使用nvim-gdb和 nvim-dap这两种情况下的显示信息。先创建一个新的配置文件为 nvimgdb.lua作为它的配置文件。因为它暂时还不支持lua的配置所以这里我们使用vim原生的写法。vim.cmd([[ let g:nvimgdb_config_override = { \ 'sign_breakpoint': [''], \ 'sign_current_line': '', \ } ]])在这里定义了断点和执行到当前行的配置,这里只能统一图标,还无法做到完全像使用 dap那样显示。另外我们可以在 nvimgdb_config_override 这个变量中定义如下快捷键以保证在调试时拥有同样的体验 \ 'key_next': '<F10>', \ 'key_step': '<F11>', \ 'key_continue': '<F5>',最后我们发现当我们输入文件名进行调试的时候,它会一直闪屏。这是因为它会不断根据我们输入的内容在文件系统中匹配合适的可执行文件,为了解决闪屏问题我们屏蔽它的这个特性,我们可以使用如下的配置来解决这个问题 let g:nvimgdb_use_find_executables = 0 let g:nvimgdb_use_cmake_to_find_executables = 0显示窗口的配置定义了显示形式,我们来定义显示的窗口,这里我让它显示常用的像调用栈,变量,以及watch窗口。我们先使用如下代码来将窗口分割为左右两个部分 let w:nvimgdb_termwin_command = "rightbelow vnew" let w:nvimgdb_codewin_command = "vnew"根据官方文档的描述,nvimgdb_termwin_command 是终端窗口,用来显示repl 相关信息并且与用户交互,后面我们可以对它进行切割用于显示其他信息。nvimgdb_codewin_command 是源代码窗口。这两句代码可以形成一个左右分屏的界面,左侧显示代码,右侧显示repl窗口。在gdb成功加载之后,我们可以使用命令 :GdbCreateWatch info locals来创建一个显示当前变量的窗口。默认是新创建一个窗口来显示,但是我们可以在命令前加上 belowright来指定在右侧继续分屏,它会在右下角新建窗口来显示变量。需要查看其它窗口可以对应传入不同的参数,例如传入 breakpoints来显示所有断点信息。传入的参数就是gdb中接收的对应参数。有了这些基础我们就可以对其进行配置了,我们要实现的目标就是当gdb成功加载的时候自动加载这些窗口。在vim中要实现自动化我们目前知道有两种方式,第一种使用自动命令,第二种使用插件配置中提供的回调函数。遗憾的是在这个插件中我没有找到回调函数,因此我们只能采用自动命令这种方法。根据官方的文档,我们主要使用这么两个事件——NvimGdbStart和 NvimGdbCleanup。它们一个是成功加载gdb的时候触发,一个是关闭gdb会话的时候触发。vim.cmd([[augroup GdbSession autocmd! autocmd User NvimGdbStart :lua StartGdbSession() autocmd User NvimGdbCleanup :lua EndGdbSession() augroup END]])在 StartGdbSession 函数中写入如下代码来完成窗口的配置StartGdbSession = function() vim.api.nvim_command(":belowright GdbCreateWatch backtrace") vim.api.nvim_command(":wincmd h") vim.api.nvim_command(":belowright GdbCreateWatch info locals") vim.api.nvim_command(":set wrap") vim.api.nvim_command(":wincmd k") end不知道各位小伙伴能不能理解这段代码是如何在分屏的。首先启动gdb的时候会将整个屏幕纵向分为两个部分,左侧为 code右侧为 repl窗口接着我们执行 :belowright GdbCreateWatch backtrace 它会在右下方创建一个窗口用来展示调用栈然后执行 :wincmd h来将光标移动到代码窗口上继续执行 :belowright GdbCreateWatch info locals它会在代码窗口的下方新增一个窗口用于显示变量的信息这样就将窗口分为4个部分了,左上部分显示代码,左下部分显示变量信息。右上部分是repl窗口,右下部分显示变量信息。最后我们通过 :set wrap设置窗口中自动换行,不然有些内容显示在一行不容易查看。通过 :wincmd k移动光标到 repl窗口。方便后续调试启动之后他的效果如下最后我们在结束gdb的时候做一些收尾工作,关闭我们创建的窗口EndGdbSession = function() vim.api.nvim_command(":bdelete! backtrace info locals") end这里我是根据buffer的名称来进行删除。最后的效果如下到此我们已经介绍了关于dap 的所有配置,至于其他语言相信各位小伙伴根据官方给出的示例可以独立完成配置,这里就不一一介绍了。
2022年12月30日
23 阅读
0 评论
0 点赞
2022-11-20
换工作有感
最近很长一段时间没有更新博客,更新关于vim相关的操作,主要是最近在忙于换工作的事情。其实本来我也没打算换工作的,主要是最近公司的一些骚操作让我觉得心里很不爽,所以一怒之下提出离职。
2022年11月20日
57 阅读
1 评论
1 点赞
2022-11-18
从零开始配置vim(29)——DAP 配置
首先给大家说一声抱歉,前段时间一直在忙换工作的事,包括但不限于交接、背面试题准备面试。好在最终找到了工作,也顺利入职了。期间也有朋友在催更,在这里我对关注本系列的朋友表示感谢。多的就不说了,我们正式进入vim 的配置吧上一节通过配置 python 的调试环境,我们大概了解了配置 dap 的基本步骤。首先需要一个 dap 的客户端负责在编辑器上显示各种调试信息,并且与用户进行交互。然后需要一个服务端,与客户端通信并完成调试的实际步骤。然后需要配置两个东西, dap.adapters 用来配置如何启动调试器,dap.configurations用来配置如何将当前项目加载到调试器上。本篇我们进一步配置 dap。让它变得更好用,并且介绍编译型语言(C/C++)调试的配置。优化界面回顾一下上一篇中在演示图片里面看到的效果。默认界面在断点位置以 B 来标识,当前运行的代码以 -> 来标识。看起来不那么的直观,我们先对它进行优化,我们采用 Visual Code 的调试图标来进行标识我们采用以下代码进行配置local dap_breakpoint_color = { breakpoint = { ctermbg=0, fg='#993939', bg='#31353f', }, logpoing = { ctermbg=0, fg='#61afef', bg='#31353f', }, stopped = { ctermbg=0, fg='#98c379', bg='#31353f' }, } vim.api.nvim_set_hl(0, 'DapBreakpoint', dap_breakpoint_color.breakpoint) vim.api.nvim_set_hl(0, 'DapLogPoint', dap_breakpoint_color.logpoing) vim.api.nvim_set_hl(0, 'DapStopped', dap_breakpoint_color.stopped) local dap_breakpoint = { error = { text = "", texthl = "DapBreakpoint", linehl = "DapBreakpoint", numhl = "DapBreakpoint", }, condition = { text = 'ﳁ', texthl = 'DapBreakpoint', linehl = 'DapBreakpoint', numhl = 'DapBreakpoint', }, rejected = { text = "", texthl = "DapBreakpint", linehl = "DapBreakpoint", numhl = "DapBreakpoint", }, logpoint = { text = '', texthl = 'DapLogPoint', linehl = 'DapLogPoint', numhl = 'DapLogPoint', }, stopped = { text = '', texthl = 'DapStopped', linehl = 'DapStopped', numhl = 'DapStopped', }, } vim.fn.sign_define('DapBreakpoint', dap_breakpoint.error) vim.fn.sign_define('DapBreakpointCondition', dap_breakpoint.condition) vim.fn.sign_define('DapBreakpointRejected', dap_breakpoint.rejected) vim.fn.sign_define('DapLogPoint', dap_breakpoint.logpoint) vim.fn.sign_define('DapStopped', dap_breakpoint.stopped)上面的代码主要配置了显示的颜色和图标。最终调试的效果如下图所示然后我们需要提供一个可用的界面用来显示调试过程中的各种信息,包括变量值和调用栈。完成这个工作的是插件 nvim-dap-ui 。我们使用如下的代码进行安装use { "rcarriga/nvim-dap-ui", requires = {"mfussenegger/nvim-dap"} }这个插件里面包装了很多调试相关的窗口,例如变量监控、调用栈等等。我们可以对他进行配置,让这些窗口元素出现在我们希望它出现的位置。为了加载这个插件我们还是按照之前的惯例,为它准备一个单独的配置文件,并且加载它。local dapui = require("dapui") dapui.setup({})我们可以使用该插件中的函数 toggle() 开打开或者关闭这些调试窗口。最终的效果就像这样每次都输入这个函数来打开和关闭调试窗口比较麻烦,因此我们这里可以使用以下代码来实现自动加载和关闭local dapui = require("dapui") dapui.setup({}) local dap = require("dap") dap.listeners.after.event_initialized["dapui_config"] = function() dapui.open({}) end dap.listeners.before.event_terminated["dapui_config"] = function() dapui.close({}) end dap.listeners.before.event_exited["dapui_config"] = function() dapui.close({}) end这段代码在 dap 的事件中注册了几个回调函数,当对应的事件发生时会调用对应的函数,我们在 dap 的调试启动时打开调试窗口,在结束时关闭调试窗口最后关于界面方面的优化再来推荐一个插件——nvim-dap-virtual-text 它的作用是在调试过程中,在变量附近事实显示变量的值。我们可以在 dap-ui 的配置文件中对他进行配置require("nvim-dap-virtual-text").setup({ enabled = true, enable_commands = true, highlight_changed_variables = true, highlight_new_as_changed = false, show_stop_reason = true, commented = false, only_first_definition = true, all_references = false, filter_references_pattern = '<module', virt_text_pos = 'eol', all_frames = false, virt_lines = false, virt_text_win_col = nil })上述的配置是官方给出的,我原封不动的复制过来了。它的效果如下图所示:配置c++基础调试环境终于到了本文最重要的环节了,就是配置 c/c++ 的调试环境,上一篇我们讲解了 Python 的配置,它代表了脚本类解释型语言的调试配置,C/C++ 代表了编译型语言的调试配置。针对 C/C++ 的调试我们选用 cpptools 作为 dap 的服务端。首先通过 MasonInstall cpptools 来下载安装它,也可以通过 :Mason 命令在图形化的界面上进行安装。然后我们还是按照之前的顺序来对他进行配置,首先配置它的加载方式local dap = require("dap") dap.adapters.cppdbg = { id = "cppdbg", type = 'executable', command = "~/.local/share/nvim/mason/bin/OpenDebugAD7", }这里我们设置它以 executable 的方式启动(在客户端调试时启动)。然后指定可执行程序的路径,如果这里报找不到 OpenDebugAD7 这种错误,可以将 ~ 改为 /home/user 这样的具体目录。然后我们配置一下客户端与服务器通信相关的内容dap.configurations.cpp = { { name = "Launch file", type = "cppdbg", request = "launch", program = function() return vim.fn.input("Path to executable: ", vim.fn.getcwd() .. "/", "file") end, cwd = "${workspaceFolder}", stopAtEntry = true, }, } dap.configurations.c = dap.configurations.cpp最后我们通过一个 dap.configurations.c= dap.configurations.cpp 让c++和 c使用同一个配置。因为 C/C++ 是编译运行的,在调试的时候其实调试的是它生成的可执行程序,所以这里每次在调试的时候需要手工指定要调试的可执行程序。最后别忘了在 ftplugin/cpp.lua 中加载它另外需要注意,因为可执行程序运行时是不依赖源代码的,但是调试的时候想让调试器能够准确的知道当前在源码的位置并且能够显示当前变量的值,这个时候需要在可执行程序中打包符号表,对于linux 的 C/C++ 程序来说,只需要在编译的时候给gcc/g++ 传递 -s 参数即可。我们写一个简单的 C程序来进行实验#include <stdio.h> int main (int argc, char *argv[]) { printf("hello world\n"); for (size_t i = 0; i < 10; i++) { printf("i = %ld\n", i); } return 0; }注意: 这里我们使用的调试器仍然是gdb, cpptools 只是在上层进行了一层封装。因此这里能调试的前提是安装了gdb 调试器到此我们将关于 dap 调试的部分都基本介绍完了。其实 dap 也并没有想象中那么难,目前从安装到配置使用,都有大量的插件来方便我们使用,而且官网上基本都有配置的介绍,没有特殊需求只需要将标准配置原样拷贝粘贴即可。下一篇我们将补充一些关于 dap 的其他内容,并介绍 neovim + gdb 的组合,敬请期待!
2022年11月18日
32 阅读
0 评论
0 点赞
1
...
10
11
12
...
37