幻影彭的彩虹

记录青春的扇区

在无数久的 🐦咕咕咕 后一个博客它建成了!

在无数久的 🐦咕咕咕 后这个博客它复活了!

博客主要会收录这些东西:

  • OI/ACM 相关

    • 考试技巧
    • 题解
    • 算法理解
  • 学习笔记

  • 面向各种人群的科普

  • 我的开源项目

  • 生活中有趣的事

  • 一些奇思妙想

我是谁:

  • 如果你线下认识我,可以叫我 "毛Ker" 或者 "老毛"
  • 如果你线上认识我,可以叫我 "彭彭"。
  • 这些 ID 都是我:huan-yp幻影彭huan_yphuan_yp2002

联系我:

  • QQ:3051561876

前情提要

发生该事故后,NcatBot 为了避免被卷入有关风波,进行了一些紧急操作以避险。由于是首次遇到此类事件,应对经验不足,导致丢失了约 6 h 的工作代码,同时也切实反映出数据安全的重要性。

本文就 321 原则,提供一个 Windows 操作系统下简易但切实可行的数据安全方案。

321 原则:三份数据、两块介质、一处异地,少一个都可能翻车!

321 原则是一种广泛应用于数据备份和数据安全领域的最佳实践。其核心思想是:

  • 3:至少保留 3 份数据副本(包括原始数据和备份)。
  • 2:将数据存储在 2 种不同的介质上(如硬盘、移动硬盘、云存储等)。
  • 1:至少有 1 份备份存放在异地(如云端或物理隔离的地点)。

通过遵循 321 原则,即使遇到硬件故障、误操作、自然灾害或勒索软件攻击等突发事件,也能最大程度地保障数据的完整性和可恢复性。这一原则简单易行,适用于个人用户、小型团队以及企业级的数据安全需求。

情况简析

对于个人开发者来说,三种情况的概率排序为 误操作 >> 硬件故障 > 其他所有情况。如果是开源代码,那么数据的 safety(数据完整性与可恢复性)要求是远大于 security(数据保密性与防止未授权访问)要求的。

对于开发者来说,数据风险主要在于劳动成果的损失,基于这个原则,我设计了以下的备份方案。

第一道防线:VS Code 的 Local-History,秒级回血

Local-History 是 Visual Studio Code 编辑器的一个插件,它能够自动记录你对文件的每次修改,形成本地的版本历史。这是防范误操作的第一道防线,简单粗暴但非常有效。

插件简介

10 s 即可快速完成安装,极大提升代码安全性。在 VSCode 中搜索并安装 "Local History" 插件即可。

每当你阶段性的需要保存文件时(按下 ctrl+s),插件会自动拷贝一个带时间戳的副本存储在本地,简单粗暴。

这种方式虽然简单,但对于日常的误删、误改等操作具有极强的防护作用。尤其是进行 git 有关的操作时。

最佳实践

  • 🚨 需要将 .history 加入 .gitignore

  • 建议 ctrl+, 找到 saveDelay 设置为 15 s 或者 30 s(连续多长时间无更改就保存),提高空间效率。

第二道防线:Restic 快照,防的是‘整盘蒸发’

有时候脑抽达到了一种地步————把整个项目文件夹都给扬了,这时候 Local History 就有些回天乏术了。

再或者,哪天物理机器突然抽风坏掉了,那又咋办?

我们需要一个更好的备份操作。

情况介绍

为了防止误删,我们需要在别处完整的建立一个备份区域。这个备份区域是不能采用 "同步" 策略的,而应该采用 "快照" 策略。

同步和快照分别是什么?
image-20250906190851595
image-20250906190904014

由于我们能够接受一定程度的数据损失(例如 1 h 的工作成果损失),所以采用时效性稍差的 "快照" 仍然可以有效的解决问题。

特别的,为了防止单盘损坏,我们更需要在与系统盘所在物理硬盘不同的另一个盘上做备份。这是所谓的两种介质

另外,为了防止极端情况发生,我们还需要一份异地。这里我采用了 SMA 共享文件夹方案,这种方案仍然可以用 restic 实现。

操作过程

设置工作区变量

🚨 首先设置工作区目录变量,方便后续使用:

1
2
3
4
5
6
7
8
9
10
# 设置工具安装目录变量
$TOOLS_DIR = "C:\tools"
# 设置本地仓库目录变量(先创建对应目录)
$BACKUP_DIR_LOCAL = "D:\restic\MyRepo"
# 设置远程备份目录变量(先创建对应目录)
$BACKUP_DIR_REMOTE = "\\192.168.1.111\share\MyRepo"
# 设置密码(请牢记)
$PASSWORD = "YourStrongPassword"
# 目标目录(写绝对路径)
$TARGET_DIR = "C:/Users/yourname/Desktop/Proj"

下载有关软件

1
2
3
4
5
6
mkdir $TOOLS_DIR
Invoke-WebRequest -Uri https://ghfast.top/https://github.com/restic/restic/releases/download/v0.17.1/restic_0.17.1_windows_amd64.zip ` -OutFile "$TOOLS_DIR\restic.zip"
Expand-Archive "$TOOLS_DIR\restic.zip" -DestinationPath $TOOLS_DIR
Rename-Item "$TOOLS_DIR\restic_0.17.1_windows_amd64.exe" -NewName "restic.exe"
Remove-Item "$TOOLS_DIR\restic.zip"
setx PATH "%PATH%;$TOOLS_DIR" # 永久加入 PATH

初始化有关仓库

1
2
3
4
5
6
7
cd $TOOLS_DIR
$Env:RESTIC_REPOSITORY_LOCAL = $BACKUP_DIR_LOCAL
$Env:RESTIC_PASSWORD = $PASSWORD # 仓库密码
restic init
$Env:RESTIC_REPOSITORY_REMOTE = $BACKUP_DIR_REMOTE
$Env:RESTIC_PASSWORD = $PASSWORD # 仓库密码
restic init

写入密码

1
cat $PASSWORD | Out-File -FilePath "$TOOLS_DIR\restic_password.txt" -Encoding ASCII

备份脚本

将脚本内容写入 $TOOLS_DIR 下的 backup.ps1

展开即可抄作业
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@"
param(
[string]`$Repo,
[string]`$Pass
)
`$Env:RESTIC_REPOSITORY = `$Repo
`$Env:RESTIC_PASSWORD = `$Pass
# 实际备份
restic backup ``
--tag hourly ``
--exclude-caches ``
--exclude "*.log" ``
--exclude "node_modules" ``
$TARGET_DIR
# 清理旧快照
restic forget --prune ``
--keep-hourly 72 --keep-daily 30 --keep-weekly 16
"@ | Out-File -FilePath "$TOOLS_DIR\backup.ps1" -Encoding UTF8

设置 Windows 定时任务

展开即可抄作业
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1. 本地备份任务 - 每小时
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) `
-RepetitionInterval (New-TimeSpan -Hours 1) `
-RepetitionDuration (New-TimeSpan -Days 3650)
$action = New-ScheduledTaskAction -Execute "PowerShell.exe" `
-Argument "-WindowStyle Hidden -ExecutionPolicy Bypass -File $TOOLS_DIR\backup.ps1 `
-Repo $BACKUP_DIR_LOCAL -Pass $PASSWORD"
Register-ScheduledTask -TaskName "ResticLocal" -Trigger $trigger -Action $action `
-User "$env:USERNAME" -Settings (New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries)

# 2. NAS备份任务 - 每小时(错开 15 min 减轻网络突刺)
$trigger2 = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(15) `
-RepetitionInterval (New-TimeSpan -Hours 1) `
-RepetitionDuration (New-TimeSpan -Days 3650)
$action2 = New-ScheduledTaskAction -Execute "PowerShell.exe" `
-Argument "-WindowStyle Hidden -ExecutionPolicy Bypass -File $TOOLS_DIR\backup.ps1 `
-Repo $BACKUP_DIR_REMOTE -Pass $PASSWORD"
Register-ScheduledTask -TaskName "ResticNas" -Trigger $trigger2 -Action $action2 `
-User "$env:USERNAME" -Settings (New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable)

再一道保险

需要保证备份程序的正确设置。

试运行备份

创建一个测试文件:

1
New-Item -Path $TARGET_DIR\test.txt -ItemType File

运行备份:

  • 打开 "任务计划程序",找到 "ResticLocal" 和 "ResticNas" 任务,点击运行。

  • 终端运行以下命令检查备份完整性:

1
2
echo $PASSWORD | restic snapshots -r $BACKUP_DIR_LOCAL
echo $PASSWORD | restic snapshots -r $BACKUP_DIR_REMOTE
  • 如果均显示了一张包含了备份时间、备份目录的表,则备份成功。

定期检查备份完整性

设置脚本:

展开即可抄作业
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@'
param(
[string]$Repo,
[string]$Pass
)

$Env:RESTIC_REPOSITORY = $Repo
$Env:RESTIC_PASSWORD = $Pass

function Write-EventLogError {
param($Message)
Write-Error $Message
New-EventLog -LogName Application -Source "ResticVerify" -ErrorAction SilentlyContinue
Write-EventLog -LogName Application -Source "ResticVerify" -EntryType Error -EventId 1 -Message $Message
}

function Show-ToastNotification {
param(
[string]$Title,
[string]$Message
)

# 方法2: 系统托盘气球
Add-Type -AssemblyName System.Windows.Forms
$notify = New-Object System.Windows.Forms.NotifyIcon
$notify.Icon = [System.Drawing.SystemIcons]::Information
$notify.BalloonTipIcon = "Error"
$notify.BalloonTipTitle = $Title
$notify.BalloonTipText = $Message
$notify.Visible = $true
$notify.ShowBalloonTip(5000)
Start-Sleep -Seconds 6
$notify.Dispose()
}

# 统一错误处理函数
function Invoke-ResticCheck {
param(
[scriptblock]$Command,
[string]$StepName
)
try {
Write-Output "执行步骤: $StepName"
& $Command
if ($LASTEXITCODE -ne 0) { throw "restic 返回非零退出码 $LASTEXITCODE" }
}
catch {
$errMsg = "$StepName 失败: $($_.Exception.Message)"
Show-ToastNotification -Title "备份验证失败" -Message $errMsg
Write-EventLogError $errMsg
exit 1
}
}

# 1. 基础 check
Invoke-ResticCheck -Command { restic check } -StepName "restic check"

# 2. 抽检 10%
Invoke-ResticCheck -Command { restic check --read-data-subset=10% } -StepName "restic check --read-data-subset=10%"

Write-Output "TestPass"
'@ | Out-File -FilePath "$TOOLS_DIR\verify.ps1"

设置定时任务:

展开即可抄作业
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 本地仓库验证任务
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 04:30
$action = New-ScheduledTaskAction -Execute "PowerShell.exe" `
-Argument "-WindowStyle Hidden -ExecutionPolicy Bypass -File $TOOLS_DIR\verify.ps1 `
-Repo $BACKUP_DIR_LOCAL -Pass $PASSWORD"
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
Register-ScheduledTask -TaskName "ResticVerifyLocal" -Trigger $trigger -Action $action -Settings $settings -User $env:USERNAME

# NAS仓库验证任务(错开 30 min 执行)
$trigger2 = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 05:00
$action2 = New-ScheduledTaskAction -Execute "PowerShell.exe" `
-Argument "-WindowStyle Hidden -ExecutionPolicy Bypass -File $TOOLS_DIR\verify.ps1 `
-Repo $BACKUP_DIR_REMOTE -Pass $PASSWORD"
$settings2 = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
Register-ScheduledTask -TaskName "ResticVerifyNas" -Trigger $trigger2 -Action $action2 -Settings $settings2 -User $env:USERNAME

试运行验证

1
2
PowerShell.exe -File $TOOLS_DIR\verify.ps1 -Repo $BACKUP_DIR_LOCAL -Pass $PASSWORD
PowerShell.exe -File $TOOLS_DIR\verify.ps1 -Repo $BACKUP_DIR_REMOTE -Pass $PASSWORD
  • 如果显示 "TestPass",则验证成功。

总结

Why 定期验证

定期验证的目的是确保备份的完整性,事实上,备份由于偶然因素损坏的概率和硬件损坏的概率大致相当,定期验证可以及时发现备份损坏的问题。

即使备份发生损坏,同时发生主数据损坏的概率极低。只需要重新修复备份即可。

该方案的优点

  • 基本践行 321 原则:
    • Local-History 作为第一道防线,能够秒级恢复工作区数据。
    • Restic 作为第二和第三道防线,完成两种介质一份异地备份。
  • 一次配置,永久使用,备份操作无需任何手动介入。

该方案的不足

  • 密钥相关:
    • 🚨 没有提供密钥备份方案,需要自行多端备份恢复密钥。
    • 🚨 密钥明文存储,对于勒索性丢失有一定风险。
  • 🚨 需要自行配置一个支持 SMB 的 NAS,并且需要确保 NAS 的稳定性。
  • 备份触发时无法做到完全无感,会打断工作聚焦。
  • 告警机制不够完善,Windows 弹窗日志偶尔会被忽略,可以考虑发送邮件等更加完善的方式。

网络安全绝非儿戏!!!

事件简述

参考资料

2025年9月5日晚,公网上大量未配置访问令牌(token)的 OneBot 服务被攻击者批量调用,诱导 QQ 机器人发布不当内容,导致众多 Bot 账号和群聊被平台封禁。由于 NapCat 框架默认把服务监听在 0.0.0.0 且用户多为新手,未设 token 的实例最多,因此成为“重灾区”。事件暴露出 OneBot 协议“token 可选”以及部分框架默认配置过于宽松的安全缺陷。也提醒开发者必须把安全性置于易用性之前,强制或显著提示用户完成最小化安全配置。

NcatBot 本体相关代码审计

附带启动相关安全隐患

  • 沿用了 NapCat 的 WebUI 弱 Token,没有做强制修改。此次攻击主要针对 OneBot 端口,没有针对 NapCat WebUI 的入侵,故 NcatBot 系列软件幸免遇难。
  • 默认采用无 Token、强制监听本地的策略,但是仍然可以通过设置来监听 0.0.0.0。幸而能够正常查看文档获知 ws_listen_ip 的用户都具备一定网络安全意识,采用了强密码或者严格防火墙。强制监听本地策略是本次 NcatBot 用户群体免受攻击的主要原因

文档相关安全隐患

  • Linux 安装部分的文档中,直接不加风险说明的详细指导了用户开放防火墙端口,对于部分新手用户,存在极高的被攻击隐患。
  • 远端模式的文档中没有说明潜在的安全隐患,文档编写经验不足。

预期改进

  • 增强对启用远端模式的限制,对于有隐患的操作提高操作门槛。
  • 本地模式严格检查 WebUI 和 Websockets 的安全状态。
  • 文档设计充分考虑安全因素,避免有隐患的引导。

启发

网络安全不是儿戏,作为开源软件开发者,尤其是涉及到开放公网端口的软件开发,一定要认真审计代码中的潜在风险。不要把安全托付给用户的认知

当易用性和安全性要做出 TradeOff 时,安全性是绝对不可牺牲的;法律风险比用户减少问题大得多。

多听从社区的安全建议,不要固执己见;没有不值得利用的漏洞,只有你没想到的作用

网络安全就在身边,不要把 "小软件"、"个人配置" 的网络安全不当回事,明知有风险时不可抱有侥幸心理;来自公网的恶意无处不在,在 NAT 后生活了十几年的我们在初期也许还不能完全意识到这种恶意,那就让这次经历成为一次宝贵的教训。

守护网络安全,从你我做起。

谈谈操作日志

最近手上有好几项工作,都感觉是需要写日志的,随便聊聊吧。

哪些工作要写日志

  • 以后可能还需要重复操作,流程相对复杂的工作。

    • 我配置了一个 openwrt 的路由器,刷入 openwrt 的流程其实挺繁琐的,我自己查阅了很多资料才解决。此外,有些资源其实在网上已经很难获取(例如原版的 clash 内核,对应的预编译固件)我对这些资源做了留档,并在日志中记录了留档的位置。

    • 计软智学院赛事部,一些自动化脚本的操作文档。之前一直是手工处理,通过加人来减少单人的工作负担。我写好了 Python 脚本。由于又涉及到和 SEUOJ 数据库的交互,实际上走完一遍流程的耗时不算短(10min)左右,这种事情是间隔几个月来一次,需要写日志来告诉自己怎么做,避免多次阅读源代码

    这些日志应该在第一次操作时就写好,写的越早,节省的时间越多。此外,留档的话题是之后的事,我可能还会写一篇文章来谈留档。

  • 需要让他人接手的一的周期很长的工作。

    • SEUOJ 的运维日志,包括各个东西的配置放在哪里的,配置了哪些自动化任务,服务是怎么被启动的等等。事实上,一台状态不明的但是能用的服务器比一台全新的服务器更加可怕。这些元数据的丢失使得另外的人在接手时出现误操作的概率大大提升。我自己接手 SEUOJ 的初期就出了很多问题,现在已经半年了,摸清楚机子的脾性后胆子也就大了起来,开始维护起运维日志,减少下一任上手的门槛。
    • 赛事部操作脚本,老东西总有毕业的一天,也很难保证接下来的小东西有足够的技术能力从头搞一份,我这些代码大概率会变成祖传代码,写点稍微详细的操作步骤有利于后面的人接手。

实际上这两种日志还有细微的差别。操作日志和博客又有一定差别。具体的,我认为精细程度上,博客>给别人看的日志>给自己参考的日志。

虽然这么说,给自己参考的日志其实给到一个和自己水平差不多的人,他也能看懂的,比如何山直接拿我路由器刷机日志去自己操作了一遍,也没出什么大的幺蛾子。

openwrt 刷机日志

包含内容

  • 最重要的几篇参考资料,github 仓库地址等。
  • 资源留档的路径。
  • 大体操作流程,精细到和我同水平时可以构建出完整流程,识别出关键步骤。
  • 可能的误操作后的还原流程,踩的坑点。
  • 一些其它相关的配置方式(获取这些信息相对容易,但是记录下来可以显著减少时间)

留档

  • 一些花了很大力气才找到的资源(主要是二进制文件)

SEUOJ 运维日志

包含内容

  • 机器的密码。
  • 服务的配置路径主路径资源路径日志路径
  • 对服务做细微调整时,留下的操作日志(供后来者要做调整时参考)。
  • 常规操作时(调整数据库,查询数据)的记录以及 SQL 语句,方便后续继续使用。
  • 额外写了一篇 HTTPS 证书维护的日志,性质和 openwrt 的操作日志差不多。

闲聊

  • 有些东西是方便以后自己再做一些东西。
  • 有些是为了后来人接手(比如 HTTPS 证书的维护,我已经配置好了自动的东西,但是还是要写,毕竟后面的人不知道我干了啥)

赛事部脚本操作日志

包含内容

  • 写明哪些量是需要每次操作时更改的,哪些函数可能需要更改。
  • 写明运行脚本的环境,和所需数据,指明获取所需数据的方法,和上游工作流对接
  • 写明脚本运行前后应该进行哪些人工操作完成整个工作流。

闲聊

  • 后续小登不知道技术实力如何(但是有 AI 之后可以预见的是很难再找到一个和我同年龄时同水平的运维)。
  • 所以做前辈的要铺好路,少走弯路,降低门槛,让小登能打怪升级升上来。

关于日志

有些事情单独开一篇博客太浪费,但是确实不得不记录一下,所以有了这个日志。目前打算是按月开。

VSCode 远程连接到服务器上开发卡死

不要连接到东西太多的目录,有些插件会扫目录,消耗巨大多内存。

建议是单独开一个 workspace 来干活。

另外远程服务器起码有 4GB 内存,2GB 内存纯属冤大头。

Python slots

Python 的 __slots__ 机制,可以限制实例的属性,只允许在创建实例时定义的属性,不允许动态添加属性。

如果尝试为没有在 slots 中声明的变量赋值,会引发 AttributeError。

git 拉取远端其它分支并与本地关联

1
git checkout -b feature-branch origin/feature-branch

git 删除分支

删除本地分支:

1
git branch -d feature-branch

删除远端分支:

1
git push origin --delete feature-branch

Python lambda 闭包问题

lambda 做函数绑定的时候变量是绑定到上的变量的,而不是绑定到上的。

例如下面循环的例子:

1
2
3
4
funcs = []
for i in range(10):
funcs.append(lambda x: x + i)
print(funcs[0](10))

输出是 19,而不是 10,因为查找到的 i 是循环结束后域里面的 9。

正确的做法是:

1
2
3
4
funcs = []
for i in range(10):
funcs.append(lambda x, i=i: x + i)
print(funcs[0](10))

C++ 全局变量的声明和定义

全局变量的声明和定义是分开的,声明是在 .h 中使用 extern 关键字,定义是 .cpp 中使用 = 赋值。

另外就算是用了 #ifdef XXX_H,也不能在 .h 里面定义。不然多个 .cpp 都包含了 .h,还是分开编译的,就会报重定义的错误。

类的静态成员变量是不允许在声明的时候初始化的,只能定义的时候初始化。

Python 线程独立资源

有时候每个线程需要一些独立的资源来执行一些操作,这个可以用 threading.local() 来实现。

一般需要配合 concurrent.futures 的线程池来使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

def initializer():
"""初始化线程独立资源"""
driver = uc.Chrome(use_subprocess=True, options=options, driver_executable_path=DRIVER_PATH)

def exact(i):
"""任务"""
driver.get(url)

def crawler():
global driver_pools
driver_pools = threading.local()
with concurrent.futures.ThreadPoolExecutor(max_workers=5, initializer=initializer) as executor:
# 提交任务
futures = [executor.submit(lambda i=i: exact(i)) for i in range(1, 100)]
# 等待任务完成
for future in concurrent.futures.as_completed(futures):
future.result()

Python 异步任务、线程任务

这两个任务一般来说只要开始阻塞执行后,都没办法强制中止的。

后台多线程任务可以设置成 daemon=True,关闭主程序的时候自动关闭后台任务。

异步任务一般要避免出现阻塞,否则主线程会卡死。

另外如果一定要强行中止线程,可以考虑 concurrent.futuresThreadPoolExecutor._threads.clear 方法。

Python Queue

线程安全的 Queue.get 在使用的时候不管是不是保证有东西,最好都加点 timeout。

nullptr 和 iterator::end()

处理指针类型一定要想好有没有可能是 nullptr

处理迭代器的时候一定要想好有没有可能是 end()

C++ 容器

迭代的时候别TM的做删除操作,会漏内存。

git 设置代理

有时候 git 走不了系统代理,可以配置一个全局的代理设置:

1
2
git config --global http.proxy http://127.0.0.1:7890
git config --global https.proxy http://127.0.0.1:7890

C++ iostream

istreamostream 是两个抽象类,不能直接实例化,用的时候应该把一个 ifstream 或者 ostream 的子类实例化,然后绑定到 istream 或者 ostream 上。

cerr, cout 这些都是 ostream 的子类,所以有时候

1
2
ifstream fin("in.txt");
istream &in = fin;

C++ Fsanitize

不要开 O2 优化,开了就定位不了错误。

VMWARE 无法打开内核设备

对应虚拟机 .vmx 文件中,将 vmci0.present 改为 "FALSE"

Win11 自定义快捷键反应慢

参考

就是开始菜单 (建议 Win+Q 搜索 设置)找到 "设置", 点击 "应用设置", 把 "后台组件权限" 改为 "从不" 即可。

简介

左偏树是一种可并堆,核心操作是 \(O(\log{n} + \log{m})\) 的 merge 操作。

通过 merge 操作来实现 push 和 pop 操作。

基本信息

  • 左偏树的结构是二叉树
  • 每个节点具有一个额外属性 dist。定义一个节点是边缘节点,当且仅当它的儿子个数不为 2。dist 表示该节点往儿子方向走,走到边缘节点需要经过的最小边数,空节点 dist 定义为 -1。
  • 左偏树每个节点的左子树的 dist 不小于右子树的 dist,所以显然有 dist = right_son->dist + 1

算法流程

push

创建一个新的堆,只分配一个节点,将新堆合并进原有堆。

pop

原有根节点删去,合并其左右儿子,得到新的根节点。

merge

  1. 找到根节点 val 较小的那个堆,将它的根节点作为新堆的根节点,它的左儿子作为新堆的左儿子,它的右儿子和另一个堆合并,作为新堆的右儿子。
  2. 合并时遇到一个堆为空时,非空堆即合并结果,可以直接返回。
  3. 如果合并后右儿子的 dist 大于左儿子的 dist,交换两个儿子。

复杂度分析

结论:

  • push:\(O(\log{n})\)
  • pop:\(O(\log{n})\)
  • merge:\(O(\log{n} + \log{m})\)

证明:

  1. 若一个节点的 dist 为 \(x\),则它及其子树至少有 \(2^x\) 个节点。故 dist 的值是 \(O(\log{n})\) 级别的。
  2. 进行合并时,每递归一层,参与合并的两个堆的 dist 之和减少 1,故递归层数为 \(O(\log{n} + \log{m})\)

实现

结构

  • 一个内部的类 Node 表示左偏树的一个节点,包含 distval 两个变量,以及 son[0], son[1] 两个指针,分别指向左儿子和右儿子。
  • 一个类 Heap,表示一个堆,包含一个 Node 指针,表示根节点。

声明

这里希望练习使用 C++11 中的智能指针移动语义,所以声明为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Heap{
private:
class Node{
public:
unique_ptr<Node> son[2];
int val, dist;
Node(int val=0){
this->son[0]=nullptr;
this->son[1]=nullptr;
this->val=val;
this->dist=0;
}
};
unique_ptr<Node> root;
public:
/*
其它部分暂时省略
*/
};

构造函数

1
2
3
4
5
class Heap{
Heap(unique_ptr<Node> root){
this->root=move(root);
}
};

通过传入一个 unique_ptr<Node>,构造一个 Heap 对象。

unique_ptr 是一种智能指针,它指向的对象是它独享的,不能被其它东西访问。

unique_ptr 仅支持移动语义,这用于转移对象的所有权,对象所有权转移后,原来的 unique_ptr 将失效。

unique_ptr 不支持拷贝和赋值操作,所以上面的构造函数严格来说是有问题的,如果传入的 unique_ptr 不是右值则会尝试调用不存在的拷贝构造函数,导致编译错误。故应该将参数声明为右值引用(注意区分右值引用常值引用)。

具体如下:

1
2
3
4
5
class Heap{
Heap(unique_ptr<Node> &&root){
this->root=move(root);
}
};

至于为什么第一份代码是正确的,是因为只要保证传入的 unique_ptr 是右值,编译器就会优先自动调用移动构造函数,所以不需要在调用函数时显式地写出 move

至于为什么第二份代码中右值引用root 仍然需要使用 move 来显式的转化为右值,这是因为右值在绑定到一个右值引用后,其本身在作用域内是一个具名变量,行为会退化退化为左值。,故需要显式的使用移动语义来调用移动构造函数。

merge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Heap{
Heap& merge(Heap &&other){
if(root==nullptr){
root=move(other.root);
}
else if(other.root==nullptr){

}
else{
if(root->val > other.root->val){
swap(this->root, other.root);
}
root->son[1] = move(Heap(move(root->son[1])).merge(move(other.root)).root);
if(root->son[0] == nullptr ||
(root->son[1] != nullptr && root->son[0]->dist < root->son[1]->dist)){
swap(root->son[0], root->son[1]);
}
if(root->son[1] != nullptr)
root->dist = root->son[1]->dist + 1;
else
root->dist = 0;
}
return *this;
}
};

merge 返回一个左值引用是为了方便链式调用。

有几个细节:

  • swap: 可以用于交换 unique_ptr, 它应该是实现了这种移动语义的交换。

  • 隐式构造: Heap(xxx).merge(move(other.root)).root 这里使用了隐式构造Heap.merge 支持的参数是一个右值的 Heap,传入时传的一个右值的 unique_ptr,由于定义了右值 unique_ptrHeap 的构造函数,这里直接隐式调用了构造函数,构造了一个右值的 Heap 并作为参数传递给了 merge

  • 返回左值引用: return *this 返回左值引用的目的是方便链式调用,至于为什么临时构造的 Heap 能返回一个左值引用,是因为临时对象生存周期是到表达式结束为止,而临时对象在生命周期内可以被左值引用。

push && pop && top

1
2
3
4
5
6
7
8
9
10
11
12
13
class Heap{
int top() const{
return root->val;
}
void push(int x){
this->merge(unique_ptr<Node>(new Node(x)));
}
int pop(){
this->root = move(Heap(move(root->son[0])).merge(move(root->son[1])).root);
// 当 merge 函数返回时, 它返回的是临时对象的左值引用, 临时对象在生命周期内可以被左值引用.
return top();
}
};

比较简单, 不解释了.

左值右值的理解

简单的说:

  • 右值是只能放到表达式右边的值,左值是既能放到表达式右边,也能放到左边的值

  • 右值一般是没有命名的值,左值一般是有名字的值

  • 右值是临时的值,左值是持久的值

更具体一点:

左值的特性

  • 有固定内存地址:左值通常存储在堆或栈上,可以通过取地址操作符(&)获取其地址。

  • 可以被多次访问:左值的生命周期较长,可以在多个语句中被访问。

  • 可以被修改:左值通常可以被修改,例如变量赋值操作。

右值的特性

  • 没有固定内存地址:右值通常是临时对象,没有固定的存储位置,或者其存储位置在表达式结束后立即失效。

  • 不能被取地址:右值不能使用取地址操作符(&)获取其地址。

  • 不能被多次访问:右值的生命周期仅限于当前表达式,不能被多次访问。

  • 通常不可修改:右值通常是不可修改的,因为它们是临时的。

右值举例

C++ 的值除了右值就是左值,下面几个右值的例子:

  • 函数的返回值是一个右值。
  • 字面量,10, "hello" 是右值。
  • 临时对象是右值,例如 c = a + b; 中的 a + b 这个整体。

下面是左值的例子:

  • 命名变量名是左值,例如 int a = 10; 中的 aa = b = c = 10;a, b, c 都是左值。

左值引用和右值引用

名词解释

1
2
3
4
5
6
7
8
9
int a = 10; // 左值 被赋值为 右值 , a 是左值, 10 是右值.
int &&x1 = 10; // 右值引用 绑定到 右值, x1 是右值引用, 10 是右值, 允许.
int &&x2 = a; // 右值引用 绑定到 左值, x2 是右值引用, a 是左值, 允许, 一般用于模板编程.
int &y = a; // 左值引用 绑定到 左值, y 是左值引用, a 是左值, 允许.
int &y1 = 10; // 左值引用 绑定到 右值, y1 是左值引用, 10 是左值, 编译错误.
int &y2 = x1; // 左值引用 绑定到 右值引用, y2 是左值引用, x1 是右值引用, 编译错误.
int &&z1 = y; // 右值引用 绑定到 左值引用, z1 是右值引用, y 是左值引用, 允许, 并且, 支持引用折叠语法, 所以 z1 的实际行为是左值引用.
int &z2 = z1; // 左值引用 绑定到 可折叠为左值的 右值引用, z2 是左值引用, z1 是右值引用, 但可折叠到对 a 的左值引用, 允许.
int &z3 = x2; // 左值引用 绑定到 可折叠为左值的 右值引用, z3 是左值引用, x2 是右值引用 但可折叠到对 a 的左值引用, 允许.

右值引用的特性

右值引用似乎是命名变量吧?

所以:右值引用可以在不发生内存操作的前提下将临时右值变为左值,可以延长右值的生命周期,降低读写内存的开销。

左值引用的特性

左值引用等价于为变量取了一个别名。

特别的,在函数传参如果使用左值引用传参,函数类通过左值引用可以修改原变量的值。

引用折叠

在不发生编译错误的情况下, 可以支持引用折叠, 名词解释中的 z1, z2, z3 例子解释了这个特性。

具体的,在发生多次引用绑定时,按照以下规则折叠:

  • 左 + 左 = 左
  • 右 + 左 = 左
  • 左 + 右 = 左
  • 右 + 右 = 右

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include "bits/stdc++.h"

using namespace std;

class Heap{
private:
class Node{
public:
unique_ptr<Node> son[2];
int val, dist;
Node(int val=0){
this->son[0]=nullptr;
this->son[1]=nullptr;
this->val=val;
this->dist=0;
}
};
unique_ptr<Node> root;
public:
Heap(unique_ptr<Node> &&root){
this->root=move(root);
}
int top() const{
return root->val;
}
void push(int x){
this->merge(unique_ptr<Node>(new Node(x)));
}
Heap& merge(Heap &&other){
if(root==nullptr){
root=move(other.root);
}
else if(other.root==nullptr){

}
else{
if(root->val > other.root->val){
swap(this->root, other.root);
}
root->son[1] = move(Heap(move(root->son[1])).merge(move(other.root)).root);
if(root->son[0] == nullptr ||
(root->son[1] != nullptr && root->son[0]->dist < root->son[1]->dist)){
swap(root->son[0], root->son[1]);
}
if(root->son[1] != nullptr)
root->dist = root->son[1]->dist + 1;
else
root->dist = 0;
}
return *this;
}
int pop(){
this->root = move(Heap(move(root->son[0])).merge(move(root->son[1])).root);
// 当 merge 函数返回时, 它返回的是临时对象的左值引用, 临时对象在生命周期内可以被左值引用.
return top();
}
};

int main(){
Heap h(nullptr);
int n;
cin >> n;
for(int i=1;i<=n;i++){
int tp, x;
cin >> tp;
switch (tp){
case 1:
cin >> x;
h.push(x);
break;
case 2:
cout << h.top() << endl;
break;
case 3:
h.pop();
break;
default:
throw "unsupported operation";
}
}
return 0;
}

C 使用 malloc() 和 free() 来动态分配和释放内存,C++ 使用 new 和 delete 来动态分配和释放内存。

C 管理数据的方式

malloc && free

malloc 的时候,会在上分配一块内存,然后返回一个指向这块内存第一个位置的指针。同时会记录一个 metadata,存储数据大小和分配状态等信息。

malloc 分配内存初值是随机的,需要手动初始化。

当使用 free 释放内存时,会检查 metadata,根据 metadata 决定所要释放数据的大小。

metadata 的存储位置一般在首地址之前,因此 free 的时候必须 free 首地址

结构化数据

用 struct 之类的结构体来管理数据时,指针类型直接指示了应该如何处理内存中的数据。

C++ 管理数据的方式

new && delete

直接 new 一个数据类型,比如 new int,不会设置 metadata,而是直接使用指针类型来管理内存大小,并根据指针类型调用对应构造函数来初始化数据。

new 的数据必须通过 delete 来删除,并且必须显式指定指针类型,因为 delete 不仅需要指针类型指示的内存大小,还会调用对应析构函数来释放内存。

new [] && delete []

new [] 会分配一块连续的内存,然后返回一个指向这块内存第一个位置的指针。同时会记录一个 metadata,存储数据大小和分配状态等信息。new [] 分配时会调用对应构造函数来初始化数据。

delete [] 会检查 metadata,根据 metadata 决定所要释放数据的大小。delete [] 会调用对应析构函数来释放内存。

软件级是相对于程序级的概念,软件级应用往往包含多目录多文件的大量源代码,有复杂的第三方库依赖关系

软件级应用的编译用时往往较长,并且过程相对繁琐。

我们这里介绍使用 MinGW 系列工具和 CMake 编译 cpp 软件级应用的过程和知识。

各种文件

CMakeLists.txt

CMakeLists.txt 一般用于跨平台的大型软件级项目,用于指示 CMake 生成平台对应的编译选项,也就是 Makefile 文件。

Makefile

Makefile 文件指定 make 工具编译生成 include/lib/bin/ 等成品的方式,我安装的是 MinGW套件,命令是 mingw32-make

.dll/.so

这两个是动态链接文件,.dll 是 Windows 下的,.so 是 Linux 下的。

MinGW 默认是动态链接的,编译生成的 .exe 如果找不到 .dll 就运行不了。

.dll 或者 .so 默认只会在系统路径和工作目录两个地方去找

.o

.o 是可重定向目标文件,是汇编过程生成的源文件机器语言代码。

.a

.a 是静态链接归档文件,如果采用静态链接的方式编译,就需要在编译时加上这个。

它等价于把若干个 .o 打包在了一起。

C++ 编译速通

编译步骤:

  1. 预处理:把 #include 的东西全部粘贴到对应的位置,由 .cc.cpp 生成 .i.ii 预处理文件
  2. 编译:编译器用 C/CPP 代码生成汇编代码,由 .i.ii 生成 .s 汇编文件
  3. 汇编:由汇编器将汇编代码编程二进制代码,由 .s 生成 .o 目标文件
  4. 链接:由链接器把汇编的机器代码 .o,打包的静态链接库 .a,动态链接库 .

静态链接和动态链接的区别:

  • 静态链接在编译时便将中的实现放入了 .exe 文件,在运行 .exe 时就将代码放入内存
  • 动态链接在运行时才去 .dll 中查找实现,并在调用对应函数时将这部分代码放入内存

无论是静态链接还是动态链接,编译时都需要指定链接库。静态和动态的区别在于将对应代码加入内存的时间

实现通常写在 .cpp 文件中,定义通常写在 .h 文件中,#include 时一般只会 #include .h 文件,所以如果编译时不指定链接库,链接步骤就会报 Undefined Refference 错误。

GNU-make 工具的使用

类 UNIX 环境下它叫 make,但是我们用的 MinGW,反正我这里叫 mingw32-make,路径是 path/to/mingw64/bin/mingw32-make.exe ,使用时记得加环境变量。

make 和 Makefile 结合,执行一些特定的命令,完成项目的编译,避免手敲 gcc 命令。

命令格式:

1
[mingw32-]make [<目标>] <可选参数...>

常用可选参数:

  • -n:不执行,只打印要执行的命令。
  • -f:指定 Makefile 的路径,默认情况下 Makefile 的(相对)路径为 Makefilemakefile
  • -j[<num>]:多线程编译,例如 -j4 表示四线程编译。

目标是指 make 要执行的具体任务,如果没有指定目标,那么会执行第一个非伪目标

Makefile 解读

一般情况下需要我们写 makefile 的机会不是很多,会读和改就可以了。

变量操作

  • <VAR_NAME> ?= <EXPR> 条件赋值,如果还没定义这个变量才赋值,规则同递归赋值
  • <VAR_NAME> = <EXPR 递归赋值计算,每次使用时重新计算
  • <VAR_NAME> := <EXPR 立即赋值计算,只计算一次。
  • $(<VAR_NAME>):引用变量。

目标

目标的格式是 <target>:[<dependencies>]

当 make 执行某个目标时,会对依赖项进行检查,如果依赖项对应的文件不存在,则会将先执行依赖的名字对应的目标

例如:

1
2
3
4
5
foo: foo.o
gcc -o foo foo.o
foo.o: foo.c
gcc -c foo.o foo.c
echo "compiled foo.o"

执行 make 时,过程如下:

  1. 没有指定目标,会找到第一个目标 foo 执行。

  2. 查找其依赖 foo.o,发现不存在,于是查找目标 foo.o

  3. 找到 foo.o 其依赖 foo.c 作为一个文件存在,于是执行 gcc -c foo.o foo.c 编译生成 foo.o

  4. 返回 2. 中步骤,依赖 foo.o 已经存在,执行 gcc -o foo foo.o 生成 foo[.exe]

目标规则

有时候可能有很多文件,某些文件的生成方式,例如 .o,是一致的,这时可以利用目标规则来定义目标。

查找目标时,规则优先级为:越具体越优先

目标规则大致分为三种:

  1. 显式目标规则

    见上。

  2. 显示模式规则

    显式模式规则使用通配符 % 来匹配任意字符,包括目录分隔符。其余同上。

    下面的 $<$@自动变量,下面的部分有详细解释。

    1
    2
    3
    %.o: %.c
    echo "Using generic rule"
    gcc -c $< -o $@
  3. 隐式模式规则

    隐式规则模式用于指定生成对应后缀名的文件,常见的有:

    • .c.o:.c 文件生成 .o 文件。
    • .cpp.o:.cpp 文件生成 .o 文件。
    • .cpp.exe:.cpp 文件申城 .exe 文件。

伪目标

一般情况下,目标会作为一个文件,通过判断文件是否已经生成来判断是否已经完成该目标。

但是,如果修改了 .cpp 源代码,但是 makefile 中有生成 .o 的过程,.o 还被保留了,那么再次 make 的时候检测到 .o 已经存在,只会用已经存在的 .o 重新链接生成一次可执行文件,没有达到重新编译的效果。所以,往往需要定义一个"清理操作",来清理相关的文件以便重新完整编译。

"清理操作" 显然不生成文件,为了完成这个操作,引入了 "伪目标" 概念,伪目标在文件开头用 .PHONY 声明,例如:

1
2
3
4
5
6
7
.PHONY: clean
all: clean foo.o
g++ -o foo foo.o
foo.o: foo.c
g+++ -c foo.o foo.c
clean:
rm *.o

这样便可以在每次执行 make 时清理缓存的 .o 文件达到重新编译的效果。

提醒:

  • 伪目标表示一个过程,作为依赖时一定会被执行,如果执行返回代码为 0 才判定为完成,否则判定为没有完成,可能报错。
  • 严格来说 all 也应该被定义为伪目标,实际上由于不会生成 all 文件,也就无所谓,clean 有时候不会被声明为伪目标。
  • make 不指定目标时,如果存在非伪目标,那么执行第一个非伪目标,否则仍然执行第一个目标

自动变量

自动变量是执行规则时根据上下文动态生成的变量

常用自动变量表如下:

  1. $@

    • 含义:表示当前规则的目标文件(Target)。

    • 用途:常用于指定输出文件名。

    • 示例

      1
      2
      foo.o: foo.c
      gcc -c foo.c -o $@

      如果目标是 foo.o$@ 的值就是 foo.o


  1. $<

    • 含义:表示依赖列表中的第一个依赖项。

    • 用途:常用于单个依赖项的场景,例如编译 .c 文件时指定源文件。

    • 示例

      1
      2
      foo.o: foo.c header.h
      gcc -c $< -o $@

      如果目标是 foo.o$< 的值是 foo.c


  1. $^

    • 含义:表示依赖列表中的所有依赖项(以空格分隔)。

    • 用途:常用于需要处理多个依赖项的场景,例如链接多个对象文件。

    • 示例

      1
      2
      foo: foo.o bar.o
      gcc -o $@ $^

      如果目标是 foo$^ 的值是 foo.o bar.o

CMake 系列工具使用

不同平台使用的编译工具不一定相同,有时候根据实际情况,也可能会选择性编译整个项目的一部分。如果只提供 Makefile 可能不方便。这个时候可以利用 CMake 系列工具和 CMakeLists.txt 来根据具体需求生成特定的 Makefile

CMake 工具可以在这里下载。

检查一个项目根目录是否包含 CMakeLists.txt 以便确认它是否支持使用 CMake 系列工具。

CMake-GUI 有个 Bug,详见

下面是 CMake 的 UI:

image-20250309195350524

配置源代码和构建路径

两个位置需要填写源代码目录(包含 CMakeLists.txt)和构建目录(自己创建一个)。

image-20250309195243176

Configure

点击 Configure,会先让你选编译器之类的:

image-20250309200631860

我们用 MinGW,翻一下找到选上就行,点 Finish 会自动读取所需配置项,读完之后你可以用 UI 界面方便的对它们进行改动。

Configure 过程也会读取平台信息等,大型项目 Configure 用时会很长。

如果改动了配置,需要再点一次 Configure

由于 CMake 缓存相关的问题,如果第一次 Configure 爆红,也需要再点 Configure 一次解决问题。

Generate

完成 Configure 后,点 Generate,会在 build 目录生成 Makefile 和所需要的文件,在 build 目录执行 make 命令即可。

直接编译

gcc

命令格式:

1
gcc <功能参数> [主参数...] [包含目录参数...] [链接目录参数...] [链接参数...] [编译选项...]
  1. 功能参数

    • -o:完成链接停止。
    • -c:完成汇编停止。
    • -s:完成编译停止。
    • -E:完成预处理停止。
  2. 主参数

    主参数用于指定执行这次操作所需的各种文件的位置,包括 .cpp 文件、.a 文件、.o 文件等,如果这里没有显式指定,可以通过下面的参数补充指定

  3. 包含目录

    包含文件不能在主参数部分指定,必须位于包含文件搜索目录下。

    #include 除了在工作目录和系统路径搜索之外,还可以使用多个 -I path\to\include 来指定包含文件搜索目录

  4. 链接目录

    使用 -L 参数指定链接搜索目录,需要和链接参数配合使用

  5. 链接参数

    使用 -l 参数,语法规则为 -l[链接名](注意没有空格),优先链接到链接目录下动态链接文件 /path/lib[连接名].dll 文件。如果不存在动态链接,则会链接到静态链接文件 /path/lib[链接名].a 文件。

    链接库可以在主参数中直接指定,写它的路径即可。

    例如 -L ./lib/ -lzlib 等价于主参数中添加 ./lib/libzlib.dll 或者 ./lib/libzlib.a

  6. 编译选项

    • --static:强制使用静态链接。
    • -O2:开启 O2 优化。
    • -g:生成调试符号表启用 gdb 调试功能。
    • -Wl,-rpath,<path/to/dll>:指定运行时链接搜索目录,运行时最优先到 path/to/dll 去搜索 .dll 文件,一般使用相对路径

示例(我使用了 [] 和 () 来划分部分,实际使用时需要去掉):

1
[g++](命令) [-o main.exe](功能参数) [cli/main.cpp resource.o](主参数) [-I "C:\Users\huany\Desktop\work_space\ziptools-install\minizip-install\include" -I "C:\Users\huany\Desktop\work_space\ziptools-install\zlib-install\include"](包含目录) [-L "C:\Users\huany\Desktop\work_space\ziptools-install\minizip-install\lib\" -L "C:\Users\huany\Desktop\work_space\ziptools-install\zlib-install\lib\"](链接目录) [-lminizip -lzlibstatic](链接参数) [-fpermissive --static](编译选项)

ar

使用 ar 工具将多个 .o 文件打包成一个 .a 静态库文件。例如:

1
ar cr libmylib.a file1.o file2.o

这里,libmylib.a 是生成的静态库文件名,file1.ofile2.o 是输入的目标文件。

动态链接

我觉得自己造的轮子就没必要搞动态链接了,全静态吧。想起来我再补上。

软件的发布

发布 .cpp 编译生成的 .exe 软件时,需要考虑到用户的机子上没有运行环境的事实,一般采用两种方式:

  • 全静态链接 --static
  • 同时发布所需的 .dll 文件,放在 .exe 同级目录下或者使用 -rpath 编译参数。
  • 折中

练习——Windows 编译 OpenCV

OpenCV 是使用最为广泛的计算机视觉库,编译文档十分完善,也有数量可观的资料可供查询。但 OpenCV 体型巨大,编译用时较长。

OpenCV源码

编译完成后应该得到以下文件:

  • 包含目录 include/
  • 静态链接库 lib/
  • 动态链接和其它二进制文件库 bin/

练习——Windows 系统编译 minizip

minizip 是 zlib 库的一个子库,能够支持压缩和解压。minizip 目前已经放弃维护,相比成熟的 OpenCV,可能需要修改一些编译命令才能完成编译。

你需要完成 minizip 和 zlib 的编译,得到以下文件:

  • zlib 的包含目录 include/ 和静态链接库 lib/
  • minizip 的包含目录 include/ 和静态链接库 lib/

提醒:

  • 注意,官方的 makefile 中可能没有生成目录的步骤,你可能需要手动创建目录或者更改 makefile。

  • minizip 的 makefile 无法针对 windows 使用,请自行排查其错误并进行修改后完成编译,或者使用 g++ 手动编译生成有关文件。

  • 你可以用 CMake 完成 zlib 的编译,也可以阅读下面文档手动编译:

    To compile all files and run the test program, follow the instructions given at the top of Makefile.in. In short "./configure; make test", and if that goes well, "make install" should work for most flavors of Unix. For Windows, use one of the special makefiles in win32/ or contrib/vstudio/ . For VMS, use make_vms.com.

  • minizip 源代码位于 zlib/contrib/minizip/,请自行解读代码结构并进行操作。注意其依赖的 zlib 相关的配置。

大练习——编译 NcatBot 发行包

背景简介

ncatbot 旨在让用户无门槛使用,开发者只关注业务代码,提供了一个 windows 平台下的 .exe 安装部署工具。

该工具只包括一个 main.exe 主程序,能够无下载过程的配置基础 Python 环境,并安装 ncatbot 本体,调用 ncatbot-cli 完成后续交互。

相应的代码 main.cpp,Python 环境压缩包 package.zip 都已经给出,请编译出可执行文件 main.exe

有关资源

指示

main.cpp 开头部分含有编译命令,参考这一部分编译命令,你需要完成如下工作:

  • 完成 zlib,minizip 的编译。
  • 完成 package.zip 的编译。
  • 正确书写设置 zlib,minizip 的路径和编译命令。
  • 编译 main.cpp 并链接其它上述资源。

NcatBot-Release 中含有编译好的 zlib,minizip,package,如果你实在无法完成这些部分,可以下载使用并完成接下来的部分。

附录

命令格式书写语法

这是一种约定俗称的记号,而不是一个严格规范,每位开发者使用的书写方式不一定相同,也不会特意严格按照要求书写,下面给出我的习惯记号。

  • [EXPR]:表示这一部分是可选的。
    • [mingw32-]make:可能是 mingw32-make 或者 make
    • 有些人也会写成 (),但一般不会写成 <>
  • <VAR>:表示一个变量,需要根据实际情况更改。
    • 有时候你会明显感觉到不用 <> 包起来也表示一个变量,自己灵活处理,例如 path/to/zlib/ 可能表示 C:/Program Files(x86)/zlib/
    • 有时候也会写成 []
  • <VAR...>:表示这里的参数个数是动态变化的,每个参数用空格分隔,可以是 0 个。
    • <编译选项...> 可能表示 -O2 -fpermissive 或者 -O2 --static 或者 --static 或者啥都没有。
    • 有时候也会写作 [VAR...] 或者 [<VAR>...]

更多自动变量

  1. $?

    • 含义:表示依赖列表中比目标文件更新的依赖项(以空格分隔)。

    • 用途:常用于条件编译或增量构建,只处理那些真正需要更新的文件。

    • 示例

      1
      2
      foo.o: foo.c header.h
      gcc -c $< -o $@

      如果 header.h 被修改,$? 的值是 header.h


  1. $*

    • 含义:表示目标文件的主名(不包含扩展名)。

    • 用途:常用于生成与目标文件相关的其他文件名,例如中间文件或依赖文件。

    • 示例

      1
      2
      3
      %.o: %.c
      gcc -c $< -o $@
      echo "$* is compiled"

      如果目标是 foo.o$* 的值是 foo


  1. $+

    • 含义:表示依赖列表中的所有依赖项(以空格分隔,与 $^ 类似,但保留重复项)。

    • 用途:在某些需要保留重复依赖项的场景中使用。

    • 示例

      1
      2
      foo: foo.o foo.o bar.o
      gcc -o $@ $+

      如果目标是 foo$+ 的值是 foo.o foo.o bar.o


  1. $|

    • 含义:表示依赖列表中的顺序依赖项(Order-only prerequisites)。

    • 用途:用于指定那些仅用于顺序控制的依赖项,这些依赖项的更新不会触发目标的重新构建。

    • 示例

      1
      2
      foo: bar.o | dir
      gcc -o $@ $<

      如果目标是 foo$| 的值是 dir


  1. $(@F)$(@D)

    • $(@F):表示目标文件的文件名部分(不包含路径)。

    • $(@D):表示目标文件的目录部分(不包含文件名)。

    • 示例

      1
      2
      3
      4
      build/foo.o: src/foo.c
      gcc -c $< -o $@
      echo "File: $($(@F))"
      echo "Directory: $($(@D))"

      如果目标是 build/foo.o

      • $(@F) 的值是 foo.o
      • $(@D) 的值是 build

  1. $(*F)$(*D)

    • $(*F):表示 $* 的文件名部分。

    • $(*D):表示 $* 的目录部分。

    • 示例

      1
      2
      3
      4
      build/foo.o: src/foo.c
      gcc -c $< -o $@
      echo "File: $($(*F))"
      echo "Directory: $($(*D))"

      如果目标是 build/foo.o

      • $(*F) 的值是 foo
      • $(*D) 的值是 build

折腾 vmware

ubuntu24.04 环境。

config apt source

打开 /etc/apt/source.list.d/ubuntu.sources,在 URIs 一项中,http://archive.ub.... 前面加上 .cn,也就是 https://cn.archive.ub....

第二项那个 security 不用改。

vmware tools

1
2
3
4
5
sudo su
apt-get update
apt-get install open-vm-tools
apt-get install open-vm-tools-desktop
reboot

共享文件夹

开共享后, 客户机共享文件夹目录: /mnt/hgfs/

重安装的网络问题

重新安装 vmware 时,可能会遇到以下问题:

  • 安装程序在 "配置网络驱动" 时卡死
  • 安装后客户机死活连不上网。

这是由于卸载时没有删除注册表导致的,解决方案(参考)如下:

  • 在下载的软件中翻一翻,找到 "注册表"
  • 一键扫描然后修复。
  • vmware 点 "编辑","虚拟网络编辑器",还原默认设置。

输入法

百度输入法很靠谱,它甚至附带了安装说明。

百度输入法

0%