首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
105 阅读
2
nvim番外之将配置的插件管理器更新为lazy
78 阅读
3
2018总结与2019规划
62 阅读
4
PDF标准详解(五)——图形状态
40 阅读
5
为 MariaDB 配置远程访问权限
33 阅读
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
emacs
linux
文本编辑器
Java
elisp
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
累计撰写
314
篇文章
累计收到
31
条评论
首页
栏目
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
页面
归档
友情链接
关于
搜索到
314
篇与
的结果
2025-05-06
读《经济学通识》
本书是薛兆丰之前发表过的有关经济学相关文章的一个集合。这些文章利用经济学知识分析相关的社会的问题,在实践中学习经济学。之前我看过一些经济学相关的书籍,那些要么是文绉绉的理论看的人头大,要么是浅尝辄止,感觉不到相关的点。这一系列的文章通过社会问题来聊其中涉及的经济学知识,在实践中学习。算是一本比较好的入门经济学的书籍。书中也不乏很多妙语。下面我截取一些我比较喜欢的句子来谈谈自己的看法例如开头就说人生至少面临4项普遍约束:(1)东西不够;(2)生命有限、(3)互相依赖、(4)需要协调。这四项对应着四类经济学理论:(1)需求定律;(2)利息理论;(3)制度理论;4(宏观理论)。仔细想想,总结的这四项约束实在是太精辟了,特别是东西不够和生命有限。我们无时无刻不在面临着东西不够的窘境。大到教育、医疗、国家的资源;小到到底今晚是吃火锅好还是吃炒菜好。我们面临着普遍的选择问题,而经济学就是为了计算每种选择的成本与收益,帮助我们做决策的。一般的一个不“认钱”的社会,它就不得不“认人”当需求无法通过价格反馈给市场时,就必然会造成物质短缺。如果不通过价格来分配物资,就会有一套严格的制度来对人划分369等,按等级分配。一旦需要人为的区分需求,必然会导致权力的腐败。有一句名言:“权力不会向穷人敞开,而金钱会”供求先决定产品的价格,而最终产品的价格再来决定原材料的成本虽然下意识的认为,商家会在成本的基础上提价并作为商品最终的售价。但是根据需求理论,价格是由需求决定的。商家也只能根据需求定价,然后在这个定价的基础之上压缩成本价。成本太高最终会导致商品因为价格过高而无人问津。任何国际贸易,都不仅会使得交易双方双赢,还导致生产效率较低的第三方受损交易的双方是共赢的,而会损害未成交的其他潜在商家的损失。一般来说商家与商家才是竞争对手,同样的客户与客户间也是竞争关系汇率是由甲国人民对乙国资源的需求,以及乙国人民对甲国资源的需求共同决定的。这里还是可以用需求理论来解释,当一国对另一国资源的需求提升,那么对它国货币的需求就会提升。也就是说它国货币会增值。耐用品的现值,取决于人们对其未来效用的预期。一项资产的价值,总是它未来收入的折现,而过去投入的成本是沉没成本,不论大小都不影响资产的现值典型的应该是股票这一资产的价值,股票升值并不是说当前公司有价值,而是人们认为公司未来会比较有价值。所以永远无法评估明天股票是涨还是跌。因为参与市场交易的每个人对公司未来价值都有不同的评估方式。同样的我们投资一个孩子的教育并不是说孩子现在很出色,而是通过教育孩子的未来大概率会出色。普通人有四种办法增加收入:挣钱、借钱、卖家当、抢钱;而政府增加收入的方式也有四种:挣钱、借钱、卖家当,不然就是征税和印钱。先谈挣钱。古今中外的经验告诉我们,政府不善于挣钱,尽管政府官员可以很聪明,政府可以拥有海量的市场数据,政府也具备做事的魄力,但致命之处是:政府官员不是公共资源的私有者,他们对资源利用的决策,可能只顾任期之内的效果,不可能看到遥远的未来。“争取和保住位置”是他们的目的,“做事”是他们的手段;而不是相反,他们不可能把做事当作目标,而把“争取和保住位置”当作手段,不是说政府官员中缺乏志向高远之人,而是说政治的现实约束,决定了官员必定“先占位置,始能做事”花自己的钱办自己的事,花别人的钱办自己的事、花自己的钱办别人的事、花别人的钱办别人的事,效率是依次递减的人的思想五花八门,而人的行动却大同小异;因为前者不用承担代价,后者承担代价。不要看一个人怎么说,而要看他怎么做。很多时候,经济学确实如作者是所说,是一门提升认知,给自己洗脑的学科。通过学习经济学,对一些社会现象有了一定的认知。同时也可以指导我们如何做决策。小到今晚吃什么、大到一生于谁为伴。其实都可以利用经济学相关知识,在权衡利弊之后行动。行动是要付出代价的,天下没有免费的午餐,即使有,也需要付出时间于其他人去竞争。
2025年05月06日
2 阅读
0 评论
0 点赞
2025-04-22
Emacs 折腾日记(二十一)——编辑能力提升
上一篇文章,我们补充了一些基本的配置,并且关闭了一些默认的行为。这里我们继续对它进行配置。本篇将要使用一些插件来修改默认的编辑行为进一步提高编辑文本的效率。avy 插件基础用法在vim中有 easymotion 可以使用,在Emacs中可以使用 avy 插件。它的功能于前面介绍的easymotion 类似。通过下面的代码来安装(use-package avy :ensure t :after general ;; 确保 general 插件已经安装 :config (setq avy-timeout-seconds 0.5) (my-leader-def "f" 'avy-goto-char-timer))我们在安装配置的时候使用之前定义的leader键来定义它的一些行为。首先定义 SPC-f 来进行快速跳转。avy-goto-char-timer 的功能与 easymotion 类似,将要查找的字符使用不同的字母进行标识,然后根据下一步的输入来确定光标的位置,例如下面的例子我们可以连续输入一段内容减少待筛选项。当然这个速度要快,否则在一定时间内没有输入文本之后,avy会认为已经结束输入了。这个等待的时间我们在上面通过 avy-timeout-seconds 定义的是 0.5。当然也可以扩大这个时间范围进阶用法这个简单的功能只是 avy 功能的冰山一角。它还有许多有用的功能,如果能熟练使用将会极大的提高编辑文本的效率。实际上avy 在筛选、跳转之前可以执行用户指定的动作,它支持哪些动作呢?我们可以在输入筛选的部分文本后输入?,查看支持的动作。我们来举一个例子:下面有一段文本,我希望将text1复制到最后一行,那么可以这么操作使用<leader>f 激活 avy,然后输入筛选文本输入 Y 表示复制整行输入对应字符表示选中text1 所在行此时我们会发现,text1 已经被复制到光标所在行了多行操作和块操作在vim中我们介绍了多行操作,主要是使用C-v 来选中某些行,然后通过使用A 或者 I 来选中行尾或者行首,并进入插入模式。这种方式可以同时在对应位置插入多个相同的字符。emacs 中的 evil 插件也可以进行相同的操作。但是对于行间或者每行在不同位置插入的情况就不适用了。我们可以使用插件 evil-multiedit 来达到这一效果。evil-multiedit 深度绑定了vim的快捷键,在选中区域之后可以直接使用vim中的 I/A 来编辑选中区域的首部或者尾部。也可以使用 ciw 之类的同时修改多个选中区域。在选区时既可以使用vim 中的 * 来查找并选中,也可使用 / 来搜索并且同时选中多个。这个插件相当于增强了vim的多行编辑功能我们使用下列的代码来安装和简单的配置(use-package evil-multiedit :ensure t :after evil :config (evil-multiedit-default-keybinds) (my-leader-def "m m" #'evil-multiedit-match-and-next ; 标记当前符号并跳转下一个 "m M" #'evil-multiedit-match-and-prev ; 标记当前符号并跳转上一个 "m a" #'evil-multiedit-match-all ; 标记所有相同符号 "m r" #'evil-multiedit-restore ; 恢复单光标模式 "m c" #'evil-multiedit-toggle-or-restrict ; 切换选区/限制编辑区域 ))我们可以使用 <leader> mm 来选中符合条件的项也可以使用 <leader> ma 来选择所有符合条件的项好了,本节到此也就结束了,本节依靠两个插件,进一步模拟vim相关的功能,并且对vim原有的功能进行了一定程度的补强。熟练使用这两个插件将会对编程的效率有一个进一步的提升。作为一个普通的文本编辑器也足够了。
2025年04月22日
6 阅读
0 评论
0 点赞
2025-04-19
读 《莫兰传》
这个标题看着像一个人物传记,但是实际上它是一系列的推理小说。这个系列的小说是我之前在网上找到的鬼马星作品集。说到这个作者,我还是从高中时期买过的一些推理小说杂志上听说的。之前只在杂志上零星的看过一些片段,只知道是一个中国的作家。这是第一次全面的阅读她的作品。这个作品集中当然不止 《莫兰传》这一个系列,还有好几个系列,但是长时间读同一个风格的作品难免有些审美疲劳,我想先暂停一下其他内容的阅读,看看其他的书籍,到后面无聊的时候再往后继续读。《莫兰传》主角当然是莫兰,她出生在一个家境优渥,父母恩爱的家庭。而男主角高竞从小经历了父亲去世,母亲将父亲去世归咎于他并且从此不待见他,而他的妹妹也是一个只知索取却从来没有感恩过哥哥的自私自利的人,好在出生并成长在这样家庭中的高竞并没有因此消沉或者走入歧途,相反他却是一个热心肠并且极富正义感的好警察。这个系列就是围绕着男女主互相配合侦破一系列迷案展开的。书中一直穿插着描述了主角莫兰和高竞的感情线。从20岁高竞见到莫兰一见钟情到最后女主成年后二者修成正果。这些感情线同样充斥了误会的争吵,二人在一起时的甜蜜,有种看言情小说的味道。但也是紧张案件中间的调味剂,使人因为案件紧绷的神情能有丝丝的放松,甚至会因为二人间的甜蜜日常会心一笑。与其说这一系列的侦探是莫兰,不如说是二人组都在案件中扮演者各自的侦探角色。作者擅长多视角叙事,经常在莫兰、高竞、凶手和其他第三方人员的视角间切换,并且在切换中从不同人的视角中给读者一些用于推理的线索。在《隔墙玫瑰》这个案件中,因为高竞失忆,作者在两人间的视角切换进行的极为自然,最终导向真相,最终二者携手破案。并且最终作者给了高竞一个很好的结局。高竞因为失忆忘记了成年前的记忆,算是摆脱了原生家庭的桎梏,迎来了新生。要最令人失望的要数《宴无好宴》,这个案子开头确实挺吸引人的。四个月前探员李耀明被杀,警察局档案室警员乔纳的同事若琳约乔纳在某个茶餐厅见面,并且讲述了丈夫最近的一些异常。但是在乔纳中间离开一段时间之后若琳被杀。随后若琳的丈夫也被人从地铁站台推下,被人谋杀。并且在其中还牵涉出警局的内鬼,以及背后的黑色势力——司徒雷。开局这么吸引人,但是随着故事的推进,也越来越无聊。主副线剧情混乱,在前面几部塑造的人物形象完全变了样,只是为了男女主之间的感情线服务,男女主之间也似乎是为了谈恋爱而谈恋爱。这部书中充斥着爱情,似乎正义、真相都为了爱情让步。最后的也是草草结束,感觉作者在憋大招结果确给我来了个大的。虽然这一系列有这样那样的缺点,但是却不失为一本好的小说,故事性也足够。就像东野圭吾那样神一本鬼一本,作者也不可能每本都优秀。作者塑造的人物也是个性鲜明。作者的推理小说具体是什么派别,到底是本格派、社会派还是其他的我也说不准,但是给我的感觉有点像看柯南。推理只是服务于剧情,服务于人物塑造。我倾向于叫这个风格为偶像派推理小说,将来改编成电视剧,男女主弄些流量小生、俊男靓女,谈着恋爱就把案子破了,这系列小说给我的就是这么一种感觉。作者有中国的“阿加莎”之称,但是我感觉这系列的作品还达不到阿婆的高度。其实写小说特别是推理小说的读后感写不了多少字,其他的小说介绍一下大概的情节,再分析一下人物感觉就很圆满。但是推理小说最大的魅力就是当你什么都不知道随着阅读的深入,跟着作者的节奏,最后恍然大悟那一刻。要是提前被剧透了一些细节,阅读的快感就大打折扣了。
2025年04月19日
5 阅读
0 评论
0 点赞
2025-04-15
读 《沉默的大多数》
早些时候看到网上有人说可以读王小波的书学习一下怎么写作。所以我找了这本书看了一下。但是看完给我的感觉一般。这是一本杂文集,里面的文章确实式杂文,给我的感觉就是整个文章的节奏都比较杂,想到什么就说什么。但是大部分内容又很贴合文章标题,都在围绕一个主题在谈。这个可能就是杂文的一个形式。说实话我对于一般的文学作品做到很深的品评。我对于文学的造诣也一般,文学作品对我来说只有谈论的话题是不是我感兴趣的,讲的故事是不是比较有趣的。文学作品里面哪些句子写的漂亮,我能学到什么,这点我完全没有感觉。这本书里面大部分的文章我都不怎么感兴趣,特别是中间关于国外生活的。不知道作者是不是因为早些年知青下乡插队的经历,作者在很多文章中都有一种知识分子对普通农民的一种优越感,总喜欢通过那段经历来论证自己与普通人的不同。虽然从中可以看到作者的一些思考和对一些经历的反思,但是读起来我感觉一般般,虽然说不上坏但是也不算好。真正我比较喜欢的就两篇 《花剌子模信使问题》和 《沉默的大多数》。下面就谈谈我对这两篇文章的感受《花剌子模信使问题》花剌子国有一个奇怪的风俗,凡是给国王带来好消息的信使都能得到升迁,给国王带来坏消息的信使最终都会被惩罚甚至被送去喂狮子。时间久了国王那里就只能听到好消息了,但是坏消息是不是不存在呢?当然不是。文章通过这一个预言故事来警示人们,不能只听好消息,或者只喜欢给自己拍马屁的人,又或者害怕得罪人,只讲好话不讲坏话。这些都有问题,最重要的是要客观的看待问题,我想这就是我在中学时学习到的“实事求是”。当初一直以为有什么说什么,实事求是本来就很容易,自己就是这么做的,这是一句废话,专门讲这个有点浪费时间。但是随着自己年纪的增长,以及混社会的日子越来越久,对这个词的认识也越来越深。实事求是最重要的就是客观,客观的看待人,看待事。有时候并不是我们故意不客观,故意高估或者低估,而是本身对事物的认识就不足。因为我们潜意识里面都偏好好消息,例如明明没有财务知识,金融知识,恰巧进入股市赚了一点钱,这个时候大脑就简单的给我报告了一个好消息,我是股神,我判断的是对的。又或者自己这些年吃到了时代红利,挣了不少钱。这时候好消息就来了,这个好消息就是我高人一等。看着这些“不求上进”的年轻人,不找工作居然想躺平,作为“成功人士”自然看不惯这种行为,想当然的去批判他们。寓言故事很简单,当然国王也很傻。我有时候也自信我比故事中的国王高明,但是在潜意识里面我可能也当过这样的国王,我的大脑自动过滤了好消息而忽略的坏消息。我想要提升自己这方面的能力还是要保持客观,多学一点其他方面的知识。很多时候换一个角度,换一种知识分析同样的问题,可能就能得到好消息或者坏消息。《沉默的大多数》作者在这篇文章中点出中国人大多数比较沉默,而且也分析了自己比较沉默的一些理由。这里我就不列举这些理由了。我喜欢这篇文章主要是我有一些共鸣。其实不光是我,网络上大部分网民都是沉默的大多数。很多网民都是只看不发言,特别是在一些两方对立比较激烈的情况下,他们只看不站队,也不发言。当然我也佩服那些能引经据典,口若悬河,妙语连珠的人。我在生活中也是一个沉默的人。首先我觉得一个人在人群中什么都想说几句,什么都能说几句,必然是会在人群中比较显眼。这种人一般是群体的中心。但是我不太喜欢这样,或许是我不喜欢那种引人注意的感觉,特别是陌生人注意到我会让我觉得很不自在,也可能是我害怕在公众面前因为说错话而出丑,还可能是我不想因为说了一些争议的话引来别人的辩论或者人身攻击。还有一个原因,随着年岁的增长自己也慢慢的觉察到当初的一些观点和认知错的离谱,如果当初在朋友或者陌生人面前侃侃而谈,而今发现自己曾今是多么的幼稚,错的多么离谱。这段往事就是自己的黑历史了,而且也是徒增笑料罢了,还不如保持沉默。另一个原因就是这些年自己也读过一些书,总是有种这个世界太复杂而我自己懂的太少。相同的事,不同的人能从不同的角度分析得到完全不同的结论,而且都有证据和推理过程来论证自己的结论,世上高人这么多,哪轮得到我这种小卡拉米来发表什么高论。所以在网络上我也是沉默的大多数。但是一个人不说话总是闷着也怕憋坏了。所以我建立了一个自己的博客,有时候这里说点心里话,也写点自己想说的,算是给自己一个说话的渠道。以上都是我个人的自言自语,有胡说八道的成分在里面。各位读者看了就行,不要来反驳我或者人身攻击,也请日后不要拿这些矫情的文字来取笑我!
2025年04月15日
3 阅读
0 评论
0 点赞
2025-04-12
使用CMake跨平台的一些经验
使用CMake构建工程的一个原因是不希望Windows上有一个工程文件,Linux又单独配置一套工程文件。我们希望对于整个工程只配置一便,就可以直接编译运行。CMake本身也具备这样的特性,脚本只用写一次,在其他平台能自动构建工程。这里谈的跨平台主要是如何组织工程和编写CMake来针对Windows和Linux进行一些特殊的处理,在这里说说我常用的几种办法介绍这些方法的前提是有一些代码只能在Windows上跑,例如调用Win32 API或者使用GDI/GDI+,而在Linux上也有一些Windows不支持的代码。我们就需要一些方法来隔离开这两套代码。假设现在有一个项目,它有一套共同的接口对外提供功能,而在Windows上和Linux上各有一份代码来实现这些接口。可以假设有一套图形相关的功能,对外采用统一的接口,具体实现时Windows上使用GDI+,而Linux上使用其他方案来实现。现在整个项目的目录结构如下. ├── include └── platform ├── linux └── windowsinclude 目录用来对外提供接口,是导出的头文件。platform隔离了Windows和Linux上的实现代码。使用宏来控制我们知道Windows和Linux平台有特定编译器定义的宏,根据这些宏是否定义我们可以知道当前是在Linux还是Windows上编译,是需要编译成32位或者64位程序,又或者编译成debug版本或者release版本。例如下面是我用的简单的判断版本的方式#define MY_PLATFORM_WINDOWS 0 #define MY_PLATFORM_LINUX 1 #define MY_PLATFORM_APPLE 2 #define MY_PLATFORM_ANDROID 3 #define MY_PLATFORM_UNIX 4 #define MY_ARCH32 1 #define MY_ARCH64 2 #if defined(_WIN32) || defined(_WIN64) #define MY_PLATFORM MY_PLATFORM_WINDOWS #ifdef _WIN64 #define PLATFORM_NAME "Windows 64-bit" #define MY_ARCH MY_ARCH64 #else #define PLATFORM_NAME "Windows 32-bit" #define MY_ARCH MY_ARCH32 #endif #elif defined(__APPLE__) #include "TargetConditionals.h" #define MY_PLATFORM MY_PLATFORM_APPLE #ifdef ARCHX64 #define MY_ARCH MO_ARCH64 #else #define MY_ARCH MO_ARCH32 #endif #if TARGET_IPHONE_SIMULATOR #define PLATFORM_NAME "iOS Simulator" #elif TARGET_OS_IPHONE #define PLATFORM_NAME "iOS" #elif TARGET_OS_MAC #define PLATFORM_NAME "macOS" #endif #elif defined(__linux__) #define MY_PLATFORM MY_PLATFORM_LINUX #if defined (ARCHX64) || defined (__x86_64__) #define MY_ARCH MY_ARCH64 #else #define MY_ARCH MY_ARCH32 #endif #define PLATFORM_NAME "Linux" #elif defined(__unix__) #ifdef ARCHX64 #define MY_ARCH MY_ARCH64 #else #define MY_ARCH MY_ARCH32 #endif #define PLATFORM_NAME "Unix" #define MY_PLATFORM MY_PLATFORM_UNIX #else #error "Unknown platform" #endif上面代码根据一些常见的编译器宏来决定是什么版本,并且根据不同的版本将MY_PLATFORM 进行赋值。后面只需要使用 MY_PLATFORM 进行版本判断即可。同样的关于架构使用 MY_ARCH 来判断。例如根据架构来定义不同的数据长度#if (MY_PLATFORM == MY_PLATFORM_WINDOWS) typedef __int64 MY_INT64; typedef unsigned __int64 MY_UINT64; #else typedef long long MY_INT64; typedef unsigned long long MY_UINT64; #endif #if (MY_ARCH == MY_ARCH64) typedef MY_UINT64 MY_ULONG_PTR; typedef MY_INT64 MY_INT_PTR; #else typedef MY_UINT MY_ULONG_PTR; typedef MY_INT MY_INT_PTR; #endif定义的常见的数据结构之后,对于一些接口的视线就可以利用宏来隔开// platform/windows/image.c #if (MY_PLATFORM == MY_PLATFORM_WINDOWS) // todo something #endif// platform/linux/image.c #if (MY_PLATFORM == MY_PLATFORM_LINUX) // todo something #endif这样我们可以利用上一节介绍过的 cmake 的 file 或者 aux_source_directory 将整个platform目录都包含到工程里面。使用cmake来判断版本除了在C/C++ 源码中利用编译器特定的宏来判断版本,其实CMake自身也有一些方式来判断编译的版本。CMake 检测操作系统使用 CMAKE_SYSTEM_NAME 来判断。这里要提一句,CMake中的变量本质上都是一个字符串值,没有严格的区分类型,所以 set(variable 1) 和 set(variable "1") 在底层存储都是字符串 1。所以cmake在定义变量的时候可以不使用双引号,但是对于特殊的字符串,例如带有空格的字符串,为了避免语法上的歧义,可以带上双引号。虽然说底层存储的都是字符串,但是在上层判断变量是否相等的时候却又区分数字和字符串。判断变量是否等于某个值可以使用 EQUAL 或者 STREQUAL。EQUAL 是用来判断数字类型是否相等,一般判断版本号或者数字参数。而 STREQUAL 来判断字符串是否相等,一般用来判断配置选项、路径、平台标识符。例如这里的 CMAKE_SYSTEM_NAME 就需要采用 STREQUAL 来判断# 检测平台 set(PLATFORM_WINDOWS 1) set(PLATFORM_LINUX 2) set(PLATFORM_MACOS 3) if(CMAKE_SYSTEM_NAME STREQUAL "Windows") set(PLATFORM ${PLATFORM_WINDOWS}) elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") set(PLATFORM ${PLATFORM_LINUX}) elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") set(PLATFORM ${PLATFORM_MACOS}) endif()判断架构可以使用 CMAKE_SIZEOF_VOID_P。顾名思义,它表示一个void* 指针变量的大小,8位就是64位,4位则是32位架构。if(CMAKE_SIZEOF_VOID_P EQUAL 8) set(PLATFORM_ARCH "x64") elseif(CMAKE_SIZEOF_VOID_P EQUAL 4) set(PLATFORM_ARCH "x86") else() message(FATAL_ERROR "Unsupported architecture pointer size: ${CMAKE_SIZEOF_VOID_P}") endif()至于判断当前编译的版本是debug 还是 release 可以使用 CMAKE_BUILD_TYPE 来判断,它的值主要有4个,分别是 Debug、RelWithDebInfo、MinSizeRel、Release。它们四个各有特色。其中 RelWithDebInfo 是带有符号表的发布版,便于调试,它的优化级别最低。MinSizeRel和Release在优化上各有千秋,前者追求最小体积,后者追求最快的速度,所以后者有时候会为了运行速度添加一些额外的内容导致体积变大。我们可以在cmake文件中判断对应的值以便做出一些额外的设置。例如if(CMAKE_BUILD_TYPE STREQUAL "Debug") add_compile_definitions(-D_DEBUG) else() add_compile_definitions(-DNDEBUG) endif()有了这些基础,我们可以在不同的条件下,定义不同的编译宏,然后根据编译宏的不同在C/C++ 源码中判断这些宏从而隔离不同平台的实现代码通过cmake list 来隔离不同平台的代码使用 file 或者 aux_source_directory 的到的是一个源代码文件的列表。我们可以操作这个列表来达到控制编译文件的需求。cmake 中针对列表的操作符是 list,它的定义如下:Reading list(LENGTH <list> <out-var>) list(GET <list> <element index> [<index> ...] <out-var>) list(JOIN <list> <glue> <out-var>) list(SUBLIST <list> <begin> <length> <out-var>) Search list(FIND <list> <value> <out-var>) Modification list(APPEND <list> [<element>...]) list(FILTER <list> {INCLUDE | EXCLUDE} REGEX <regex>) list(INSERT <list> <index> [<element>...]) list(POP_BACK <list> [<out-var>...]) list(POP_FRONT <list> [<out-var>...]) list(PREPEND <list> [<element>...]) list(REMOVE_ITEM <list> <value>...) list(REMOVE_AT <list> <index>...) list(REMOVE_DUPLICATES <list>) list(TRANSFORM <list> <ACTION> [...]) Ordering list(REVERSE <list>) list(SORT <list> [...])官方提供了这么一些操作list的操作符,但是在这个需求中我们只需要两个操作符APPEND 和 REMOVE_ITEM 即可。后面的参数分别是源列表,以及需要增加或者删除的项,它们都可以传入多项。但是删除时它是根据传入字符串,从列表中进行字符串比较,如果相等则进行删除。所以在传入路径的时候需要特别注意,不能一个传入全路径一个传入相对路径或者一个传入 / 开头的路径,另一个传入 ~ 开头的路径。上述两个操作都可以传入多个单个的字符串也可以传入一个列表。如果我们有下列目录结构src/platform/windows src/platform/linux src/*.cpp src/other/*.cpp也就说我们将不同平台的代码放入到src目录,并且src目录也有其他代码,我们如果使用 file 操作符来查找src目录中的源码文件必定会包含两个平台的实现代码。这个时候就可以考虑使用REMOVE_ITEM 根据平台来舍弃一些代码,例如file(GLOB_RECURSE SOURCES ${PROJECT_SOURCE_DIR}/src/*.c ) if(CMAKE_SYSTEM_NAME STREQUAL "Windows") file(GLOB_RECURSE NOT_INCLUDE ${PROJECT_SOURCE_DIR}/src/platform/linux/*.cpp ) elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") file(GLOB_RECURSE NOT_INCLUDE ${PROJECT_SOURCE_DIR}/src/platform/windows/*.cpp ) endif() list( REMOVE_ITEM SOURCES SOURCE ${NOT_INCLUDE} )又或者我们采用最上面的给出的目录结构,也就是说platform 目录位于src目录之外,相对于src来说是额外添加的代码文件,那么就可以使用 APPEND 来进行添加if(CMAKE_SYSTEM_NAME STREQUAL "Windows") aux_source_directory(${PROJECT_SOURCE_DIR}/platform/windows PLATFORM_SOURCE) elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") aux_source_directory(${PROJECT_SOURCE_DIR}/platform/linux PLATFORM_SOURCE) endif() list(APPEND SOURCES ${PLATFORM_SOURCE})通过 toolchain 文件来组织平台特殊配置cmake 允许我们在生成Makefile的时候指定toolchain 文件来实现一些自定义的配置。例如可以根据平台的不同将生成路径指定在对应的toolchain中。toolchain 的语法与cmake语法相同,例如针对Windows和Linux可以创建 win32_toolchain.cmake win64_toolchain.cmake linux_86_toolchain.cmake 和 linux_x64_toolchain.cmake 文件来区别我还是以上一篇文章中多工程嵌套的例子作为示例来演示如何使用,它的目录结构如下. ├── calc │ ├── add.cpp │ ├── CMakeLists.txt │ ├── div.cpp │ ├── mult.cpp │ └── sub.cpp ├── CMakeLists.txt ├── include │ ├── calc.h │ └── sort.h ├── sort │ ├── CMakeLists.txt │ ├── insert_sort.cpp │ └── select_sort.cpp ├── test_calc │ ├── CMakeLists.txt │ └── main.cpp └── test_sort ├── CMakeLists.txt └── main.cpp这个例子我们只需要改动根目录下的生成库和可执行程序的路径。cmake_minimum_required(VERSION 3.15) project(test) add_subdirectory(sort) add_subdirectory(calc) add_subdirectory(test_calc) add_subdirectory(test_sort)这个文件只需要保留最基础的配置即可,而生成程序的路劲都在 toolchain 中。下面是 linux_x86_toolchain.cmake 文件的内容set(LIBPATH ${PROJECT_SOURCE_DIR}/lib/linux/x86) set(EXECPATH ${PROJECT_SOURCE_DIR}/bin/linux/x86) set(HEADPATH ${PROJECT_SOURCE_DIR}/include) if(CMAKE_BUILD_TYPE STREQUAL "Debug") set(CALCLIB calc_d) set(SORTLIB sort_d) set(CALCAPP test_calc_d) set(SORTAPP test_sort_d) else() set(CALCLIB calc) set(SORTLIB sort) set(CALCAPP test_calc) set(SORTAPP test_sort) endif() set(CMAKE_SYSTEM_PROCESSOR i686) set(CMAKE_C_FLAGS "-m32 -L/usr/lib32" CACHE STRING "" FORCE) set(CMAKE_CXX_FLAGS "-m32 -L/usr/lib32" CACHE STRING "" FORCE) set(CMAKE_EXE_LINKER_FLAGS "-m32 -L/usr/lib32" CACHE STRING "" FORCE)这个文件我们定义了debug和release的库名称和生成的路径,并且指定相关参数用于生成32位程序。CMAKE 中定义了一堆 CMAKE_LANGUAGE_FLAGS 这些都是相关工具的参数,这里的FLAGS 分别是 gcc 和 ld 编译链接的参数。在使用时直接用命令 cmake .. -DCMAKE_TOOLCHAIN_FILE=../linux_x32_toolchain.cmake -DCMAKE_BUILD_TYPE=Debug 生成Makefile。Windows平台上上面的参数稍微有些差距。例如下面是 windows_x32_toolchain.cmake 的定义# 静态库生成路径 set(LIBPATH ${PROJECT_SOURCE_DIR}/lib/windows/x86) # 可执行程序的存储目录 set(EXECPATH ${PROJECT_SOURCE_DIR}/bin/windows/x86) # 头文件路径 set(HEADPATH ${PROJECT_SOURCE_DIR}/include) if(CMAKE_BUILD_TYPE STREQUAL "Debug") # calc库名称 set(CALCLIB calc_d) # sort 库名称 set(SORTLIB sort_d) # 测试程序的名字 set(CALCAPP test_calc_d) set(SORTAPP test_sort_d) else() # calc库名称 set(CALCLIB calc) # sort 库名称 set(SORTLIB sort) # 测试程序的名字 set(CALCAPP test_calc) set(SORTAPP test_sort) endif() set(CMAKE_GENERATOR_PLATFORM "Win32" CACHE STRING "Target Platform") # 指定32位编译器路径 set(CMAKE_C_COMPILER "$ENV{VCToolsInstallDir}/bin/Hostx86/x86/cl.exe") set(CMAKE_CXX_COMPILER "$ENV{VCToolsInstallDir}/bin/Hostx86/x86/cl.exe") set(CMAKE_SYSTEM_PROCESSOR x86)需要注意的是 CMAKE_GENERATOR_PLATFORM 对应的是VS 中的解决方案平台,也就是 Win32 和 x64 这两个选项。 而 CMAKE_SYSTEM_PROCESSOR 对应的是VS中的目标计算机选项,一般是X86、X64 或者 ARM64 和 AMD64。我们可以使用命令 cmake -G "Visual Studio 15 2017" -A "win32" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE="..\windows_x86_toolchain.cmake" .. 这里指定使用 VS 2017 进行构建,构建架构是32位,版本是Debug。命令成功执行之后会生成一个.sln 文件,我们可以用VS打开然后在VS中编译,也可以执行使用命令 cmake --build . --config Debug 来编译。一般来说我习惯使用后者,过去使用vs 打开.sln 可以在vs中进行开发,如今vs 已经可以打开并编译cmake工程,所以现在我基本不使用 .sln 文件了,除非公司项目要求使用 .sln。好了,目前我掌握的关于cmake的内容就是这些了,我利用这些知识已经完成了公司项目的跨平台开发和部署。后续如果有新的需求说不定我会学点新的内容,到时候再来更新这一系列文章吧!!!
2025年04月12日
6 阅读
0 评论
0 点赞
2025-04-01
CMake 基础
很遗憾直到现在才开始接触cmake,过去都在微软的vs IDE上编写c++程序,即使引用第三方的库直接使用cmake也能编译成功,很少关注它本身的内容。但是现在我有一项工作的内容就是将在Windows平台上的c++程序移植到Linux平台上。我想选择cmake作为支持跨平台的构建工具。因此提前学了点cmake的基础知识。cmake本身并不能直接编译和链接程序,它是一个构建程序。主要作用就是根据cmake脚本来生成Makefile文件,以供nmake、gun make等工具来生成可执行程序。编译exe简单的hello world使用cmake需要提供一个CMakeLists.txt 的脚本文件,这个名称是固定的,位置一般在项目的根目录。假设现在有一个简单的hello world程序,它的项目目录可能如下v1 ├── CMakeLists.txt ├── main.cpp我们可以使用如下cmake脚本cmake_minimum_required(VERSION 3.15) set(CMAKE_CXX_STANDARD 11) project(test) add_executable(hello ./main.cpp)第一句的含义是指定使用cmake最小的版本为3.15;第二句的含义是使用c++ 11标准第三句的含义是指定项目名称第四句的含义是生成可执行程序的名称为hello,并且指定要编译的源文件是当前目录下的 main.cpp 文件。工程中有多个源文件时,add_executable 后面可以加多个源文件路径一般来说cmake脚本都会包含这么几条语句脚本编写完毕后,需要使用cmake命令进行编译。该命令可以接受一个参数用于指定CMakeLists.txt 文件所在的路径,执行之后会生成一大堆中间文件和对应的Makefile文件。这些都会生成在当前执行cmake命令时所在路径。所以为了便于管理,一般会在适当位置建立一个新的build目录。这个时候整个命令如下mkdir build cd build cmake .. make前面我们在项目根目录下新建一个build目录用于保存中间文件,然后切换到build目录中。接着执行cmake命令并给出对应CMakeLists.txt 所在的路径。执行成功后会在build目录中生成一个Makefile文件,最后就是执行make命令来生成可执行程序这样最简单的一个hello world工程就编译完成了。指定可执行程序的路径生成的可执行文件路径就在当前的build目录下,如果我们要指定可执行程序的路径,可以使用变量 EXECUTABLE_OUTPUT_PATH。它是cmake内置的变量,保存的是可执行程序输出的路径。在cmake中可以使用set来给变量赋值。到此我们的cmake脚本可能是这样的cmake_minimum_required(VERSION 3.15) set(CMAKE_CXX_STANDARD 11) project(test) set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) add_executable(hello ./main.cpp)这里涉及到cmake中变量的定义和使用。其实cmake中变量特别简单,cmake中的变量全都是字符串,定义和设置变量值都是用set 操作符。而要取变量的值则使用 ${} 来包住一个变量名。另外cmake使用 EXECUTABLE_OUTPUT_PATH 作为可执行程序的输出路径,这里我们设置输出路径为工程目录下的bin目录下面。这里的 PROJECT_SOURCE_DIR 表示的是当前项目的目录指定头文件所在路径这里我们来一个复杂一点的项目作为演示,这个项目的目录结构如下. ├── include │ └── calc.h └── src ├── add.cpp ├── div.cpp ├── main.cpp ├── mul.cpp └── sub.cpp这种工程中,include目录放头文件,src目录放源文件,calc.h 中定义了4个函数分别实现加减乘除四则运算。它们的实现分别在 add.cpp、sub.cpp、mul.cpp、div.cpp 中,而main.cpp主要负责调用这些函数实现。main.cpp 的代码如下#include <stdio.h> #include "calc.h" int main (int argc, char *argv[]) { int a = 30; int b = 10; printf("a + b = %d\n", add(a, b)); printf("a - b = %d\n", sub(a, b)); printf("a * b = %d\n", mul(a, b)); printf("a / b = %d\n", div(a, b)); return 0; }这里我们要解决一个问题,因为main.cpp在src中,而 calc.h在include目录中,它们并不在同一目录下,代码中直接引用它会提示找不到对应的头文件。我们当然可以写出 include "../include/calc.h" 来修正它,但是项目中文件多了,不同路径的源文件要写这种相对路径就是一种折磨了。一般的经验是给出头文件的路径,后面所有源文件都根据这个路劲来组织包含头文件的相对路径。这里我们需要指定include作为头文件的路径。cmake中使用 include_directories 来指定头文件路径,它可以接受多个目录表示可以从这些目录中去查找头文件。所以这个项目的cmake文件可以这么写cmake_minimum_required(VERSION 3.15) set(CMAKE_CXX_STANDARD 11) project(test) set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) include_directories(${PROJECT_SOURCE_DIR}/include) add_executable(hello ./src/add.cpp ./src/sub.cpp ./src/mul.cpp ./src/div.cpp ./src/main.cpp)遍历目录中的源文件上面的示例中我们发现 add_executable 后面加了好多cpp文件,这个项目比较小只有这么几个文件,如果一个项目有几百个源文件,并且每个源文件都在不同的目录,我们把每个源文件都这样一个个的写出来,不知道要写到什么时候呢。是否有办法能一次获取目录中的所有cpp文件,并保存在一个变量中,在需要指定源文件的场合直接使用这个变量,这样就简单很多了。cmake中当然有这个方法,它提供了两种方式来实现这个需求。第一种方式是使用 aux_source_directory。它接受一个目录,将指定目录中的所有源文件以list的形式放入到指定变量中,使用它可以将之前的cmake文件改写成下列形式cmake_minimum_required(VERSION 3.15) set(CMAKE_CXX_STANDARD 11) project(test) set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) aux_source_directory(${PROJECT_SOURCE_DIR}/src SOURCES) include_directories(${PROJECT_SOURCE_DIR}/include) add_executable(hello ${SOURCES})这里我们遍历src目录中的所有源文件,将结果放入到变量SOURCES中。最后在add_executable中将这个结果传入,作为源文件参与最后的编译。第二种方式是可以使用file函数,它能遍历指定目录中的指定文件,并且将结果返回到对应参数中,它的使用方式如下file(<GLOB|GLOB_RECURSE> <variable> [LIST_DIRECTORIES])第一个参数是 GLOB 或者是 GLOB_RECURSE。后者表示递归遍历所有子目录中的文件。第二个参数是变量,最后会将遍历的结果放入到这个变量中。第三个参数是一个可选的,它表示筛选条件,可以填入多个条件。我们可以将上面的aux_source_directories 替换成 file,写成如下形式file(GLOB_RECURSE SOURCES ${PROJECT_SOURCE_DIR}/src/*.cpp)编译静态库和动态库我们再来修改一下这个工程。我们将四则运算的操作独立出来编译为一个静态库,然后在另一个工程中链接这个库并调用这些函数。这个时候可以这么组织工程,在上一个工程的基础上删除main.cpp 就可以了。编译静态库可以使用 add_library 操作符,它用来生成库文件。它可以编译动态库或者静态库。第一个参数是库的名称,最终会生成一个名称为 libname.a 或者 libname.so 的文件,其中name是我们指定的第一个参数;第二个参数是STATIC 或者 SHARED 分别是编译静态库和动态库。第三个参数是编译时需要参与便于的代码源文件。 所以我们的CMakeLists.txt 文件可以这样写cmake_minimum_required(VERSION 3.15) set(CMAKE_CXX_STANDARD 11) project(test) set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) file(GLOB_RECURSE SOURCES ${PROJECT_SOURCE_DIR}/src/*.cpp) include_directories(${PROJECT_SOURCE_DIR}/include) # 编译动态库 # add_library(mylib SHARED ${SOURCES}) # 编译静态库 add_library(mylib STATIC ${SOURCES})上面的配置中,使用 LIBRARY_OUTPUT_PATH 来指定库文件生成的路径,最终会在bin目录下生成一个名为 libmylib.so 或者 libmylib.a 的库文件链接静态库和动态库上面我们编译生成了静态库和动态库,该如何在工程中引用它们呢?引用动态库或者静态库可以使用 target_link_libraries。它可以链接静态库或者动态库。在指定要链接的库名称为name 之后,它默认会优先从用户指定的位置查找名为 libname.a 或者 libname.so 的库,如果用户未指定位置或者在指定位置未找到对应的库,那么它会到系统库中查找,都找不到则会报错。我们可以通过 link_directories 来指定库文件的路径,下面是一个示例cmake_minimum_required(VERSION 3.15) set(CMAKE_CXX_STANDARD 11) project(test) include_directories(${PROJECT_SOURCE_DIR}/include) set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) link_directories(${PROJECT_SOURCE_DIR}/lib) add_executable(hello ${PROJECT_SOURCE_DIR}/main.cpp) target_link_libraries(hello mylib )target_link_library 需要放到 add_executable 或者 add_library 之后,它的第一个参数就是我们在 add_executable 或者 add_library 中给定的生成程序的名称。添加编译宏一般来说,在代码中对于debug版本会额外的输出一些日志信息用于调试,或者根据不同版本来调整某个数据结构的定义,例如#ifdef X64 typedef unsigned long long ULONG_PTR #else typedef unsigned long ULONG_PTRVS 中可以通过预处理器来指定编译时的宏,而GCC 可以通过-D 来指定宏。cmake中也类似,它可以通过 add_compile_definies 来指定宏。它传入的参数于GCC定义宏类似,以-D开头后面跟宏的名称,例如要定义名为 _DEBUG 的宏,可以写成 -D_DEBUG。定义宏后面还可以使用 = 来指定宏的值。下面是一个具体的例子#include <stdio.h> int main (int argc, char *argv[]) { #ifdef _DEBUG printf("this is debug version\n"); #endif printf("the app version is %s\n", VERSION); return 0; }cmake_minimum_required(VERSION 3.15) set(CMAKE_CXX_STANDARD 11) project(test) set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) add_compile_definitions( -D_DEBUG -DVERSION="1.0.1") add_executable(hello ${PROJECT_SOURCE_DIR}/main.cpp)多个工程嵌套一般在项目中,可能有多个子项目,例如一个web商场可能有前后端之分。在cmake中项目有子工程的话,将各个子工程放到主工程的子目录下,然后使用 add_subdirectory 将各个子项目连接起来。下面是一个具体的例子. ├── calc │ ├── add.cpp │ ├── CMakeLists.txt │ ├── div.cpp │ ├── mult.cpp │ └── sub.cpp ├── CMakeLists.txt ├── include │ ├── calc.h │ └── sort.h ├── sort │ ├── CMakeLists.txt │ ├── insert_sort.cpp │ └── select_sort.cpp ├── test_calc │ ├── CMakeLists.txt │ └── main.cpp └── test_sort ├── CMakeLists.txt └── main.cpp上述项目有4个子工程,分别是四则运算的calc 、排序算法的 sort。以及对应的测试用例test_calc 和 test_sort。算法编译成静态库,测试工程直接链接对应的静态库。基于以上布局,我们在主工程的 CMakeLists.txt 可以这么写cmake_minimum_required(VERSION 3.15) project(test) # 定义变量 # 静态库生成路径 set(LIBPATH ${PROJECT_SOURCE_DIR}/lib) # 可执行程序的存储目录 set(EXECPATH ${PROJECT_SOURCE_DIR}/bin) # 头文件路径 set(HEADPATH ${PROJECT_SOURCE_DIR}/include) # calc库名称 set(CALCLIB calc) # sort 库名称 set(SORTLIB sort) # 测试程序的名字 set(CALCAPP test_calc) set(SORTAPP test_sort) # 添加子目录 add_subdirectory(sort) add_subdirectory(calc) add_subdirectory(test_calc) add_subdirectory(test_sort)在这个文件我们定义了一些其他工程都会用到的一些配置,例如包含的头文件路径、生成程序的路径。以及项目中包含的子项目。在最外层定义的变量可以直接在子工程的cmake 配置文件中使用。这里有点像派生类可以使用基类定义的变量。在calc 子工程中,可以这么配置cmake_minimum_required(VERSION 3.15) project(calc) # 指定要编译的源文件 aux_source_directory(./ SOURCES) # 指定头文件的路径 include_directories(${HEADPATH}) # 指定生成库的路径 set(LIBRARY_OUTPUT_PATH ${LIBPATH}) # 指定生成库的名称 add_library(${CALCLIB} STATIC ${SOURCES})calc 子工程使用根目录工程中定义的变量指定了生成库的路径、库名称。并且直接定义编译成静态库在test_calc 这个测试程序中,可以这么配置cmake_minimum_required(VERSION 3.15) project(test_calc) # 指定头文件的路径 include_directories(${HEADPATH}) # 指定生成exe的路径 set(EXECUTABLE_OUTPUT_PATH ${EXECPATH}) # 指定库文件的目录 link_directories(${LIBPATH}) # 生成可执行文件名称 add_executable(${CALCAPP} ./main.cpp) target_link_libraries( ${CALCAPP} ${CALCLIB} )在测试工程中使用父工程中定义的变量指定了生成程序的路径以及链接库的路径。其他的工程与上面两个子工程的配置类似,只需要改一些变量。就可以运行了。至此, 已经介绍完了使用cmake配置工程的一些基本配置。我们几乎可以将VS 中的项目配置一比一的使用上述内容使用cmake复刻一遍。至于跨平台的配置,无外乎是一些常见的标志判断,根据条件设置变量即可。后续如果我还有好的cmake使用实践也会分享出来。
2025年04月01日
9 阅读
0 评论
0 点赞
2025-03-28
Emacs 折腾日记(二十)——修改emacs的一些默认行为
上一篇我们完成了emacs输入法的配置以及将emacs配置成了使用vim的操作方式。但是emacs目前有些默认行为我不太喜欢,这节我们一起来修改它备份设置我们打开emacs的配置文件所在路径,发现有大量的~结尾的文件,这是emacs的备份文件。这里,我们不使用这个特性,可以通过git等版本管理软件进行版本的控制和备份的管理。而且去掉这些还能让目录干净点。(setq make-backup-files nil) ; 不自动备份 (setq auto-save-default nil) ; 不使用Emacs自带的自动保存将用户设置独立开来在修改这些配置的时候经常会发现在init.el 中出现类似下面的代码被修改(custom-set-variables ;; custom-set-variables was added by Custom. ;; If you edit it by hand, you could mess it up, so be careful. ;; Your init file should contain only one such instance. ;; If there is more than one, they won't work right. '(package-selected-packages nil)) (custom-set-faces ;; custom-set-faces was added by Custom. ;; If you edit it by hand, you could mess it up, so be careful. ;; Your init file should contain only one such instance. ;; If there is more than one, they won't work right. )这里保存的是使用编辑器接口产生的配置信息。如果让它们随意堆砌在init.el 中不利于版本的管理,我们将它放入到另一个文件中(setq custom-file (expand-file-name "~/.emacs.d/custom.el")) (load custom-file 'no-error 'no-message)之前我们用 require 来加载一个代码文件,这里我们使用 load 来加载代码文件。它们有什么区别呢?首先 require 需要加载一个已经被定义为库的代码文件,也就是通过 provide 定义的库文件。而load传入文件路径来加载其次 require 会根据 provide 定义的库文件自动处理库文件,每个库文件只加载一次,并且会自动处理依赖。而 load 这些操作都需要手动进行load 可以根据if条件来有选择的加载不同的库文件。而 require 则无法做到load 可以进行错误处理,例如上面我们定义在加载时通过 noerror 限制错误,通过 no-message 不输出信息。而 require 是严格报错的。其他的一些基础设置这里再添加一些其他的基础配置(fset 'yes-or-no-p 'y-or-n-p) ;; 将所有的 yes-or-no-p 都替换为 y-or-n-p (setq confirm-kill-emacs #'y-or-n-p) ; 在关闭 Emacs 前询问是否确认关闭,防止误触 (electric-pair-mode t) ; 自动补全括号 (column-number-mode t) ; 在 Mode line 上显示列号 (global-auto-revert-mode t) ; 当另一程序修改了文件时,让 Emacs 及时刷新 Buffer (delete-selection-mode t) ; 选中文本后输入文本会替换文本(更符合我们习惯了的其它编辑器的逻辑) (add-hook 'prog-mode-hook #'hs-minor-mode) ; 编程模式下,可以折叠代码块 (add-hook 'prog-mode-hook #'show-paren-mode) ; 编程模式下,光标在括号上时高亮另一个括号(fset 'yes-or-no-p 'y-or-n-p) 将所有的 yes-or-no-p 都替换为 y-or-n-p。这样在每次确定的时候能从 yes 或者 no的输入变成输入 y 或者 n,能少输入几个字符。这里又看到了一个新的符号# ,它代表的意思是取符号的函数部分。前面我们介绍符号的时候说,符号有两个部分的值,变量值和函数值。我们可以通过 function 来获取符号的函数部分的值。它的作用等同于 `(setq confirm-kill-emacs (function y-or-n-p))` 。这里又有一个新的函数 function。我们在介绍符号的时候介绍过使用 symbol-function 来获取符号的函数,那么他们两个有什么区别呢?首先 function 返回的是函数对象,而 symbol-function 返回函数本身。这个比较的抽象,我们使用例子来说明(setq bar "I am a bar variable") (defun bar() "I am a bar function") (function bar) ;; ==> bar (symbol-function 'bar) ;; ==> #[nil ("I am a bar function") (t)] (functionp bar) ;; ==> nil (functionp (function bar)) ;; ==> t上面的例子中,我们实际上定义了bar的变量部分和函数部分的值。同一 bar 符号它既可以作为变量使用,也可以作为函数使用。我们在使用 function 对 bar 求值的时候,得到的返回虽然也是 bar 但是它返回的是它的函数部分,而 symbol-function 则直接返回函数的结构,因为lisp代码本身就是一个列表结构,所以这里它返回的实际上是函数的代码。它返回的比 function 更加的底层。下面我们使用 functionp 进行了测试,发现 function 返回的是一个函数对象。虽然在理解上有些差别,但是都可以直接通过 funcall 来调用(bar) ;; ==> "I am a bar function" (funcall bar) ;; ==> error (funcall (function bar)) ;; ==> "I am a bar function" (funcall (symbol-function 'bar)) ;; ==> "I am a bar function"我们发现当一个符号既有值部分,又有函数部分,是无法通过 funcall 来直接调用的。所以上述代码使用 # 这个语法糖来保证后续正常调用这个符号对应的函数部分。
2025年03月28日
8 阅读
0 评论
0 点赞
2025-03-25
Emacs 折腾日记(十九)——配置输入法和vim操作方式
上一篇文章中,我们将Emacs变得稍微好看了点。换成了自己喜欢的主题和颜色,这样每天用起来也比较养眼,不会特别排斥。本篇文章的主要任务就是配置输入法方便输入中文以及将vim的操作模式搬到Emacs中。进一步提到Emacs的可用性配置中文输入法系统基本环境配置在配置输入法之前,需要系统支持中文,并且有对应的中文字体可以显示中文,而且还需要对应的输入法框架支持。首先我们安装中文字体和语言包,本人之前有一篇介绍如何搭建wsl2+archlinux的文章,已经完成了这一步。但是为了没看过那篇文章的读者不用再去费劲的找那篇文章,我还是把命令贴出来:sudo pacman -S wqy-zenhei wqy-microhei noto-fonts-cjk # 安装中文字体 sudo pacman -S fcitx5-im fcitx5-chinese-addons # 安装中文输入法框架及中文引擎 sudo pacman -S fcitx5-qt fcitx5-gtk # GUI支持接着配置 Locale ,我们需要编辑 /etc/locale.gen 文件,取消下面两行的注释en_US.UTF-8 UTF-8 zh_CN.UTF-8 UTF-8编辑完成之后,调用 sudo locale-gen 命令生成 locale。接着我们需要在 ~/.bashrc 或者 ~/.zshrc 又或者其他shell的配置文件中加入输入法的配置export GTK_IM_MODULE=fcitx5 export QT_IM_MODULE=fcitx5 export XMOIFIERS=@im=fcitx5然后我们启动fcitx5服务dbus-launch fcitx5 --disable=wayland -d &这个时候我们可以启动emacs,并且执行 M-x toggle-input-method 或者使用 C-\ 来切换输入法。只是默认的 chinese-py 输入法比较难用。所以我们需要换一个输入法并且给出拼音的词库emacs-rime 配置这个配置是我在 Emacs-China论坛 的一篇文章中发现这个输入法。可以访问 输入法源代码地址 ,它已发布到 Melpa,所以我们可以通过use-package 或者emacs自带的package来安装和管理。这里还是使用 use-package在安装之前,需要安装librime 和 fcitx5-rime。在arch中使用sudo pacman -S librime fcitx5-rime接着安装雾凇拼音,但是根据emacs-rime 官方文档的说法,最好不要将~emacs-rime~ 与 ~fcitx-rime~ 共用用户数据目录。所以这里我们我们将它放到其他目录git clone https://github.com/iDvel/rime-ice ~/.config/rime --depth=1然后我们进行emacs-rime的配置和安装(use-package rime :ensure t :custom (default-input-method "rime") (rime-posframe-properties (list :background-color "#333333" :foreground-color "#dcdccc" :font "WenQuanYi Micro Hei Mono-14" :internal-border-width 10)) (rime-user-data-dir "~/.config/rime") (rime-show-candidate 'posframe))到这里我们使用 C-\ 就可以愉快的输入中文了Vim 操作方式本系列文章并没有像一般的Emacs教程那样给各位读者介绍Emacs的操作和快捷键。因为我觉得Emacs可以很方便的变成跟vim一样的编辑器,既然读者们都熟悉vim,那就没必要单独的学习一套Emacs操作,直接继承vim的操作就行。这里说一个题外话,我觉得一个编辑器如果不支持vim的操作模式,要么就是使用的人不多,要么就是不够开放。对我来说这种编辑器平时就没有学习和使用的必要了。我们使用evil 插件来模拟vim的操作。(use-package evil :ensure t :init (evil-mode))我们执行完这句代码之后会发现已经进入了vim的normal模式了。这个时候又可以愉快的使用vim的操作方式来编辑文本了。但是查阅了关于evil的文档后发现,evil本身并不支持像vim那样设置leaderkey,我们要结合其他插件来达到这一效果。我们使用 general 插件来模拟并设置leaderkey(use-package general :ensure t :config (general-evil-setup t))插件安装完成之后可以使用下面的代码来设置leaderkey(general-create-definer my-leader-def :states '(normal insert visual emacs) :prefix "SPC" :non-normal-prefix "C-,")这里使用 general 提供的 general-create-definer 来定义自己的leader键,它可以定义leader键的作用范围和触发方式。my-leader-def 是一个符号它代表着我们在这里定义的leader键,后续可以通过它来结合其他按键来实现快捷键绑定。这里的 :states 表示作用的范围,在启用evil插件之后我们在 normal、insert、visual、emacs 这几个模式中启用这个leader键。通过 :prefix 定义leader键,这里我定义leader键为空格。最后一个参数 :non-normal-prefix 定义在非normal 模式下使用 C-, 来作为leader键。后面我们就可以通过这个 my-leader-def 这个符号来绑定快捷键了。下面提供一个例子来演示如何绑定快捷键。前面介绍vim相关内容的时候提到过,我们使用 <leader>ee 来快速打开配置文件,使用 <leader>ss 来重新加载配置,在这里实现以下emacs版本的这套功能。我们先来实现这两个功能函数(defun open-my-emacs-config() (interactive) (find-file "~/.emacs.d/init.el")) (defun source-my-emacs-config() (interactive) (eval-buffer (get-buffer "~/.emacs.d/init.el")))实现了这样的函数之后就是针对这些命令来绑定快捷键了。我们使用下面的代码来绑定(my-leader-def "ss" 'open-my-emacs-config) (my-leader-def "ee" 'source-my-emacs-config)至此我们使用evil 和 general 插件完成了一个简单的Emacs vim化的改造。利用这些简单的配置后面在使用的时候应该会更加的得心应手。随着代码越来越多,需要用git管理起来,每篇文章更新的代码我都会传到GitHub上供读者参考本篇代码
2025年03月25日
9 阅读
0 评论
0 点赞
2025-03-17
Emacs 折腾日记(十八)——改变Emacs的样貌
截止到上一篇文章为止,之前教程 的内容都看完了,虽然它的后记部分提供了一些后续进阶的内容需要我们自己读手册。但是我不太想继续在elisp上死磕了。看着自己学了那么久的elisp,但是自己的emacs仍然没有半点改变,这个时候各位读者的兴趣一定会大打折扣,是时候试试配置一下自己的emacs了。教程后记中提到的内容等配置的时候涉及到了再来了解吧所谓人靠衣装马靠鞍,一个编辑器好不好用首先要看的就是它好不好看,对于难看的编辑器可能一眼就要给它发卡了——“你是一个很优秀的编辑器,但是我们不合适”。所有配置时第一件事就是将emacs变帅变好看。emacs的基础配置Emacs 在加载的时候会首先读取 ~/.emcas.d/init.el 中的代码。整个配置程序的入口就在这里。但是如果我们一股脑将所有代码都写在这个文件中日后想要维护肯定是不方便的,所以在写配置之前需要了解一些它的模块化提供一个模块,我们只需要在代码文件最后的位置写上 (provide 'package-name) 这样的代码即可。这里的 provide。可以理解为导出,后面是导出模块的名称。在需要引入模块时,只需要添加一行 (require 'basic)。但是与其他语言类似,有时候会出现找不到对应的模块,这里涉及到一个查找路径的问题。Emacs 中加载路径被保存在变量 load-path 中。该变量是一个list,我们可以将指定路径放入到这个变量中来添加用户定义代码的路径。load-path 中的目录顺序决定了 Emacs 搜索文件的优先级。如果多个目录中存在同名的 Lisp 文件,Emacs 会优先加载 load-path 中靠前的目录中的文件。因此,你可以通过调整 load-path 的顺序来控制加载的优先级。为了添加路径到 load-path 中,我们要了解一个新的函数, add-to-list。为什么这里我们不使用 push 或者其他之前学过的操作list的函数呢?最关键的一点是 add-to-list 具有去重的功能,能避免多次重复加入同一个路径。如果我们将用户代码放入到 ~/.emacs.d/lisp 这个目录中,我们可以使用下面的代码;; init.el (add-to-list 'load-path "~/.emacs.d/lisp")前面的知识介绍完了,现在我们新建 ~/.emacs.d/lisp/basic.el 文件,进行基础的配置。目前添加的代码主要是取消Emacs上的滚动条、菜单栏、工具栏、以及每次打开的开始界面。;; basic.el ;; 禁止菜单栏、工具栏、滚动条模式,禁止启动屏幕和文件对话框 (menu-bar-mode -1) (tool-bar-mode -1) (scroll-bar-mode -1) (setq inhibit-splash-screen t) ;; 禁止启动画面 ;; 显示行号 (setq display-line-numbers-type 'relative) ;;显示相对行号 (global-display-line-numbers-mode 1) (provide 'basic)然后在启动的时候使用它;; init.el (add-to-list 'load-path "~/.emacs.d/lisp") (require 'basic)重启Emacs就能看到具体的效果了使用包管理器要想它变的好看,最好的办法是加载开源大佬提供的主题。作为一个小菜鸡不太可能自己开发重型的功能,我们要做的这是将大佬提供的包整合到自己的配置中。所以我们先来介绍包和包管理器。这里的包我们可以理解为提供了某种功能的模块,有点类似与C/C++ 的静态库或者Java的类库。Emacs中的包管理主要通过 package.el 模块提供。它包含了模块的查找,下载,更新以及删除等操作。它的一些常用命令如下:M-x list-packages:列出所有可用的包,并进入包管理界面。M-x package-install:安装指定的包。M-x package-refresh-contents:刷新包列表,获取最新的包信息。M-x package-upgrade:更新所有已安装的包。M-x package-delete:删除指定的包。Emacs中默认的仓库是 Emacs 默认使用 MELPA(Milkypostman’s Emacs Lisp Package Archive)作为主要的包仓库。MELPA 提供了大量高质量的第三方包。除此之外,还有其他仓库,如:GNU ELPA:官方仓库,包含 Emacs 自带的包。MELPA Stable:提供稳定版本的包。NonGNU ELPA:包含一些非 GNU 的包。因为国内的网络环境,我们常常需要使用国内的源。这里我们创建一个新的文件 package 用来管理包。;; package-conf.el (require 'package) (setq package-enable-at-startup nil) (setq package-archives '(("gnu" . "https://mirrors.tuna.tsinghua.edu.cn/elpa/gnu/") ("nongnu" . "https://mirrors.tuna.tsinghua.edu.cn/elpa/nongnu/") ("melpa" . "https://mirrors.tuna.tsinghua.edu.cn/elpa/melpa/"))) (package-initialize) ;; You might already have this line (provide 'package-conf)这里我使用清华源,各位读者可以选择自己喜欢的源。上面的代码我们使用 package-enable-at-startup 来控制Emacs是否自动初始化package包管理器。这里为了更灵活我们禁止它自动初始化,改由手动初始化。我们可以在代码中使用类似于 (package-install 'package-name) 的方式来自动安装包,但是这里介绍更加高级的包管理器——use-package。如果使用Emacs原生的包管理器,那么就是先安装,然后想办法组织包配置的代码,这样将安装与配置分散起来了,不利于管理。使用use-package 可以方便的将它们组织起来。本质上use-package 提供了一系列的宏将包的安装和包的配置组合到一起,方便维护。而且它还提供了一些高级的特性方便我们灵活的控制各种配置生效的时间。我们可以使用如下语句进行安装;; package-conf.el (unless (package-installed-p 'use-package) (package-refresh-contents) (package-install 'use-package))它的基本语法如下:(use-package package-name :keyword1 value1 :keyword2 value2 ...)它的常用关键字如下::ensure: 确保包已安装。如果包未安装,use-package 会自动安装它,一般使用Emacs自带的包这里设置成nil,安装第三方的包,这里设置成t:init:在包加载之前执行的代码:config: 在包加载之后执行的代码:bind: 为包中的函数绑定快捷键:mode: 为特定文件类型启用包:hook: 在特定模式下启用包:defer: 延迟加载包,直到首次使用包:custom: 设置包的变量除了这些我们可以使用 :if 或 :when 关键字实现更复杂的条件加载。或者通过 :requires 关键字来指定包的依赖项配置主题说了这么多,我们使用 use-package 来安装一个主题来提高一下Emacs的颜值。这里我选用 doom-themes 包中的 doom-dracula 主题。关于ui部分的配置,我们都放在 ~/.emacs.d/lisp/init-ui.el 中;; init-ui.el (use-package doom-themes :ensure t :config (load-theme 'doom-dracula t)) (provide 'init-ui)我们在init.el 中加载init-ui之后,再次打开效果如下:设置字体我们在介绍文本属性的时候使用过face这个属性,Emacs中跟文字显示相关的属性都是face,它包括:字体、字号、颜色、背景。我们之前介绍了一系列的函数来处理字体字号,但是之前介绍的只能绑定到具体的文字上,默认的字体字号使用那些函数是无法设置的。我们可以使用 set-face-attribute 来设置字体属性。该函数的定义如下(set-face-attribute FACE FRAME &rest ARGS)参数face 表示设置的是哪个部分的字体属性,例如 default(默认字体) 、mode-line (状态栏字体)、region (选中区域字体)等。我们可以使用 M-x list-faces-display 来查看支持的face参数 frame 表示需要设置哪个窗口框架(通常用 nil 表示当前窗口或所有窗口)的字体属性。参数 ARGS 来设置具体的字体属性。以下属性可用于控制字体和样式:属性名功能描述示例值:family字体名称(需系统已安装)"Fira Code", "Consolas":height字号(以百分比或绝对点数表示,默认 100 = 10pt)120(12pt), 14(14pt):weight字重(如正常、加粗)'normal, 'bold:slant字体倾斜'normal, 'italic:width字体宽度(如压缩或扩展)'normal, 'condensed:underline下划线样式nil(无), t(实线):foreground前景色(文本颜色)"#FFFFFF", "red":background背景色"#333333":inherit继承其他 FACE 的属性'fixed-pitch这里我打算使用 Source Code Pro 字体,可以在init-ui.el 中这么设置(set-face-attribute 'default nil :family "Source Code Pro" :height 120)到此为止我们已经给Emacs做了基本的美化,日常使用也不那么碍眼了。
2025年03月17日
5 阅读
0 评论
0 点赞
2025-03-16
Emacs 折腾日记(十七)——文本属性
我们在上一篇中介绍了如何对文件中的文本进行操作,本篇主要来介绍关于文本的属性。是的,文本也有属性。这里的文本属性有点类似于Word中的文字属性,文本中对应的字符只是文本属性的一种,它还包括文本大小、字体、颜色等等内容。Emacs中的文本也是类似的。于符号的属性类似,文本的属性也是由键值对构成。名 字和值都可以是一个 lisp 对象,但是通常名字都是一个符号,这样可以用这个 符号来查找相应的属性值。复制文本通常都会复制相应的字符的文本属性,但是 也可以用相应的函数只复制文本字符串,比如 substring-no-properties、 insert-buffer-substring-no-properties、buffer-substring-no-properties。产生一个带属性的字符串可以用 propertize 函数。(propertize "abc" 'face 'bold) ;; ⇒ #("abc" 0 3 (face bold))这里我们使用 face 来设置它的字体为粗体。或者我们可以使用C-x C-f 任意的打开或者创建一个文本文件,在文件中输入(insert (propertize "abc" 'face 'bold))我们可以看到它会在当前光标后面插入一个粗体的 abc 字符串。需要注意的是,我们在*scratch* buffer 中是无法看到这个效果的。因为*scratch* buffer 中开启了font-lock-mode。正如它的名字表示的那样,它锁定了字体,它里面的字体属性都是实时计算出来的。在插入文本之后它的属性很快就被修改了。因为*scratch* buffer 本质上还是一个elisp的编程环境,它里面有关于elisp的语法高亮、自动对齐等特性。它会自动的修改输入的文本属性。我们可以使用 (font-lock-mode - 1) 来关闭这个mode,然后执行上述代码就可以看到具体的效果了。虽然文本属性的名字可以是任意的,但是一些名字是有特殊含义的。属性名含义category值必须是一个符号,这个符号的属性将作为这个字符的属性face控制文本的字体和颜色font-lock-face和 face 相似,可以作为 font-lock-mode 中静态文本的 facemouse-face当鼠标停在文本上时的文本 facefontified记录是否使用 font lock 标记了 facedisplay改变文本的显示方式,比如高、低、长短、宽窄,或者用图片代替help-echo鼠标停在文本上时显示的文字keymap光标或者鼠标在文本上时使用的按键映射local-map和 keymap 类似,通常只使用 keymapsyntax-table字符的语法表read-only不能修改文本,通过 stickness 来选择可插入的位置invisible不显示在屏幕上intangible把文本作为一个整体,光标不能进入field一个特殊标记,有相应的函数可以操作带这个标记的文本cursor(不知道具体用途)pointer修改鼠标停在文本上时的图像line-spacing新的一行的距离line-height本行的高度modification-hooks修改这个字符时调用的函数insert-in-front-hooks与 modification-hooks 相似,在字符前插入调用的函数insert-behind-hooks与 modification-hooks 相似,在字符后插入调用的函数point-entered当光标进入时调用的函数point-left当光标离开时调用的函数composition将多个字符显示为一个字形这些东西我觉得也不需要记住,在需要的时候查查文档就好了。但是我参考的教程把它列出来了,那么我也在这里列出来把。由于字符串和缓冲区都可以有文本属性(如果没有特别指定文本对象的属性,那么默认使用缓冲区定义的文本属性),所以下面的函数通常不提供特定参数就是检 查当前缓冲区的文本属性,如果提供文本对象,则是操作对应的文本属性。查看文本属性查看文本对象在某处的文本属性可以用 get-text-property 函数。(setq foo (propertize "abc" 'face 'bold)) ;; ⇒ #("abc" 0 3 (face bold)) (get-text-property 0 'face foo) ;; ⇒ bold这里有两个问题需要注意一下,首先我们使用 propertize 为abc设置了文本属性face的值为bold,也就是将字符串设置为粗体。但是其中的0 和 3 代表什么呢?这里的0和3代表的是采用这个属性的字符在字符串中的范围,上面的代码中,整个abc字符串都采用整个属性,所以它的范围是[0, 3) 这个区间。要验证这一点我们可以使用下列代码(setq foo (concat "abc" (propertize "cde" 'face 'bold))) ;; ⇒ #("abccde" 3 6 (face bold)) (insert foo) 我们插入foo发现,它会插入 "abccde" 这么几个字符串,但是只有 "cde" 三个是加粗的根据这个提示,很明显的,get-text-property 中输入的0代表的就是“abc”字符串第0个,也就是字符a的属性。get-char-property 和 get-text-property 相似,但是它是先查找 overlay 的 文本属性。overlay 是缓冲区文字在屏幕上的显示方式,它属于某个缓冲区,具 有起点和终点,也具有文本属性,可以修改缓冲区对应区域上文本的显示方式。get-text-property 是查找某个属性的值,用 text-properties-at 可以得到某 个位置上文本的所有属性。修改文本属性put-text-property 可以给文本对象添加一个属性。它也是需要传入一个范围值,例如我们在前面foo的基础上使用以下代码(put-text-property 0 3 'face 'italic foo)我们再针对 foo 执行插入操作,此时会发现 abc 这个子串变成斜体了。和 put-text-property 类似,add-text-properties 可以给文本对象添加一系列的属性。和 add-text-properties 不同,可以用 set-text-properties 直接设置文本属性列表。你可以用 (set-text-properties start end nil) 来除去 某个区间上的文本属性。也可以用 remove-text-properties 和 remove-list-of-text-properties 来除去某个区域的指定文本属性。这两个函数的属性列表参数只有名字起作用,值是被忽略的。以下的例子还是建立在之前的 foo 变量之上,此时它的值为 #("baccde" 0 3 (face italic) 3 6 (face bold))。也就是前三个字符是斜体,后三个是加粗(set-text-properties 0 1 nil foo) ;; 取消了 a 字符的文本属性 foo ;; ⇒ #("abccde" 1 3 (face italic) 3 6 (face bold)) (remove-text-properties 2 4 '(face nil) foo) foo ;; ⇒ #("abccde" 1 2 (face italic) 4 6 (face bold)) (remove-list-of-text-properties 4 6 '(face nil) foo) foo ;; ⇒ #("abccde" 1 2 (face italic))查找文本属性文本属性通常都是连成一个区域的,所以查找文本属性的函数是查找属性变化的 位置。这些函数一般都不作移动,只是返回查找到的位置。使用这些函数时最好 使用 LIMIT 参数,这样可以提高效率,因为有时一个属性直到缓冲区末尾也没 有变化,在这些文本中可能就是多余的。next-property-change 查找从当前位置起任意一个文本属性发生改变的位置。 next-single-property-change 查找指定的一个文本属性改变的位置。 next-char-property-change 把 overlay 的文本属性考虑在内查找属性发生改 变的位置。next-single-property-change 类似的查找指定的一个考虑 overlay 后文本属性改变的位置。这四个函数都对应有 previous- 开头的函数,用于查找当前位置之前文本属性改变的位置(setq foo (concat "abc" (propertize "edf" 'face 'bold) (propertize "hij" 'pointer 'hand))) ;; ⇒ #("abcdefhji" 3 6 (face italic) 6 9 (face bold)) (next-property-change 1 foo) ;; ⇒ 3 (next-single-property-change 1 'pointer foo) ;; ⇒ 6text-property-any 查找区域内第一个指定属性值为给定值的字符位置。 text-property-not-all 和它相反,查找区域内第一个指定属性值不是给定值的 字符位置。(text-property-any 0 9 'face 'bold foo) ;; ⇒ 3 (text-property-not-all 3 9 'face 'bold foo) ;; ⇒ 6
2025年03月16日
5 阅读
0 评论
0 点赞
1
2
3
...
32