Why Tokyo？ 我想亲眼看看这个世界，未来应该还会去很多除了中国以外的地方，日本东京是第一站。 从小所看的日本文化作品 - 动漫｜电视剧｜电影 中国、美国和欧洲的我都有看，日本占比较多罢了。 小学时候， TVB 周一至周五下午的《放学 ICU》、各种粤语配音的动漫、周六上午的蒙面超人，周日的超级战队，基本有空就看；星空卫视和华娱卫视各个时间段的中配动漫也是有啥看啥。 初高中，疯狂网上冲浪，看土豆网优酷网各种影视网站，在上面追番。从那时候看番到现在都有个习惯，每个季度出的新番，我都会先看第一集，通过看第一集判断自己是否会感兴趣，是否选择继续追番。 基本上什么题材的文化作品我都看过。 而日本的动漫及影视的取材多来自于现实场景，想实际看看那些地方。 全文放在 Notion 那，该文有太多的照片，我懒的再将照片存储在该博客的服务器里了。
杂谈杂谈，随便写写记录。 先上一个图！！！这个时代信息量真是太多了，每个发言者都有自己的视角，各位见仁见智啦！ 注意，这里的汕头，我可能主要指龙湖金平这两个区域，因为我工作地点在龙湖，住也在龙湖，金平龙湖大部分地方骑单车都能骑过去。可能我所看的汕头，主要也是这两个地方给我展现的。 至于汕头其它地区，澄海，潮阳，濠江，都是跟其它人一起开车去探索的，都是为了恰吃的；潮南区也就只是去过一次两英生态公园休闲下；南澳坐公交去，也就呆了半天就走了。 额外潮州市离龙湖蛮近，开车去过牌坊街和广济楼。 谈谈工作 0820 发，是因为此时我已经离职了！很多事也就能讲讲了。 汕头真法外之地！在这打工，有太多默认的，不合理的要求！ 真就除了公务员，其它绝大数工作都是单休的，很少有企业是双休的，顶多大小周。 节假日有时还需员工返回来加班。例如，过年放七天，它都要你回来上两天。不过我直接不理，去旅行了。 这里的企业加班大都是没有加班费用（我这个企业也没有），只能将加班的时长用于顶替正常上下班的时长，即周一至周五，某一天，你请假，只要额外加班了8小时，那你请的那一天假的工资照领。 我呆的这个企业是更看重加班时长的，加班时长在绩效考核评定占蛮大一部分。不过我每次都自评 C，次次也拿的 C，我打工，给多少钱做多少事，不求有功，但求无过，想要我积极一点，那就是另外的价钱。主要拿了最高的 A，也不会加多少工资，因为我的工资基数太低了 - 8000，而且它是算 20% 的工资，8000 的 20% 再乘以绩效系数 1.6，就 2560，就比我原工资多 560。正常工作时长，加上加班时长，算算时薪，低的无话可说，这我会拼？我佛的很！开心消消乐再充 20！！！ 我加班时长不多，但能按时按质完成任务，我 leader 就也奈何不了我，因为我很清楚，他绝不会主动将我辞退的，他不想给 N + 1 的。应该这么说，汕头的绝大多数企业就不会主动送人 N + 1，只会试图通过言语恶心员工，或者安排不合理的工作给员工，让员工主动有离职的想法。不过我都 hold 住了那些工作，这个企业的技术需求不难（之后我会出个视频说说），主要也是业务多，至于领导们的恶心发言（年轻人就是要多吃苦，多奉献，多加班，少谈钱），画大饼什么的。这些我都左耳进，右耳出，直接忽略，算是我这两年打工锻炼出来的能力，除了给钱能激励下我这打工人，至于说对工作更上进点，对我来说很难的。 N + 1 赔偿是指有劳动合同法第四十条规定的情形之一的，用人单位除了正常支付经济补偿金后，额外支付劳动者一个月工资，可以解除劳动合同。 N 就是根据员工在用人单位的工作年限，每满一年，补偿1个月的工资，不满半年的，算半个月，半年以上的，算1个月。 领导们老说员工要对工作有责任心，那谁对我们员工身心健康有责任心？ 我就这么说吧，身心健康真的很重要，要不然去医院看个病，得场病，有可能像个关不上的水龙头，那个钱似水一般哗哗地往外流。就真的，讲实话，有些疾病，医保也报销不了多少，得一场大病，可真就是极速返贫了。看我博客的大都和我年龄相仿，或许都会觉得自己身体很不错，我只能说，别太自信了，现代人可都有点亚健康在，保险点，可以的话，还是每年花个 3000 左右（南方医院的价格）做个全身检查，看看身体哪里会有问题，免得有一天就忽然爆出来，那到时，就可真叫做难顶。我个人认为每年花 3000 左右的身体检查费用还是挺值。 多喝水！心态好！目前个人努力的回报太低了，不值得太拼的，当然也不是说不去从外部汲取知识什么的，也不敢去做什么事情。只是我认为在目前这个社会发展阶段，在保障自己身心健康的前提下，再积极去搞钱比较合适。年轻人多搞钱还是要的。 所以我也算是蛮放肆的，不想加的班那就走吧，leader 说了我，说了我的那几天，我就安分点咯，之后不想加班，照走就走，就这一过程，我和 leader 如此反复多次。他说了我一年多，但就是不辞退我，说实话，有点失望，最终还是我主动离职，走了离职流程。我个人还真蛮想拿 N + 1 的。(T_T) 这加班现状我觉得汕头很难改变，至少五六年内都很难，一方面是我国目前的经济大家都能看得到，另一方面，讲道理，部分潮商属实很会压榨！ 就算想投诉企业，呃，潮汕耶！人际关系可太复杂了！某些力量也太强大了！行政效率也拉胯的不行，光是各个城市都会有的大学生就业补贴，它给我拖了整整半年才汇款到我账户上。 讲讲汕头非公务员的五险一金。 汕头公务员的五险一金很厉害，不是很了解，不展开，只是道听途说罢了。因为接触到的汕头公务员都感觉很谨慎，无法与这些人交换有效信息。 非公务员的五险都蛮一般的，不过有些企业还啥保障都没有，员工真主打一个奉献。 汕头公积金，大部分企业都是按照汕头最低工资标准 1730 缴纳的，直到今年7月，貌似汕头强制要求企业要按照员工工资来缴纳，它才给我弄到 8000 的标准，但缴存比例是从 8 改为 6 了。 不过也还是很少。 讲讲关系户。 为什么讲，其实每个地方都会有啦。但因为在潮汕地区，那就更甚。 大部分年轻人除非自己有着货真价实的实力，才有机会获得一些资源，而有关系的人就很轻而易举地获得了。 讲讲程序员这技术岗位的薪资，说我的，不过我了解到汕头地区大都是这样。 我这企业，基本上若无岗位变动，就是每年加薪 500 罢了，有岗位变动 level up，貌似是加 1000 至 2000 左右，感觉变动蛮小。汕头地区做技术岗位的最高薪水貌似在 20000 至 30000 这区间。 不过每个地区的薪水都是综合多方面给的，汕头给的这钱是有考虑到在这工作的打工人，普遍都有自己的住所，无需在租房上有额外开销，即使我在这没有住所，公司也会有提供，个人仅需支付每月 60 ，水电费加起来也很低。 在这工作一年，我有点理解为什么中间年龄阶段的年轻人大都外流或者在本地坚持考公了。因为在这打工，待遇并非会有多好，且工作一段时间，基本上都清楚自己这份职业的发展尽头，生活感觉也一眼望到头，很难说有什么其它机会。 谈谈教育 学生群体特别多，竞争挺卷。 且汕头也真是当双减政策不存在，真法外之地！ 我偶尔周六加班，上下班，能看到一堆学生成群的上学或放学。 高三群体，暑假很多 8 月初就开学了，寒假，我感觉这些学生也没有放多少假。 这里的汕头图书馆我逛了，大都是教辅类的，很少其它知识的书籍。 全都是在做作业。 然后这里教育的花费貌似不低，特别是有些私立学校，我没有太多了解，全是听别人说的，不展开说了。 谈谈医疗 哎，红包红包红包。 太服了！ 此次国家医疗反腐，不知道有没有人关注，潮汕地区好多医院都有人被揪出来了。我这里说潮汕地区，就不止是汕头这样。 我在这没有看过病，但我和这里的人打交道，年龄相仿的年轻人都吐槽过陪父母看病，有没有给红包，医生那个区别对待就会非常明显。 也不止医疗这方面是这样，这里的店铺有些消防来检查，店铺老板也要意思意思。 听这些人吐槽，我作为一个旁观者，也真是被无语到，这。 就很讲那种送礼，即使大家明知道这行为不好，但是又不得不这样，否则自己的事情，别人就不怎么上心。 就有些医者不首先讲自己的医德，会先跟你讲讲你给的红包，ta 满不满意。服了。 就妥妥收割。而且感觉汕头老人还真是不少，就一个城市人口的比例来说。 谈谈旅游业与美食 汕头市是疫情后兴起的一个旅游城市。 汕头本身就有一定的文化底蕴，值得探索，且美食很多，能吸引游客前去。 汕头目前的旅游业重点放在「 打卡 」这一关键词上，它翻新了大量旧时的建筑物，开设一些文化宣传点，去给游客展现潮汕的文化，让游客前去打卡，其实这 Ok 的。但有些不当人的奸商属实想在汕头转型旅游城市的这一阶段搞搞事，卖些劣质的纪念品给游客，旅游景点的食物都设置些不合理的价格。真本地人看了，都要说声真能宰！ 其实汕头要发展旅游业的另一关键就是要「 诚实买卖 」！ 有一些店，就只想着做一次性买卖。 说个很离谱的事，无语死我了。 我是个假的潮汕人（祖籍普宁），我会听但不怎么会说。 有些店它是没有菜单的，你进去，要招呼服务员点单，然后当时我说普通话，我说来个普通的牛肉粿条，服务员用普通话回我，大份还是小份，我说一个人，小份可以吗？服务员说可以，小份一份 25。上菜后，感觉，小份肉好少，心想 25 好亏。 然后这家店来了一些人用潮汕话点单，这些人也人手点了一份牛肉粿条，服务员说账时，用潮汕话回应 20，20，20，给 60。 尼玛，这，说普通话自动加 5 块是吧。肉还那么少。我那时心想，这不如我直接去吃麦当劳 26 块四件套得了。 就吃的感觉是真材实料的，但是买卖是这样做的吗？不能这样的，不得人心。 我偶尔逛小公园，能听到很多游客吐槽吃的也卖太贵了，并非一个正常旅游景点该有的价格。 大都是龙湖金平的一些店较坑。其它地方也有很多不错的美食店。 汕头的各种粿，湿炒牛肉粿条，各种粿条汤，清汤牛肉火锅，粥底牛肉火锅，我在这住，常吃的就是这些。我个人觉得来汕头旅游，这些都值得一试。出名的生腌我没吃惯。汕头有很多值得吃的，看个人口味去旅游探索吧。 汕头规划下道路吧！ 这很重要，要想富，请先修路。 路，我在广深骑单车，感觉不尽兴，但是安全。 在汕头的路，不是在市区，东海岸那或者新溪附近，我骑的很开心很尽兴，注意点，没出过事。 但在龙湖金平一些地方，即使我已经很小心了，我都有可能出意外，就这里有些骑电动车的人，骑电动车骑出了摩托车的感觉，一个个风驰电掣。不清楚有些是不是还改装过，速度快的不对劲。 我在汕头一年，已经在身上留下不少的伤疤，这一年感觉摔的次数加起来比我人生前 22 年骑单车摔的都多。 就汕头感觉是一个慢节奏的城市，但是上下班阶段，那就会变成一个极快节奏的城市，一个个开车的，骑电动的，都感觉好强，这里车祸发生频率真不低。 个人认为，来汕头这开车绝对锻炼车技！！！ 谈谈传统习俗 好多呀！ 或者应该说是庙会活动好多呀！ 但我也只有过年期间，在普宁实际见过英歌舞，貌似汕头也有。实话实说，英歌舞就我个人而言，我当时看的时候内心没什么感觉，因为我不清楚这个故事背景。就问问，大家毕业后，还都记得水浒传的故事吗？我不记得，不过那个英歌舞的气势还是挺足的。的确很适合过年时来表演。 然后过完年回公司上班，一些水群，大家就都会分享 ta 们过年时拍的视频，看那些视频，让我感觉好夸张，今年潮汕地区的过年真热闹无比。 可以上网搜搜「 2023 潮阳迎老爷 」，那阵势，太夸张了！真报复性过年！ 至于原本想说的普宁与汕头的不同，我这段时间和一些人聊过，感觉更是因为这两地产业发展的不同以及所处地域不同而形成的，感觉展开不了说什么。我想说的不同也是一些饮食上，一些习俗上的，但归根到底，也还是潮汕文化。 End 总之，从我个人来看，这之后，这座城市并不再适合我继续呆下去了。
目标驱动 - 逐步逐步做成这个 TUI 程序 起因 前几天逛 0xFFFF 社区，看到一个帖子 - 为什么我折腾这些小众技术？，主要有个回复让我脑子有点想法 -- 「 现在 GitHub 上出现了这么多活跃的 TUI 项目（支持显示全彩全分辨率图片甚至播放视频的 Terminal、IDE、漂亮的 TUI 文件管理器、音乐播放器等等），反倒是计算机届的文艺复兴了。 」 熟悉我开发习惯的就知道，我挺喜欢在终端 Terminal 下完成一些工作，就心想要不我也开发个小小 TUI 程序用用。 写该文的时候，Vim 的创作者 Brarm Moolenaar 去世了，这里 RIP 下，毕竟自大二起就使用着 Vim，这个东西对我开发真是非常便捷，感谢 Brarm Moolenaar 的贡献。 就想着可以做个简单天气查询的 TUI 程序。 因为我目前办公的位置离窗边蛮远，且现在天气热，太阳晒，全都拉上窗帘，我就看不见窗外啥天气了。 某日中午出去吃饭，居然下太阳雨，直接淋成「 落汤鸡 」。 同样的悲催事可不能再遭遇一次，现在离开座位，要出办公楼前，必看看天气预报，决定是否带个雨伞。 一般我看天气的步骤如下： 拿起手机解锁 滑到最左边的天气界面 那我想更懒惰点，反正每一天下来，我对着电脑比对着手机更多，且我每台电脑的终端我都不会关掉的，常驻后台。 不如做个 TUI 程序，这样我想查看天气的时候，步骤就变成了： 快捷键唤起终端 输入命令，按下回车，就可看到天气信息 且这样我每天就可以少拿起手机了。因为怎么说，我有时拿起手机看个天气，有可能不知不觉的刷了「 亿 」会儿手机，那放下手机，回到电脑前继续做些事情，脑子又可能会有点小卡壳，不知道自己刚才在干嘛（如果在敲代码，又可能会忘了某一个逻辑是要怎么做的，要花些时间回忆下）。 不过其实这影响也不大，只是我想着如果把工作任务早点做完，那我摸鱼就摸的更心安理得，好像更不错耶。（￣︶￣）↗ OK 清楚目标了，那就开始吧! 思考下吧！ 理解业务 这个东西的展现形态为一个 Terminal 下即可运行的程序。 运行这个程序，会显示我所在地的天气情况。 问题来了 - 我想要看到什么样的天气信息呢？ 明确下要展示的天气信息： 目前天气情况，即运行该程序那时刻的天气是咋样的，我要知道 未来天气情况，即未来几个小时或几天内的天气情况，我想知道 确定所用技术栈 我作为一个开发者，我掌握着什么样的技术栈，做这个东西用什么技术比较合适，这些要确定下来。 如果是在 Terminal 下运行的程序，那用很多语言都可以完成的，只不过我最近常用一门叫 Rust 的语言。 下面是我问 ChatGPT，用 Rust 做命令行程序的优势是什么，它给出的回答。见仁见智啦！ Rust 作为一种现代的系统级编程语言，拥有许多优势，使其成为开发命令行程序的理想选择。以下是 Rust 在开发命令行程序方面的一些优势： 1. 性能：Rust 是一门性能优秀的语言，它的执行速度接近于 C 和 C++，这使得命令行程序在处理大规模数据和复杂计算时表现出色。 2. 安全性：Rust 的所有权和借用系统能够在编译时检查内存安全性和数据竞争，有效地避免了许多常见的运行时错误和安全漏洞，使得命令行程序更加健壮和安全。 3. 并发支持：Rust 内置了对并发编程的支持，可以轻松地编写多线程的命令行程序，充分利用现代多核处理器的性能优势。 4. 轻量级：Rust 的标准库非常精简，这使得生成的可执行文件相对较小，节省了系统资源，并且适合用于开发轻量级的命令行工具。 5. 跨平台支持：Rust 提供了广泛的平台支持，可以轻松地在不同操作系统上运行编译好的命令行程序，这为开发跨平台工具提供了便利。 6. 依赖管理：Rust 的包管理器 Cargo 提供了简单易用的依赖管理功能，可以方便地引入外部库和工具，加速开发过程。 7. 社区和生态系统：Rust 社区非常活跃，并且拥有丰富的生态系统，你可以轻松地找到许多现成的库和工具，以及丰富的文档和教程，帮助你更高效地开发命令行程序。 综上所述，Rust 的性能、安全性、并发支持和跨平台能力使其成为开发命令行程序的优秀选择。无论是开发简单的工具还是复杂的命令行应用，Rust 都能够提供高效、安全和可靠的解决方案。 接下来就是关键的环节。 把业务需求翻译成技术需求 开发前准备相关资源 我这里的数据是有关天气的，那我就需要一个天气数据源。 这个网上选择很多，我这里用的是心知天气，调用它提供的 API 返回的数据格式为 JSON 类型，便于处理。 梳理出技术需求吧！ 调用心知天气的 API，获取天气数据；Rust 有提供 reqwest 库，可以用来发起网络请求； 能处理返回来的 JSON 数据，Rust 有提供 serde 库，可以用来处理 JSON 数据； 制作成一个命令行程序，要考虑命令行程序的一些特点，比如：命令行参数的处理，以及提供 --help 的选项让使用者清楚该程序的使用方法； 动起手来吧！ 指挥下 ChatGPT 帮我写些代码 (〃∀〃) 我好懒～～～ do { 把我的技术需求输入给 ChatGPT; 它负责具体实现; 我 Code Review 它给我展示的代码; if (我满意了) { break; } } while(我不满意) 最喜欢 Code Review 这件事情。现在平日开发，自己提升最多的一个环节就是在 Code Review 这里。 最终呈现出来的代码就是这个仓库 - weather_cli 代码很简单的，非常直白。 End 文章字数不多，就是叙述我完成这个目标（做一个天气查询的 TUI 程序）的一个过程。我给自己制定的目标去驱动我去完成这个事情，这个过程中，我也算学到了一些新知识（和 ChatGPT 对话学到的）。 再说说我现在开发的一些感受，软件开发需要你是怎么样的? 假如有个任务给你了，你应该： 理解业务，这件事是什么？ 沟通上下游，现在基本都会有的。目前这时代绝非一个靠单打独斗就能做成一些事情的时代啦。 做成这件事，是不是会要对某些编程语言和技术栈熟悉，要清楚是什么，如果有不会的，那就去学得了。要相信自己的学习能力。 在理解业务的前提下，去把业务需求翻译成技术需求，最好梳理出的技术需求是层层递进的，这样自己慢慢实现了一个个技术需求，就能得到一个反馈，清楚自己离那个最终待解决的目标还有多远。 有一点点的算法能力，有些问题的解决额外需要数学知识。本科生推荐必学「 高等数学 && 线性代数 && 数论 && 初高中三角函数知识也可别忘了（如果做 Qt 绘图会用到）」。 注意我这里说的是「 必 」哦，我个人啊，我个人啊，就建议是必学。疑惑的话，我只能说现实就是残酷呀，目前感觉绝大多数程序员的差异化就在这些部份上，不认同的，也请当我乱说。毕竟就我来说啊，数学这部分外的一些什么新框架，什么新技术啦，我都是上 YouTube 上看印度老哥讲解学会的（dddd），或者直接看官方技术文档就行啦。 多喝水！心态好！坐久了就站起来动动！尽可能挖掘出做这件事的乐趣。很难说，就一种开发的感觉，进入「 心流状态 」，这说法合适吗？呀，真不好描述。文笔烂。 总之，Have Fun！ Just For Fun！
After10 - 把悲傷留在山上 有句歌词很棒「 请不要将这刻痛苦判下重罪 」 这几年，这个阶段，我完全认为有些问题根本不是我们个人的问题，而是「 其它 」导致的，未来的很多事情还是不好说的。 我谜语人，谨言慎行 doge。 保持内心平和吧！该干嘛干嘛。 作词：After10 作曲：After10 编曲：关礼琛、After10 年少你试过快乐透 每日像未活够 何以霎眼 你已累透 半夜 尚未可休 当天筑起 一些小岛 浸没了 消散以后 几多青春 几多理想 已经弃守 谁说中了 你已大透 发白日梦亦够 时间到了 叫你内疚 已尽力便好收 一艘方舟 轻轻飘走 心血只得到扣留 多么不甘 开不了口 化不了 但困在眼眸 曾经艰苦到底 一对手 哪管到审判前 这时候 花光了气力来奋斗 怎么去放手 曾经只想放低 高飞远走 作一个好借口 也许会变得富有 唯一颗初心 拒绝接受 回到这里 再进或退 也像活在梦里 回到过去 纵已逝去 昨日历炼不衰 伤心的追 开心的追 放下这一切顾虑 怎么的追 请不要将这刻痛苦判下重罪 曾经艰苦到底 一对手 哪管到审判前 这时候 花光了气力来奋斗 怎么去放手 曾经只想放低 高飞远走 作一个好借口 也许会变得富有 唯一颗初心 拒绝撤走 落寞时在街上 即使再输一仗 难声张 悲伤留在山上 只需放声高唱 回声响 一天你在那方 照着你的故事再闯 曾经艰苦到底 一对手 叫醒昨天理想 这时候 跟一个价值来奋斗 捉紧到永久 仍憧憬一对手 打出缺口 抱紧你相信的 挽手去踏破尽头 寻找这一代 自由
写于 2023 年 3 月 24 日。 以剔除 Qt 的 QThread 为目标而诞生的项目。设计的线程基类能搭配类似 Qt 中的信号与槽机制来使用，且利用现代 C++ 提供的互斥量和条件变量，让函数能够同步或异步运行，这样派生类（某业务工作线程）能持有定时器（线程）来做些定时任务（异步）。 cxx_thread 起因 和 记一次「 包轮子 」的经历 的起因一样，只不过这个项目是为了剔除 Qt 的 QThread 而诞生的。 拆分目标 目标 -> 替换掉 QThread 分析下 QThread 这里主要看 QThread 的使用方式，看它如何被使用的。 第一种示例代码 class Worker : public QObject { Q_OBJECT public slots: void doWork(const QString &parameter) { QString result; /* ... here is the expensive or blocking operation ... */ emit resultReady(result); } signals: void resultReady(const QString &result); }; class Controller : public QObject { Q_OBJECT QThread workerThread; public: Controller() { Worker *worker = new Worker; worker->moveToThread(&workerThread); connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater); connect(this, &Controller::operate, worker, &Worker::doWork); connect(worker, &Worker::resultReady, this, &Controller::handleResults); workerThread.start(); } ~Controller() { workerThread.quit(); workerThread.wait(); } public slots: void handleResults(const QString &); signals: void operate(const QString &); }; 第一种是在一个继承自 QObject 的子类中，将 QThread 作为成员变量 workerThread，用 moveToThread() 函数将该类所创建的对象移动到 workerThread 中，connect() 函数将信号与槽连接起来，在槽函数中编写需要在线程中处理的逻辑代码。然后再调用 QThread 的 start() 函数来启动 workerThread，这样槽函数就能在一个新的线程中运行。 第二种示例代码 class WorkerThread : public QThread { Q_OBJECT void run() override { QString result; /* ... here is the expensive or blocking operation ... */ emit resultReady(result); } signals: void resultReady(const QString &s); }; void MyObject::startWorkInAThread() { WorkerThread *workerThread = new WorkerThread(this); connect(workerThread, &WorkerThread::resultReady, this, &MyObject::handleResults); connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater); workerThread->start(); } 第二种是写子类继承自 QThread，然后重写 run() 函数，这样就可以在 run() 函数中写线程的逻辑。 需要剔除 QThread 的代码中使用方式为第一种。 Signals and Slots Across Threads Qt 有提供一些信号与槽的连接选项 Qt::ConnectionType，这些选项能控制信号与槽的连接方式，比如是否是异步的，是否是阻塞的等等。 简单梳理需求 信号与槽机制以及管理信号的消息队列（要有生产者和消费者） 槽函数 Async 异步执行或 Sync 同步执行 线程能够持续运行，直至被销毁 实现需求 搜索资料并理解学习 抱着网上可能已经有现成解决方案的心态去互联网上搜索 /doge sigslot -- 提供了如何处理参数传递的思路 std::static_pointer_cast -- 转换类型的方法 Concurrency support library / CPP-Concurrency-In-Action-2ed-2019 -- 有关 C++ 并行（多线程）开发的知识，这里主要用到 std::thread、std::mutex 和 std::condition_variable 。 信号与槽机制 看看需要剔除 QThread 的那份代码，业务线程自创建后便持续存在，等待着主线程发送一个指令（即调用业务线程的公有成员函数），当业务线程接受到主线程某一个指令后，业务线程能根据该指令（被调用的公有函数内部再调用 emit 发送信号函数）去处理相对应的业务逻辑（调用对应的槽函数）。 这样梳理下，即需要有一张关系映射表去将主线程要调用的指令函数和对应的业务逻辑关联起来，即业务线程内部有一张将信号与槽函数关联起来的表。 那时立马想到的是维护一个 std::map 数据结构，即 std::map<Signal, Slot>，其中 Signal 为整型，Slot 为对应的槽函数，这样就能将一个信号与一个槽函数对应起来，但是需要考虑到函数参数传递的问题，以及可能一个信号会对应到多个槽函数的情况，会要把 Slot 设置成一个容器去包含多个槽函数，又要考虑到函数执行顺序的问题，这样就会变得很复杂。 那就换个思路，既然是要将信号与槽函数关联起来，先得有信号 ID 和槽函数。 那么继承基类 ThreadBase 而派生的业务线程类中必须要有定义信号和槽函数。 这里用 enum 枚举类型定义 Signal 信号，一般如下： enum Signal : int { SayHello_Signal, SayGoodBye_Signal, WillDo_Signal, PlanToDo_Signal, AskAQuestion_Signal, GetAQuestion_Signal, AnswerAQuestion_Signal, GetAAnswer_Signal, WantToSleep_Signal, DestroyTimer_Signal }; 设计一个结构体来存储信号以及需要携带的参数数据，命名为 SignalMsg ，如下： struct SignalMsg { public: SignalMsg(int signal, std::shared_ptr<void> msg) : _signal(signal), _msg(msg) {} int GetSignal() const { return _signal; } std::shared_ptr<void> GetMsg() const { return _msg; } private: int _signal = -1; // -1: destroy thread std::shared_ptr<void> _msg; // data required by the slot function }; 在派生类中根据信号与其对应的槽函数所需参数来编写些公有成员函数，让主线程能够通过调用这些公有成员函数来发送消息（信号与参数数据），示例代码如下： void Human::SendWillDoSignal(const std::string& doWhat) { std::shared_ptr<std::string> msgData = std::make_shared<std::string>(doWhat); std::shared_ptr<SignalMsg> signalMsg = std::make_shared<SignalMsg>( WillDo_Signal, std::static_pointer_cast<void>(msgData)); SendSlotFuncSyncRunMsg(std::move(signalMsg)); } void Human::SendPlanToDoSignal(const Plan& plan) { std::shared_ptr<Plan> msgData = std::make_shared<Plan>(plan); std::shared_ptr<SignalMsg> signalMsg = std::make_shared<SignalMsg>( PlanToDo_Signal, std::static_pointer_cast<void>(msgData)); SendSlotFuncSyncRunMsg(std::move(signalMsg)); } 这里使用 std::static_pointer_cast 将其它类型的智能指针转换成 std::shared_ptr<void> 类型的智能指针，便于后续的参数传递。 而槽函数的就一般会在命名后面加上个 Slot 后缀以区别其它函数，如 SayHelloSlot() 。 在一个名为 UserCustomFunction 函数使用 switch case 判断语句和 enum 枚举类型将信号 Signal 与 Slot 槽函数对应起来。 UserCustomFunction 函数为 ThreadBase 基类的纯虚函数，需要在派生类中实现。 示例代码如下： // Human.h virtual void UserCustomFunction(std::shared_ptr<SignalMsg> signalMsg) override; // Human.cpp void UserCustomFunction(std::shared_ptr<SignalMsg> signalMsg) { switch (signalMsg->GetSignal()) { case SayHello_Signal: { SayHelloSlot(); break; } case SayGoodBye_Signal: { SayGoodByeSlot(); break; } case WillDo_Signal: { std::string doWhat = *(std::static_pointer_cast<std::string>(signalMsg->GetMsg())); WillDoSlot(doWhat); break; } case PlanToDo_Signal: { auto plan = *(std::static_pointer_cast<Plan>(signalMsg->GetMsg())); PlanToDoSlot(plan); break; } ...... ...... ...... } } 该函数接受一个 SignalMsg 类型的参数，即信号以及携带的参数数据。根据 Siganl 信号的值，去调用对应的槽函数。 这里使用了 std::static_pointer_cast 函数将 std::shared_ptr<void> 类型的参数数据转换成对应的类型，再将参数数据传递给槽函数去处理。 而 UserCustomFunction 函数的调用则是在 ThreadBase 基类的 Process 函数中，如下： void ThreadBase::Process() { std::shared_ptr<SignalMsg> signalMsg; while (1) { { // get signal msg } if (signalMsg->GetSignal() == DestroyThread_Signal) break; UserCustomFunction(signalMsg); } } Process 函数为一个 while 循环，用 std::thread 类型创建一个线程时绑定该 Process 函数，这样该业务线程就能持续运行，就解决掉上述需求中的第三点。 OK，这样生产者和消费者就有了。 生产者就是主线程，通过调用业务线程提供的公有成员函数来发送信号与参数数据。 消费者就是业务线程，在 Process 函数获取到消息，传递给 UserCustomFunction 函数，再由 UserCustomFunction 函数调用对应的槽函数。 这样就实现了主线程与业务线程的通信。 之后就需要有一个消息队列维护这些消息，这里使用 std::queue 类型来存储消息 std::shared_ptr<SignalMsg> 。 通用的变量和函数都放在 ThreadBase 基类中，如下： class ThreadBase { ..... ..... ..... protected: /// Build the relationship between the signal and the slot function /// @param[in] signalMsg - message (signal, data required for slot function) virtual void UserCustomFunction(std::shared_ptr<SignalMsg> signalMsg) = 0; private: /// Process the message queue void Process(); private: const int DestroyThread_Signal = -1; private: std::unique_ptr<std::thread> _thread; std::queue<std::shared_ptr<SignalMsg>> _signalMsgQueue; }; Process 函数也演变成如下： void ThreadBase::Process() { std::shared_ptr<SignalMsg> signalMsg; while (1) { { if (_signalMsgQueue.empty()) continue; signalMsg = std::move(_signalMsgQueue.front()); _signalMsgQueue.pop(); } if (signalMsg->GetSignal() == DestroyThread_Signal) break; UserCustomFunction(signalMsg); } } 槽函数异步执行或同步执行 业务线程可以持有定时器（线程）去做定时任务（另一些依附该业务而做的事情）。定时器被业务线程所持有，其创建与销毁都要通过调用业务线程提供的 CreateTimer 和 DestroyTimer 。且定时任务不阻塞主线程，不使用 std::thread 中提供的 detach 方法的情况下，定时任务应异步执行。 同时，有时在主线程给业务线程发送一个指令后，须确保该指令对应的业务逻辑被执行后，主线程才可继续执行。 即须确保包含业务逻辑的槽函数是能同步执行的。 使用 C++ 提供的 std::mutex 互斥量和 std::condition_variable 条件变量来实现同步。 SiganlMsg 结构体中增加一个 bool 类型的 _wait 成员变量，用来标识该消息是否需要同步执行，即是否需要等待槽函数执行完毕后再继续执行。 反之，如果 _wait 为 false ，则表示该消息不需要同步执行，即不需要等待槽函数执行完毕后再继续执行，即槽函数异步执行。 更新后头文件代码如下： struct SignalMsg { public: SignalMsg(int signal, std::shared_ptr<void> msg) : _wait(false), _signal(signal), _msg(msg) {} bool GetWait() const { return _wait; } void SetWait(bool wait) { _wait = wait; } int GetSignal() const { return _signal; } std::shared_ptr<void> GetMsg() const { return _msg; } private: bool _wait = false; // async: false, sync: true (default: false) int _signal = -1; // -1: destroy thread std::shared_ptr<void> _msg; // data required by the slot function }; class ThreadBase { ...... ...... ...... public: ...... /// Send a message to the message queue (async) /// @param[in] signalMsg - message (signal, data required for slot function) void SendSlotFuncAsyncRunMsg(std::shared_ptr<SignalMsg> signalMsg); /// Send a message to the message queue (sync) /// @param[in] signalMsg - message (signal, data required for slot function) void SendSlotFuncSyncRunMsg(std::shared_ptr<SignalMsg> signalMsg); protected: /// Build the relationship between the signal and the slot function /// @param[in] signalMsg - message (signal, data required for slot function) virtual void UserCustomFunction(std::shared_ptr<SignalMsg> signalMsg) = 0; private: /// Send a message to the thread queue (async or sync) /// @param[in] wait - async: false, sync: true /// @param[in] signalMsg - message (signal, data required for slot function) void SendMsg(bool wait, std::shared_ptr<SignalMsg> signalMsg); /// Process the message queue void Process(); private: const int DestroyThread_Signal = -1; private: std::unique_ptr<std::thread> _thread; std::mutex _mutex; std::condition_variable _cv; std::queue<std::shared_ptr<SignalMsg>> _signalMsgQueue; bool _syncProcessed = false; }; 对应的 CPP 文件代码如下： void ThreadBase::SendSlotFuncAsyncRunMsg(std::shared_ptr<SignalMsg> signalMsg) { SendMsg(false, std::move(signalMsg)); } void ThreadBase::SendSlotFuncSyncRunMsg(std::shared_ptr<SignalMsg> signalMsg) { SendMsg(true, std::move(signalMsg)); std::unique_lock<std::mutex> lock(_mutex); try { _cv.wait(lock, [this] { return _syncProcessed; }); } catch (...) { // Ensure _syncProcessed is set to true even if an exception is thrown } } void ThreadBase::SendMsg(bool wait, std::shared_ptr<SignalMsg> signalMsg) { if (!_thread) return; signalMsg->SetWait(wait); // Add the message to the queue std::unique_lock<std::mutex> lock(_mutex); _signalMsgQueue.emplace(std::move(signalMsg)); _cv.notify_one(); if (wait) { _syncProcessed = false; // Wait for the message to be processed by the worker thread synchronously _cv.wait(lock, [this] { return _syncProcessed; }); } } void ThreadBase::Process() { std::shared_ptr<SignalMsg> signalMsg; while (1) { { // Wait for a message to be added to the queue std::unique_lock<std::mutex> lock(_mutex); _cv.wait(lock, [this] { return !_signalMsgQueue.empty(); }); if (_signalMsgQueue.empty()) continue; signalMsg = std::move(_signalMsgQueue.front()); _signalMsgQueue.pop(); } if (signalMsg->GetSignal() == DestroyThread_Signal) break; UserCustomFunction(signalMsg); if (signalMsg->GetWait()) { std::lock_guard<std::mutex> lock(_mutex); _syncProcessed = true; _cv.notify_one(); } } } ThreadBase 基类的公有成员函数 ThreadBase::ThreadBase() : _thread(nullptr) {} ThreadBase::~ThreadBase() { DestroyThread(); } bool ThreadBase::CreateThread() { if (!_thread) { _thread = std::make_unique<std::thread>(&ThreadBase::Process, this); } return true; } void ThreadBase::DestroyThread() { if (!_thread) return; // Send a message to the thread queue to destroy the thread { std::lock_guard<std::mutex> lock(_mutex); _signalMsgQueue.emplace(std::make_shared<SignalMsg>( DestroyThread_Signal, std::shared_ptr<void>(nullptr))); _cv.notify_one(); } // Wait for the thread to be destroyed _thread->join(); _thread = nullptr; } std::thread::id ThreadBase::GetThreadId() { return _thread ? _thread->get_id() : std::thread::id(); } std::thread::id ThreadBase::GetCurrentThreadId() { return std::this_thread::get_id(); } CreateThread 在派生类的构造函数中被调用，用来创建线程。 当派生类创建的对象被销毁时，会调用基类的析构函数，从而调用 DestroyThread 来销毁线程。DestroyThread 会向消息队列中发送一个销毁线程的消息，中止 Process 函数的执行，避免线程被销毁后继续执行，从而导致程序崩溃。同时，DestroyThread 会调用 std::thread::join 函数，等待线程执行完毕，再销毁线程。 提供了 GetThreadId 和 GetCurrentThreadId 两个函数，用来获取线程 ID 或当前线程 ID。 std::thread 是 C++11 中引入的一个多线程库，用于创建和管理线程。在使用 std::thread 时，可以使用 get_id() 函数获取线程的 ID 值。 线程 ID 值是一个唯一的标识符，用于识别不同的线程。在多线程程序中，可以使用线程 ID 值来跟踪和调试线程的执行过程。例如，可以使用线程 ID 值来输出线程的执行顺序，或者在发生死锁或其他错误时跟踪线程的状态。 End 其实这类 ThreadBase 的实现网上一大把，但其实都是根据自己需要解决的业务场景而去设计的，没有一个是万能的 /doge，还是需要根据自己的需求来设计。 自己这样造一个轮子，其实感觉思路并不难。前期就能快速有个雏形。 我用 GoogleTest 做单元测试，TDD 开发，写测试，只有测试是通过的才能说明编写的代码能达到我的设计期望。 有了雏形，之后就是不断重构，改命名，改写法，改数据结构等等，中间花费了大量时间去修改，试错。其间过程很混乱，好在有 GoogleTest 能够快速的让我知道我改的对不对，TDD 很方便。 总的来说，重构的过程还是很有意思的，也是学习的过程。 对于 C++11 提供的 Concurrency support library 以及类型转换 cast 这两个知识点也是熟练了些。
写于 2023 年 3 月 17 日。 重新拾起 C++ 开发已经有 8 个月了，自己主导做的一件事情。 三个看法，一是记住 C++ 能不造轮子，就别造轮子了；二是能做单元测试的东西，就要做单元测试；三是好好利用 ChatGPT 之类的东西来优化代码 -/- doge cxx_crud_file 起因 被分配了一个任务，用现今的 C++ 标准改写一个原先用 Qt 编写的代码。就是要去除掉那些以 Q 开头的代码（QString、QVariant、QSetting 和 QFile 等等）。 重新用 C++ 标准改写的这份代码，扩展性就会更强，当然 Qt 的生态也不错，不过是业务需求如此罢了，不展开具体内容。 开始「 模仿 」 这份 Qt 编写的代码中有自己的文件读写工具类 FileTools（是根据实际业务产生的，即各种规定好的读写要求，怎么读取，怎么写入数据等等）。 用 Qt 构建的 FileTools 类， 其代码长度大概为两千多行左右 只不过这个类是基于 Qt 整个语言生态构造的，现在打算要去除 Qt 罢了。 我一开始，就是简单仿造这个 FileTools 类，函数名大致不变，函数内部的处理逻辑也大致不变，入参和返回值的类型由 Qt 的数据结构改成 C++ 标准下的，例如 QString 用 std::string 代替。 但其实，在修改这个 FileTools 类时，发现这其实算不上是个类，只是把功能函数全当作成员函数罢了，要使用这些函数时，就用这个类创建一个对象，通过这个对象来调用所需的功能函数。 大概是刚从前端转到做 C++ 的开发，我一开始也觉得没什么问题，就是 函数式编程 嘛。 但是 C++ 的一大精髓就是 OOP（面向对象开发），这个类弄得更像面向过程的。 且这个类里有蛮多方法其实可以继承复用的，阅读其中的代码，有点乱。 越开发，越能闻到代码传出的那股 「 坏味道 」。 该换思路来做。 开始「 包轮子 」 包包包，有现成的，优秀的，经过试验和测试的功能函数，我是绝对不会重复造轮子的，我是个很懒的开发者 /doge。 测试驱动开发 Test-Driven Development (TDD) VSCode | CMake | C++ | TDD 整个项目使用 GoogleTest 来做单元测试，开发过程中，先编写测试用例，然后再编写代码，最后再运行测试用例，看看是否通过，不通过，则修改代码，直到通过为止。 记住，测试代码应当与你功能代码的「 体量 」差不多大。 UFile 重新拾起 C++ 开发，看了蛮多 C++ 的书籍（《clean C++》、《Effective C++》等），以及看 cppreference 官网内容，感觉要做的文件读写工作。一些功能函数可以基于 C++ 标准提供的 <filesystem> 文件系统库来做。 UFile #ifndef UFILE_HPP_ #define UFILE_HPP_ #include <iostream> #include <string> #include "Tools.hpp" #if defined(__clang__) || defined(__GNUC__) #define CPP_STANDARD __cplusplus #elif defined(_MSC_VER) #define CPP_STANDARD _MSVC_LANG #endif #if CPP_STANDARD >= 201103L && CPP_STANDARD < 201703L #include <cstring> #include <experimental/filesystem> namespace fs = std::experimental::filesystem; #endif #if CPP_STANDARD >= 201703L #include <filesystem> namespace fs = std::filesystem; #endif class UFile { private: fs::path _p; public: explicit UFile(const std::string &path) : _p(path) {} explicit UFile(const fs::path &path) : _p(path) {} std::string path() { return _p.string(); } }; #endif // UFILE_HPP_ 将 <filesystem> 的 path 类作为私有变量使用； 因为要适配 Linux 和 Windows 平台以及不同的 C++ 标准，加个宏定义来做判断。 这样就是一个基类，接下来对各种类型处理的文件类都继承该基类。 TxtFile #ifndef TXTFILE_HPP_ #define TXTFILE_HPP_ #include <fstream> #include "UFile.hpp" class TxtFile : public UFile { private: std::string _data; public: explicit TxtFile(const std::string& path) : UFile(path) {} explicit TxtFile(const fs::path& path) : UFile(path) {} std::string getData() const { return _data; } void setData(const std::string& data) { _data = data; } bool readData() { std::ifstream file(path()); if (!file) { return false; } _data.assign((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>()); file.close(); return true; } bool writeData() { std::ofstream file(path()); if (!file) { return false; } file << _data; return true; } bool appendWriteData(const std::string& data) { std::ofstream file(path(), std::ios_base::app); if (!file) { return false; } file << data; return true; } }; #endif // TXTFILE_HPP_ 文本文件主要是对字符串的处理，根据需求考虑读取，写入和追加写入这三个功能。 就是调用 <fstream> 这个库中的 std::ifstream 和 std::ofstream 类对文本文件做处理，有个数据流的概念，那三个功能就是对数据流做些处理即可。 IniFile #ifndef INIFILE_HPP_ #define INIFILE_HPP_ #include "UFile.hpp" #include "ini/SimpleIni.h" class IniFile : public UFile { private: CSimpleIniA _ini; public: explicit IniFile(const std::string &path) : UFile(path) {} explicit IniFile(const fs::path &path) : UFile(path) {} ~IniFile() {} bool setup() { _ini.SetUnicode(); SI_Error rc = _ini.LoadFile(path().c_str()); if (rc < 0) return false; return true; } void getFromIni(const std::string &section, const std::string &key, std::string &param, const char *defaultVal) { param = _ini.GetValue(section.c_str(), key.c_str(), defaultVal); } template <typename T> void getFromIni(const std::string &section, const std::string &key, T &param, const T defaultVal) { std::string paramType = typeid(T).name(); std::string tempParam; tempParam = _ini.GetValue(section.c_str(), key.c_str(), std::to_string(defaultVal).c_str()); if (paramType[0] == 'i') param = static_cast<T>(stoi(tempParam)); else if (paramType[0] == 'f') param = static_cast<T>(stof(tempParam)); else if (paramType[0] == 'd') param = static_cast<T>(stod(tempParam)); else if (paramType[0] == 'b') if (tempParam == "false" || tempParam == "0") param = false; else if (tempParam == "true" || tempParam == "1") param = true; } template <typename T> void getFromIni(const std::string &section, const std::string &key, T *param, const T *defaultVal, const int &size) { int index = 0; std::string paramType = typeid(T).name(); if (_ini.GetValue(section.c_str(), key.c_str()) == nullptr) while (index <= size - 1) { param[index] = defaultVal[index]; index++; } else { std::string tempParamArrayStr = _ini.GetValue(section.c_str(), key.c_str()); std::vector<std::string> tempParamArray = split(tempParamArrayStr, " ,\t\n"); if (paramType[0] == 'i') for (int i = 0; i < tempParamArray.size(); ++i) param[index++] = static_cast<T>(stoi(tempParamArray[i])); else if (paramType[0] == 'f') for (int i = 0; i < tempParamArray.size(); ++i) param[index++] = static_cast<T>(stof(tempParamArray[i])); else if (paramType[0] == 'd') for (int i = 0; i < tempParamArray.size(); ++i) param[index++] = static_cast<T>(stod(tempParamArray[i])); while (index <= size - 1) { param[index] = defaultVal[index]; index++; } } } void setToIni(const std::string &section, const std::string &key, const char *fromValue) { _ini.SetValue(section.c_str(), key.c_str(), fromValue); } template <typename T> void setToIni(const std::string &section, const std::string &key, const T fromValue) { std::string valueType = typeid(T).name(); std::string toValue; if (valueType[0] == 'i') toValue = std::to_string(fromValue); else if (valueType[0] == 'f') toValue = std::to_string(fromValue); else if (valueType[0] == 'd') toValue = std::to_string(fromValue); else if (valueType[0] == 'b') if ((bool)fromValue == false) toValue = "false"; else if ((bool)fromValue == true) toValue = "true"; _ini.SetValue(section.c_str(), key.c_str(), toValue.c_str()); } template <typename T> void setToIni(const std::string &section, const std::string &key, const T *fromValueArr, const int &size) { if (size <= 0) return; std::string valueType = typeid(T).name(); std::string toValueArr; if (valueType[0] == 'i') for (int i = 0; i < size; ++i) { toValueArr += std::to_string(fromValueArr[i]); if (i != size - 1) toValueArr += ", "; } else if (valueType[0] == 'f') for (int i = 0; i < size; ++i) { toValueArr += std::to_string(fromValueArr[i]); if (i != size - 1) toValueArr += ", "; } else if (valueType[0] == 'd') for (int i = 0; i < size; ++i) { toValueArr += std::to_string(fromValueArr[i]); if (i != size - 1) toValueArr += ", "; } _ini.SetValue(section.c_str(), key.c_str(), toValueArr.c_str()); } void save() { std::string output; _ini.Save(output); _ini.SaveFile(path().c_str()); } }; #endif // INIFILE_HPP_ *.ini 文件是 Initialization file 的缩写，即为初始化文件，是 Windows 系统配置文件所采用的存储格式，统管 Windows 的各项配置。 用的一开源库 simpleini，再根据实际业务需求封了一层功能函数，能对 ini 文件做读取 getFromIni 和写入 setToIni 操作，用函数重载即可对不同的参数类型做处理。 JsonFile #ifndef JSONFILE_HPP_ #define JSONFILE_HPP_ #include "UFile.hpp" #include "json/json.hpp" using json = nlohmann::json; class JsonFile : public UFile { public: json _data; explicit JsonFile(const std::string &path) : UFile(path) {} explicit JsonFile(const fs::path &path) : UFile(path) {} ~JsonFile() {} bool setup() { std::ifstream file(path()); if (!file) { return false; } file >> _data; file.close(); return true; } void getFromJson(const std::string &key, std::string &param, const std::string &defaultVal) { if (key == "") { param = defaultVal; return; } json temp = _data; std::vector<std::string> keyArr = split(key, "."); for (int i = 0; i < keyArr.size() - 1; ++i) if (temp.contains(keyArr[i])) temp = temp.at(keyArr[i]); if (temp.contains(keyArr[keyArr.size() - 1])) param = temp.at(keyArr[keyArr.size() - 1]); else param = defaultVal; } template <typename T> void getFromJson(const std::string &key, T &param, T defaultVal) { if (key == "") { param = defaultVal; return; } json temp = _data; std::vector<std::string> keyArr = split(key, "."); for (int i = 0; i < keyArr.size() - 1; ++i) if (temp.contains(keyArr[i])) temp = temp.at(keyArr[i]); if (temp.contains(keyArr[keyArr.size() - 1])) param = temp.at(keyArr[keyArr.size() - 1]); else param = defaultVal; } void getFromJson(const std::string &key, std::string *param, const std::string *defaultVal, const int &size) { int index = 0; if (key == "") { for (int i = index; i < size; ++i) param[i] = defaultVal[i]; return; } json temp = _data; std::vector<std::string> keyArr = split(key, "."); for (int i = 0; i < keyArr.size() - 1; ++i) if (temp.contains(keyArr[i])) temp = temp.at(keyArr[i]); if (temp.contains(keyArr[keyArr.size() - 1])) { const json thisKeyArrValue = temp.at(keyArr[keyArr.size() - 1]); for (int i = 0; i < thisKeyArrValue.size(); ++i) param[index++] = thisKeyArrValue[index]; } if (index < size) for (int i = index; i < size; ++i) param[i] = defaultVal[i]; } template <typename T> void getFromJson(const std::string &key, T *param, const T *defaultVal, const int &size) { int index = 0; if (key == "") { for (int i = index; i < size; ++i) param[i] = defaultVal[i]; return; } json temp = _data; std::vector<std::string> keyArr = split(key, "."); for (int i = 0; i < keyArr.size() - 1; ++i) if (temp.contains(keyArr[i])) temp = temp.at(keyArr[i]); if (temp.contains(keyArr[keyArr.size() - 1])) { const json thisKeyArrValue = temp.at(keyArr[keyArr.size() - 1]); for (int i = 0; i < thisKeyArrValue.size(); ++i) param[index++] = thisKeyArrValue[index]; } if (index < size) for (int i = index; i < size; ++i) param[i] = defaultVal[i]; } void save() { std::ofstream file(path()); file << _data; file.flush(); } }; #endif // JSONFILE_HPP_ 也用的一开源库 nlohmann/json，和 IniFile 一样做法啦。 BmpFile #ifndef BMPFILE_HPP_ #define BMPFILE_HPP_ #include "UFile.hpp" #include "bmp/BMP.h" class BmpFile : public UFile { public: explicit BmpFile(const std::string& path) : UFile(path) {} explicit BmpFile(const fs::path& path) : UFile(path) {} ~BmpFile() {} }; #endif // BMPFILE_HPP_ 呃，也用的开源库，是公司的前辈 clone 的，我没找到来源，不过还是日常感谢无私的开源贡献者。 百度百科 - BMP 是英文 Bitmap（位图）的简写，它是 Windows 操作系统中的标准图像文件格式，能够被多种 Windows 应用程序所支持。随着 Windows 操作系统的流行与丰富的 Windows 应用程序的开发，BMP 位图格式理所当然地被广泛应用。这种格式的特点是包含的图像信息较丰富，几乎不进行压缩，但由此导致了它与生俱来的缺点--占用磁盘空间过大。所以，BMP 在单机上比较流行。 这个开源库的接口考虑的很全面了，处理目前的业务够用了，就没做二次封装，不包了，直接用，仅仅是继承于 UFile，便于做路径处理。 BinFile 主要用于二进制文件的处理（读取、写入、追加），和 TxtFile 一样，使用的都是 std::ifstream 和 std::ofstream，打开选项多了个 std::ios::binary，用于指定以二进制方式打开文件，以及需要用到 seekg 来定位文件指针，计算出文件的长度。 #ifndef BINFILE_HPP_ #define BINFILE_HPP_ #include <fstream> #include <vector> #include "UFile.hpp" class BinFile : public UFile { private: std::vector<uint8_t> _data; int _length = 0; public: explicit BinFile(const std::string &path) : UFile(path) {} explicit BinFile(const fs::path &path) : UFile(path) {} void setData(const std::vector<uint8_t> &data) { _data = data; } std::vector<uint8_t> getData() const { return _data; } void setLength(const int &length) { _length = length; } int getLength() const { return _length; } bool readData() { std::ifstream file(path(), std::ios::in | std::ios::binary); if (!file) { return false; } file.seekg(0, std::ios::end); _length = (int)file.tellg(); file.seekg(0, std::ios::beg); _data.resize(_length); file.read((char *)_data.data(), _length); file.close(); return true; } bool writeData() { std::ofstream file(path(), std::ios::out | std::ios::binary); if (!file) { return false; } file.write((const char *)_data.data(), _length); file.close(); return true; } bool appendWriteData(std::vector<uint8_t> data) { std::ofstream file(path(), std::ios::out | std::ios::binary | std::ios::app); if (!file) { return false; } _data.insert(_data.end(), data.begin(), data.end()); _length = (int)_data.size(); file.write((const char *)_data.data(), _length); file.close(); return true; } }; #endif // BINFILE_HPP_ 用来处理以下两种数据： dat .dat 是二进制文件； img 处理图像文件其实也是对二进制的处理； 小造轮子 UString #ifndef USTRING_HPP_ #define USTRING_HPP_ #include <regex> #include <string> #include <vector> class UString { private: std::string _content; public: UString() : _content("") {} explicit UString(const std::string &content) : _content(content) {} ~UString() {} UString args(const std::string &substitution) { std::string upToDateContent = ""; std::string suffix = ""; std::regex percentSign("%([1-9]{1})"); auto content_begin = std::sregex_iterator(_content.begin(), _content.end(), percentSign); auto content_end = std::sregex_iterator(); for (std::sregex_iterator i = content_begin; i != content_end; ++i) { std::smatch match = *i; std::string match_str = match.str(); int index = match_str[1] - '1'; if (index == 0) upToDateContent += match.prefix().str() + substitution; else upToDateContent += match.prefix().str() + "%" + std::to_string(index); suffix = match.suffix(); } upToDateContent += suffix; return UString(upToDateContent); } UString args(const char *substitution) { return args(std::string(substitution)); } template <typename T> UString args(T substitution) { return args(std::to_string(substitution)); } template <class T, class... Args> UString args(T head, Args... rest) { UString result(_content); result = result.args(head); result = result.args(rest...); return result; } operator std::string() { return _content; } const char *c_str() { return _content.c_str(); } friend std::ostream &operator<<(std::ostream &os, const UString &uString) { os << uString._content; return os; } }; #endif // USTRING_HPP_ 其实就是 QString 有一个 arg 的方法（用于字符串的构建）太好用了，软件组舍不得，想保留这个方法。 不过 C++ 标准的 std::string 没有类似的功能函数，那我就浅浅造个小轮子。 造起来不难，小小难点就三个： 正则表达式库 <regex> 的使用，用于字符串的替换； 链式调用，那就函数重载； 传递多个参数，模板接受，然后递归即可，会自动识别对应的同名函数做处理； 要重载一下 << 操作符，否则 std::ostream 输出流识别不了 UString 这个类。 还有一个类型重载，这样就能用 = 操作符，将右值（Ustring 对象）重载为 std::string。 借轮子 Variant 用于替代 Qt 中的 QVariant，处理不同数据类型之间的转换。 前辈在 CSDN 上找的，软件组的大家在实际使用中，根据业务的需求，有做一些修改和 bug 修复。 这个代码太长了，不展示出来，看这里
写于 2023 年 3 月 2 日。 需要重复处理的文件过多了，就别 Ctrl C、Ctrl V 了,写点 Python 最合适了。 代码背景 #pragma once 和 #ifndef 都可用于防止头文件被两次编译。 不过 #pragma once 是依托微软编译器來使用，不支持跨平台。 #ifndef 的写法是 C++ 标准支持的，能够跨平台使用。 因此用 #ifndef 來替换掉原先代码中存在的 #pragma once。 用用 Python 利器 一般处理多文本文件，我就会分成两块： 遍历多文件，这里即 traverse_directory，目录遍历文件的代码可复用； 文件文本处理，这里即 replace_the_marco，文本处理的代码根据需求來做修改； import os def replace_the_marco(cur_file_name: str) -> bool: flag: bool = False basename: str = os.path.basename(cur_file_name) file_name: str = basename.split(".")[0].upper() suffix: str = basename.split(".")[1].upper() macro: str = file_name + "_" + suffix + "_" first_sentence: str = "#ifndef " + macro second_sentence: str = "#define " + macro third_sentence: str = "#endif // " + macro lines: list[str] = [] with open(cur_file_name, mode="r", encoding="utf-8") as file: index: int = 0 # 只找前三行 for line in file: temp_item: str = line.strip() if index < 3: if temp_item == "#pragma once": flag = True index = index + 1 lines.append(line) file.close() if flag: print("\nModify the macro for " + basename + "!\n") with open(cur_file_name, mode="w", encoding="utf-8") as file: for line in lines: if "#pragma once" in line: file.write("%s\n" % first_sentence) file.write("%s\n" % second_sentence) else: file.write("%s" % line) file.write("\n%s\n" % third_sentence) file.close() else: print("The " + basename + " does not need to be modified!\n") return flag def traverse_directory(cur_dir: str): for cur_dir_item in os.listdir(cur_dir): cur_path_name: str = cur_dir + "/" + cur_dir_item if cur_dir_item[-4:] == ".hpp" or cur_dir_item[-2:] == ".h": replace_the_marco(cur_path_name) elif os.path.isdir(cur_path_name): traverse_directory(cur_path_name) if __name__ == "__main__": folder_absolute_path: str = "" traverse_directory(folder_absolute_path) 小坑，遇到一些 Visual Studio 下创建的文件，格式为 ansi，需要转为 utf-8 才可再用上面的脚本处理，分另一个脚本文件处理此过程。这样跑完一个脚本，可以验证一个阶段。 import chardet import codecs import os def ansi_to_utf_8(cur_file_name: str): with open(cur_file_name, "rb") as f: r = f.read() f_charInfo = chardet.detect(r) # 获取文本编码信息 charset = f_charInfo["encoding"] if charset != "utf-8" and charset != "UTF-8-SIG": print(cur_file_name, charset) f = codecs.open(cur_file_name, "r", "ansi") ff = f.read() file_object = codecs.open(cur_file_name, "w", "utf-8") file_object.write(ff) def traverse_directory(cur_dir: str): for cur_dir_item in os.listdir(cur_dir): cur_path_name: str = cur_dir + "/" + cur_dir_item if ( cur_dir_item[-4:] == ".hpp" or cur_dir_item[-2:] == ".h" or cur_dir_item[-4:] == ".cpp" or cur_dir_item[-2:] == ".c" ): ansi_to_utf_8(cur_path_name) elif os.path.isdir(cur_path_name): traverse_directory(cur_path_name) if __name__ == "__main__": folder_absolute_path: str = "" traverse_directory(folder_absolute_path)
水篇博文 /doge。 开头挂个教程 Learning Prompt 开头挂几个视频链接 使用 ChatGPT 的一些例子 超级小華 不高兴就喝水 个人感觉，如果说互联网的出现，将知识从 “实体” 形式转换为 “数字” 形式存储起来，让人们相较过往，更容易接触到各种各样的知识。那么，ChatGPT 这种可对话的 AI，很容易地将知识做一个整合，将整合的内容再反馈给提问者。 Web Searching Skills 过往，一般（说我自己），在计算机方面打算做一个小玩具（桌面计时器啦），会组合几个搜索的关键词，去 Google 上搜索，然后再从搜索结果中找到自己想要的答案。整个过程会花费掉一些时间，而且，搜索结果中的内容，往往不是自己想要的，还需要再花费一些时间去过滤掉不想要的内容。最终还要我自己整合出我所需要的信息。 但现在，我可以直接描述我打算做一个什么东西，ChatGPT 会给我一些尽可能接近的回答，我再继续追问，ChatGPT 返回的答案，会越来越接近我想要的答案。这个过程，相较我自己去搜索，会更加快速。 但我对于 ChatGPT 返回的回答，其所述说的知识准确性也是需要再三确认的。记住，最终做判断还是我们人类本身，AI 只是辅助我们做决策。 网络上有关 ChatGPT 这类 AI 的应用有很多视频或文章，基本现在各大资讯渠道有关这方面的资讯应该不少。我这里只介绍我个人所用到的，谈谈使用感受。 Easy 生成配置环境教程 太爽了，说清楚你什么系统（最好能说版本号就说），你要做什么事情，做这个事情大概会安装什么，做个甲方，给 ChatGPT 提需求，ChatGPT 就能从训练的数据（无数前人配置环境踩过的坑）中找到适合你的配置环境教程。当你想去学习某些新技术，很难再卡在配置环境那一环节了。 小白入门新技术，输出 “Hello World！”。 生成目标脚本 年初，还打算自我增值，学一波 Linux 下的 shell 脚本知识，ChatGPT 一出，我已经失去继续学习的欲望。我只要面向 ChatGPT 编程即可。而且本身 shell 脚本的很多知识就很模块化，将各种 shell 命令根据你所描述的需求去组合并不是一件难事（对 ChatGPT 而言）。不仅是 shell，对于 Python 这门脚本语言来说，ChatGPT 也能生成目标脚本去处理一些文本数据或者做一些自动化操作。 生成函数（函数式编程） 函数式编程中的函数 == 真正的数学函数，其引用透明，即只要使用相同的输入调用函数，将始终得到相同的输出。 这类函数让 ChatGPT 生成真是嘎嘎简单。只要你描述清楚你的需求，ChatGPT 就能生成你想要的函数。 这是我重新拾起 C++ 开发 5 个月后写的一个字符串分割函数（网上找了一份 Linux 下 C 语言，加了个 Windows 的版本，测试能跑就没管了）。 std::vector<std::string> split(const std::string &data, const std::string &separator) { std::vector<std::string> result; if (data == "") return result; #if defined(_MSC_VER) char *next_token = NULL; char *token = strtok_s((char *)data.c_str(), separator.c_str(), &next_token); while (token) { result.push_back(token); token = strtok_s(NULL, separator.c_str(), &next_token); } #elif defined(__GNUC__) char *token = strtok((char *)data.c_str(), separator.c_str()); while (token) { result.push_back(token); token = strtok(NULL, separator.c_str()); } #endif return result; } 这是 ChatGPT 生成的函数，我只是把我上面写的这个函数丢给它，让它帮我优化，就返回一个十分简练的函数，一对比，就很不错。ChatGPT 非常适合写这种和业务逻辑无关的函数。毕竟网络上的相关实现，想来这么多年也是积攒了不少。 std::vector<std::string> split(const std::string &data, const std::string &separator) { std::vector<std::string> result; std::stringstream ss(data); std::string item; while (std::getline(ss, item, separator[0])) { result.push_back(item); } return result; } 数学计算（问的别太夸张） 感觉，就我大学学过的数学相关的知识（高数、线性代数和概率论等等），ChatGPT 都能给我很好的回答。当然，也有可能是因为我所需解决的数学问题相较来说还蛮普通。问的时候最好别想着只要个结果，最好要让它把全部推导过程说清楚，否则那个可信度，自行根据自己的数学知识再做判断。 单元测试（TDD） TDD（Test-Driven Development）是一种软件开发过程，它是由 Kent Beck 在 1999 年提出的。TDD 是一种通过编写测试来驱动开发的过程。在 TDD 过程中，开发者先编写一个失败的单元测试，然后编写一些代码来使单元测试通过，最后重构代码。 接上面的函数式编程，其引用透明的特性，ChatGPT 很容易根据这些函数编写对应的单元测试，测试函数的输入输出是否符合预期。 Still Hard 复杂一点点的业务逻辑 这方面，ChatGPT 还是有点弱，即使你自认为能描述清楚你的业务需求，ChatGPT 也不一定能给你一个满意的答案。毕竟业务这种东西本身就带有主观性，而且，ChatGPT 是通过大量的数据训练出来的一种产物，ChatGPT 本身带的客观性相较来说还是蛮强的。所以，即使 ChatGPT 能生成一些代码，还是需要你自己去根据业务的实际情况去做些调整。 这里放一个我在做的文件读写工具的链接，ChatGPT 能帮我编写不错的文件读写代码（文件读写相关的代码网络上太多了），但是它不理解我在文件读写之上，根据所需的业务代码而确定的接口，那个工具是为了替换 Qt 代码而创建的，但要确保接口（返回值与传递参数）不变，要不然到时整体代码的改动会很大，改的很累。即使我把这个接口规范丢给 ChatGPT，它也不能给我一个满意的答案。这一块还是需要我自己去做调整。 End ChatGPT 的出现，让我感觉这是一个新的 Apple 时刻（iPhone 触屏手机的出现）。未来，使用 ChatGPT 这类 AI 会越来越普遍，而且，这类 AI 会越来越智能。总之，还蛮期待，这会发展成什么样子。 这篇文章会持续更新，看看 ChatGPT 还能做些什么。