# c++学习笔记 **Repository Path**: luo_jun99/cpp_learning ## Basic Information - **Project Name**: c++学习笔记 - **Description**: No description available - **Primary Language**: C++ - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2022-01-12 - **Last Updated**: 2026-03-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # c/c++ 开发环境搭建 > 所有资源参考 https://www.bilibili.com/video/BV1sW411v7VZ?p=4&spm_id_from=pageDriver ## 一、在wsl中安装 linux > wsl官方文档: https://docs.microsoft.com/zh-cn/windows/wsl/basic-commands ### 1、列出已安装的 Linux 发行版 ```cmd wsl --list --verbose ``` ### 2、将 WSL 版本设置为 1 或 2 ```cmd wsl --set-version Ubuntu-20.04 2 ``` ### 3、进入 ubuntu-20.04 ```cmd PS C:\Users\aa> ubuntu2004.exe # 查看 ubuntu-20.04 所在的路径 # 注意要加上 .exe 后缀 PS C:\Users\aa> where.exe ubuntu2004.exe C:\Users\aa\AppData\Local\Microsoft\WindowsApps\ubuntu2004.exe # 可以将上面的路径设置为 idea terminal 的默认路径 # idea -> file -> settings -> tools -> terminal -> application settings -> shell path # 之后 idea 中的 terminal 默认就是使用 ubuntu-20.04 了 ``` ### 4、idea中直接使用 ubuntu-20.04 的缺点 ```cmd # 打开时,工作目录只能在 家目录,不能自动切换到当前工程的目录 PS C:\Users\aa> ubuntu2004 -h Launches or configures a Linux distribution. Usage: Launches the user's default shell in the user's home directory. install [--root] Install the distribuiton and do not launch the shell when complete. --root Do not create a user account and leave the default user set to root. run Run the provided command line in the current working directory. If no command line is provided, the default shell is launched. config [setting [value]] Configure settings for this distribution. Settings: Sets the default user to . This must be an existing user. help ``` ### 5、在windows资源管理器的任意路径使用命令 ```cmd # 先拿到 bash 的路径 PS C:\Users\aa> where.exe bash C:\Windows\System32\bash.exe # 可以看出和 cmd 在同一个路径 PS C:\Users\aa> where.exe cmd C:\Windows\System32\cmd.exe # 实战的时候,使用 ubuntu2004 命令也可以,虽然这个命令在其他路径 ``` ### 6、idea 中使用 bash.exe 启动,并切换到 ubuntu-20.04 的 fish ```cmd # idea 的终端路径设置为 C:\Windows\System32\bash.exe # 进入 ubuntu-20.04 C:\Users\aa>ubuntu2004 Welcome to fish, the friendly interactive shell Type help for instructions on how to use fish luo@DESKTOP-83PJLKP ~> vi .bashrc # 最末尾添加 fish # 通过 bash.exe 登录之后,会自动切换到 fish luo@DESKTOP-83PJLKP ~> tail -n 1 .bashrc fish ``` ### 7、ubuntu-20.04 镜像源 需要注意版本 ubuntu 镜像源的版本 ```shell deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse deb-src http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse deb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse deb-src http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse deb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse deb-src http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse deb http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse deb-src http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse deb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse deb-src http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse 原文链接:https://blog.csdn.net/u010953609/article/details/106758854/ ``` ## 二、在linux中安装编译器和debug工具 ```shell # 注意:一定要 ubuntu 的 apt 源一定要匹配 对应的 20.04 版本,否则以下工具会安装不上 sudo apt install llvm -y sudo apt install -y clang sudo apt install -y clangd ``` ## 三、在vscode中安装相关插件 ### 1、Remote - WSL ### 2、通过 Remote - WSL 插件将以下插件安装到wsl的linux子系统中 ### 3、clangd ### 4、CodeLLDB > 注意:因为网络原因,很有可能会安装失败:可以到插件详情页找到插件仓库,具体地址:https://github.com/vadimcn/vscode-lldb/releases/tag/v1.6.10 > 一定要下载 linux 版本的 > 然后使用vscode快捷键 ctrl + shift + p > 输入 vsix 将下载的 codeLLDB 安装到 wsl linux 子系统 ### 5、配置 .vscode 以本工程为样例,进行配置,即可开启 debug 调试 c/c++ #### (1)launch.json #### (2)tasks.json ## 四、使用vscode配合cmake tools插件调试CMakeLists.txt项目 ### 1、在linux中安装cmake ```shell sudo apt install -y cmake ``` ### 2、vscode 中安装 cmake 和 cmake tools 插件 ### 3、根目录创建 CMakeLists.txt ### 4、vscode中使用命令行配置CMake ```shell # 注意 有 > 才是执行命令 >CMake:Configure ``` > 然后会选择工具链,可选 clang 或 gcc > 在最下面的状态栏就能 点击 build 按钮 > 接下来可以删除 .vscode/tasks.json 文件,因为不再需要了 ### 5、使用cmake tools 配置 debug > 参考:https://github.com/microsoft/vscode-cmake-tools/blob/main/docs/debug-launch.md#debug-using-a-launchjson-file ```shell # 其中有 // Resolved by CMake Tools: "program": "${command:cmake.launchTargetPath}", #将其覆盖到 .vscode 下面的 launch.json 中 ``` > 之后选择 f5 就能进行调试 ### 6、为clangd添加静态分析配置文件 > 每次 build 之后,在build 目录都会有一个 compile_commands.json 文件 > 可以将其告知 clangd,让clangd 以支持静态分析优化 > 方法:在vscode的设置中搜索 clangd 添加以下参数 > --compile-commands-dir=${workspaceFolder}/build ### 7、使用llvm工具链格式化代码 在根目录下新建 `.clang-format` 文件,此文件为yaml格式 添加设置 > 配置来源: https://gist.github.com/Goose-Bomb/bacc64efade1fa6398dea8685245895c > 官方文档: https://clang.llvm.org/docs/ClangFormatStyleOptions.html ```yaml BasedOnStyle: LLVM # 继承自哪一个基础配置,在其基础上进行修改 UseTab: Never IndentWidth: 4 DerivePointerAlignment: false PointerAlignment: true AlwaysBreakAfterReturnType: None AlwaysBreakTemplateDeclarations: true AlwaysBreakBeforeMultilineStrings: true AlignOperands: true AlignAfterOpenBracket: true AlignConsecutiveBitFields: true AlignConsecutiveMacros: true ConstructorInitializerAllOnOneLineOrOnePerLine: true AllowAllConstructorInitializersOnNextLine: false BinPackArguments: false BinPackParameters: false IncludeBlocks: Regroup ``` 之后,使用右键格式化,就能使用了 ## 五、wsl-ubuntu-20.04 启用 openssh-server ### 1、启动 openssh-server ```shell # 先安装 openssh-server sudo apt search sshd # 没有key,启动失败 luo@DESKTOP-83PJLKP /e/ssh> service ssh restart sshd: no hostkeys available -- exiting. # 生成 key ssh-keygen -A ssh-keygen: generating new host keys: DSA Could not save your public key in /etc/ssh/ssh_host_dsa_key.1gWiN3yK7t: Permission denied # 提示没有权限 sudo ssh-keygen -A ssh-keygen: generating new host keys: DSA # 更改配置为:允许密码登录 luo@DESKTOP-83PJLKP /e/ssh> vi sshd_config PasswordAuthentication yes # 手动启动 ssh luo@DESKTOP-83PJLKP /e/init.d> sudo ./ssh start * Starting OpenBSD Secure Shell server sshd ``` ### 2、配置 ssh 免密登录 服务器操作 ```shell # 编辑配置文件 sudo vi /etc/ssh/sshd_config # 启用认证文件 # Expect .ssh/authorized_keys2 to be disregarded by default in future. AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2 # 在 家目录 创建 .ssh 目录 # 并创建 .ssh/authorized_keys touch ~/.ssh/authorized_keys ``` 客户端操作 ```shell # 在 client 客户机 上生成 客户机的公钥和私钥 ssh-keygen -t rsa -C 1007052116@qq.com # 列出文件 luo@DESKTOP-83PJLKP ~/.ssh> ll total 8.0K # 私钥 -rw------- 1 luo luo 2.6K Feb 25 09:04 id_rsa # 公钥 -rw-r--r-- 1 luo luo 571 Feb 25 09:04 id_rsa.pub # 将公钥的内容 拷贝到 服务器的 /home/luo/.ssh/authorized_keys 中 ``` 重新加载 sshd 配置文件 ```shell sudo /etc/init.d/ssh reload sudo /etc/init.d/ssh stop sudo /etc/init.d/ssh start sudo /etc/init.d/ssh status ``` ### 3、ubuntu 列出已安装的软件列表 ```shell # 获取在本地安装的软件包列表 apt list --installed ``` ## 六、gcc编译器 ### 1、编译引用了`pthread.h`的程序 ```shell # gcc编译时,需要添加 -lpthread 参数 gcc timedlock.c -lpthread ``` ```shell # CMake 编译时,需要在CMakeLists.txt中添加 # 首先调用一个CMake模块,适合此平台的线程包,然后设置CMAKE_THREAD_LIBS_INIT变量(以及其他一些变量)。它不告诉CMake将任何可执行文件与它找到的任何线程库链接起来。您告诉CMake使用 target_link_libraries()命令将可执行文件与线程库链接起来。所以,举个例子,让我们说你的程序叫做测试。要将它链接到需要的线程: # 扫描 pthread 库 find_package(Threads) # 将 pthread 库 链接到 main 中 target_link_libraries(main ${CMAKE_THREAD_LIBS_INIT}) ``` ### 2、pthread_create() ```c // 注意: extern int pthread_create (pthread_t *__restrict __newthread, const pthread_attr_t *__restrict __attr, void *(*__start_routine) (void *), void *__restrict __arg) __THROWNL __nonnull ((1, 3)); // 其中第三个参数void *(*__start_routine) (void *) // 只能接受 void* aa(void * args); 类型的方法,不能将指针类型从 void* 改为其他任何类型,不然编译器会警告 ``` # c++ 智能指针理解 ## 一、为什么为了配合unique_ptr,方法要把参数声明为 T t,而不是引用,然后在调用方使用move传递右值,这是最佳实践吗,如果T没有移动构造函数,是不是不能用move,导致只能退化到使用拷贝构造函数,而unique_ptr删除了拷贝构造函数,导致只能使用move和移动构造函数 ### 🎯 结论先行 > 是的,这是目前公认的最佳实践(通常被称为 "Take by value" 或 "Copy/move then move" 惯用法)。 > > 这种写法之所以好,是因为它虽然看起来像是多了一次拷贝(或移动),但它极其通用且异常安全。对于 unique_ptr 这种只能移动不能拷贝的类型,编译器会自动处理,不会出现你担心的“退化到拷贝”的问题。 std::unique_ptr 的特性: 它删除了拷贝构造函数(delete),只保留了移动构造函数。 ### 编译器的匹配机制: * 当你传入一个左值(具名变量)时:func(ptr)。编译器发现 T 是 unique_ptr,它想去调用拷贝构造函数来初始化参数 t,结果发现拷贝被禁用了,于是编译直接报错。 * 当你传入一个右值(临时对象)时:func(std::move(ptr)) 或 func(make_unique<...>())。编译器发现没有拷贝构造函数,但有移动构造函数,于是自动调用移动构造函数。 > 关键点: 这种写法其实是强制要求调用方必须使用 std::move 或者传入临时对象。这恰恰符合 unique_ptr 的设计哲学——“明确的所有权转移”。 ### 最佳实践:传值(推荐) ```c++ void setPtr(std::unique_ptr t) { member_ptr = std::move(t); } ``` 调用方式: * setPtr(std::make_unique()); // 临时对象,完美 * setPtr(std::move(local_ptr)); // 显式移动,清晰表达了所有权转移的意图 ### 优势 * 代码最简洁: 只需要写一个函数。 * 通用性最强: 它能完美处理所有类型的 T(无论是 int、std::string 还是 std::unique_ptr)。 * 意图最明确: 参数是“值”传递,意味着函数内部拥有了该对象的一个副本(对于 unique_ptr 来说,这个“副本”其实是通过移动“偷”来的)。 ### 💡 最终建议 你的理解完全正确:因为 unique_ptr 删除了拷贝构造函数,所以当参数是 T t 时,编译器别无选择,只能使用移动构造函数。 ### unique_ptr 实现原理 #### 禁用拷贝构造函数和赋值操作 ```c++ // 禁止通过拷贝构造创建新对象 unique_ptr(const unique_ptr&) = delete; // 禁止通过赋值操作让已有对象指向别人的资源 unique_ptr& operator=(const unique_ptr&) = delete; ``` ##### ✅ 结果: ```c++ unique_ptr p2 = p1; → 编译错误 p2 = p1; → 编译错误 ``` 这样就从根源上杜绝了双重释放的风险。 #### 使用移动构造和移动赋值实现 unique_ptr 的唯一性 与拷贝不同,unique_ptr 提供了移动构造函数和移动赋值运算符: ```c++ unique_ptr(unique_ptr&& other) noexcept; // 移动构造 unique_ptr& operator=(unique_ptr&& other) noexcept; // 移动赋值 ``` 它们的行为是: * “偷走” other 的指针 * 把 other 的指针置为 nullptr * 原对象不再拥有资源,不会在析构时释放 这样就实现了安全的所有权转移,而不是危险的共享。 ##### 那么unique_ptr 在传入方法或者从方法返回调用的是移动构造还是移动赋值呢 ###### 简单来说: * 从函数返回、传参(按值)、用返回值初始化变量 → 调用 移动构造函数 * 把返回值或右值赋给一个已存在的变量 → 调用 移动赋值运算符 ###### ✅ 场景一:调用 移动构造函数(创建新对象) 1. 函数返回 unique_ptr ```c++ std::unique_ptr create() { return std::make_unique(42); // 返回临时对象 } auto p = create(); // ← 这里调用 移动构造函数 ``` p 是新定义的变量。 * 编译器用 create() 返回的临时 unique_ptr 构造 p。 * → 调用 unique_ptr(unique_ptr&&)(移动构造函数)。 > 📝 注:现代编译器通常会优化掉这次移动(RVO/NRVO),但语义上仍是“可移动构造”。 2. 按值传参(参数是 unique_ptr) ```c++ void foo(std::unique_ptr param) { /* ... */ } auto p = std::make_unique(10); foo(std::move(p)); // ← 调用 移动构造函数 初始化 param ``` * param 是函数内部的新对象。 * 它通过移动 std::move(p) 的结果来构造自己。 * → 调用移动构造函数。 3. 直接初始化新变量 ```c++ auto p1 = std::make_unique(1); auto p2 = std::move(p1); // ← p2 是新变量,调用 移动构造函数 ``` ###### ✅ 场景二:调用 移动赋值运算符(修改已有对象) 1. 将返回值赋给已有变量 ```c++ std::unique_ptr p; // 已存在(当前为 nullptr) p = create(); // ← 调用 移动赋值运算符 ``` * p 已经存在(可能持有资源,也可能为空)。 * create() 返回一个临时 unique_ptr。 * p = ... 表示赋值,不是初始化。 * → 调用 operator=(unique_ptr&&)(移动赋值运算符)。 > 💡 移动赋值内部会: > > 先 delete 掉 p 原本管理的内存(如果有的话) > > 再“偷走”右边临时对象的指针 2. 用 std::move 赋值给已有变量 ```c++ auto p1 = std::make_unique(1); auto p2 = std::make_unique(2); p2 = std::move(p1); // ← p2 已存在,调用 移动赋值运算符 ``` ##### 简单验证 > ⚠️ 注意:在 Release 模式下,a = create() 可能因 RVO(返回值优化) 而不调用任何构造函数,直接在 a 的位置构造。但语义上它仍然是“可移动构造”的。 [点击这里直接跳转源代码进行测试](./user_programs/3_my_unique_ptr/test.cpp) ```c++ #include struct MyPtr { int* ptr; MyPtr(int v) : ptr(new int(v)) {} // 禁止拷贝构造 MyPtr(const MyPtr& other) = delete; // 禁止拷贝赋值 MyPtr& operator=(const MyPtr& other) = delete; // 移动构造 MyPtr(MyPtr&& other) noexcept { std::cout << "Move constructor\n"; ptr = other.ptr; other.ptr = nullptr; } // 移动赋值 MyPtr& operator=(MyPtr&& other) noexcept { std::cout << "Move assignment\n"; if (this != &other) { delete ptr; // 清理自己原来的资源 ptr = other.ptr; // 偷走对方的 other.ptr = nullptr; // 让对方变空 } return *this; } ~MyPtr() { delete ptr; } }; MyPtr create() { return MyPtr(42); } int main() { MyPtr a = create(); // 输出: Move constructor (或被 RVO 优化掉) MyPtr b(10); b = create(); // 输出: Move assignment } ``` ## 二、把左值/右值转换为右值-> std::move()函数解析 ```c++ template typename remove_reference::type&& move(T&& t) { return static_cast::type&&>(t); } ``` ### 🧩 1. T&& t:万能的接收器 这叫**“转发引用(Forwarding Reference)”或“通用引用”**。 它的神奇之处: 它既能接收左值(比如 int a),也能接收右值(比如 10)。 引用折叠(Reference Collapse): 这是 C++ 的一条底层规则,保证了 T&& 的灵活性。 如果你传进来一个左值 int a,编译器会自动把 T 推导为 int&,然后 T&& 就变成了 int& &&,根据折叠规则最后变成 int&(左值引用)。 如果你传进来一个右值 int(10),T 就是 int,T&& 就是 int&&。 简单说: T&& 是一个“变形金刚”,你给它什么,它就能变成什么类型的引用来接收。 ### 🧩 2. remove_reference::type:去皮留馅 这是为了处理一个细节:如果传进来的是一个引用(比如 int&),我们只想拿到它原本的类型(int)。 std::remove_reference 是一个模板元函数,它的工作就是把 T& 或 T&& 上的“引用符号”扒掉,只剩下 T。 为什么要这么做? 因为我们最终要返回的是 T&&(右值引用)。如果 T 本身已经带引用了(比如 int&),我们得先把它变成 int,然后再加 &&,变成 int&&。 ### 🧩 3. static_cast<...&&>:强制变身(核心动作) 这是 std::move 唯一干活的地方。 它把传进来的参数 t,强制转换成“右值引用”类型。 这就是那个“谎言”: 即使 t 是一个有名字的变量(左值),经过这个 static_cast 一转,编译器就“假装”看不见它的名字了,把它当成一个临时的右值。