Seeker.Log

一个笨拙的探索者的思考

0%

libcurl交叉编译

需要源码交叉编译libcurl操作步骤

背景

有时候需要交叉编译libcurl,比如目标机器是32位系统的,但是本地机器是64位系统的,而且由于某些原因,我们无法在32位系统上直接编译,所以需要用到交叉编译

编译openssl

libcurl是依赖openssl的,所以先编译openssl的32位库

完整编译选项配置如下:

1
setarch i386 ./config -m32 –prefix=/home/muhan/openssl/ –openssldir=/home/muhan/openssl/ -Wl,-rpath,/usr/local/openssl/lib shared

详细选项含义如下:

1
2
3
4
配置-m32 指定编译32位的库
配置–prefix 指定openssl的安装目录
配置–openssldir 指定openssl的头文件目录
配置shared关键字 指定编译时生成动态库(libssl.so/libcrypto.so及其相关软连接)

然后再make && make install 即可

编译安装zlib

有时候有的系统是默认安装了32位zlib库的,那么就可以跳过这一步,但是有的系统需要自己下载编译zlib-32位库

完整编译选项配置如下:
直接修改CMakeLists.txt文件,增加以下两行

1
2
set(CMAKE_C_FLAGS “-m32”)
set(CMAKE_CXX_FLAGS “-m32”)

详细选项含义如下:

1
2
配置CMAKE_C_FLAGS 指定编译32位库环境
配置CMAKE_CXX_FLAGS 指定编译32位库环境

然后再mkdir build && cd build && cmake .. && make && make install 即可

编译安装libcurl

最后就是编译libcurl

完整编译选项配置如下:

1
./configure PKG_CONFIG_PATH=/home/muhan/openssl CFLAGS=”-m32” CPPFLAGS=”-I/home/muhan/openssl/include” LDFLAGS=”-L/home/muhan/openssl/lib”

详细选项含义如下:

1
2
3
4
配置PKG_CONFIG_PATH 指定启动openssl选项(启动这个选项,就会默认链接lssl,lcrypto,lz三个库)
配置CFLAGS 指定编译32位库环境
配置CPPFLAGS 指定链接的库的头文件
配置LDFLAGS 指定链接的库的路径

然后再make && make install 即可

总结

当编译第三方库的时候,如果有CMakeLists.txt,直接用CMakeLists.txt编译就很方便;
如果只有configure,那么需要先了解编译选项

执行./configure –help 来查看当前支持的编译选项
然后根据提示配置一下我们需要指定的选项,比如自己指定的openssl的版本的库和头文件路径名,比如CC的版本,比如安装路径等等
(当然,如果不需要额外配置这些东西的话,直接走默认配置的话,那么直接执行./config 或者 ./configure 就行)

然后在生成Makefile之后,再make && make install 即可

debian10系统原本为64位系统,如何配置32位编译and运行环境

背景

原本安装的是debian10的64位系统,但是因为有些第三方程序是32位的,需要在这个系统上编译运行,那么需要配置一下必要的环境

当32位程序放在debian64位系统里无法编译or运行时,如何判断是因为没有配置32位编译or运行环境

一般来说,编译32位程序时,如果出现以下报错,可以认为没有配置32位编译or运行环境

1
fatal error: bits/libc-header-start.h: No such file or directory

运行32位程序时,如果出现以下报错,也可以认为没有配置32位编译or运行环境

1
2
3
//明明有执行程序test(32位的),但是执行./test的时候却报错
test:
no such file or directory

出现以上两种情况的任何一个时,一般可以判断时由于64位debian系统下,没有配置32位程序的编译or运行环境,需要执行以下两个步骤

解决方案

一、执行sudo apt-get install gcc-multilib

安装gcc-multilib

1
2
//安装gcc-multilib
sudo apt-get install gcc-multilib

二、执行sudo apt-get install g++-multilib

安装g++-multilib

1
2
//安装g++-multilib
sudo apt-get install g++-multilib

总结

因为从官网直接下载下来的debian10的64位安装镜像在安装完成后,原始的debian系统是不支持32位程序运行的,所以需要对环境进行配置,所以做个记录,免得下次忘记了

openvpn交叉编译

需要源码交叉编译openvpn的操作步骤

背景

因为某些原因,只能选择交叉编译,当前平台是64位的,但是目标平台是32位的,目标平台又禁止了apt-get等相关操作,所以必须要源码交叉编译openvpn及其相关依赖库。因为之前交叉编译的时候,大多第三方库都是有现成的CMaKeLists.txt或者是Makefile,所以编译起来倒是也方便,但是,openvpn它只有configure文件,它是需要执行./configure成功设置好各种环境变量配置以后,才能生成Makefile的。于是,一开始踩了很多坑,在此做一个记录……

一、预先配置的环境变量

export CC=指定交叉编译时gcc编译版本
export CXX=指定交叉编译时g++编译版本

二、编译openssl

完整编译选项配置如下:
setarch i386 ./config -m32 –prefix=/home/muhan/openssl/ –openssldir=/home/muhan/openssl/ -Wl,-rpath,/usr/local/openssl/lib shared

详细选项含义如下:
配置-m32 指定编译32位的库
配置–prefix 指定openssl的安装目录
配置–openssldir 指定openssl的目录
配置shared关键字 指定编译时生成动态库(libssl.so/libcrypto.so及其相关软连接)

然后再make && make install 即可

三、编译安装lzo

完整编译选项配置如下:
./configure –enable-shared –prefix=/home/muhan/lzo/usr –with-sysroot=指定交叉编译时的sysroot的路径名

详细选项含义如下:
配置–enable-shared 指定编译时生成动态库(liblzo2.so及其相关软连接)
配置–prefix 指定lzo安装路径
配置–with-sysroot 指定交叉编译时的sysroot

然后再make && make install 即可

四、编译安装openvpn

完整编译选项配置如下:
./configure CC=指定交叉编译时gcc的那个版本 –prefix=/home/muhan/openvpn/ LZO_CFLAGS=”-I/home/muhan/lzo/usr/include” LZO_LIBS=”-L/home/muhan/lzo/usr/lib -llzo2” OPENSSL_CFLAGS=”-I/home/muhan/openssl/include” OPENSSL_LIBS=”-L/home/muhan/openssl/lib -lssl -lcrypto” –disable-plugin-auth-pam

详细选项含义如下:
配置CC的版本
配置–prefix 指定openvpn安装目录
配置LZO_CFLAGS 指定lzo的头文件路径
配置LZO_LIBS 指定lzo的库文件路径及所链接的库
配置OPENSSL_CFLAGS 指定openssl的头文件路径
配置OPENSSL_LIBS 指定openssl的库文件路径及所链接的库
配置–disable-plugin-auth-pam 禁掉pam模块

然后再make && make install 即可

总结

当编译第三方库的时候,如果没有CMakeLists.txt,也没有Makefile,只有config或者configure,那么需要先了解编译选项

执行./config –help 或者 ./configure –help 来查看当前支持的编译选项
然后根据提示配置一下我们需要指定的选项,比如自己指定的openssl的版本的库和头文件路径名,比如CC的版本,比如安装路径等等
(当然,如果不需要额外配置这些东西的话,直接走默认配置的话,那么直接执行./config 或者 ./configure 就行)

然后在生成Makefile之后,再make && make install 即可

git cherry-pick 用来把某些特定提交从一个分支“拣选”到当前分支,相当于“只搬你想要的那几次提交”,而不是把整个分支合并过来。

就是说:我只要这几个 commit,不要整条分支历史。


一、什么是 cherry-pick

简单来说,cherry-pick 就是将某个分支上的特定提交(commit),复制并应用到当前分支上。

它与 Merge 的区别在于:

  • Merge:把另一个分支的所有改动一股脑合过来。
  • Cherry-pick:只把特定的某次(或某几次)提交合过来,其他的不要。

注意:Cherry-pick 会产生一个新的提交 SHA-1 值,虽然内容一样,但在 Git 看来这是一个全新的提交。


二、什么时候用 cherry-pick(使用场景)

1. 热修复回补:把线上修复同步到其它分支

常见流程:

  • release/hotfix 上修了一个 bug 并发布
  • 需要把同样的修复回补到 maindevelop(或多个维护分支)
    这时 cherry-pickmerge 更直接:只带走“修复提交”,不带走其它分支差异。

2. 挑选单个功能/提交:从别的分支拿一小段变更

例如某个 feature 分支上有一个独立的小优化提交,你当前分支也需要,但你不想把整个 feature 合并过来(因为它还没完成/包含其它改动)。

3. 修复“提交到了错误分支”

你本来应该把提交放到 feature/a,结果在 feature/b 上提交了:

  • cherry-pick 把提交拣到正确分支
  • 然后在错误分支上回滚/删除(看团队策略)

4. 分支策略限制:禁止 merge,只允许线性回补

一些团队对 release 分支要求很严格:只允许挑选经过验证的修复提交,禁止直接合并开发分支。cherry-pick 很适合这种“审计式回补”。


三、基本操作指南

1. 挑选单个提交(最常用)

假设你想把 feature 分支上的提交 a1b2c3d 应用到 main 分支。

  1. 切换到目标分支
    1
    git switch main
  2. 执行 cherry-pick
    1
    git cherry-pick a1b2c3d

2. 挑选多个提交

挑选不连续的多个提交:

1
git cherry-pick commit_id_1 commit_id_2

这会按顺序一次应用这两个提交。

挑选连续的一段提交(区间):

1
git cherry-pick start_commit_id^..end_commit_id
  • 注意中间是两个点 ..
  • start_commit_id^ 表示包含起始提交(如果不加 ^ 就不包含起始提交,这是 Git 区间的特性)。

3. 挑选提交但不立即提交(只拿代码)

如果你只想把改动拿过来,放在暂存区(Staged),不想自动生成 Commit,可以加 -n--no-commit

1
git cherry-pick -n a1b2c3d
  • 场景:你想拿过来改点东西再提交,或者你想把多个 cherry-pick 的改动合并成一个新的提交。

四、冲突处理

Cherry-pick 本质上也是一种合并操作,所以非常容易遇到冲突

当执行 git cherry-pick 遇到冲突时,Git 会暂停操作。你需要解决冲突:

  1. 查看冲突文件
    1
    git status
  2. 手动解决冲突(编辑代码,保留你想要的部分)。
  3. 标记冲突已解决
    1
    git add <path/to/conflict-file>
  4. 继续执行 cherry-pick
    1
    git cherry-pick --continue
    (此时不需要 git commit,该命令会自动弹窗让你编辑提交信息并生成提交)

如果想放弃 cherry-pick:

1
git cherry-pick --abort

这会回到操作前的状态。


五、技巧与注意事项

1. 保留原作者信息

Cherry-pick 默认会保留原提交的作者(Author)信息,但提交者(Committer)会变成你。这通常是期望的行为。

2. 加上来源标记 (-x)

如果你想在提交信息里记录这个提交是从哪里摘过来的,可以加 -x 参数:

1
git cherry-pick -x a1b2c3d

提交信息末尾会自动添加一行:(cherry picked from commit a1b2c3d...)

建议:在团队协作中,加上 -x 是个好习惯,方便溯源。

3. 避免频繁 Cherry-pick

虽然好用,但不要滥用。如果你发现自己在频繁地在分支间倒腾提交,说明你的分支策略可能出了问题。频繁 Cherry-pick 会导致产生大量重复内容的提交(Duplicate Commits),增加未来合并时的冲突风险。


六、总结

命令 作用
git cherry-pick <commit-hash> 把指定提交应用到当前分支
git cherry-pick -x <commit-hash> 应用并自动追加来源说明(推荐)
git cherry-pick -n <commit-hash> 应用改动但不生成提交(只放暂存区)
git cherry-pick --continue 解决冲突后继续
git cherry-pick --abort 放弃操作,回退

Cherry-pick,能让你在处理紧急修复、特定功能迁移时非常方便。

在 Git 的世界里,git rebase(变基)大概是最常用的命令之一。

很多人因为害怕“把代码搞丢”而只敢用 git merge,导致提交历史里充斥着无意义的 Merge branch 'master' into feature 节点,或者像蚯蚓一样弯弯曲曲的分支线。

但是如果使用 git rebase,就会把你的提交历史变成一条优雅的直线。

rebase 的核心作用:把一串提交“搬家”到新的基底上,从而让提交历史更线性、更易读。你可以把它理解为:把分支上的提交“重放”到另一个分支(或另一个提交)之后。


一、 核心概念:Rebase 到底是在干什么?

简单来说,Rebase = 重新(Re)定义起点(Base)。

想象一下,你从 master 分支切出了一个 feature 分支写代码。
在你写代码的几天里,同事向 master 推送了新代码。此时,你的 feature 分支的“地基”已经过时了。

Merge vs Rebase

  • **Merge (合并)**:保留所有历史,创建一个新的“合并节点”。就像把两条河流汇聚在一起,虽然真实,但如果合并频繁,历史线会变得像蜘蛛网一样乱。
  • Rebase (变基):把你在这个分支上的所有修改“剪”下来,然后贴到 master 的最新位置后面。就像你时光倒流,假装你的代码是刚刚基于最新的 master 写出来的。

结果:你的历史线变成了一条干净的直线。


二、 场景一:同步上游代码 (不用 -i)

场景:你在开发 feature 分支,准备提 Pull Request,但发现 master 已经更新了。为了避免冲突,或者为了让提交历史好看,你需要把 master 的新代码同步过来。

操作步骤

1
2
3
4
5
6
7
8
9
# 1. 切换到master,把最新的代码同步过来
git checkout master
git pull

# 2. 再切换到你的开发分支
git checkout feature

# 3. 执行变基(把 feature 接到 master 的最前面)
git rebase master

可能会发生什么?

如果一切顺利,Git 会自动搞定。但通常会遇到冲突(Conflict)。此时 Git 会停下来让你解决。

解决冲突流程:

  1. 打开代码编辑器,手动解决冲突文件。
  2. 将解决后的文件放入暂存区:
    1
    git add <文件名>
  3. 注意:不要 commit! 而是告诉 rebase 继续:
    1
    git rebase --continue

(如果你中途后悔了,想放弃变基,执行 git rebase --abort 即可回到操作前。)


三、 场景二:整理提交历史 (使用 -i)

这是 Rebase 最神的地方。

场景:你在开发某个功能,为了保存进度,你可能提交了这样的 Log:

  • feat: 完成登录功能
  • fix: 修复一个拼写错误
  • wip: 还没写完,先存一下
  • fix: 刚才有个 bug 没改对

这堆乱七八糟的提交如果推送到公司仓库,会被同事鄙视的。你需要把这 4 个提交合并成 1 个 完美的提交。

操作步骤

假设我们要整理最近的 4 次提交:

1
git rebase -i HEAD~4

这里的 -i 代表 Interactive(交互式)

交互界面怎么玩?

输入命令后,Git 会自动打开 Vim(或你配置的编辑器),内容大概长这样:

1
2
3
4
5
6
7
8
9
10
11
pick 1a2b3c4 feat: 完成登录功能
pick 5d6e7f8 fix: 修复一个拼写错误
pick 9g0h1i2 wip: 还没写完,先存一下
pick 3j4k5l6 fix: 刚才有个 bug 没改对

# Commands:
# p, pick = use commit (保留该提交)
# r, reword = use commit, but edit the commit message (保留提交,但修改注释)
# s, squash = use commit, but meld into previous commit (合并到上一个提交)
# f, fixup = like "squash", but discard this commit's log message (合并但丢弃注释)
# d, drop = remove commit (删除该提交)

你的任务是修改每一行开头的单词:

我们想把后面 3 个都“挤”进第 1 个里,所以修改如下:

1
2
3
4
pick 1a2b3c4 feat: 完成登录功能
s 5d6e7f8 fix: 修复一个拼写错误
s 9g0h1i2 wip: 还没写完,先存一下
s 3j4k5l6 fix: 刚才有个 bug 没改对

保存并退出(Vim 中按 Esc 然后输入 :wq)。

Git 会弹出第二个编辑器窗口,让你编写合并后的 最终 Commit Message。修改完保存退出,你的 4 个提交就变成 1 个了!


四、 黄金法则:绝对不要 Rebase 公共分支!

这是使用 Rebase 唯一的、绝对的红线。

原则:

只有当这个分支 只有你一个人在用(还是本地分支)时,你才能随便 Rebase。

为什么?
Rebase 会修改提交的 Hash ID(因为它重写了历史)。
如果你把 master 分支或者同事正在开发的 feature-shared 分支给 Rebase 了,同事那边的代码就会和远程仓库彻底对不上号,导致灾难级的冲突。

一句话总结:已推送到远程且有人协作的分支,老老实实用 Merge;本地自己玩的分支,大胆用 Rebase。


五、 总结

需求 命令 效果
同步 master 代码 git rebase master 你的分支基于最新的 master,历史是一条直线。
合并琐碎提交 git rebase -i HEAD~N 进入交互模式,使用 squash 压缩 N 个提交。
修改某次提交信息 git rebase -i HEAD~N 进入交互模式,使用 reword 修改注释。
删除某次提交 git rebase -i HEAD~N 进入交互模式,使用 drop 丢弃某次提交。
变基失败想重来 git rebase --abort 回到变基之前的状态,无事发生。

学会 Git Rebase,是提高使用git效率的必经之路~

背景

有时候可能会需要交叉编译,所以需要知道平台上编译出来的版本到底是64位还是32位

file指令查看动态库是32位还是64位

如图:file libcurl.so 查看当前编译的libcurl.so是32位还是64位的
file_so.jpg

objdump -a指令查看静态库是32位还是64位的

如图:objdump -a libtest.a 查看当前编译的静态库libtest.a是32位还是64位的
objdupm_a.jpg

readelf -h指令查看静态库or动态库是32位or64位,及编译平台运行平台等信息

如图:readelf -h libssl.so 查看编译的动态库lib
Class字段显示当前库是32位or64位
Machine字段显示当前库运行的目标机器系统

readelf_h.jpg

ifconfig指令配置网卡信息

背景

linux/unix系统下,输入ifconfig指令,发现只有lo本地回环,没有网卡信息,为了能够正常上网,需要配置一下网卡信息

当输入指令

1
2
ifconfig eth0 192.168.1.100
(可以自行设置ip地址)

会提示报错信息

1
2
SIOCSIFADDR: No such device
eth0: ERROR while getting interface flags: No such device

此时,我们会发现设置网卡eth0失败了,提示我们没有这个网卡。那么,很有可能,我们的网卡不叫“eth0”这个名字,而不是我们真的没有网卡。

ifconfig -a

输入指令

1
ifconfig -a

此时,我们可以看到系统列出来我们所有的网卡名,如下图:

ifconfig-a指令显示所有网卡

配置网卡的ip

接下来,我们发现自己系统的网卡果然没有eth0这个网卡名,但是有en0,所以,接下来我们设置来配置这个网卡的ip

1
2
ifconfig en0 192.168.1.100
(可以自行设置ip地址)

然后回车后,就发现设置ip成功啦~

总结

一般情况下,因为我们常见的linux下的网卡名字都是eth0,所以习惯性的就直接配置eth0的ip,但是有的系统并不是用eth0当网卡名,所以还是要先输入ifconfig -a指令,来查看一下我们当前的系统下,网卡的名字都是什么,然后再去配置ip

背景

在 C/C++ 开发中,项目里需要在程序运行期间加载一个外部的代码库(而不是在编译时链接),而且根据不同的配置,需要加载不同的代码库,进行热更新,那直接考虑的就是插件架构。

操作系统提供的原生 API 是完全不同的,如果是在linux下,使用libdl库。一般使用这个库就是为了做插件系统加载器(可以说是它最主要的用途了)。

主要代码

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
#include <dlfcn.h>

short loadLibDemo::realLoadLib(const string& libPath)
{
if(libPath.length() <= 0)
return -1;

libHandle = dlopen(libPath.c_str(), RTLD_GLOBAL | RTLD_NOW);
if(NULL==libHandle)
{
printf("dlopen fail %s\n",dlerror());
return -1;
}

pOne = (PFUN_APIOne)dlsym(libHandle, "one_fun_api");
if(NULL == pOne)
{
printf("[PFUN_APIOne] dlsym fail %s\n",dlerror());
return -1;
}

pTwo = (PFUN_APITwo)dlsym(libHandle, "two_fun_api");
if(NULL == pTwo)
{
printf("[PFUN_APITwo] dlsym fail %s\n",dlerror());
return -1;
}

pThree = (PFUN_APIThree)dlsym(libHandle, "three_fun_api");
if(NULL == pThree)
{
printf("[PFUN_APIThree] dlsym fail %s\n",dlerror());
return -1;
}

pFour = (PFUN_APIFour)dlsym(libHandle, "four_fun_api");
if(NULL == pFour)
{
printf("[PFUN_APIFour] dlsym fail %s\n",dlerror());
return -1;
}

pFive = (PFUN_APIFive)dlsym(libHandle, "five_fun_api");
if(NULL == pFive)
{
printf("[PFUN_APIFive] dlsym fail %s\n",dlerror());
return -1;
}

pSix = (PFUN_APISix)dlsym(libHandle, "six_fun_api");
if(NULL == pSix)
{
printf("[PFUN_APISix] dlsym fail %s\n",dlerror());
return -1;
}

return 0;
}

完整代码已经放在了https://github.com/TreeAndFlower/loadlibdemo-linux

注意点

主要是,要对导出的动态库里的API,记得“extern c”

当没有extern c的时候,编译出来的动态库里API如下:

默认是g++编译,所以有前缀后缀

当extern c添加了以后,编译出来的动态库API如下:

相当于选择了gcc编译,没有多余的Z11前缀v后缀等内容

PS:

这个示例代码,仅在linux平台下生效,因为链接的dl库,头文件是#include <dlfcn.h>,这些是linux平台的;
windows下用Kernel32.lib库,头文件是Windows.h

centos或mac下使用locate指令时,报错(/var/db/locate.database)

centos或mac下使用locate指令时,出现报错信息The locate database (/var/db/locate.database)

在centos系统下,使用locate指令报错

使用locate指令时,出现报错信息’var/lib/mlocate/mlocate.db’:No such file or directory时,处理方案如下:

1
2
3
//输入以下指令即可
updatedb
//需要等待一段时间,因为生成数据库需要一段时间

稍等一段时间之后,再输入locate指令,即可发现可以使用了

在mac系统下,使用locate指令报错

当在mac上使用locate指令时,报错如下:
使用locate指令时报错

解决方案如下:

  1. 先根据刚才的提醒,输入sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.locate.plist
    根据提示,输入launchtcl指令

  2. 然后,输入指令sudo /usr/libexec/locate.updatedb
    生成数据库

输入上面这个指令后,会等待好久一段时间,要稍微等待一会儿

  1. 最后再次输入locate指令,发现locate指令已经生效啦
    locate指令已经生效啦

参考资料

因为我之前经常在ubuntu下,都没有碰到过locate指令不好用的情况,最近需要在centos和mac下操作,忽然发现居然loacte指令使用失效,于是上网查找了解决办法,经过尝试了一些方案后,终于在centos和mac下可以使用locate指令了,因此做出了以上的总结~

感谢网友们无私分享的解决方案~
参考的网友方案:https://www.jianshu.com/p/d8f4f9e4b58c

看到项目代码里的构造函数,有时候用explicit,有时候不用,引发了我的思考,到底这个关键字什么时候用呢?

带着这个问题,我查看了一些资料。

比较官方的解释是:

explicit关键字用于禁止构造函数的隐式类型转换

当构造函数被标记为explicit时,编译器不会自动使用该构造函数进行隐式转换。


听得似懂非懂。

还是找些具体的例子看看来说吧。

没有explicit导致的重载决议歧义错误

C++ 有一个默认特性:如果一个构造函数只接受一个参数,编译器会默认认为它定义了一种“隐式转换路径”。

在这个例子里,由于单参数构造函数没有explicit,所以没有禁止隐式转换,导致重载决议歧义错误,出现编译错误。
代码如下:

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
// errDemo.cc
#include <iostream>

class Cents {
int cents_;
public:
Cents(int c) : cents_(c) { std::cout << "Cents: " << c << "\n"; }
};

class Dollar {
int dollar_;
public:
Dollar(int c) : dollar_(c) { std::cout << "Dollar: " << c << "\n"; }
};

void payBill(Cents amount) {
std::cout << "Paying with cents\n";
}

void payBill(Dollar amount) {
std::cout << "Paying with dollars\n";
}

int main() {
payBill(500); // ❌ 编译错误:ambiguous call
return 0;
}

由于两个函数的匹配程度完全相同,都需要一次用户定义的隐式转换, 编译器无法确定选择哪一个,导致编译错误
重载决议歧义错误

这时,我们通过使用 explicit,要求必须明确意图的进行调用,于是修改上面的代码

1
2
3
4
5
6
7
8
9
10
// 1. 构造函数前加explicit关键字
explicit Cents(int c) : cents_(c) { std::cout << "Cents: " << c << "\n"; } // 禁止隐式转换
explicit Dollar(int c) : dollar_(c) { std::cout << "Dollar: " << c << "\n"; } // 禁止隐式转换


// 2.明确调用意图
// payBill(500); // ❌ 此时会报编译错误:没有匹配的重载
// error: no matching function for call to ‘payBill(int)
payBill(Cents(500)); // ✅ 必须显式转换, 明确意图
payBill(Dollar(500)); // ✅ 必须显式转换, 明确意图

明确意图,显式转换


没有explicit导致的性能陷阱

在性能关键、资源敏感的软件开发中,没有使用explicit,超出预期的隐式转换,可能无意间消耗内存资源。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Matrix {
double* data;
size_t size_;
public:
Matrix(size_t size) : size_(size) {
data = new double[size * size]; // 昂贵的内存分配!
}
};

void compute(const Matrix& m);

// 性能陷阱
compute(1000); // 意外创建了1000x1000的矩阵!

这时,通过使用 explicit,要求必须明确意图的进行调用,修改上面的代码

1
2
3
4
5
6
7
8
// 1. 构造函数前加explicit关键字
explicit Matrix(size_t size) : size_(size) {
data = new double[size * size];
}

// 2.明确调用意图
// compute(1000); // ❌ 编译错误
compute(Matrix(4)); // ✅ 明确知道在创建矩阵

这种场景,主要防止的是,有可能开发人员根本没有意识到会触发内存分配,或者误在性能敏感的代码中意外创建大对象。
当然,如果的确需要创建,就显式构造,开发知道自己在干什么,可以对自己的行为负责。

加上explicit对列表初始化的影响

分析完单参数构造函数非必要最好加上explicit关键字之后,我们再看看,如果多参数构造函数加上explicit,又会有什么影响呢?

官方的说法是:当explicit构造函数接受std::initializer_list时,会失去所有数量的初值的隐式转换能力。
(说起来有点绕口,直接看例子就清晰了。)

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 Container {
public:
// 非explicit版本
Container(std::initializer_list<int> values);
};

class SafeContainer {
public:
// explicit版本
explicit SafeContainer(std::initializer_list<int> values);
};

// 对比效果
Container c1 = {}; // ✅ 0个初值 隐式转换
Container c2 = {42}; // ✅ 1个初值 隐式转换
Container c3 = {1, 2, 3}; // ✅ 多个初值 隐式转换

SafeContainer sc1 = {}; // ❌ 失去0个初值的隐式转换
SafeContainer sc2 = {42}; // ❌ 失去1个初值的隐式转换
SafeContainer sc3 = {1, 2, 3}; // ❌ 失去多个初值的隐式转换

// 必须使用直接初始化
SafeContainer sc4{}; // ✅ 0个初值
SafeContainer sc5{42}; // ✅ 1个初值
SafeContainer sc6{1, 2, 3}; // ✅ 多个初值

加上explicit对多参数构造函数的影响

如果是普通的多参数构造函数加上explicit,又会有什么影响呢?
先说答案:直接初始化仍然不受影响,但拷贝初始化会受到影响。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
class P {
public:
explicit P(int a, int b, int c);
};

P obj1{1, 2, 3}; // ✅ 直接初始化,总是可以
P obj2(1, 2, 3); // ✅ 直接初始化,总是可以


P obj3 = {1, 2, 3}; // ❌ 拷贝初始化,explicit会阻止
P obj4 = P{1, 2, 3}; // ✅ 显式构造后拷贝,可以

为什么加explicit会影响拷贝初始化呢?
分析下来是因为拷贝初始化需要两步:

  1. {1, 2, 3}创建临时对象(需要隐式调用构造函数)
  2. 将临时对象拷贝给目标变量
    explicit阻止了第一步的隐式调用。

有些场景需要加,但是有些场景我们又不应该使用explicit

拷贝和移动构造函数绝对不要加explicit

虽然 C++ 语法上允许你给拷贝/移动构造函数加上 explicit,但在工程实践中,这样做基本等于“自杀”,或者说是给使用者(包括你自己)制造巨大的麻烦。

如果说拷贝和移动构造函数加了explicit,这会发生什么?
答案是:会破坏基本语义,破坏函数传递,破坏容器使用,破坏返回值语义。
品一品,是不是这么回事。
上代码细看。

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
// 永远不要这样做
explicit MyClass(const MyClass& other); // ❌
explicit MyClass(MyClass&& other); // ❌

// 1.破坏基本语义
MyClass obj1;
MyClass obj2(obj1); // ✅ 允许:直接调用(像函数调用一样)
MyClass obj3 = obj1; // ❌ 如果拷贝构造是explicit,这会编译错误!因为等号 "=" 语义上要求隐式转换。
// 这意味着你无法再使用最自然的 = 进行赋值初始化了。

// 2.破坏函数传递
void process(MyClass obj); // 按值传递
MyClass original;
process(original); // ❌ 如果拷贝构造是explicit,无法传递参数!这是因为为了把 original 传进函数,需要构造一个临时对象。这是一次隐式动作,被 explicit 拦截了。
// 你被迫要写成这样这种恶心的代码:
process(MyClass(original)); // ✅ 显式强转
// 如果所有的类都这样写,C++ 的函数调用简直没法看了。

// 3.破坏容器使用
std::vector<MyClass> vec;
MyClass obj;
vec.push_back(obj); // ❌ 容器无法工作!
// 标准库容器依赖于拷贝/移动语义
// 很多 STL 操作内部逻辑是:在内存中放置新对象时,使用了 = 语义或者隐式构造语义。
// 如果拷贝/移动构造函数是 explicit 的,你的类基本上就告别 STL 标准库了,除非你用非常小心翼翼的 emplace 操作,但也很容易踩雷。

// 4.破坏返回值语义
MyClass createObject() {
MyClass obj;
return obj; // ❌ 无法返回对象!
}
// 在语义检查阶段,return 语句被视为一种“隐式构造”

关于拷贝/移动构造函数加explicit会破坏返回值语义,有些小伙伴可能像我一样,在这里会有一个疑问:“C++ 不是有 RVO/NRVO (返回值优化) 吗?编译器不是会把拷贝直接消除掉吗?既然消除了,为什么还要检查构造函数?”

这是因为这里有两步:

语义检查 (Semantic Check):编译器的第一步是检查“如果你要拷贝,代码写得对不对”。这一步要求拷贝/移动构造函数必须是可访问的且非 explicit 的。

代码生成与优化 (Code Generation & Optimization):只有通过了语义检查,编译器才会进行第二步优化(RVO),在运行时消除这次拷贝。

所以,结论就是:即使 RVO 会在运行时消除拷贝,explicit 依然会在编译时导致报错,因为它阻断了语义检查阶段的合法性。


正确的做法

1
2
3
4
5
6
7
8
9
10
class MyClass {
public:
// ✅ 正常的拷贝和移动构造函数
MyClass(const MyClass& other) = default;
MyClass(MyClass&& other) = default;

// 或者如果不需要拷贝,就删除它们
MyClass(const MyClass&) = delete;
MyClass(MyClass&&) = delete;
};

总结

explicit 的核心目的是防止意外的类型转换(比如把 int 变成 String)。

但是,拷贝和移动本身就是同一种类型的传递,它们在 C++ 语言层面被设计为一种“自然流动”的操作。阻断这种流动(加上 explicit),就会导致这个类在 C++ 的生态系统中寸步难行。

参考 Google C++ 编程规范的建议,我的使用原则目前是:

(1)原则上,所有的单参数构造函数都应该加上 explicit
除非你真的希望用户使用这种隐式转换带来便利。(例如模拟基础数据类型)

(2)多参数构造函数(C++11 之前通常不需要,但 C++11 引入了列表初始化 {})按具体场景决定加不加explicit
如果构造函数支持列表初始化,且你不想让 {1, 2} 隐式变成你的对象,也要加 explicit。

(3)拷贝/移动构造函数,绝对不要加 explicit

(大家有什么使用习惯吗?欢迎评论区交流)