C++
C++是一种功能非常强大的编程语言,可以广泛用于很多领域,在服务器开发,音视频,高性能计算等,需要高效率的场景具有很大的优势,程序员深入学习C++很有必要
C++ 开发环境
C++可以跨平台,可以开发很多领域,那么自然也有很多不同的开发环境配置,主要是根据平台,编译器,领域这几个方面来选用适合的开发环境
选择策略:
一般来说如果windows平台 + msvc编译器,那么直接使用Vistual Studio。
跨平台开发 + gcc/g++编译器,可以使用vscode或者CLion
Visual Studio
VS项目的创建与编译
创建一个vs项目的时候,提供了很多种选项,例如控制台项目,windows桌面应用程序。
它们之间的区别:
- 提供的初始代码不同 (很容易理解,功能不同,提供的初始代码也会不同)
- 使用的链接库不同(重点)
这里演示一下VS中,如何查看和更改链接库:
VS的解决方案下可以包含多个项目,vs是以文件组成的项目为最小编译单元的,也就是说vs无法单独编译一个源文件。
VS项目调试
vs项目调试方法:
在想要停止的语句的上打断点,然后调试(快捷键 F5),程序机会在断点行执行前停下。
需要知道的是,为了支持调试功能,编译和链接必须添加很多额外的东西,所以使用 debug功能,必须以调试的方式进行编译;直接运行(快捷键 CTRL + F5)是无法调试的。
VS解决方案,项目,目录结构详解
一个正常的VS项目目录结构如下
我把每一个文件都通过记事本打开看了一遍,总结了这些文件的作用。
项目配置文件
demo1.vcxproj 项目配置文件,添加/导入项目时,就是使用这个文件
demo1.vcxproj.filters 项目配置目录文件,里面配置了IDE中项目的目录信息
demo1.vcxproj.user 项目用户配置文件,用户的自定义配置,一般把项目给第三方的时候,删除这个文件
编译文件
Debug文件夹 存储32位平台下的Debug模式下编译后的文件
Release文件夹 存储32位平台下的Release模式下编译后的文件
x64文件 存储64位平台下的编译文件,里面还有Debug,Release文件夹,分别存储不同模式下编译后的文件
相关源码
.cpp 源文件
.h 头文件
一个正常的VS解决方案的目录结构如下
解决方案的配置文件
.vs文件夹 解决方案中的一些详细配置信息
demo1.sln 解决方案的配置文件,打开解决方案时,使用这个文件
项目
demo1 解决方案中的每个项目对应每个文件夹
编译文件
Debug, Release,x64 这些与项目结构类似,存放不同平台不同模式下编译后的文件
VS 项目重命名
VS项目的重命名,如果直接在IDE中重命名项目,项目目录中的配置文件,编译文件,以及项目本身的名字是不会更改的,这不是我们想要的,所以以下给出项目真正重命名的方法:
这里先说一下卸载项目和移除项目的区别:
卸载项目:解决方案中卸载项目后,解决方案中显示项目已卸载,生成解决方案的时候不会编译这个项目;之后我们只要不改变项目目录名称,以及项目配置文件名称,我们还是可以直接在IDE中重新加载进来的;
移除项目:解决方案中直接将项目排除,排除后解决方案中看不到该项目,不能在IDE中重新加载了;只能通过添加新项目的方式,将项目重新添加进来;
重命名项目:
- IDE中移除项目
- 将项目目录,项目配置文件(.vcxproj文件,.vcxproj.filters文件) 重命名为新项目名称
- 删除编译文件(Debug文件夹,x86文件夹),项目用户配置文件(.vcxproj.user文件),保留相关源码(.h,.cpp文件)
- IDE中重新添加项目,重新生成
重命名项目可能遇到的问题:
IDE中重命名项目,未将对象引用设置到对象的实例,说明解决方案没有同步,把解决方案下的.vs文件夹删掉,然后再重新打开解决方案,重命名项目;
VS常用快捷键
注释/取消注释:CTRL + K, CTRL + C/U;
代码格式化:CTRL + K, CTRL + E;
vscode + CMake + clangd
Vscode配置C++
vscode配置C++开发环境,一般使用MinGW,直接参考官方教程:https://code.visualstudio.com/docs/cpp/config-mingw
然后记得配置环境变量,工具链中所有的可执行文件都放入了C:/msys64/ucrt64/bin该目录,所以我们把这个目录添加到环境变量
注意,使用不同的shell,环境变量的选取和加载顺序不同,例如git bash shell,它会先加载git bash相关的环境变量,再添加主机的环境变量,qt也是同理,先加载qt文件的环境变量,再添加主机的环境变量,但是msys2,它只会添加自己的环境变量,不会添加主机的环境变量,它和主机之间是完全隔离的
MSYS2, MSYS, MinGW之间的关系
MSYS2、MSYS和MinGW之间的关系可以通过它们的历史背景和功能来理解。以下是对这三者的详细讲解:
(1) MSYS(Minimal SYStem)
- 定义:MSYS是一个轻量级的Unix环境,旨在为Windows用户提供一个类Unix的命令行界面。它最初是为了支持MinGW(Minimalist GNU for Windows)项目而开发的。
- 功能:MSYS提供了一些基本的Unix工具(如bash、make、grep等),使得在Windows上使用GNU工具链变得更加容易。它允许开发者在Windows上编写和运行Unix风格的脚本。
- 局限性:MSYS的功能相对有限,主要用于提供一个基本的开发环境,支持一些简单的构建和编译任务。
(2) MinGW(Minimalist GNU for Windows)
- 定义:MinGW是一个为Windows平台提供GNU工具链的项目,允许开发者在Windows上编译和运行本地的Windows应用程序。
- 功能:MinGW提供了GCC(GNU Compiler Collection)编译器及其相关工具,支持C、C++等语言的编译。它生成的可执行文件是Windows本地的,不依赖于Cygwin等其他层。
- 局限性:MinGW本身并不提供完整的Unix环境,主要关注于编译和构建Windows应用程序。
(3) MSYS2
-
定义:MSYS2是MSYS的一个更新和扩展版本,结合了MSYS和MinGW的优点,提供了一个更现代化的开发环境。
-
功能:
- 包管理:MSYS2使用
pacman作为包管理器,允许用户轻松安装、更新和管理软件包。 - 多种环境支持:MSYS2支持MSYS(用于Unix命令行工具)和MinGW-w64(用于编译Windows本地应用程序),同时支持32位和64位编译。
- 丰富的软件库:MSYS2提供了大量的预编译软件包,用户可以通过包管理器轻松获取所需的工具和库。
- 包管理:MSYS2使用
-
优势:MSYS2比MSYS更强大,提供了更好的包管理和更新机制,适合现代开发需求。
(4) 总结
- MSYS是一个基本的Unix环境,主要用于支持MinGW。
- MinGW是一个为Windows提供GNU编译器的项目,专注于编译Windows应用程序。
- MSYS2是MSYS的现代化版本,结合了MSYS和MinGW的优点,提供了更强大的功能和更好的用户体验。
这三者之间的关系可以看作是一个演变过程,MSYS2是对MSYS和MinGW的整合与扩展,旨在为Windows开发者提供一个更完整的开发环境。
vscode中使用CMake
在vscode中使用cmake,直接安装extenstion:cmake tools,就会打包安装所有cmake相关的工具
CMake内置命令
(1) 项目和版本相关命令
cmake_minimum_required
设置项目所需的最低 CMake 版本
cmake_minimum_required(VERSION 3.10)
project
定义项目名称和支持的语言
project(MyProject
VERSION 1.0
LANGUAGES CXX C
)
(2) 目标创建命令
add_executable
创建可执行文件目标
add_executable(app main.cpp utils.cpp)
add_library
创建库目标
# 静态库
add_library(mylib STATIC source1.cpp source2.cpp)
# 动态库
add_library(mylib SHARED source1.cpp source2.cpp)
# 模块库
add_library(mylib MODULE source1.cpp source2.cpp)
add_custom_target
创建自定义目标
add_custom_target(docs
COMMAND doxygen Doxyfile
)
(3) 目标配置命令
target_link_libraries
为目标链接库
target_link_libraries(app
PRIVATE mylib
PUBLIC otherlib
)
target_include_directories
设置目标的头文件包含路径
target_include_directories(app
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include
PUBLIC /usr/local/include
)
target_compile_definitions
为目标添加编译宏定义
target_compile_definitions(app
PRIVATE DEBUG_MODE
PUBLIC USING_LIBRARY
)
target_compile_options
设置编译选项
target_compile_options(app
PRIVATE -Wall -Wextra
PUBLIC -O2
)
(4) 条件和控制流命令
if/elseif/else/endif
条件判断
if(UNIX)
# Unix 系统特定配置
elseif(WIN32)
# Windows 系统特定配置
else()
# 其他系统
endif()
option
定义可选编译选项
option(USE_OPENGL "Enable OpenGL support" ON)
if(USE_OPENGL)
find_package(OpenGL REQUIRED)
endif()
(5) 变量和路径相关命令
set
设置变量
# 普通变量
set(SOURCES main.cpp utils.cpp)
# 缓存变量
set(MY_VAR "value" CACHE STRING "Description")
# 环境变量
set(ENV{PATH} "$ENV{PATH}:/new/path")
list
列表操作
# 追加元素
list(APPEND SOURCES extra.cpp)
# 删除元素
list(REMOVE_ITEM SOURCES extra.cpp)
# 排序
list(SORT SOURCES)
(6) 查找和依赖命令
find_package
查找外部依赖库
find_package(OpenCV REQUIRED)
find_package(Boost COMPONENTS system filesystem)
find_path
查找头文件路径
find_path(HEADER_DIR "myheader.h"
PATHS /usr/include /usr/local/include
)
find_library
查找库文件
find_library(MATH_LIB m
PATHS /usr/lib /usr/local/lib
)
(7) 安装和导出命令
install
定义安装规则
# 安装可执行文件
install(TARGETS app
RUNTIME DESTINATION bin
)
# 安装头文件
install(FILES header.h
DESTINATION include
)
(8) 其他实用命令
message
输出消息
message(STATUS "Configuring project")
message(WARNING "This is a warning")
message(FATAL_ERROR "Compilation cannot continue")
include
包含其他 CMake 脚本
include(CMakePrintHelpers)
include(GNUInstallDirs)
(9) 高级命令
macro/function
定义可重用的 CMake 代码块
# 函数
function(my_function ARG1 ARG2)
message(STATUS "Function called with ${ARG1} and ${ARG2}")
endfunction()
# 宏
macro(my_macro ARG1)
message(STATUS "Macro called with ${ARG1}")
endmacro()
(10) 综合示例
MyProject/
├── CMakeLists.txt
├── main.cpp
├── utils.cpp
├── source1.cpp
├── source2.cpp
├── header.h
└── Doxyfile # 如果你使用 Doxygen 文档生成
# (1) 项目和版本相关命令
cmake_minimum_required(VERSION 3.10) # 设置所需的最低 CMake 版本
project(MyProject VERSION 1.0 LANGUAGES CXX C) # 定义项目名称和支持的语言
# (2) 目标创建命令
add_executable(app main.cpp utils.cpp) # 创建可执行文件目标
add_library(mylib STATIC source1.cpp source2.cpp) # 创建静态库目标
# (3) 目标配置命令
target_link_libraries(app PRIVATE mylib) # 为目标链接库
target_include_directories(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) # 设置头文件包含路径
target_compile_definitions(app PRIVATE DEBUG_MODE) # 添加编译宏定义
target_compile_options(app PRIVATE -Wall -Wextra) # 设置编译选项
# (4) 条件和控制流命令
if(UNIX)
message(STATUS "Configuring for Unix")
elseif(WIN32)
message(STATUS "Configuring for Windows")
else()
message(STATUS "Configuring for other systems")
endif()
# 可选编译选项
option(USE_OPENGL "Enable OpenGL support" ON)
if(USE_OPENGL)
find_package(OpenGL REQUIRED) # 查找 OpenGL 库
target_link_libraries(app PRIVATE OpenGL::GL) # 链接 OpenGL
endif()
# (5) 变量和路径相关命令
set(SOURCES main.cpp utils.cpp) # 设置源文件列表
list(APPEND SOURCES extra.cpp) # 追加额外的源文件
message(STATUS "Sources: ${SOURCES}") # 输出源文件列表
# (6) 查找和依赖命令
find_package(Boost COMPONENTS system filesystem REQUIRED) # 查找 Boost 库
find_path(HEADER_DIR "myheader.h" PATHS /usr/include /usr/local/include) # 查找头文件路径
find_library(MATH_LIB m PATHS /usr/lib /usr/local/lib) # 查找库文件
# (7) 安装和导出命令
install(TARGETS app RUNTIME DESTINATION bin) # 安装可执行文件
install(TARGETS mylib DESTINATION lib) # 安装库文件
install(FILES header.h DESTINATION include) # 安装头文件
# (8) 其他实用命令
message(STATUS "Configuring project: ${PROJECT_NAME} v${PROJECT_VERSION}") # 输出项目配置消息
include(CMakePrintHelpers) # 包含其他 CMake 脚本
# (9) 高级命令
function(my_function ARG1 ARG2) # 定义函数
message(STATUS "Function called with ${ARG1} and ${ARG2}")
endfunction()
macro(my_macro ARG1) # 定义宏
message(STATUS "Macro called with ${ARG1}")
endmacro()
# 调用函数和宏
my_function("Hello" "World")
my_macro("Test")
cmake内置命令
以下是 CMake 中最常用和重要的内置变量详细解析:
(1) 路径相关变量
项目路径
CMAKE_SOURCE_DIR # 顶层源代码目录(顶层 CMakeLists.txt 所在目录)
CMAKE_CURRENT_SOURCE_DIR # 当前处理的 CMakeLists.txt 所在目录
CMAKE_BINARY_DIR # 顶层构建目录
CMAKE_CURRENT_BINARY_DIR # 当前构建目录
示例:
message(STATUS "项目根目录: ${CMAKE_SOURCE_DIR}")
message(STATUS "当前源码目录: ${CMAKE_CURRENT_SOURCE_DIR}")
(2) 系统和编译器相关变量
系统识别
CMAKE_SYSTEM_NAME # 操作系统名称(Linux, Windows, Darwin)
CMAKE_SYSTEM_VERSION # 操作系统版本
CMAKE_SYSTEM_PROCESSOR # 处理器架构
# 平台判断
WIN32 # Windows 平台
UNIX # Unix 类系统
APPLE # macOS 系统
LINUX # Linux 系统
编译器相关
CMAKE_CXX_COMPILER # C++ 编译器路径
CMAKE_C_COMPILER # C 编译器路径
CMAKE_COMPILER_IS_GNUCXX # 是否为 GCC 编译器
MSVC # 是否为 MSVC 编译器
#编译器版本
CMAKE_CXX_COMPILER_VERSION # C++ 编译器版本
示例:
if(WIN32)
message(STATUS "当前系统: Windows")
elseif(UNIX)
message(STATUS "当前系统: Unix-like")
endif()
message(STATUS "编译器: ${CMAKE_CXX_COMPILER}")
message(STATUS "编译器版本: ${CMAKE_CXX_COMPILER_VERSION}")
(3) 构建类型相关变量
CMAKE_BUILD_TYPE # 构建类型(Debug, Release, RelWithDebInfo, MinSizeRel)
CMAKE_CONFIGURATION_TYPES # 多配置生成器的可用配置类型
# 编译选项和标志
CMAKE_CXX_FLAGS # C++ 全局编译选项
CMAKE_CXX_FLAGS_DEBUG # Debug 模式下的编译选项
CMAKE_CXX_FLAGS_RELEASE # Release 模式下的编译选项
示例:
# 设置默认构建类型
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Build type" FORCE)
endif()
# 添加编译选项
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")
(4) 安装和导出相关变量
CMAKE_INSTALL_PREFIX # 安装根目录(默认 /usr/local)
CMAKE_INSTALL_LIBDIR # 库文件安装目录
CMAKE_INSTALL_BINDIR # 可执行文件安装目录
CMAKE_INSTALL_INCLUDEDIR # 头文件安装目录
示例:
# 自定义安装前缀
set(CMAKE_INSTALL_PREFIX "/opt/myapp" CACHE PATH "Installation prefix")
# 安装目标
install(TARGETS myapp
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}/static
)
(5) 编译工具链相关
CMAKE_TOOLCHAIN_FILE # 工具链文件路径
CMAKE_CROSSCOMPILING # 是否交叉编译
CMAKE_HOST_SYSTEM_NAME # 主机系统名称
示例:
# 检查是否交叉编译
if(CMAKE_CROSSCOMPILING)
message(STATUS "正在进行交叉编译")
endif()
(6) 高级配置变量
CMAKE_MODULE_PATH # CMake 模块搜索路径
CMAKE_PREFIX_PATH # 依赖库搜索路径
#标准设置
CMAKE_CXX_STANDARD # C++ 标准版本
CMAKE_CXX_STANDARD_REQUIRED # 是否强制要求标准版本
示例:
# 添加自定义模块路径
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules")
# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
(7) 完整示例:综合使用内置变量
cmake_minimum_required(VERSION 3.15)
project(SystemInfoProject)
# 系统信息
message(STATUS "操作系统: ${CMAKE_SYSTEM_NAME}")
message(STATUS "系统版本: ${CMAKE_SYSTEM_VERSION}")
message(STATUS "处理器架构: ${CMAKE_SYSTEM_PROCESSOR}")
# 编译器信息
message(STATUS "编译器: ${CMAKE_CXX_COMPILER}")
message(STATUS "编译器版本: ${CMAKE_CXX_COMPILER_VERSION}")
# 构建类型配置
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Build type" FORCE)
endif()
message(STATUS "构建类型: ${CMAKE_BUILD_TYPE}")
# 平台特定配置
if(WIN32)
add_definitions(-DWINDOWS_PLATFORM)
elseif(UNIX)
add_definitions(-DUNIX_PLATFORM)
endif()
# 编译选项
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 安装配置
set(CMAKE_INSTALL_PREFIX "/opt/myapp" CACHE PATH "Installation prefix")
# 添加可执行文件
add_executable(system_info main.cpp)
# 安装目标
install(TARGETS system_info
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
vscode + clangd
clangd的代码提示和补全,非常地强大,这里记录一下 vscode如何配置clangd。
Windows
windows平台,在vscode上安装clangd比较简单。
直接在extenstions里搜素clangd,然后install这个插件即可,安装好后,重新打开窗口检查是否安装成功。clangd.exe存储在如下目录:
clangd path: C:/Users/ventu/AppData/Roaming/Code/User/globalStorage
/llvm-vs-code-extensions.vscode-clangd/install/18.1.3/clangd_18.1.3/bin/clangd.exe
mac
Ubuntu
(1) vscode 安装插件 clangd
(2) 然后Ctrl + shift + p reload window,一般这个时候会弹出install clangd的窗口,install即可。如果弹出的窗口显示fail,可以试着关闭代理reload window后再次尝试。
(3) 如果isntall失败,那么我们手动install,https://clangd.llvm.org/installation.html
由于我们是ubuntu,所以安装clangd-linux-18.1.3.zip
安装好后,得到一个压缩包,我们解压后,将clangd_18.1.3放入对应install目录下
"clangd.path": "/home/xxxx/.vscode-server/data/User/globalStorage
/llvm-vs-code-extensions.vscode-clangd/install/18.1.3/clangd_18.1.3/bin/clangd",
vscode下,ctrl + , 打开settings界面,然后输入proxy,打开settings.json (注意一定要选择正确的主机),最后设置clangd.path。
(4) 检查是否安装成功,vscode打开panel,然后输入clangd,check for language server update,如果已经安装会显示安装的版本。然后download language server,会提示发现installed clangd,直接set default,最后restart language server,reload window。到此,vscode上配置clangd完成。
C++ 基本特性
这部分主要围绕C++基本特性进行记录,主要包括常用关键字,指针,引用,内存相关的知识点。
阅读指南:
每篇文章,一般开头会有介绍/用法,入门和工程使用一般看这部分即可,也是知识体系的核心模块。
其余的进阶研究,在后面以2级标题的形式进行添加研究。
new delete 运算符
介绍
在C++中,new和delete是用于动态内存管理的运算符。它们允许程序在运行时分配和释放内存,这对于处理不确定大小的数据结构(如链表、树等)非常重要。下面是对这两个运算符的详细讲解:
1. new 运算符
- 功能:
new运算符用于在堆上动态分配内存。它返回一个指向所分配内存的指针。 - 语法:
Type* pointer = new Type; // 分配一个Type类型的对象
Type* arrayPointer = new Type[size]; // 分配一个Type类型的数组
- 示例:
int* p = new int; // 分配一个int类型的内存
*p = 10; // 给分配的内存赋值
int* arr = new int[5]; // 分配一个包含5个int的数组
for (int i = 0; i < 5; ++i) {
arr[i] = i; // 初始化数组
}
2. delete 运算符
- 功能:
delete运算符用于释放之前通过new分配的内存。使用delete可以避免内存泄漏。 - 语法 + 示例:
delete p; // 释放之前分配的int内存
delete[] arr; // 释放之前分配的int数组内存
3. 注意事项
- 内存泄漏:如果使用
new分配的内存没有用delete释放,就会导致内存泄漏,程序的内存使用量会不断增加。 - 双重释放:对同一块内存使用
delete两次会导致未定义行为,因此在释放内存后,最好将指针设置为nullptr。 - 构造和析构:使用
new分配对象时,会调用对象的构造函数;使用delete释放对象时,会调用对象的析构函数。
用法
new可以分配单个对象的内存,也可以分配数组对象的内存;分配的内存默认是在堆区;
相关术语: new分配单个对象的内存 <=> new创建单个对象 <=> new单个对象 new分配数组对象的内存 <=> new创建数组对象 <=> new数组对象
new的时候 语法层面有三种写法,(..,), (), 没有();分别对应有参初始化,无参初始化,不初始化;
new单个对象
(1) 对象是普通变量,可以分配对应的内存
(...)直接初始化,允许;()值初始化,允许,初始化为0;- 没有
()默认初始化,允许,分配的内存未定义;
(2) 对象是类对象,会调用构造函数,如果没有对应的构造函数,就会报错
(...)直接初始化,允许,找到对应构造函数初始化,没有找到报错()值初始化,允许,调用默认构造函数初始化,没有找到报错- 没有
()默认始化,允许,调用默认构造函数初始化,没有找到报错
new数组对象
(1) new普通变量数组,可以使用()将所有对象全部初始化为0
(...)直接初始化,不允许;会报错()值初始化,允许,数组中对象全部初始化为0;- 没有
()默认初始化,允许,分配的内存未定义;
(2) new类对象数组,有没有()都一样,均使用默认构造函数,如果没有默认构造函数就 会报错
(...)直接初始化,不允许;会报错()值初始化,允许,调用默认构造函数初始化,没有找到报错- 没有
()默认初始化,允许,调用默认构造函数初始化,没有找到报错
代码:
#include <iostream>
#include <string>
class Test
{
public:
Test() {}
};
class TestA
{
public:
TestA(int i_) : i(i_) {}
private:
int i;
};
int main() {
// 1. new可以在堆上分配 单个对象 的内存 <=> new可以在堆上创建 单个对象 <=> new单个对象
// 以下对象 指 分配的对象/创建的对象/new的对象
// 1.1 对象是普通变量,分配对应的内存
int *pi = new int(10); // 堆上分配int对象的内存,直接初始化
std::cout << *pi << std::endl;
int* pk = new int(); // 堆上分配int对象的内存,值初始化为0;
std::cout << *pk << std::endl;
int *pj = new int; // 堆上分配int对象的内存,默认初始化,分配内存未定义
std::cout << *pj << std::endl;
delete pi;
delete pj;
delete pk;
// 1.2 对象是类对象,会调用对应的构造函数,如果没有对应的构造函数,就会报错
std::string *pString1 = new std::string("hello world"); // 找到对应的构造函数初始化
std::cout << *pString1 << std::endl;
std::string *pString2 = new std::string(); // 调用默认构造函数初始化
std::cout << *pString2 << std::endl;
std::string *pString3 = new std::string; // 调用默认构造函数初始化
std::cout << *pString3 << std::endl;
// 这里演示 new单个类对象,找不到对应构造函数报错
//Test* t1 = new Test(10); // 找不到对应构造函数报错
delete pString1;
delete pString2;
delete pString3;
// 2. new可以在堆上分配 数组对象 的内存 <=> new可以在堆上创建 数组对象 <=> new数组对象
// 2.1 new 普通变量 数组 可以使用()将所有对象全部初始化为 0 => 只有()初始化合法
int *p1 = new int[100](); // 分配数组对象内存,值参初始化,数组中所有对象全部初始化为0
std::cout << p1[20] << std::endl;
int *p2 = new int[100]; // 分配数组对象内存,默认初始化,分配的内存未定义
std::cout << p2[20] << std::endl;
//int* p3 = new int[100](10); // 分配数组对象内存,直接初始化,报错,语法规定不允许
//std::cout << p2[20] << std::endl;
delete[] p1;
delete[] p2;
//delete[] p3;
// 2.2 对于 类对象 数组 有没有“()”都一样,均使用默认构造函数,如果没有默认构造函数就会报错
std::string *pString4 = new std::string[100](); // 分配数组对象内存,数组中所有对象 使用默认构造函数初始化
std::cout << pString4[20] << std::endl;
std::string* pString5 = new std::string[100]; // 分配数组对象内存,数组中所有对象 使用默认构造函数初始化
std::cout << pString5[20] << std::endl;
//std::string* pString6 = new std::string[100]("hello world"); // 分配数组对象内存,有参初始化,报错,语法规定不允许
//std::cout << pString6[20] << std::endl;
// 这里演示 new类对象数组,找不到默认构造函数报错
//TestA* t2 = new TestA[100];
delete[] pString4;
delete[] pString5;
//delete[] pStirng6;
return 0;
}
运行结果:
上面代码 注释的地方都是之前提到的的问题
- 内存未定义
- 没有找到对应的构造函数
- new数组对象不允许直接初始化
总结:
- new单个对象,语法层面上有,直接/值/默认 初始化都可以;但是new数组对象上,语法层面上不允许直接初始化;
- new 单个类对象 和 new 类对象数组时,就是要找对应的构造函数,没有找到就会报错
new的所有初始化
1. 基本初始化方式
1.1 默认初始化
int* ptr1 = new int; // 未初始化,内置类型值是随机的
1.2 值初始化
int* ptr2 = new int(); // 初始化为0
1.3 直接初始化
int* ptr3 = new int(42); // 初始化为42
1.4 列表初始化(C++11引入)
int* ptr4 = new int{42}; // 使用花括号初始化
2. 对象初始化方式
2.1 默认构造函数
class MyClass {
public:
MyClass() { value = 0; }
MyClass(int x) : value(x) {}
int value;
};
// 默认构造函数
MyClass* obj1 = new MyClass();
2.2 带参数构造函数
// 带参数的构造函数
MyClass* obj2 = new MyClass(10);
2.3 列表初始化构造函数
// 列表初始化
MyClass* obj3 = new MyClass{10};
3. 数组初始化方式
3.1 默认数组初始化
// 默认初始化数组
int* arr1 = new int[5]; // 未初始化
3.2 值初始化数组
// 值初始化数组
int* arr2 = new int[5](); // 全部元素初始化为0
3.3 直接初始化数组
// 部分初始化数组
int* arr3 = new int[5]{1, 2, 3, 4, 5}; // C++11开始支持
4. 多维数组初始化
4.1 二维数组初始化
// 二维数组初始化
int** matrix1 = new int*[3];
for (int i = 0; i < 3; ++i) {
matrix1[i] = new int[4](); // 每行初始化为0
}
// 列表初始化二维数组(C++11)
int** matrix2 = new int*[2]{
new int[3]{1, 2, 3},
new int[3]{4, 5, 6}
};
5. 复杂对象初始化
5.1 复杂类的构造函数初始化
class ComplexClass {
public:
ComplexClass() = default;
ComplexClass(int a, double b, std::string c)
: x(a), y(b), str(c) {}
int x;
double y;
std::string str;
};
// 多参数构造函数初始化
ComplexClass* complex1 = new ComplexClass(10, 3.14, "Hello");
6. 智能指针初始化(现代C++推荐)
6.1 unique_ptr
#include <memory>
// 使用 make_unique
std::unique_ptr<int> uptr1 = std::make_unique<int>(42);
// 直接构造
std::unique_ptr<MyClass> uptr2 = std::make_unique<MyClass>(10);
6.2 shared_ptr
// 使用 make_shared
std::shared_ptr<int> sptr1 = std::make_shared<int>(42);
// 直接构造
std::shared_ptr<MyClass> sptr2 = std::make_shared<MyClass>(10);
7. 特殊初始化场景
7.1 placement new
// 在预分配的内存上构造对象
char buffer[sizeof(MyClass)];
MyClass* placementObj = new (buffer) MyClass(100);
完整示例代码
#include <iostream>
#include <string>
#include <memory>
class MyClass {
public:
MyClass() : value(0) {
std::cout << "默认构造函数" << std::endl;
}
MyClass(int x) : value(x) {
std::cout << "带参数构造函数: " << value << std::endl;
}
~MyClass() {
std::cout << "析构函数" << std::endl;
}
int value;
};
int main() {
// 基本类型初始化
int* a = new int; // 未初始化
int* b = new int(); // 初始化为0
int* c = new int(42); // 初始化为42
int* d = new int{42}; // 列表初始化
// 对象初始化
MyClass* obj1 = new MyClass(); // 默认构造
MyClass* obj2 = new MyClass(10); // 带参数构造
MyClass* obj3 = new MyClass{20}; // 列表初始化
// 数组初始化
int* arr1 = new int[5]; // 未初始化
int* arr2 = new int[5](); // 全0
int* arr3 = new int[5]{1, 2, 3, 4, 5}; // 部分初始化
// 智能指针
auto uptr = std::make_unique<MyClass>(30);
auto sptr = std::make_shared<MyClass>(40);
// 释放内存
delete a;
delete b;
delete c;
delete d;
delete obj1;
delete obj2;
delete obj3;
delete arr1;
delete arr2;
delete arr3;
return 0;
}
注意事项
- 使用
new分配的内存必须手动释放,否则会造成内存泄漏 - 现代 C++ 推荐使用智能指针(
unique_ptr、shared_ptr) - 不同的初始化方式适用于不同的场景
- 列表初始化(
{})提供了更严格和安全的初始化方式
建议
- 尽量使用栈上对象和智能指针
- 避免手动管理动态内存
- 使用 RAII(资源获取即初始化)原则
malloc/free 和 new/delete之间的区别
参考视频:
https://www.bilibili.com/video/BV1Qm411z7AH/?spm_id_from=333.337.search-card.all.click&vd_source=cb02f779bd17a3aad9801e0c4464dfc9
自己的理解:
背景:malloc、free c语言中库函数,new、delete是c+中操作符
(1) malloc和new的区别
内存大小的计算:new自动计算所需分配内存大小,malloc需要手动计算
返回的指针类型:new返回的是对象类型的指针,malloc返回的是void*,之后进行类型转换
分配失败后的处理:neW分配失败会抛出异常,malloc分配失败返回的是NULL;
分配区域:new是在free store上分配内存,malloc堆上分配:
(2) delete和free的区别
参数区别:delete需要对象类型的指针,free是vo1d*类型的指针:
new的简要流程
- operator new
- 申请足够的空间
- 调用构造函数,初始化成员变量
delete的简要流程
- 先调用析构函数
- operator delete
- 释放空间
引申问题:
(1) malloc是怎么分配空间的?
malloc内存分配的核心机制:
1-内存分配基本流程
void* malloc(size_t size) {
// 1. 参数检查
if (size == 0) return NULL;
// 2. 内存大小调整
size_t actual_size = adjust_size(size);
// 3. 查找可用内存块
voidmemory_block = find_free_block(actual_size);
// 4. 如果没有可用内存块,向系统申请
if (memory_block == NULL) {
memory_block = request_system_memory(actual_size);
}
// 5. 标记内存块为已使用
mark_block_used(memory_block);
return memory_block;
}
2-内存分配的关键步骤
2.1-大小调整
- 对齐内存大小(通常是8或16字节对齐)
- 增加内存块管理所需的额外空间
size_t adjust_size(size_t size) {
// 内存对齐
size_t aligned_size = (size + 7) & ~0x7;
// 额外的块管理信息
return aligned_size + BLOCK_HEADER_SIZE;
}
2.2-内存块查找策略
- 空闲链表查找
- 最佳匹配算法
void* find_free_block(size_t size) {
// 遍历空闲链表
for (free_block* block = free_list_head; block != NULL; block = block->next) {
// 找到合适大小的块
if (block->size >= size) { // 从空闲链表移除
remove_from_free_list(block);
return block;
}
}
return NULL;
}
3-系统内存申请方法
3.1-小内存申请(<128KB)
- 使用
sbrk()系统调用 - 扩展进程堆空间
void* request_small_memory(size_t size) {
// 使用sbrk()扩展堆
void new_memory = sbrk(size);
// 更新堆信息
update_heap_metadata(new_memory, size);
return new_memory;
}
3.2 大内存申请(>128KB)
- 使用
mmap()系统调用 - 直接映射虚拟内存
void* request_large_memory(size_t size) {
return mmap(NULL, // 系统分配地址
size, // 请求大小
PROT_READ | PROT_WRITE, // 读写权限
MAP_PRIVATE | MAP_ANONYMOUS,
-1, // 无文件描述符
0); // 无文件偏移
}
4-内存块管理结构
// 内存块管理结构
typedef struct memory_block {
size_t size; // 块大小
int is_free; // 是否空闲
struct memory_block* next; // 下一个块
struct memory_block* prev; // 前一个块
}
memory_block;
5-内存分配算法
5.1 空闲链表管理
- 维护空闲内存块链表
- 支持块的合并和分割
void merge_free_blocks() {
memory_block* current = free_list_head;
while (current && current->next) {
// 检查相邻块是否可以合并
if (is_contiguous_and_free(current, current->next)) {
merge_blocks(current, current->next);
}
current = current->next;
}
}
6-内存对齐技术
// 内存对齐宏
#define ALIGN(size) (((size) + sizeof(size_t) - 1) & ~(sizeof(size_t) - 1))
实际分配流程总结
-
检查请求大小
-
调整内存大小(对齐)
-
查找空闲内存块
-
如果没有合适块,向系统申请内存
- 小内存:使用
sbrk() - 大内存:使用
mmap()
- 小内存:使用
-
标记内存块为已使用
-
返回内存指针
(2) mal1oc分配的物理内存还是虚拟内存?
malloc分配的是虚拟内存
- 当你调用
malloc()时,实际上分配的是虚拟内存地址空间 - 操作系统使用虚拟内存映射机制,将虚拟内存映射到物理内存
- 只有当程序实际访问这些内存时,才会触发缺页中断,真正分配物理内存页
(3) malloc调用后是否立刻得到物理内存?
不是立即获得
- 分配虚拟内存是瞬间完成的
- 物理内存是延迟分配的(按需分页)
- 只有在程序首次读写这块内存时,操作系统才会分配真正的物理内存页
- 延迟分配示例:
intptr = malloc(1024 * sizeof(int)); // 只分配虚拟内存 // 此时没有实际的物理内存分配
ptr[0] = 42; // 首次写入时,触发缺页中断,分配物理内存
(4) free(p)怎么知道该释放多大的空间?
通过内存块的元数据信息
- malloc在分配内存时,会在内存块前面添加一个头部(metadata)
- 头部记录了内存块的大小和其他管理信息
- free()通过读取这个头部,就能知道要释放的内存大小
- 内存块结构示例
struct MemoryBlock {
size_t size; // 记录内存块大小
int is_free; // 标记是否空闲
// 其他管理信息
};
(5) free释放内存后,内存还在吗?
内存仍然存在,但被标记为可重用
- free()并不会立即将内存返回给操作系统
- 内存被放回内存管理器的空闲列表
- 下次malloc可能会重用这块内存
- 只有在特定条件下(如大块内存),才会真正归还给操作系统
- 内存管理示意:
void free(voidptr) {
// 1. 找到内存块
MemoryBlock* block = (MemoryBlock*)(ptr - sizeof(MemoryBlock));
// 2. 标记为空闲
block->is_free = 1;
// 3. 可能进行块合并
merge_adjacent_free_blocks(block);
// 4. 加入空闲链表,等待重用
add_to_free_list(block);
}
(6) malloc, free, new, delete的伪代码
C语言风格:malloc 和 free
// malloc 伪代码
void* my_malloc(size_t size) {
// 1. 参数检查
if (size == 0) return NULL;
// 2. 内存大小调整(对齐)
size_t aligned_size = ALIGN(size);
// 3. 查找空闲内存块
voidmemory = find_free_block(aligned_size);
// 4. 如果没有空闲块,向系统申请内存
if (memory == NULL) {
memory = request_system_memory(aligned_size);
}
// 5. 记录内存块元数据
if (memory) {
store_block_metadata(memory, aligned_size);
}
return memory;
}
// free 伪代码
void my_free(void* ptr) {
// 1. 空指针检查
if (ptr == NULL) return;
// 2. 获取内存块元数据
MemoryBlockHeader* header = get_block_header(ptr);
// 3. 标记内存块为可用
header->is_free = true;
// 4. 尝试合并相邻空闲块
merge_adjacent_free_blocks(header);
// 5. 可能返回系统(取决于内存管理策略)
try_return_to_system(header);
}
C++风格:new 和 delete
// new 伪代码
void* operator new(size_t size) {
// 1. 调用 malloc 分配内存
void* memory = my_malloc(size);
// 2. 内存分配失败处理
if (memory == NULL) {
throw std::bad_alloc(); // C++ 特有的异常处理
}
return memory; // 返回分配的内存指针
}
// delete 伪代码
void operator delete(void* ptr) noexcept {
// 1. 空指针检查
if (ptr == NULL) return;
// 2. 释放内存(调用 free)
my_free(ptr);
}
// new 伪代码
template <typename T, typename... Args>
T* my_new(Args&&... args) {
// 1. 调用 operator new 分配内存
void* raw_memory = operator new(sizeof(T));
// 2. 在分配的内存上调用构造函数
T* object = static_cast<T*>(raw_memory);
new (object) T(std::forward<Args>(args)...); // placement new
return object; // 返回指向新对象的指针
}
// delete 伪代码
template <typename T>
void my_delete(T* ptr) {
// 1. 空指针检查
if (ptr == NULL) return;
// 2. 调用析构函数
ptr->~T();
// 3. 调用 operator delete 释放内存
operator delete(static_cast<void*>(ptr));
}
命名空间
介绍
在C++中,命名空间(namespace)是一种用于组织代码的机制,主要用于避免名称冲突。命名空间允许将标识符(如变量、函数、类等)分组,从而在不同的上下文中使用相同的名称而不会发生冲突。
入门视频:
https://www.bilibili.com/video/BV1oCmEYGEUc/?spm_id_from=333.337.search-card.all.click&vd_source=cb02f779bd17a3aad9801e0c4464dfc9
用法
C++经常需要多个团队合作来完成大型项目。多个团队就常常出现起名重复的问题,C++ 就提供了命名空间来解决这个问题。
问题演示:
TeamA和TeamB是两个团队,TeamA有代码文件,TeamA.h TeamA.cpp; TeamB有代码文件 TeamB.h TeamB.cpp; 两个团队都定义了test函数,然后main.cpp中引入了这两个团队的头文件,这样在main.cpp中调用test()就会报错。
解决方法:
每个团队使用各自的命名空间,例如TeamA团队就使用namespace A,而TeamB团队就使用namespace B,然后把团队自己的头文件和源文件都使用namespace包裹住。
对于namespace,我们还可以使用using关键字;using关键字设计的目的之一就是为了简化命名空间的。
using 关键字在命名空间方面主要有两种用法:
(1) using 命名空间::变量名;这样以后使用此变量时只要使用变量名就可以了,可以少写命名空间
注意:如果变量前本来就有命名空间,那么直接使用当前的命名空间;否则就会使用using里的命名空间;如果有多个using针对的是相同的变量名,还是会因为无法确定报错。
(2) using namspce 命名空间。这样每一个变量都会在该命名空间中寻找
注意:如果变量前本来就有命名空间,那么直接使用当前的命名空间;否则就会去using里的命名空间里寻找;如果有多个using namespace,使用的变量又在多个namespace里同名,还是会因为无法确定报错。
命名空间的实现原理:
C++最后都要转化为 C 来执行程序。在 namespace A 中定义的 Test 类,其实全名是 A::Test。 C++所有特有的库(指 c 没有的库),都使用了 std 的命名空间。比如最常用的 iostream
头文件中一定不能使用using关键字,因为这样极容易导致命名空间的污染
分析:如果在头文件中使用using 关键字,很有可能在cpp文件中调用一个名称的时候,不知道究竟属于哪一个命名空间(因为头文件可以嵌套include,很难确定使用了哪些命名空间),当文件多的时候排查很麻烦;cpp文件中是可以使用的,因为可以直接确定使用了哪些命名空间,相对来说好排查一些;
命名空间在现代C++中的优化
C++11引入了内联命名空间,C++17引入了嵌套命名空间
讲解视频:
https://www.bilibili.com/video/BV1Z14y1q7oo/?spm_id_from=333.337.search-card.all.click&vd_source=cb02f779bd17a3aad9801e0c4464dfc9
内联命名空间
嵌套命名空间
const关键字
用法
在C/C++中,const关键字用于声明常量,表示某个变量的值在初始化后不能被修改。它可以用于变量、指针、函数参数和返回值等多种场景。下面是对const关键字的详细讲解:
const修饰变量
使用const声明的变量在初始化后不能被修改。例如:
const int x = 10;
// x = 20; // 错误:不能修改常量
const修饰指针
const可以与指针结合使用
- 常量指针(Pointer to Constant):指向常量,不能修改指向的值,但可以改变指针的指向。
const Type* pointer; // const修饰指针指向的对象
const int a = 10;
const int* ptr = &a; // ptr是一个指向常量整数的指针
// *ptr = 20; // 错误:不能通过ptr修改a的值
int b = 30;
ptr = &b; // 合法:ptr可以指向其他地址
- 指针常量(Constant Pointer):指针本身是常量,不能改变指针的指向,但可以修改指向的值。
Type* const pointer; // const修饰指针本身
int a = 10;
int* const ptr = &a; // ptr是一个常量指针,指向整数
*ptr = 20; // 合法:可以通过ptr修改a的值
// ptr = &b; // 错误:不能改变ptr的指向
- 常量指针指向常量(Constant Pointer to Constant):指针和指向的值都是常量,既不能修改指针的指向,也不能修改指向的值。
const Type* const pointer;
const int a = 10;
const int* const ptr = &a; // ptr是一个指向常量整数的常量指针
// *ptr = 20; // 错误:不能通过ptr修改a的值
// ptr = &b; // 错误:不能改变ptr的指向
常量函数参数
在函数参数中使用const可以防止函数修改传入的参数,尤其是对于引用和指针类型的参数。
void func(const int* arr) {
// arr[0] = 10; // 错误:不能修改数组内容
}
常量成员函数
在类中,使用const修饰成员函数,表示该函数不会修改类的成员变量。
class MyClass {
public:
void display() const {
// this->value = 10; // 错误:不能在const成员函数中修改成员变量
}
};
常量返回值
函数可以返回const类型的值,表示返回的值不能被修改。
const int getValue() {
return 10;
}
// int val = getValue();
// val = 20; // 合法:val是一个普通变量,可以修改
总结
const关键字在C/C++中是一个重要的工具,用于提高代码的安全性和可读性。它可以帮助开发者明确哪些变量是常量,防止意外修改,从而减少错误和提高代码的可维护性。使用const的最佳实践是尽可能多地使用它,以确保代码的意图清晰。
const修饰的变量和常量的区别
const是让编译器将变量视为常量,用const修饰的变量和真正的常量有本质的区别
什么是真正的常量?
真正的常量就是字面值,它们一般都存储在只读区。
一般来说只读区中包含.text段和.rodata段(因为它们都是仅可读的),数字字面值有时会直接嵌入指令中,所以存储在.text段,而字符串字面值通常都是存储在.rodata段。
例如:
const char* str = "abcdefg";
const int a = 3;
const int b = 100;
str, a, b这些是const变量,并不是真正的常量,可以通过一些方式进行修改;而”abcdefg“,3,100这些是字面值,是真正的常量,无法修改。其中“abcdefg“这个字符串就存储在.rodata段, 而3, 100这些数字就存储在.text段中,这些都是真正的常量,无法用任何方式修改。
const修饰的变量
const修饰的变量,从内存分布的角度讲,和普通变量没有区别。
const 修饰的变量并非不可更改的,C++本身就提供了mutable 关键字用来修改const修饰的变量,从汇编的角度讲,const 修饰的变量也是可以修改的
代码分析:
#include <iostream>
#include <string>
int main() {
int i = 100; // i在栈区
const int i2 = 200; // i2也在栈区, i2的值无法修改,但是i和i2在内存上是相邻的;
static int i3; // i3在.bss段
static int i4 = 400; // i4在.data段
const static int i5 = 500; // i5在.rodata段
std::string str = "hello world"; // str在栈区,"hello world"在常量区
// 真正的常量 100 200 400 500存储在.text代码段(代码区),"hello world"存储在.rodata段(常量区)
return 0;
}
const在C和C++中的区别
- C/C++中都可以通过指针间接修改(不在只读区)const对象(全局未初始化的const对象,局部const对象);但是C中可以修改成功,C++中虽然编译器不会报错,但是修改失败,因为在使用const对象时还是使用编译期常量进行替换。
- C中的const对象可以不初始化;C++中const对象必须初始化
这一段代码,可以改成.c文件或者.cpp文件试试,会发现上面的结论。
#include <stdio.h>
const int a = 10;
int main()
{
const int b = 20;
// 使用指针强制类型转换来修改 const 对象的值
int* pa = (int*)&a;
int* pb = (int*)&b;
//*pa = 30;
*pb = 40;
printf("a = %d/n", a);
printf("b = %d/n", b);
printf("*pb = %d/n", *pb);
return 0;
}
// C中 输出 10 40 40
// C++中 输出 10 20 40
const的作用,详细分析
视频讲解: https://www.bilibili.com/video/BV1FWtre2EJo/?spm_id_from=333.337.search-card.all.click&vd_source=cb02f779bd17a3aad9801e0c4464dfc9
C/C++中的区别有误,看上面的总结,其他的没问题,可以作为回答问题的思路
extern关键字
用法
在C/C++中,extern关键字用于声明一个变量或函数的外部链接性。它的主要作用是告诉编译器该变量或函数在其他文件中定义,而不是在当前文件中定义。下面是对extern关键字的详细讲解:
变量的使用
当你在一个源文件中定义了一个变量,并希望在其他源文件中访问这个变量时,可以使用extern关键字来声明它。例如:
// file1.c
#include <stdio.h>
int globalVar = 10; // 定义一个全局变量
void display() {
printf("Global Variable: %d/n", globalVar);
}
// file2.c
#include <stdio.h>
extern int globalVar; // 声明外部变量
void modify() {
globalVar = 20; // 修改外部变量
}
在这个例子中,file1.c中定义了一个全局变量globalVar,而在file2.c中使用extern关键字声明了这个变量。这样,file2.c就可以访问和修改file1.c中定义的globalVar
函数的使用
extern关键字也可以用于函数声明,尽管在C/C++中,函数默认具有外部链接性,因此通常不需要显式使用extern。但为了清晰起见,可以这样写:
extern void myFunction(); // 声明一个外部函数
extern “C” 在C++中的使用
在C++中,extern关键字还可以与"C"一起使用,以指示编译器使用C语言的链接方式。这在C++代码中调用C语言库时非常有用,避免了C++的名称修饰(name mangling)问题。
extern "C" {
void cFunction(); // 声明一个C语言函数
}
作用域和链接性
外部链接性:使用extern声明的变量或函数可以在多个文件中共享。
内部链接性:如果在一个文件中定义了一个变量而没有使用extern,那么这个变量的作用域仅限于该文件。
总结
extern用于声明在其他文件中定义的变量或函数。
在C++中,extern "C"用于处理C和C++之间的链接问题。
extern关键字有助于管理大型项目中的变量和函数的可见性和链接性。
extern关键字的作用
视频讲解:https://www.bilibili.com/video/BV1gqpLeVEfV/?spm_id_from=333.337.search-card.all.click
extern关键字
用法
在C/C++中,extern关键字用于声明一个变量或函数的外部链接性。它的主要作用是告诉编译器该变量或函数在其他文件中定义,而不是在当前文件中定义。下面是对extern关键字的详细讲解:
变量的使用
当你在一个源文件中定义了一个变量,并希望在其他源文件中访问这个变量时,可以使用extern关键字来声明它。例如:
// file1.c
#include <stdio.h>
int globalVar = 10; // 定义一个全局变量
void display() {
printf("Global Variable: %d/n", globalVar);
}
// file2.c
#include <stdio.h>
extern int globalVar; // 声明外部变量
void modify() {
globalVar = 20; // 修改外部变量
}
在这个例子中,file1.c中定义了一个全局变量globalVar,而在file2.c中使用extern关键字声明了这个变量。这样,file2.c就可以访问和修改file1.c中定义的globalVar
函数的使用
extern关键字也可以用于函数声明,尽管在C/C++中,函数默认具有外部链接性,因此通常不需要显式使用extern。但为了清晰起见,可以这样写:
extern void myFunction(); // 声明一个外部函数
extern “C” 在C++中的使用
在C++中,extern关键字还可以与"C"一起使用,以指示编译器使用C语言的链接方式。这在C++代码中调用C语言库时非常有用,避免了C++的名称修饰(name mangling)问题。
extern "C" {
void cFunction(); // 声明一个C语言函数
}
作用域和链接性
外部链接性:使用extern声明的变量或函数可以在多个文件中共享。
内部链接性:如果在一个文件中定义了一个变量而没有使用extern,那么这个变量的作用域仅限于该文件。
总结
extern用于声明在其他文件中定义的变量或函数。
在C++中,extern "C"用于处理C和C++之间的链接问题。
extern关键字有助于管理大型项目中的变量和函数的可见性和链接性。
extern关键字的作用
视频讲解:https://www.bilibili.com/video/BV1gqpLeVEfV/?spm_id_from=333.337.search-card.all.click
auto关键字
介绍
auto是C++11引入的一个非常有用的类型推导关键字,它能够让编译器自动推断变量的类型。
基本用法
auto x = 42; // x 被推导为 int
auto y = 3.14; // y 被推导为 double
auto str = "Hello"; // str 被推导为 const char*
与容器和迭代器一起使用
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
// it 被自动推导为 std::vector<int>::iterator
}
处理复杂类型
std::map<std::string, std::vector<int>> complexMap;
for (auto& pair : complexMap) {
// pair 被推导为 std::pair<const std::string, std::vector<int>>&
}
函数返回值类型推导
auto add(int a, int b) {
return a + b; // 返回值类型自动推导为 int
}
Lambda表达式
auto lambda = [](int x) { return x * 2; };
优点:
- 减少冗长的类型声明
- 简化代码
- 在模板编程中特别有用
- 自动处理复杂类型
潜在缺点:
- 可能降低代码可读性
- 编译时间可能略微增加
- 需要程序员对类型推导有清晰理解
建议:在类型明确且简单的场景下使用auto,保持代码的清晰和可读性。
用法
auto是C++11 新加入的关键字,就是为了简化一些写法。
使用auto推断类型确实简单方便,但有个基本要求,就是在使用auto的时清楚的知道编译器会给auto推断出什么类型。
推导规则:
auto默认推导为值类型- 丢弃
const和引用 - 使用
auto&可以保留引用类型和const - 如果需要const或引用属性,可以显示添加
这里给出一个程序,充分说明auto的推导规则
int main() {
// 原始类型
int x = 10;
const int cx = 20;
// auto 推导
auto a = x; // a 是 int
auto b = cx; // b 是 int(const 被丢弃)
// 如果想保留 const,需要手动指定
auto const ca = x; // ca 是 const int
const auto cb = x; // cb 是 const int
// 引用推导
int& rx = x;
const int& crx = x;
auto r1 = rx; // r1 是 int(引用被丢弃)
auto r2 = crx; // r2 是 int(const 和引用都被丢弃)
// 保留引用和 const 需要使用auto&
auto& ref1 = rx; // ref1 是 int&
auto& ref2 = crx; // ref2 是 const int&
}
(1) auto无法推断出引用类型,要使用引用只能显示添加;
#include <iostream>
#include <boost/type_index.hpp>
using boost::typeindex::type_id_with_cvr;
int main() {
auto i1 = 100;
auto& i2 = i1;
auto i3 = i1;
std::cout << type_id_with_cvr<decltype(i1)>().pretty_name() << std::endl; // int
std::cout << type_id_with_cvr<decltype(i2)>().pretty_name() << std::endl; // int &
std::cout << type_id_with_cvr<decltype(i3)>().pretty_name() << std::endl; // int
return 0;
}
(2) auto无法推断出const,要使用引用只能自己显示添加
#include <iostream>
#include <boost/type_index.hpp>
using boost::typeindex::type_id_with_cvr;
int main() {
int i = 100;
const auto i2 = i;
std::cout << type_id_with_cvr<decltype(i2)>().pretty_name() << std::endl; // const int = int const
return 0;
}
(3) auto关键字在推断引用的类型时:
使用auto时,引用和const会被剥离
使用auto&时,保留引用的特性,包括const属性。
#include <iostream>
#include <boost/type_index.hpp>
using boost::typeindex::type_id_with_cvr;
int main() {
int i = 100;
const int& refI = i;
auto i2 = refI; // 推断引用的类型
auto& i3 = refI;
auto& i4 = i;
std::cout << type_id_with_cvr<decltype(i2)>().pretty_name() << std::endl; // int
std::cout << type_id_with_cvr<decltype(i3)>().pretty_name() << std::endl; // int const &
std::cout << type_id_with_cvr<decltype(i4)>().pretty_name() << std::endl; // int &
return 0;
}
(4) auto关键字在推断类型时,如果没有引用符号,会忽略值类型的const修饰(本身的const,顶层const),而保留指向对象的const修饰(底层const),典型的就是指针;
#include <iostream>
#include <boost/type_index.hpp>
using boost::typeindex::type_id_with_cvr;
int main() {
int i = 100;
const int* const pi = &i;
auto pi2 = pi; // 忽略值类型的const,保留指向对象的const
const int i2 = 100;
auto i3 = i2; // 忽略值类型的const
std::cout << type_id_with_cvr<decltype(pi2)>().pretty_name() << std::endl; // const int * = int const *
std::cout << type_id_with_cvr<decltype(i3)>().pretty_name() << std::endl; // int
return 0;
}
(5) auto关键字在推断类型时,如果有了引用符号,那么值类型的const修饰 和 指向对象的const修饰 都会保留
#include <iostream>
#include <boost/type_index.hpp>
using boost::typeindex::type_id_with_cvr;
int main() {
int i = 100;
const int* const pi = &i;
auto& pi2 = pi; // 都保留
const int i2 = 100;
auto& i3 = i2; // 都保留
std::cout << type_id_with_cvr<decltype(pi2)>().pretty_name() << std::endl; // const int * const & = int const * const &
std::cout << type_id_with_cvr<decltype(i3)>().pretty_name() << std::endl; // const int & = int const &
return 0;
}
auto 不会影响编译速度,甚至会加快编译速度。因为编译器在处理 XX a = b 时,当 XX 是传统类型时,编译期需要检查 b 的类型是否可以转化为 XX。当 XX 为 auto 时,编译期 可以按照 b 的类型直接给定变量 a 的类型,所以效率相差不大,甚至反而还有提升。
最重要的一点,就是 auto 不要滥用,对于一些自己不明确的地方不要乱用 auto, 否则很可能出现事与愿违的结果,使用类型应该安全为先。
auto 主要用在与模板相关的代码中,一些简单的变量使用模板常常导致可读性 下降,经验不足还会导致安全性问题。
现代auto的用法
视频讲解:https://www.bilibili.com/video/BV1b94y1k7dm/?spm_id_from=333.337.search-card.all.click&vd_source=cb02f779bd17a3aad9801e0c4464dfc9
指针和引用
介绍
在C++中,指针和引用是两种重要的概念,它们都用于间接访问变量,但它们的使用方式和特性有所不同。下面是对指针和引用的详细讲解:
指针(Pointer)
定义:指针是一个存储内存地址的变量,使用 * 声明指针类型,可以通过指针间接访问和修改变量的值。
声明:使用*符号来声明指针。
int* ptr; // ptr是一个指向int类型的指针
初始化:指针可以通过取地址运算符&来初始化:
int a = 10;
int* ptr = &a; // ptr现在指向变量a的地址
解引用:使用*运算符可以访问指针所指向的值:
int value = *ptr; // value现在是10
指针的运算:指针可以进行算术运算,例如加减操作,通常用于数组的遍历。
空指针:指针可以被设置为nullptr,表示它不指向任何有效的内存地址:
int* ptr = nullptr;
引用(Reference)
定义:引用是某个变量的别名,使用 & 声明引用类型,必须在声明时初始化,一旦绑定,不能更改引用的对象。
声明:使用&符号来声明引用。例如:
int a = 10;
int& ref = a; // ref是a的引用
使用:引用可以像普通变量一样使用:
ref = 20; // 这将改变a的值为20
引用的特性:
引用必须在声明时初始化。
引用不能为nullptr,也不能指向不同的变量。
引用通常用于函数参数和返回值,以避免复制开销。
指针与引用的比较
总结
指针和引用在C++中各有其用途。指针提供了更大的灵活性和控制,但也带来了更高的复杂性和潜在的错误(如悬空指针)。引用则提供了一种更安全和简洁的方式来处理变量,尤其是在函数参数传递时。理解这两者的区别和使用场景对于编写高效和安全的C++代码至关重要。
初学者入门可以看视频,简单讲解:https://www.bilibili.com/video/BV18exnehEeF/?spm_id_from=333.337.search-card.all.click&vd_source=cb02f779bd17a3aad9801e0c4464dfc9
指针和引用的关系
引用就是作用阉割(更安全)的指针(可以视为“type *const” 指针常量,所以引用必须上来就赋初值,不能设置为空),编译器不将引用视作对象,操作引用相当于操作引用指向的对象。也就从根本是杜绝了引用篡改内存的能力
进一步解释:
指针是一个变量,可以存储不同的地址(可以指向不同的内存)所以指针是可以修改内存的;引用是更安全的指针,它是类型*const,存储的地址不能改变(和第一次指向的内存锁死了)它其实是一个指针常量,杜绝了引用篡改内存的能力;
#include <iostream>
int main() {
int i = 20;
int& refI = i;
int* const pi= &i; // int& = int* cosnt 引用 = 指针常量
std::cout << "refI: " << refI << ", *p: " << *pi << std::endl;
return 0;
}
指针和引用的作用和区别
下面的思维导图总结的很好
野指针,悬挂指针,如何避免使用野指针和悬挂指针
野指针:野指针是指未初始化的指针
int* ptr; // 未初始化的指针,这是一个野指针
*ptr = 10; // 危险!可能导致程序崩溃
悬挂指针:悬挂指针是指原本指向的内存已经被释放或不再有效的指针
int* createDanglingPointer() {
int x = 10;
return &x; // 返回局部变量的地址,函数结束后x将被销毁
} // x 被销毁后,返回的指针变成悬挂指针
如何避免野指针:指针定义时需要初始化
如何避免悬挂指针:delete后,指针置空
或者直接使用智能指针
左值,右值,左值引用,右值引用
介绍
左值(Lvalue)
左值是可以出现在赋值语句左边的表达式。具有以下特征:
- 有持久的内存地址
- 可以被取地址
- 可以被赋值
int x = 10; // x是左值
int* ptr = &x; // 可以取x的地址
x = 20; // 可以被赋值
右值(Rvalue)
右值是只能出现在赋值语句右边的表达式。具有以下特征:
- 临时的
- 不可取地址
- 不可被赋值
int y = x + 5; // (x + 5)是右值
int z = 10; // 10是右值
左值引用(Lvalue Reference)
左值引用是传统的引用,使用 & 符号声明。
- 只能绑定到左值
- 可以读写原始对象
- 不能绑定到右值(C++11之前)
int x = 10;
int& ref = x; // ref是x的左值引用
ref = 20; // 通过引用修改原值
右值引用(Rvalue Reference)
右值引用是C++11引入的新特性,使用 && 符号声明。主要用于移动语义和完美转发。
- 可以绑定到右值
- 主要用于移动语义和完美转发
- 可以“窃取“临时对象的资源
int&& rref = 10; // 右值引用
右值引用的主要应用场景
移动语义
class MyString {
public:
// 移动构造函数
MyString(MyString&& other) noexcept {
// 直接转移资源,避免深拷贝
}
};
完美转发
template<typename T>
void wrapper(T&& arg) {
// 完美转发参数
foo(std::forward<T>(arg));
}
左值和右值的转换
int x = 10;
int&& rref = std::move(x); // 将左值x转换为右值引用
实际应用示例
#include <iostream>
#include <utility>
void processValue(int& x) {
std::cout << "Lvalue reference" << std::endl;
}
void processValue(int&& x) {
std::cout << "Rvalue reference" << std::endl;
}
int main() {
int a = 10;
processValue(a); // 调用左值引用版本
processValue(10); // 调用右值引用版本
processValue(std::move(a)); // 调用右值引用版本
return 0;
}
总结
- 左值:有标识符,可寻址
- 右值:临时的,不可寻址
- 左值引用:传统引用,绑定左值
- 右值引用:C++11特性,支持移动语义和完美转发
- std::move() 可以将左值转换为右值引用
- 右值引用支持移动构造和移动赋值,减少不必要的内存拷贝
用法
左值,右值
C++任何一个对象要么是左值,要么是右值 int i = 10,i 和 10 都是对象,i是左值,10是右值;
左值:拥有地址属性的对象就叫左值,左值来源于c语言的说法,能放在“=”左面的就是左值,注意,左值也可以放在“=”右面。
右值:没有地址属性的对象就叫做右值,注意,右值绝对不可以放在等号左面
有地址属性,就代表可以操作地址,没有地址属性,就无法操作操作地址;
一般来说, 判断一个对象是左值还是右值,就看对象有没有地址属性。
比如临时对象,就都是右值,临时对象没有地址属性,无法操作地址。 注意:左值也可以放在“=”右面,但右值绝对不可以放在等号左面
小测验:
#include <iostream>
int main() {
int i = 10;
int i2 = (i + 1); // i + 1 临时对象 右值
++i = 200; // ++i 先给i加1,然后返回i,i是有地址的,左值
i++; // i++ 先返回一个临时变量,临时变量的值 = i的值,然后临时变量的值 + 1
// 返回的是临时变量,当然无法使用地址
return 0;
}
引用分类
普通左值引用:就是一个对象的别名,只能绑定左值,无法绑定常量对象
#include <iostream>
// 因为引用相当于别名,如果这里可以绑定的话,
// 我们只要修改refI的值,那么i的值可以绕过这个const修饰符而被修改,那么const就没有意义了
int main() {
const int i = 100;
int& refI = i; // 非法,左值引用不允许绑定常量对象
refI = 200;
int j = 100;
int& refJ = j; // 合法
return 0;
}
const 左值引用:可以对常量起别名,可以绑定左值和右值
#include <iostream>
int main() {
const int i = 100;
const int& refI = i; // 绑定左值 合法
const int& refI1 = (i + 1); // 绑定右值 合法;
return 0;
}
右值引用:只能绑定右值的引用
#include <iostream>
// 右值引用 只能绑定右值
int main() {
int i = 100;
int&& rrefI = 200; // 右值引用,绑定右值合法
int&& refI1 = i; // 右值引用,绑定左值不合法
return 0;
}
左值引用与右值引用的区别?右值引用的意义?
https://www.bilibili.com/video/BV1eN4y1R7Me?spm_id_from=333.788.videopod.sections&vd_source=cb02f779bd17a3aad9801e0c4464dfc9
move函数 临时对象
用法
(1) move函数:右值看重对象的值而不考虑地址,move函数可以对一个左值使用,使操作系统不再在意其地址属性,将其完全视作一个右值。
#include <iostream>
// 右值引用 只能绑定右值
int main() {
int i = 100;
int&& rrefI = std::move(i); // std::move(i)这个整体是右值
i = 20; // 但是i还是左值,千万不要再使用i,否则move就没有意义了;
std::cout << "i: " << i << " rrefI: " << rrefI << std::endl;
return 0;
}
move函数让操作的对象失去了地址属性,所以我们有义务保证以后不再使用该变量的地址属性,简单来说就是不再使用该变量,因为左值对象的地址是其使用时无法绕过的属性
(2) 临时对象:右值都是不体现地址的对象。那么,还有什么能比临时对象更加没有地址属性呢?
所以右值引用主要负责处理的就是临时对象。 程序执行时生成的中间对象就是临时对象,注意,所有的临时对象都是右值,因为临时对象产生后很快就可能被销毁,使用的是它的值属性
可调用对象
介绍
在C++中,可调用对象是指任何可以像函数一样被调用的对象。这些对象可以是函数、函数指针、函数对象(仿函数)或 lambda 表达式。
普通函数
普通函数是最基本的可调用对象。你可以直接通过函数名来调用它们。
#include <iostream>
void sayHello() {
std::cout << "Hello, World!" << std::endl;
}
int main() {
sayHello(); // 调用函数
return 0;
}
函数指针
函数指针是指向函数的指针,可以用来调用指向的函数。它们允许在运行时选择要调用的函数。
#include <iostream>
void sayHello() {
std::cout << "Hello, World!" << std::endl;
}
int main() {
void (*funcPtr)() = sayHello; // 定义函数指针并指向 sayHello
funcPtr(); // 通过函数指针调用函数
return 0;
}
函数对象(仿函数)
函数对象是重载了 operator() 的类的实例。它们可以像普通函数一样被调用,且可以保存状态。
#include <iostream>
class Functor {
public:
void operator()() const {
std::cout << "Hello from Functor!" << std::endl;
}
};
int main() {
Functor f; // 创建函数对象
f(); // 调用函数对象
return 0;
}
Lambda 表达式
Lambda 表达式是 C++11 引入的一种轻量级的可调用对象。它们可以捕获周围的变量,并且可以像函数一样被调用。
#include <iostream>
int main() {
auto lambda = []() {
std::cout << "Hello from Lambda!" << std::endl;
};
lambda(); // 调用 lambda 表达式
return 0;
}
std::function
std::function 是一个通用的可调用对象包装器,可以存储任何可调用对象,包括普通函数、函数指针、函数对象和 lambda 表达式。它提供了统一的接口来调用这些对象。
#include <iostream>
#include <functional>
void sayHello() {
std::cout << "Hello from std::function!" << std::endl;
}
int main() {
std::function<void()> func = sayHello; // 使用 std::function
func(); // 调用
return 0;
}
可调用对象的应用
可调用对象在许多场景中非常有用,例如:
- 回调函数:在事件驱动编程中,常常需要将函数作为参数传递,以便在特定事件发生时调用。
- STL算法:STL(标准模板库)中的许多算法(如
std::sort)接受可调用对象作为参数,以便在排序或查找时使用自定义的比较逻辑。 - 多线程:在多线程编程中,可以将可调用对象传递给线程,以便在新线程中执行。
用法
如果一个对象可以使用调用运算符“()”,()里面可以放参数,这个对象就是可调用对象
可调用对象分类
(1) 函数:函数自然可以调用()运算符,是最典型的可调用对象
(2) 仿函数:具有operator()函数的类对象,此时类对象可以当做函数使用,因此称为仿函数
#include <iostream>
class Test // 有operator()函数
{
public:
void operator()(int i)
{
std::cout << i << std::endl;
std::cout << "hello world" << std::endl;
}
};
int main()
{
Test t; // t此时就是一个仿函数
t(20);
return 0;
}
(3) lambda 表达式:就是匿名函数,普通的函数在使用前需要找个地方将这个函数定义,于是 C++提供了 lambda 表达式,需要函数时直接在需要的地方写一个 lambda 表达式,省去了定义函数的过程,增加开发效率
#include <iostream>
int main()
{
[] {
std::cout << "hello world" << std::endl;
}();
return 0;
}
注意:lambda 表达式很重要,现代 C++程序中,lambda 表达式是大量使用的。
lambda 表达式的格式:最少是“[] {}”,完整的格式为“[] () ->ret {}”。
lambda 各个组件介绍
-
[]代表捕获列表:表示 lambda 表达式可以访问前文的哪些变量。 基本用法
- []表示不捕获任何变量。
- [=]:表示按值捕获所有变量。
- [&]:表示按照引用捕获所有变量。 =,&也可以混合使用
- [=, &i]:表示变量 i 用引用传递,除 i 的所有变量用值传递。
- [&, i]:表示变量 i 用值传递,除 i 的所有变量用引用传递。 当然,也可以捕获单独的变量
- [i]:表示以值传递的形式捕获 i
- [&i]:表示以引用传递的方式捕获 i
-
()代表 lambda 表达式的参数,函数有参数,lambda 自然也有。
-
->ret 表示指定 lambda 的返回值,如果不指定,lambda 表达式也会推断出一个返回值的。
-
{}就是函数体了,和普通函数的函数体功能完全相同
可调用对象的常见用法
(1) 可调用对象作为函数的参数
这里使用函数指针对象举例:
#include <iostream>
void test(int i)
{
std::cout << i << std::endl;
std::cout << "hello world" << std::endl;
}
using pf_type = void(*)(int); // 函数指针
void myFunc(pf_type pf, int i) // 可调用对象作为函数的参数
{
pf(i);
}
int main()
{
myFunc(test, 200);
return 0;
}
参数使用函数指针对象 接收 函数地址,实参&test,&可以省略
C++ 类特性
类,对象,面向对象
介绍
在C++中,类、对象和面向对象编程(OOP)是核心概念。下面是对这些概念的详细讲解:
类(Class)
类是C++中的一个用户定义的数据类型,它是对象的蓝图或模板。类定义了对象的属性(成员变量)和行为(成员函数)。通过类,可以将数据和操作这些数据的函数封装在一起。
class Dog {
public:
// 成员变量
std::string name;
int age;
// 成员函数
void bark() {
std::cout << name << " says Woof!" << std::endl;
}
};
对象(Object)
对象是类的实例。通过类定义的模板,可以创建多个对象,每个对象都有自己的属性值。对象是实际使用类时的具体实体。
int main() {
Dog myDog; // 创建一个Dog类的对象
myDog.name = "Buddy"; // 设置对象的属性
myDog.age = 3;
myDog.bark(); // 调用对象的方法
return 0;
}
面向对象编程(OOP)
面向对象编程是一种编程范式,它使用“对象”来设计程序。OOP的主要特性包括:
- 封装(Encapsulation):将数据和操作数据的方法封装在一起,限制外部对内部数据的直接访问。通过访问修饰符(如
public、private、protected)来控制访问权限。
class BankAccount {
private:
double balance; // 私有成员,外部无法直接访问
public:
void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
bool withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return true;
}
return false;
}
};
- 继承(Inheritance):允许一个类(子类)继承另一个类(父类)的属性和方法,从而实现代码重用和扩展。
class Animal {
public:
void eat() {
std::cout << "Eating..." << std::endl;
}
};
class Dog : public Animal { // Dog类继承自Animal类
public:
void bark() {
std::cout << "Woof!" << std::endl;
}
};
多态(Polymorphism):允许不同类的对象以相同的方式调用同一方法,具体的实现由对象的实际类型决定。多态可以通过函数重载和虚函数实现。
class Animal {
public:
virtual void sound() { // 虚函数
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void sound() override { // 重写虚函数
std::cout << "Woof!" << std::endl;
}
};
void makeSound(Animal* animal) {
animal->sound(); // 多态
}
C++中的类和对象是实现面向对象编程的基础。通过封装、继承和多态,OOP使得代码更易于管理、扩展和重用。这种编程范式在大型软件开发中尤为重要,因为它有助于组织复杂的代码结构。
已经有了面向过程,为什么要面向对象?
- 面向对象和面向过程是一个相对的概念。
- 面向过程是按照计算机的工作逻辑来编码的方式,最典型的面向过程的语言就
是 c 语言了,c 语言直接对应汇编,汇编又对应电路。
- 面向对象则是按照人类的思维来编码的一种方式,C++就完全支持面向对象功
能,可以按照人类的思维来处理问题。
- 举个例子,要把大象装冰箱,按照人类的思路自然是分三步,打开冰箱,将大
象装进去,关上冰箱。要实现这三步,我们就要首先有人,冰箱这两个对象。人有给冰箱发指令的能力,冰箱有能够接受指令并打开或关闭门的能力。
但是从计算机的角度讲,计算机只能定义一个叫做人和冰箱的结构体。人有手
这个部位,冰箱有门这个部位。然后从天而降一个函数,是这个函数让手打开了冰
箱,又是另一个函数让大象进去,再是另一个函数让冰箱门关上。
从开发者的角度讲,面向对象显然更利于程序设计。用面向过程的开发方式,
程序一旦大了,各种从天而降的函数会非常繁琐,一些用纯 c 写的大型程序,实际
上也是模拟了面向对象的方式。
那么,如何用面向过程的 c 语言模拟出面向对象的能力呢?类就诞生了,在类
中可以定义专属于类的函数,让类有了自己的动作。回到那个例子,人的类有了让
冰箱开门的能力,冰箱有了让人打开的能力,不再需要天降神秘力量了。
总结:到现在,大家应该可以理解类的重要性了吧,这是面向对象的基石,
也可以说是所有现代程序的基石。
面向对象的三大特征
视频讲解:
https://www.bilibili.com/video/BV1c1421R71L?spm_id_from=333.788.videopod.sections&vd_source=cb02f779bd17a3aad9801e0c4464dfc9
构造函数,析构函数
介绍
在C++中,构造函数和析构函数是类的重要组成部分,它们用于对象的初始化和清理。下面是对这两个概念的详细讲解:
构造函数(Constructor)
构造函数是一个特殊的成员函数,用于初始化对象。当创建对象时,构造函数会被自动调用。构造函数的名称与类名相同,并且没有返回值。
特点:
- 名称相同:构造函数的名称与类名相同。
- 没有返回值:构造函数不返回任何值,也不可以指定返回类型。
- 可以重载:可以定义多个构造函数,参数不同以实现不同的初始化方式。
class Point {
private:
int x, y;
public:
// 默认构造函数
Point() {
x = 0;
y = 0;
}
// 带参数的构造函数
Point(int xVal, int yVal) {
x = xVal;
y = yVal;
}
void display() {
std::cout << "Point(" << x << ", " << y << ")" << std::endl;
}
};
int main() {
Point p1; // 调用默认构造函数
Point p2(10, 20); // 调用带参数的构造函数
p1.display(); // 输出: Point(0, 0)
p2.display(); // 输出: Point(10, 20)
return 0;
}
析构函数(Destructor)
析构函数是一个特殊的成员函数,用于清理对象在其生命周期内所占用的资源。当对象的生命周期结束时,析构函数会被自动调用。析构函数的名称与类名相同,但前面加上一个波浪号(~),同样没有返回值。
特点:
- 名称相同:析构函数的名称与类名相同,但前面加上
~。 - 没有参数:析构函数不接受参数,也不能重载。
- 自动调用:当对象的作用域结束或被删除时,析构函数会自动调用。
class Resource {
public:
Resource() {
std::cout << "Resource acquired." << std::endl;
}
~Resource() {
std::cout << "Resource released." << std::endl;
}
};
int main() {
Resource res; // 创建对象时调用构造函数
// 当res超出作用域时,析构函数会被调用
return 0;
}
构造函数和析构函数的作用
- 构造函数:用于初始化对象的状态,分配资源(如动态内存、文件句柄等)。
- 析构函数:用于释放对象占用的资源,防止内存泄漏和资源浪费。
动态内存分配中的构造和析构
在使用动态内存分配(如使用new关键字)时,构造函数和析构函数的作用尤为重要。
class MyClass {
public:
MyClass() {
std::cout << "Constructor called." << std::endl;
}
~MyClass() {
std::cout << "Destructor called." << std::endl;
}
};
int main() {
MyClass* obj = new MyClass(); // 调用构造函数
delete obj; // 调用析构函数
return 0;
}
用法
构造函数:
类相当于定义了一个新类型,该类型生成在堆或栈上的对象时内存排布和 c 语言相同。但是 c++规定,C++有在类对象创建时就在对应内存将数据初始化的能力,这就是构造函数。
#include <iostream>
class Test
{
public:
// 类的函数 常用写法1 直接类内部实现
Test()
{
std::cout << "默认构造函数" << std::endl;
}
Test(int i_, int j_, int k_) : i(i_), j(j_), k(new int(k_))
{
std::cout << "普通构造函数" << std::endl;
}
Test(const Test& test) : i(test.i), j(test.j), k(new int(*test.k)) // 深拷贝写法
{
std::cout << "拷贝构造函数" << std::endl;
}
~Test()
{
delete k;
}
private:
int i;
int j;
int* k;
};
int main() {
Test t1;
Test t2(1, 2, 3);
//Test t3 = t1; // 这里会报错,因为此时t1.k是nullptr,解引用会报错的
// 拷贝构造函数的两种写法
Test t4(t2);
Test t5 = t4;
return 0;
}
构造函数就是 C++提供的必须有的在对象创建时初始化对象的方法,(默认的什么都不做也是一种初始化的方式)
析构函数:
析构函数介绍:当类对象被销毁时,就会调用析构函数。栈上对象的销毁时机就是函数栈销毁时,堆上的对象销毁时机就是该堆内存被手动释放时,如果用new申请的这块堆内存,那调用 delete 销毁这块内存时就会调用析构函数。
当类对象销毁时有一些我们必须手动操作的步骤时,析构函数就派上了用场。所以,几乎所有的类我们都要写构造函数,析构函数却未必需要。
构造函数有哪些类型
在C++中,构造函数主要有以下几种类型,每种类型都有其特定的用途和特点。下面我将逐一介绍这些构造函数,并提供相应的示例代码。
默认构造函数
默认构造函数是指在没有提供任何参数的情况下被调用的构造函数。它可以是无参构造函数,也可以是带有默认参数的构造函数。
class Person {
public:
Person() { // 默认构造函数
name = "未知";
age = 0;
}
void display() {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
private:
std::string name;
int age;
};
int main() {
Person p; // 调用默认构造函数
p.display(); // 输出: Name: 未知, Age: 0
return 0;
}
带参数的构造函数
带参数的构造函数允许在创建对象时传递参数,以初始化对象的成员变量。
class Person {
public:
Person(std::string n, int a) { // 带参数的构造函数
name = n;
age = a;
}
void display() {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
private:
std::string name;
int age;
};
int main() {
Person p("Alice", 30); // 调用带参数的构造函数
p.display(); // 输出: Name: Alice, Age: 30
return 0;
}
拷贝构造函数
拷贝构造函数用于通过另一个同类对象来初始化新对象。它通常用于对象的复制。
class Person {
public:
Person(std::string n, int a) { // 带参数的构造函数
name = n;
age = a;
}
Person(const Person &other) { // 拷贝构造函数
name = other.name;
age = other.age;
}
void display() {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
private:
std::string name;
int age;
};
int main() {
Person p1("Bob", 25); // 调用带参数的构造函数
Person p2 = p1; // 调用拷贝构造函数
p2.display(); // 输出: Name: Bob, Age: 25
return 0;
}
移动构造函数
移动构造函数在C++11中引入,用于通过移动语义来初始化对象,通常用于优化性能,避免不必要的复制。
#include <iostream>
#include <string>
class Person {
public:
Person(std::string n, int a) : name(n), age(a) { // 带参数的构造函数
std::cout << "构造: " << name << std::endl;
}
Person(Person &&other) noexcept { // 移动构造函数
name = std::move(other.name);
age = other.age;
std::cout << "移动构造: " << name << std::endl;
}
void display() {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
private:
std::string name;
int age;
};
int main() {
Person p1("Charlie", 40); // 调用带参数的构造函数
Person p2 = std::move(p1); // 调用移动构造函数
p2.display(); // 输出: Name: Charlie, Age: 40
return 0;
}
委托构造函数
委托构造函数允许一个构造函数调用另一个构造函数,以减少代码重复。
class Person {
public:
Person() : Person("未知", 0) { // 委托构造函数
// 可以留空
}
Person(std::string n, int a) { // 带参数的构造函数
name = n;
age = a;
}
void display() {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
private:
std::string name;
int age;
};
int main() {
Person p; // 调用默认构造函数,实际上委托给了带参数的构造函数
p.display(); // 输出: Name: 未知, Age: 0
return 0;
}
explicit构造函数
使用explicit关键字可以防止构造函数被隐式调用,避免不必要的类型转换。
class Person {
public:
explicit Person(int a) { // explicit构造函数
age = a;
}
void display() {
std::cout << "Age: " << age << std::endl;
}
private:
int age;
};
int main() {
Person p(30); // 正确,调用构造函数
p.display(); // 输出: Age: 30
// Person p2 = 40; // 错误,不能隐式转换
return 0;
}
类中没有显示定义默认构造函数,什么时候生成默认构造函数
视频讲解:
https://www.bilibili.com/video/BV1AixqeNE6y?spm_id_from=333.788.videopod.sections&vd_source=cb02f779bd17a3aad9801e0c4464dfc9
编译器只会在有必要的时候生成默认构造函数,有必要具体看是否能正确初始化对象(成员变量)
类中没有显示定义默认拷贝构造函数,什么时候生成默认拷贝构造函数
视频讲解:
https://www.bilibili.com/video/BV1LVxje8EF4/?spm_id_from=333.788.comment.all.click&vd_source=cec2e4e6aff81caf6c36bcd4265ba034
编译器只会在有必要的时候生成默认拷贝构造函数,有必要具体看位拷贝语义是否能正确初始化对象(成员变量)
this,常成员函数,常对象
介绍
this关键字
在C++中,this是一个指针,指向当前对象的实例。它在类的成员函数中使用,允许你访问对象的成员变量和成员函数。以下是关于this指针的一些关键点:
(1) 指向当前对象:this指针是一个隐式参数,指向调用成员函数的对象。例如,在一个成员函数中,this->memberVariable可以用来访问当前对象的成员变量。
(2) 类型:this的类型是指向类的指针。例如,在类MyClass的成员函数中,this的类型是MyClass*。
(3) 用于区分成员变量和参数:当成员变量的名称与参数名称相同时,可以使用this来区分。例如:
class MyClass {
public:
int value;
MyClass(int value) {
this->value = value; // 使用this指针区分成员变量和参数
}
};
(4) 返回当前对象:this指针可以用于返回当前对象的引用,常用于链式调用。例如:
class MyClass {
public:
MyClass& setValue(int value) {
this->value = value;
return *this; // 返回当前对象的引用
}
};
(5) 在静态成员函数中不可用:this指针只能在非静态成员函数中使用,因为静态成员函数不属于任何特定对象。
(6) 常量成员函数中的this:在常量成员函数中,this的类型是指向常量对象的指针(const ClassName*),这意味着你不能在常量成员函数中修改对象的成员变量。
以下是一个简单的示例,展示了this的用法:
#include <iostream>
using namespace std;
class MyClass {
private:
int value;
public:
MyClass(int value) {
this->value = value; // 使用this指针
}
MyClass& setValue(int value) {
this->value = value; // 使用this指针
return *this; // 返回当前对象的引用
}
void display() const {
cout << "Value: " << this->value << endl; // 使用this指针
}
};
int main() {
MyClass obj(10);
obj.display(); // 输出: Value: 10
obj.setValue(20).display(); // 链式调用,输出: Value: 20
return 0;
}
常成员函数和常对象
在C++中,常成员函数和常对象是用于控制对象状态和行为的重要概念。它们通过使用const关键字来确保对象的不可变性,从而提高代码的安全性和可读性。下面是对这两个概念的详细讲解:
常对象(const Objects)
常对象是指在创建对象时,使用const关键字修饰的对象。这意味着该对象的状态(即其成员变量)不能被修改。
特点:
不可修改:常对象的非静态成员变量不能被修改,尝试修改会导致编译错误。
只能调用常成员函数
常量引用:常对象通常通过常引用传递,以确保在函数调用中不会修改对象。
class MyClass {
public:
int value;
MyClass(int v) : value(v) {}
};
void display(const MyClass& obj) {
// obj.value = 10; // 错误:不能修改常对象的成员
std::cout << "Value: " << obj.value << std::endl;
}
int main() {
const MyClass obj(10); // 创建常对象
display(obj); // 正确,传递常对象
return 0;
}
在这个示例中,obj是一个常对象,display函数接受一个常对象的引用作为参数,确保在函数内部不会修改obj的状态。
常成员函数(const Member Functions)
常成员函数是指在成员函数的声明中使用const关键字修饰的函数。这表示该函数不会修改调用该函数的对象的状态。
不能修改成员变量:在常成员函数中,不能修改任何非静态成员变量。
可以被常对象和非常对象调用
class MyClass {
private:
int value;
public:
MyClass(int v) : value(v) {}
// 常成员函数
void display() const {
std::cout << "Value: " << value << std::endl; // 可以读取,但不能修改
}
// 非常成员函数
void setValue(int v) {
value = v; // 这是一个非常成员函数,可以修改
}
};
int main() {
MyClass obj(10);
obj.display(); // 输出: Value: 10
const MyClass constObj(20);
constObj.display(); // 输出: Value: 20
// constObj.setValue(30); // 错误:不能调用非常成员函数
return 0;
}
在这个示例中,display是一个常成员函数,它可以在常对象上调用,而setValue是一个非常成员函数,不能在常对象上调用。
常成员函数与常对象的关系
常对象只能调用常成员函数:如果你有一个常对象,你只能调用该对象的常成员函数。这是为了确保对象的状态不会被意外修改。
常成员函数可以被常对象和非常对象调用:常成员函数可以被常对象和非常对象调用,但在常对象上调用时,函数内部不能修改对象的状态。
使用场景
常对象:常对象通常用于需要保护对象状态不被修改的场景,例如在函数参数中传递对象时,确保不会意外修改对象。
常成员函数:常成员函数用于提供只读访问对象状态的接口,确保对象的状态在调用过程中保持不变。
用法
常成员函数和常对象很多人并不在意,确实都写普通变量也可以;但是在大型程序中,尽量加上const 关键字可以减少很多不必要的错误。
(1) 常成员函数和常对象
常成员函数就是无法修改成员变量的函数。可以理解为将this指针指向对象用const修饰的函数;
常对象就是用 const 修饰的对象,定义好之后就再也不需要更改成员变量的值了。 常对象在大型程序中还是很有意义的;
#include <iostream>
class Test
{
public:
Test(std::string name_, int age_);
~Test();
void output(); // 普通成员函数
void display() const; // 常成员函数,无法修改成员变量;
// 常成员函数 本质是普通成员函数的this加上const修饰,但是参数中没有this,所以直接函数后加个const
std::string name;
int age;
};
Test::Test(std::string name_, int age_) : name(name_), age(age_) {}
Test::~Test(){}
// 普通成员函数实现
void Test::output()
{
std::cout << "调用普通成员函数" << std::endl;
std::cout << "name = " << name << ", age = " << age << std::endl;
}
// 普通成员函数的本质 全局函数 + this指针(Test类指针常量)
void output(Test* const myThis)
{
std::cout << "调用普通成员函数" << std::endl;
std::cout << "name = " << myThis->name << ", age = " << myThis->age << std::endl;
}
// 常成员函数实现
void Test::display() const
{
std::cout << "调用常成员函数" << std::endl;
//myThis->age = 100; // 此时不能修改指针指向的对象
}
// 常成员函数的本质 全局函数 + this指针(Test类指针常量指向常量)
void display(const Test* const myThis)
{
std::cout << "调用常成员函数" << std::endl;
//myThis->age = 100; // 此时不能修改指针指向的对象
}
int main()
{
Test t1("zhangsan", 20);
t1.output();
output(&t1); // 传入对象地址
t1.display();
display(&t1); // 传入对象地址
const Test t2("lisi", 25); // 不希望t2对象被修改,所以使用常对象
//t2.age = 80; // t2是常对象,不能修改对象t2
return 0;
}
(2) 常成员函数注意事项:
因为类的成员函数已经将 this 指针省略了,只能在函数后面加 const 关键字来实现 无法修改类成员变量的功能了(上述代码里也进行了演示)
- 常函数无法调用了普通函数,否则常函数的这个“常”字还有什么意义
解释:如果一个常函数里可以调用普通函数,那么我们可以调用set函数,去修改对象,那么此时这个常成员函数就没有意义了,所以语法规定,常函数无法调用普通函数
- 成员函数能写作常成员函数就尽量写作常成员函数,可以减少出错几率
- 同名的常成员函数和普通成员函数是可以重载的,常量对象会优先调用常成员函数,普通对象会优先调用普通成员函数
#include <iostream>
class Test
{
public:
Test(std::string name_, int age_);
~Test();
// 两个output函数是重载关系
void output(); // 普通成员函数
void output() const; // 常成员函数
std::string name;
int age;
};
Test::Test(std::string name_, int age_) : name(name_), age(age_) {}
Test::~Test(){}
void Test::output()
{
std::cout << "调用普通成员函数output" << std::endl;
std::cout << "name = " << name << ", age = " << age << std::endl;
}
void Test::output() const
{
std::cout << "调用常成员函数output" << std::endl;
std::cout << "name = " << name << ", age = " << age << std::endl;
}
int main()
{
Test t1("zhangsan", 20); // 普通对象
t1.output(); // 普通对象会优先调用普通成员函数
const Test t2("lisi", 30); // 常对象
t2.output(); // 常对象会优先调用常成员
return 0;
}
常对象注意事项:
- 常对象不能调用普通函数,原因和常成员函数不能调用普通函数是一样的
- 常成员函数和常对象要多用,这真的 是一个非常好的习惯,写大项目可以少出很多bug
inline mutable default delete
介绍
inline(内联函数)
inline关键字用于函数定义,是一个编译器优化建议,用于提高函数调用的效率。
inline int add(int a, int b) {
return a + b;
}
特点:
- 建议编译器在调用处直接展开函数,避免函数调用开销
- 编译器可以自行决定是否真正内联
- 适用于短小、频繁调用的函数
- 可以减少函数调用的栈开销
mutable(可变成员)
mutable用于修饰类的成员变量,允许在const成员函数中修改该成员变量。
class Cache {
private:
mutable int access_count = 0; // 可以在const成员函数中被修改
public:
int getData() const {
access_count++; // 即使在const函数中也可以修改
return 42;
}
};
特点:
- 允许在const成员函数中修改特定成员变量
- 常用于缓存、计数等辅助性成员
- 不影响对象的整体const语义
default(默认函数)
default用于显式地声明编译器默认生成的特殊成员函数。
class MyClass {
public:
MyClass() = default; // 显式使用编译器生成的默认构造函数
MyClass(const MyClass&) = default; // 默认拷贝构造
MyClass& operator=(MyClass&&) = default; // 默认移动赋值
};
特点:
- 明确告诉编译器使用默认实现
- 可以提高代码可读性
- 在某些情况下可以避免编译器阻止特殊成员函数的生成
delete(删除函数)
delete用于禁止使用特定的函数或运算符。
class Singleton {
public:
Singleton(const Singleton&) = delete; // 禁止拷贝构造
Singleton& operator=(const Singleton&) = delete; // 禁止拷贝赋值
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
private:
Singleton() {} // 私有构造函数
};
class NonHeap {
public:
void* operator new(size_t) = delete; // 禁止在堆上分配
void* operator new[](size_t) = delete;
};
void processOnly(int x) { /* 处理整数 */ }
void processOnly(double) = delete; // 禁止double重载
特点:
- 明确禁止某些函数的使用
- 可以阻止不期望的类型转换和函数调用
- 在编译期就能检查和阻止不正确的使用
综合示例
class SmartCache {
private:
mutable int access_count = 0;
int* data = nullptr;
public:
SmartCache() = default; // 默认构造
SmartCache(const SmartCache&) = delete; // 禁止拷贝
SmartCache& operator=(const SmartCache&) = delete;
inline int getData() const {
access_count++; // mutable允许在const函数中修改
return data ? *data : 0;
}
};
用法
inline
inline关键字作用:
在函数定义中函数返回类型前加上关键字inline就可以把函数指定为内联函数
内联函数的作用,普通函数在调用时需要给函数分配栈空间以供函数执行,压栈等操作会影响成员运行效率,于是C++提供了内联函数将函数体放到需要调用函数的地方,用空间换效率。
简单来说:普通函数调用需要分配新的栈空间,然后执行压栈等操作,而内联函数调用可以继续在当前的函数栈帧里执行,提高了运行效率,典型的以空间换时间
总结:使用 inline 关键字就是一种提高效率,但加大编译后文件大小的方式,现在随着硬件性能的提高,inline关键字用的越来越少了
inline关键字的注意事项:
(1) inline关键字只是一个建议,开发者建议编译器将成员函数当做内联函数,一般适合搞内联的情况编译器都会采纳建议
eg: 如果一个函数所需要分配的栈非常大,例如代码量很大的函数,或者是递归函数,他们需要分配的栈空间都很大,这个时候编译器就不会把它当作内联函数;
(2) 关键字 inline 必须与函数定义放在一起才能使函数成为内联,仅仅将inline放在函数声明前不起任何作用;简单来说:就是inline关键字必须与函数定义放在一起,函数声明加不加inline无所谓;
(3) 直接在类内部实现的函数,相当于函数默认加了inline关键字
inline相关代码:
#include <iostream>
class Test
{
public:
Test() {}
~Test() {}
inline void func1();
inline void func2();
void func3();
void func4()
{
std::cout << "call func4()" << std::endl;
}
};
inline void Test::func1()
{
std::cout << "call func1()" << std::endl;
}
void Test::func2()
{
std::cout << "call func2()" << std::endl;
}
inline void Test::func3()
{
std::cout << "call func3()" << std::endl;
}
int main()
{
Test t1;
t1.func1(); // 函数声明和定义都加inline关键字,建议成内联函数
t1.func2(); // 函数声明加inline,函数定义不加,不是内联函数
t1.func3(); // 函数声明不加,函数定义加inline关键字,建议成内联函数
t1.func4(); // 类内实现函数,建议成内联函数
// 总结:
// 1. 关键字 inline 必须与函数定义放在一起才能使函数成为内联,
// 2. 直接在类内部实现的函数,默认相当于加了inline关键字
return 0;
}
mutable 关键字
mutable关键字的作用:
mutable意为可变的,与const相对,被mutable修饰的成员变量,永远处于可变的状态;
mutable关键字修饰的变量在常函数中,该变量也可以被更改 (常函数中原本是不允许修改成员变量的)这个关键字在现代 C++中使用情况并不多,只有在统计函数调用次数这类情况下才推荐使用
mutable 关键字的注意事项:
(1) mutable不能修饰静态成员变量和常成员变量
mutable相关代码
#include <iostream>
class Test
{
public:
Test() {}
~Test(){}
void output() const // mutable关键字修饰的成员变量 可以在常函数里修改
{
++outputCallCount;
std::cout << "hello world" << std::endl;
}
// C++11新特性,直接初始化成员变量 <=> 代替 定义成员 + 构造函数初始化
mutable unsigned outputCallCount = 0; // mutable关键字可以修饰普通成员变量
//mutable static int i = 0; // mutable关键字不能修饰静态成员变量
//mutable const int j = 0; // mutable关键字不能修饰常成员变量
};
int main()
{
Test t1;
t1.output();
t1.output();
t1.output();
std::cout << t1.outputCallCount << std::endl;
return 0;
}
default 关键字
default 关键字的作用:
(1) 便于书写默认构造函数,默认拷贝构造函数,默认的赋值运算符重载函数,默认的析构函数,default关键字表示使用的是系统默认提供的代码,这样可以使代码更加直观,方便;
defalut 关键字注意事项:
(1) 现代 C++中,哪怕没有构造函数,也推荐将构造函数用default关键字标记,可以让代码看起来更加直观,方便
Comment
(2) 使用default关键字 语法层面就是函数声明 return_type fucntion_name (para list) = default
default相关代码:
#include <iostream>
class Test
{
public:
Test() = default; // 默认构造函数
~Test() = default; // 默认析构函数
Test(const Test& test) = default; // 默认复制构造函数
Test& operator=(const Test& test) = default; // 赋值运算符重载函数 赋值运算符
};
int main()
{
return 0;
}
delete 关键字
delete 关键字的作用:
(1) C++会为一个类生成默认构造函数,默认析构函数,默认复制构造函数,默认重载赋值运算符,在很多情况下,我们并不希望这些默认的函数被生成,为了解决这个问题,在 C++11 以前,只能有将此函数声明为私有函数或是将函数只声明不定义两种方式。于是在C++11中提供了 delete 关键字,只要在函数最后加上“=delete”就可以明确告诉编译期不要默认生成该函数
总结:delete关键字还是推荐使用的,在现代 C++代码中,如果不希望一些函数默认生 成,就用 delete 表示,这个功能还是很有用的,比如在单例模式中
delete 关键字注意事项:
(1) delete一般不会用使用在析构函数
delete相关代码
#include <iostream>
class Test
{
public:
Test() = delete; // 默认构造函数
~Test() = delete; // 默认析构函数
Test(const Test& test) = delete; // 默认复制构造函数
Test& operator=(const Test& test) = delete; // 默认重载运算符
};
int main()
{
return 0;
}
友元
介绍
在C++中,友元(friend) 是一种特殊的访问控制机制,允许其他类或其他函数访问当前类的私有成员和保护成员。友元的主要目的是为了提供更灵活的访问权限,尤其是在需要多个类之间紧密合作的情况下。
友元类型
(1) 友元函数:一个普通的函数可以被声明为某个类的友元函数,这样它就可以访问该类的私有和保护成员。
class MyClass {
private:
int data;
public:
MyClass(int val) : data(val) {}
friend void showData(MyClass obj); // 声明友元函数
};
void showData(MyClass obj) {
std::cout << "Data: " << obj.data << std::endl; // 访问私有成员
}
(2) 友元类:一个类可以被声明为另一个类的友元类,这样友元类的所有成员函数都可以访问该类的私有和保护成员。
class MyClass {
private:
int data;
public:
MyClass(int val) : data(val) {}
friend class FriendClass; // 声明友元类
};
class FriendClass {
public:
void display(MyClass obj) {
std::cout << "Data: " << obj.data << std::endl; // 访问私有成员
}
};
(3) 友元成员函数:一个类的成员函数可以被声明为另一个类的友元,这样该成员函数可以访问另一个类的私有和保护成员。
class MyClass {
private:
int data;
public:
MyClass(int val) : data(val) {}
friend void FriendClass::display(MyClass obj); // 声明友元成员函数
};
class FriendClass {
public:
void display(MyClass obj) {
std::cout << "Data: " << obj.data << std::endl; // 访问私有成员
}
};
友元特点和应用场景
- 不继承:友元关系不是继承关系,友元类或函数并不自动成为其他类的友元。
- 不对称:如果类A是类B的友元,类B并不一定是类A的友元。
- 访问权限:友元可以访问私有和保护成员,但友元本身并不是类的成员。
- 设计考虑:使用友元可以提高类之间的协作,但过度使用可能会导致代码的封装性降低,因此应谨慎使用
操作符重载:在重载某些操作符时,可能需要访问类的私有成员。
用法
友元 作用
友元是针对类来说的,友元可以让其他类或其他函数访问当前类的私有成员和保护成员
友元平常并不推荐使用,只要可以用友元写出必须用友元的重载运算符就可以了
友元 注意事项
(1) 友元会破坏封装性一般不推荐使用,所带来的方便写几个接口函数就解决了
(2) 某些运算符的重载必须用到友元的功能,这才是友元的真正用途
友元 相关代码
友元基本使用
#include <iostream>
class Test
{
friend class Test2; // 声明Test2为友元类
friend void output(const Test& test); // 声明output函数为友元函数
public:
Test() = default;
~Test() = default;
private:
std::string name = "lisi";
int age = 100;
};
// 另一个类访问私有成员
class Test2
{
public:
Test2() {};
~Test2() {};
void output(const Test& test) const // 为了在Test2类里访问Test类的私有成员,在Test类里声明Test2为友元类
{
std::cout << "name = " << test.name << ", age = " << test.age << std::endl;
}
};
// 另一个函数访问私有成员
void output(const Test& test) // 为了在output函数里访问Test的私有成员,在Test类里声明output函数为友元函数
{
std::cout << "name = " << test.name << ", age = " << test.age << std::endl;
}
int main()
{
Test t1;
Test2 t2;
t2.output(t1);
output(t1);
return 0;
}
利用公共接口代替友元
#include <iostream>
class Test
{
public:
Test() = default;
~Test() = default;
// 要想其他的类或函数 访问私有成员,友元会破坏封装性
// 最好的方式是直接多写一些公共接口就行了,也可以达到一样的效果,而且不破坏封装性
std::string getName() const{ return name; }
int getAge() const { return age; }
private:
std::string name = "lisi";
int age = 100;
};
// 另一个类访问私有成员
class Test2
{
public:
Test2() {};
~Test2() {};
void output(const Test& test) const
{ // 使用公共接口访问
std::cout << "name = " << test.getName() << ", age = " << test.getAge() << std::endl;
}
};
// 另一个函数访问私有成员
void output(const Test& test)
{
// 使用公共接口访问
std::cout << "name = " << test.getName() << ", age = " << test.getAge() << std::endl;
}
int main()
{
Test t1;
Test2 t2;
t2.output(t1);
output(t1);
return 0;
}
重载运算符
用法
在C++中,重载运算符是一种允许程序员为自定义类型(如类)定义或修改运算符的行为的机制。通过重载运算符,可以使自定义类型的对象像内置类型一样使用运算符进行操作,从而提高代码的可读性和可维护性。
重载运算符 作用
(1) 在C++中,我们希望类对象能够像基本类型对象一样进行基本操作,例如“+”、“-”、“*”、“/”,以及某些其他运算符,如“=”、“()”、“[]”、“<<”、“>>”。然而,默认情况下,类对象并不能自动支持这些运算符的操作。为了使类对象能够正确响应这些运算符,我们必须为其定义或重载运算符的行为。
(2) C++提供了一种机制来定义运算符的行为,即通过使用“operator 运算符”的语法来重载运算符,告诉编译器我们正在重载一个运算符,以便为自定义类型定义特定的操作行为。
重载运算符 注意事项
(1) 我们只能重载 C++已有的运算符,eg: 无法将**这个运算符定义为指数的形式, 因为 C++根本没有**这个运算符
(2) C++重载运算符不能改变运算符的元数,“元数”这个概念就是指一个运算符对应的对象数量,比如“+”必须为“a + b”,也就是说“+”必须有两个对象,那么“+”就是二元运算符。比如“++”运算符,必须写为“a++”,也就是一元运算符;
(3) 重载运算符的技巧:
- 如果需要调用/修改原对象,运算结果为左值,那么就返回引用;
- 如果只是访问对象,运算结果为右值,那么就返回值;
(4) 重载运算符有两种主要实现方式:
- 友元重载,直接定义全局的重载运算符函数,然后再在类中声明为友元函数
- 成员函数重载,使用类的成员函数重载运算符,第一个参数需要使用this,所以<< >>无法使用此方式重载
(5) =运算符会默认进行重载,如果不需要可以用delete关键字进行修饰。
重载运算符 相关代码
运算符有很多,我们在重载运算符的时候一般按照以下框架去写
一元运算符重载:
自增,自减 ++ --
下标 []
调用 ()
输入输出 <<,>>
二元运算符重载
基本运算 +,-,*,/
赋值 =
比较 >,<,==
三元运算符?:,不能重载
类类型转化运算符:
operator 类型
特殊的运算符:new,delete,new[],delete[]
#include <iostream>
#include <vector>
class MyInt
{
// 重载 << >> 必须使用友元,因为第一个参数无法用this访问
friend std::ostream& operator<<(std::ostream& os, const MyInt& t);
friend std::istream& operator>>(std::istream& is, MyInt& t);
public:
MyInt() : val(0) {}
MyInt(int val_) : val(val_) {}
// 重载前缀++
MyInt& operator++()
{
++val;
return *this;
}
// 重载后缀++
MyInt operator++(int)
{
MyInt tmp = *this;
val++;
return tmp;
}
// 重载前缀--
MyInt& operator--()
{
--val;
return *this;
}
// 重载后缀--
MyInt operator--(int)
{
MyInt tmp = *this;
val--;
return tmp;
}
// 重载[]
int operator[](unsigned i)const
{
return a[i];
}
// 重载()
void operator()()const
{
std::cout << "call function()" << std::endl;
}
// 重载+ -*/都差不多就不写了
MyInt operator+(const MyInt& t)
{
val += t.val;
return *this;
}
// 重载= 注意防止自赋值 然后返回原对象(返回引用)
MyInt& operator=(const MyInt& t)
{
if (this == &t) return *this;
val = t.val;
return *this;
}
// 重载 < >,>=,==这些都差不多
bool operator<(const MyInt& t)const
{
return val < t.val;
}
int val;
std::vector<int> a{ 1, 2, 3, 4, 5 };
};
std::ostream& operator<<(std::ostream& os, const MyInt& t)
{
os << t.val;
return os;
}
std::istream& operator>>(std::istream& is, MyInt& t)
{
is >> t.val;
return is;
}
int main()
{
std::cout << "测试 ++" << std::endl;
MyInt t1(10);
++t1;
std::cout << "expected: t1 = 11 " << "now: t1 = " << t1 << std::endl;
MyInt t2 = t1++;
std::cout << "expected: t1 = 12, t2 = 11 " << "now: t1 = " << t1 << " now t2 = " << t2 << std::endl;
std::cout << "探索 前缀++和后缀++ 区别" << std::endl;
int a = 3;
int b = ++(++a); // 验证 前缀++后的运算结果是左值,可以继续调用,所以重载需要返回引用
//int c = (a++)++; // 验证 后缀++后的运算结果是右值,不能继续调用,所以重载是返回值
int c = a++;
std::cout << a << " " << b << " " << c << std::endl;
// 测试 --
std::cout << "测试 --" << std::endl;
--t1;
t1--;
std::cout << "expected: t1 = 10 " << "now: t1 = " << t1 << std::endl;
// 测试[]
std::cout << "测试 []" << std::endl;
std::cout << "expected: t1[1] = 2 " << "now: t1[1] = " << t1[1] << std::endl;
// 测试()
std::cout << "测试 ()" << std::endl;
t1();
// 测试 << >>
std::cout << "测试 << >>" << std::endl;
MyInt t3;
std::cin >> t3;
std::cout << t3 << std::endl;
// 测试 +
std::cout << "测试 +" << std::endl;
MyInt t4 = t1 + t2;
std::cout << t4 << std::endl;
// 测试=
std::cout << "测试 =" << std::endl;
MyInt t5;
std::cout << t5 << std::endl;
t5 = t4;
std::cout << t5 << std::endl;
// 测试<
std::cout << "测试 <" << std::endl;
MyInt t6(10), t7(12);
std::cout << (t6 < t7) << std::endl;
//return 0;
}
C++面试题整理
QT相关 https://www.bilibili.com/video/BV1FM4m1U7EB?spm_id_from=333.788.videopod.sections&vd_source=cb02f779bd17a3aad9801e0c4464dfc9
C++相关:
https://www.bilibili.com/video/BV1Qm411z7AH?spm_id_from=333.788.videopod.sections&vd_source=cb02f779bd17a3aad9801e0c4464dfc9
https://www.bilibili.com/video/BV1RZtMeVE8r?spm_id_from=333.788.videopod.sections&vd_source=cb02f779bd17a3aad9801e0c4464dfc9
QT
QT是C++开发人员几乎都要学习的一个GUI库,常用于桌面客户端开发
QT的学习,分为以下几个大阶段
(1) QT基础
QT的环境搭建,文件类型,信号槽,定时器,容器类,数据读写,网络请求,线程库,控件
(2) QT音视频图像
ffmpeg在QT中的使用,opengl,opencv
(3) QT网络流媒体
主要就是注重流媒体客户端,
QT音视频图像
QT音视频图像,主要是围绕图像处理和渲染引擎
QT音视频图像的学习,包含音视频图像领域常用工具的理论 + 实践,之后通过一些典型的案例快速熟悉QT视音频图像领域的开发工作
(1) FFmpeg
理论 + 实践:熟悉FFmpeg在不同平台的编译过程,FFmpeg的不同版本之间的差异,FFmpeg的使用场景
案例:QT + FFmpeg开发播放器核心,熟悉FFmpeg的API,熟悉在QT中使用FFmpeg
案例:QT + FFmpeg封装mp4
案例:QT + FFmpeg开发视频格式转化工厂
(2) OpenGL
理论 + 实践:OpenGL的数学基础,OpenGL基础操作,OpenGL的shader编程,视频处理等
案例:QT + OpenGL人脸贴纸特效渲染引擎
(3) OpenCV
案例:QT + OpenCV人脸标定
案例:QT + FFmpeg开发播放器核心
搭建环境
该案例需要用到FFmpeg和x264这两个库,所以我们首先要创建一个QT工程,然后为了能正确链接FFmpeg和x264库,并且在开发中使用这些库提供的API。
QT工程中,要使用这两个库,需要去编辑配置文件,这里使用qmake去组织项目,所以就是要编辑.pro文件如果不熟悉.pro文件的编写语法,请参考之前的QT基础开发中的.pro相关文章。
要在项目中加载这些库,我们首先在项目中创建3rdparty文件夹,这里专门用来存放项目中使用的第三方库。
Win32
我们这里假设已经拿到了FFmpeg和x264的静态库文件,动态库文件,头文件。
(1) 在3rdparty中,创建win文件夹,存放win平台下,所有要使用的第三方库
(2) win文件中,创建子文件夹 libFFmpeg和libx264,因为我们要使用这两个库
(3) libxxx文件夹下存放所有关于该库的内容,一般有头文件,静态库文件,动态库文件
3rdpatry的目录结构:
3rdparty
+---mac
| +---libFFmpeg
| | +---include
| | | +---libavcodec
| | | +---libavdevice
| | | +---libavfilter
| | | +---libavformat
| | | +---libavutil
| | | +---libpostproc
| | | +---libswresample
| | | /---libswscale
| | /---lib
| | /---pkgconfig
| /---libx264
| /---lib
/---win
+---libFFmpeg
| +---bin
| +---include
| | +---libavcodec
| | +---libavdevice
| | +---libavfilter
| | +---libavformat
| | +---libavutil
| | +---libpostproc
| | +---libswresample
| | /---libswscale
| /---lib
/---libx264
/---lib
在工程目录中添加了ffmpeg和x264库,现在需要编辑pro文件,来去链接这些库,包含这些库的头文件,主要是使用INLCUDEPATH +=添加头文件,LIBS+=添加静态库文件,动态库文件我们需要自己拷贝到最终的可执行文件夹的同级目录下,这一步很关键
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets multimedia
TARGET = JCMPlayer
TEMPLATE = app
macx {
}
win32 {
DEFINES += JCMPLAYER_WINDOWS
INCLUDEPATH += $$PWD/3rdparty/win/libFFmpeg/include
LIBS += $$PWD/3rdparty/win/libFFmpeg/lib/libavformat.dll.a /
$$PWD/3rdparty/win/libFFmpeg/lib/libavcodec.dll.a /
$$PWD/3rdparty/win/libFFmpeg/lib/libavutil.dll.a /
$$PWD/3rdparty/win/libFFmpeg/lib/libswresample.dll.a /
$$PWD/3rdparty/win/libFFmpeg/lib/libswscale.dll.a /
$$PWD/3rdparty/win/libFFmpeg/lib/libpostproc.dll.a /
$$PWD/3rdparty/win/libFFmpeg/lib/libavfilter.dll.a
LIBS += -lOpengl32
}
SOURCES +=
HEADERS +=
FORMS +=
RESOURCES +=
添加好后在QT工程中,引入头文件并使用;build没有报错证明头文件和静态库都找到了,但要注意动态库是运行时加载,所以build之后我们还要去run程序,检测动态库文件是否找到,如果run没问题,那么此时win环境搭建完毕,之后就可以愉快开发了。
Mac
Linux
OpenGL 理论 + 实战
数学基础
Git
Git几乎是程序员必备技能之一,每一个程序员都应该好好学习git
本专栏为Git专栏,主要记录Git的基本使用,工作使用,以及相关文章
git分支相关操作
创建和删除分支
本地分支
创建本地分支:
git branch <branch-name>
创建一个分支并立即切换到该分支
git checkout -b <new-branch-name>
git switch -c <new-branch-name>
从特定的起点创建分支:
git checkout -b <new-branch-name> <starting-point>
git switch -c <new-branch-name> <starting-point>
删除本地分支:
git branch -d <branch-name> # 安全删除(如果分支未完全合并,则删除失败)
git branch -D <branch-name> # 强制删除
为什么要删除的分支没有合并时,删除会失败呢
批量删除本地分支:
git branch | grep “feature/” | xargs git branch -d
git branch | grep ”feature/“ | xargs git branch -D
远程分支
创建远程分支(通过推送本地分支):
git push <remote-name> <local-branch>:<remote-branch>
删除远程分支:
git push <remote-name> --delete <remote-branch>
查看分支
查看本地所有分支:
git branch
查看远程所有分支:
git branch -r
查看本地和远程所有分支:
git branch -a
切换分支
切换本地分支
git checkout <branch-name>
或使用更新的 Git 命令:
git switch <branch-name>
切换远程分支
切换到远程分支实际上是创建一个跟踪远程分支的本地分支:
git checkout -b <local-branch> <remote-name>/<remote-branch>
或使用更简洁的方式:
git checkout --track <remote-name>/<remote-branch>
使用新版 Git:
git switch -c <local-branch> --track <remote-name>/<remote-branch>
git开发常用操作
本地在dev分支,做了一些修改,现在想要切换到本地的master分支
在切换到 master 分支之前,确保你不会丢失在 dev 分支上所做的更改。以下是几种安全切换分支的方法:
提交更改
如果你已经完成了在 dev 分支上的更改,并且希望保留这些更改,可以将它们提交到 dev 分支:
git add .
git commit -m "Save changes before switching to master"
然后,你可以安全地切换到 master 分支:
git checkout master
暂存更改(Stash)
如果你不想立即提交更改,可以使用 git stash 将更改暂存起来:
git stash
这会将你的更改保存到一个临时存储区,并恢复工作目录到干净状态。然后,你可以切换到 master 分支:
git checkout master
当你准备好恢复 dev 分支上的更改时,可以使用:
git checkout dev
git stash pop
这会将暂存的更改应用到 dev 分支上。
创建一个新的分支
如果你想保留当前的更改,但又不想提交到 dev 分支,可以创建一个新的分支:
git checkout -b temp-branch
这会创建一个名为 temp-branch 的新分支,并切换到该分支。你可以在这个新分支上继续工作,或者稍后再将更改合并回 dev 分支。
总结:
- 提交更改:适用于你已经完成的工作。
- 暂存更改:适用于你想暂时保存更改但不想提交的情况。
- 创建新分支:适用于你想保留更改并继续工作的情况。
选择最适合你当前需求的方法,确保在切换分支之前不会丢失任何重要的更改。
本地dev分支合并到master分支,想以master分支为主,简单方法
如果你希望在将 dev 分支合并到 master 分支时以 master 分支的内容为主,可以使用 -X ours 选项。这样,Git 会在合并时自动选择 master 分支的内容,忽略 dev 分支的冲突部分。
简化操作步骤
切换到 master 分支:
git checkout master
合并 dev 分支并自动选择 master 的更改:
git merge -X ours dev
注意事项:
- 使用
-X ours选项时,Git 会自动解决冲突,所有冲突的文件将以master分支的内容为主。 - 在合并完成后,建议检查合并结果,确保所有更改都符合预期。
总结:
通过使用 -X ours 选项,你可以快速合并 dev 分支到 master 分支,并自动选择 master 的更改,避免手动解决冲突的步骤。这是处理合并冲突时的一种简便方法。
本地dev分支合并到master分支,想以dev分支为主,简单方法
如果你希望在合并 dev 分支到 master 分支时自动选择 dev 分支的更改,可以使用 -X theirs 选项来简化操作。这样,Git 会在合并时自动选择 dev 分支的内容,忽略 master 分支的冲突部分。
简化操作步骤
切换到 master 分支:
git checkout master
合并 dev 分支并自动选择 dev 的更改:
git merge -X theirs dev
注意事项:
使用 -X theirs 选项时,Git 会自动解决冲突,所有冲突的文件将以 dev 分支的内容为主。
在合并完成后,建议检查合并结果,确保所有更改都符合预期。
总结:
通过使用 -X theirs 选项,你可以快速合并 dev 分支到 master 分支,并自动选择 dev 的更改,避免手动解决冲突的步骤。这是处理合并冲突时的一种简便方法。
本地dev分支merge到master分支,有冲突如何解决
在将本地的 dev 分支合并到 master 分支时,如果遇到冲突,可以按照以下步骤解决冲突:
操作步骤
切换到 master 分支:
git checkout master
合并 dev 分支:
git merge dev
如果有冲突,Git 会提示你哪些文件存在冲突。
查看冲突文件: 使用以下命令查看哪些文件存在冲突:
git status
解决冲突: 打开有冲突的文件,查找冲突标记(<<<<<<<, =======, >>>>>>>)
例如:
<<<<<<< HEAD
// master 分支的内容
=======
// dev 分支的内容
>>>>>>> dev
你需要手动编辑这些文件,选择保留的内容,删除冲突标记。可以选择保留 master 的内容、dev 的内容,或者两者的组合。
标记冲突已解决: 在解决完所有冲突后,使用以下命令将已解决的文件标记为已解决:
git add <conflicted-file>
如果有多个文件,可以使用:
git add .
完成合并:
一旦所有冲突都解决并标记为已解决,使用以下命令完成合并:
git commit -m "Merge dev into master, resolved conflicts"
总结:
通过以上步骤,你可以成功解决合并冲突并将 dev 分支的更改合并到 master 分支。确保在解决冲突时仔细检查每个文件,以避免丢失重要的更改。
git远程相关操作
本地仓库初次上传到远程仓库
clone完仓库后,本地选取对应的远程分支进行开发
git checkout origin/dev
git checkout -b dev
git check -b dev origin/dev
上面两条命令组合和下面一条命令效果一样吗? 答案是不一样!
解析
(1) git checkout origin/dev
这条命令用于切换到远程的 dev 分支,但不会在本地创建一个新的分支。你将处于“游离头指针”(detached HEAD)状态,意味着你在查看远程分支的内容,但没有在本地创建一个对应的分支。
(2) git checkout -b dev
这条命令用于在本地创建一个新的分支 dev,并切换到该分支。此时,dev 分支是一个独立的本地分支,与远程分支没有直接关联。
(3) git checkout -b dev origin/dev
这条命令用于在本地创建一个新的分支 dev,并将其设置为跟踪远程的 origin/dev 分支。这样,当你在本地 dev 分支上进行更改并推送时,Git 会知道将更改推送到远程的 dev 分支。
结论
组合命令:如果你先执行 git checkout origin/dev,然后执行 git checkout -b dev,你会得到一个新的本地分支 dev,但这个分支的起点是你在 origin/dev 上的游离状态。这意味着 dev 分支不会自动跟踪 origin/dev。
单条命令:git checkout -b dev origin/dev 直接创建一个新的本地分支 dev,并将其设置为跟踪远程的 origin/dev 分支。
因此,虽然组合命令和单条命令都可以创建一个名为 dev 的本地分支,但它们的效果并不完全相同:
组合命令:创建的 dev 分支不会自动跟踪 origin/dev
单条命令:创建的 dev 分支会自动跟踪 origin/dev
如果你希望本地分支 dev 跟踪远程分支,建议使用 git checkout -b dev origin/dev
正确的组合命令
那么组合命令,如何设置自动跟踪origin/dev
如果你使用组合命令 git checkout origin/dev 和 git checkout -b dev 创建了本地分支 dev,但希望将其设置为自动跟踪远程的 origin/dev 分支,可以使用以下步骤:
(1) 创建本地分支并切换
首先,使用组合命令创建本地分支 dev:
git checkout origin/dev # 切换到远程分支,处于游离状态
git checkout -b dev # 创建本地分支 dev
(2) 设置跟踪关系
接下来,使用以下命令将本地 dev 分支设置为跟踪远程的 origin/dev 分支:
git branch --set-upstream-to=origin/dev dev
(3) 验证跟踪关系
你可以使用以下命令来验证本地分支 dev 是否正确跟踪远程分支:
git branch -vv
这将显示所有本地分支及其跟踪的远程分支。
通过以上步骤,你可以将本地分支 dev 设置为自动跟踪远程的 origin/dev 分支。这样,当你在本地 dev 分支上进行更改并推送时,Git 会知道将更改推送到远程的 dev 分支
git push远程仓库authentication出错
有很多种解决办法,我认为最好的解决办法就是使用git credential
(1) 首先就是创建token,创建完后自己可以选择保存下来,方便后续使用
(2) 还是使用git push,但是此时输入的密码就是token了,git输入密码时,不能展示密码,先复制,然后直接右键按一下就是粘贴了
(3) 为了防止每次输入token,使用git config –global credential.helper store,这样就会把token存入/~/.git-credentials中,文件内容类似于https:///<USERNAME>:/<TOKEN>@github.com
参考:
[1] https://stackoverflow.com/questions/68775869/message-support-for-password-authentication-was-removed
[2] https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage
git push大文件失败
要解决由于文件大小超过 GitHub 的限制而导致的 git push 失败问题,可以考虑使用 Git Large File Storage (LFS) Git LFS 是一个 Git 扩展,专门用于处理大文件。通过使用 Git LFS,您可以将大文件存储在外部服务器上,从而避免 GitHub 的文件大小限制。
使用方法:
(1) 安装 Git LFS:
根据您的系统,从 Git LFS 官网 下载并安装。
(2) 初始化 Git LFS:
git lfs install
(3) 跟踪大文件: 假设您的大文件路径是 Resources/ffmpeg.exe:
git lfs track "Resources/ffmpeg.exe"
(4) 添加跟踪配置到仓库:
git add .gitattributes
(5) 提交和推送文件:
git add Resources/ffmpeg.exe
git commit -m "Add ffmpeg with Git LFS"
git push origin main
git代理相关操作
设置代理和取消代理
参考:https://gist.github.com/laispace/666dd7b27e9116faece6
# 设置代理
git config --global http.proxy http://127.0.0.1:1080
git config --global https.proxy https://127.0.0.1:1080
# 取消代理
git config --global --unset http.proxy
git config --global --unset https.proxy
# 查看代理配置
~/.gitconfg 查看全局配置
查看git信息,设置git信息
在Git中,查看和设置用户信息是非常重要的,尤其是在进行版本控制时。
查看 Git 用户信息
要查看当前 Git 配置的用户信息,可以使用以下命令:
(1) 查看全局配置
git config --global user.name
git config --global user.email
(2) 查看本地仓库配置(如果在特定仓库中设置了不同的用户信息)
git config user.name
git config user.email
(3) 查看所有配置
git config --list
设置 Git 用户信息
如果你需要设置或更改 Git 的用户信息,可以使用以下命令:
(1) 设置全局用户信息(适用于所有仓库)
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"
(2) 设置本地用户信息(仅适用于当前仓库)
git config user.name "Your Name"
git config user.email "your.email@example.com"
(3) 示例
假设你想设置全局用户信息为 “Alice” 和 “alice@example.com”,可以执行以下命令:
git config --global user.name "Alice"
git config --global user.email "alice@example.com"
验证设置
设置完成后,可以再次使用以下命令来验证你的设置:
git config --global user.name
git config --global user.email
注意事项
(1) 全局与本地设置:全局设置会影响所有 Git 仓库,而本地设置仅影响当前仓库。如果在本地仓库中设置了用户信息,它将覆盖全局设置。
(2) 确保信息正确:确保你输入的用户名和电子邮件地址是正确的,因为这些信息将出现在你的提交记录中。
.gitignore使用
gitignore重新生效
把某些目录或文件加入.gitignore后,按照上述方法定义后发现并未生效。
原因是.gitignore只能忽略那些原来没有被追踪的文件,如果某些文件已经被纳入了版本管理中,则修改.gitignore是无效的。那么解决方法就是先把本地缓存删除(改变成未被追踪状态),然后再提交:
git rm -r --cached .
git add .
参考:
[1] https://cloud.tencent.com/developer/article/2220223
[2] https://blog.csdn.net/mingjie1212/article/details/51689606
git基本概念
git索引和暂存区
git索引和暂存区是同一个概念的不同称呼,在 Git中,索引(Index)或暂存区(Staging Area)是一个中间区域,用于在提交之前准备和组织更改。以下是对索引/暂存区的详细解释:
作用
索引或暂存区是一个临时区域,用于保存即将提交到版本库的更改。它允许您选择性地添加文件或文件的部分更改,以便在提交时只包含您想要的内容。
工作流程
(1) 工作目录:您在工作目录中进行更改(编辑、添加或删除文件)。
(2) 添加到暂存区:使用 git add 命令将更改添加到暂存区。这些更改现在被标记为准备提交。
(3) 提交更改:使用 git commit 命令将暂存区中的更改提交到本地仓库。提交后,暂存区会被清空。
命令
git add <file>:将文件的更改添加到暂存区。git status:查看哪些更改在暂存区中,哪些在工作目录中。git reset <file>:从暂存区中移除文件的更改,但保留在工作目录中。
博客
对于一名IT开发人员,记录个人博客,分类管理自己的知识体系,是很重要的。
经历
对于个人来说,我尝试过很多博客的方案,其实大体分为两类:
(1) 云笔记编写:
例如 飞书,语雀,有道云笔记,印象笔记,notion,onenote等等
(2) markdown编写
本地编写好markdown文件,利用static page generator,生成网页后,部署到网站:
例如 hexo,hugo,gitbook,vitepress,mdbook
需求
我的需求,其实市面上的软件很难达到,因为我想同时达到很多效果
(1) 编辑:
- 多端可编辑,并且自动同步
- 编写方式以markdown为主,同时支持markdown导出
(2) Static page generator:
- 轻松将已有的markdown文件,整理好目录结构
- 社区生态和官方维护在线
方案
最后采用了,feishu + feishu-pages + mdbook
为什么使用飞书?
我的编辑要求是,多端可编辑,并且自动同步,其实也就是云文档需求,在我使用的已有的云文档软件里,飞书给我的体验非常好,编写非常舒服,并且业界的程序员使用的也很多。
但是飞书有一个唯一的缺点,那就是不支持markdown导出,这个问题将使用feishu-pages解决
为什么使用feishu-pages
feishu-pages是一个开源项目,它主要是可以将feishu的知识库内容,按照知识库目录结构导出markdown文件,同时自动将飞书图床的图片拉取到项目的assert文件夹,可以本地查看
为什么使用mdbook
Static page generator我也试过很多已有方案,包括hexo,hugo,gitbook,mdbook等,最后我选择了mdbook
mdbook的优势
(1) 前文提到我是用feishu-pages导出具有目录结构的markdown文件,它默认使用SUMMARY.md进行组织结构的,这和gitbook以及mdbook的目录结构天然适配
(2) gitbook现在已经不再维护static page generator版本,团队专注于线上版本,所以官方已经停止维护,并且没有相应的社区在活跃了
(3) 过去的gitbook,在渲染大量markdown文件时,速度感人,所以最后使用mdbook
(4) mdbook跟gitbook的使用方法基本一致,并且官网的demo做的也很好看https://rust-lang.github.io/mdBook/
方案:飞书 + feishu-pages + mdbook
飞书编写文档
飞书支持的内容非常强大,但是飞书导出的markdown,会丧失一部分功能,所以这里为了兼容mdbook渲染后的体验,对飞书内部文档的编写要做出一定的规范要求。
规范要求主要是取决于文档 飞书文档转化markdown测试
添加yaml header
首先为了保证导出的markdown的文件名是nested url风格,我们需要在每个文档的前面手动编写yaml header也就是我们熟知的page meta
注意 slug的值,千万不能设置一些特殊的字符,否则url会不合法而失效,访问不到对应的markdown,这里推荐只是用字母,数字,下划线_,连字符-,就行了,绝对不会出错
例如本文我们可以这样设置yaml header
slug: 方案-飞书-feishu-pages-mdbook
hide: false
Feishu-pages
feishu-pages是一个第三方库,这里简单的介绍一下如何使用
配置环境
安装node,npm,yarn
使用
由于feishu-pages会持续更新,所以我们要以官方最新的使用教程为主:https://github.com/longbridgeapp/feishu-pages
官方的教程没有图片,所以有的时候会不太清楚,这里也给出另一个大佬的教程:https://github.com/ftyszyx/feishu-vitepress
(1) 新建存放feishu-pages的目录,可以取名例如 feishu-book
(2) feishu-book目录下
yarn init -y # 生成一个yarn项目
yarn add feishu-pages # 安装feishu-pages
# 从node-modules中将feishu-docx和feishu-pages拷贝到feishu-book目录下
# 拷贝feishu-book的env.default文件到feishu-book目录下,并重命名为.env
# .env文件编辑好自己的飞书相关内容后保存
yarn feishu-pages # 生成dist目录,里面存放的是飞书知识库导出的所有markdown文件和资源
mdbook
安装
mdbook的使用,推荐直接看官方教程https://github.com/rust-lang/mdBook
创建项目
(1) 新建一个mdbook项目
mdbook init md-book
cd md-book
mdbook serve --open
md-book目录结构
其中src为markdown文件存放的目录,book为渲染后的页面存放的目录
为了之后的正常部署,我们自己调整一下目录结构,将src文件夹删除,然后book.toml的src = “.”,为什么要这样做,之后详细解释。
(2) md-book和book目录下,初始化git仓库,然后添加remote。mdbook分两个部分上传到远程仓库,
- mdbook根目录下,上传到远程仓库的main分支,用来方便github直接查看markdown文件
- mdbook book目录下,上传到远程仓库的gh-pages分支,用来部署网站
# md-book下
git init
git remote add origin https://github.com/vendestine/vendestine.github.io.git
# book目录下
git init
git remote add origin https://github.com/vendestine/vendestine.github.io.git
# 查看是否添加远程仓库成功
git remote -vv
编写book.toml,安装第三方插件
官方推荐的插件列表:https://github.com/rust-lang/mdBook/wiki/Third-party-plugins
个人认为必要的插件列表
- mdbook-yml-header:由于我们的飞书文档前面加了yaml header,所以这里一定要使用第三方插件
- mdbook-katex: 弃用,mardown公式的渲染,官方好像没有集成,使用这个插件
- mdBook-pagetoc:弃用,生成右侧文章目录,会影响很多其他的内容
- mdbook-toc:弃用,也是生成文章目录,但是不美观
这里提出我的book.toml文件,主要是参考官方的用法
[book]
authors = ["vendestine"]
language = "en"
multilingual = false
src = "."
title = "md-book"
[rust]
edition = "2018"
[output.html]
smart-punctuation = true
mathjax-support = true
git-repository-url = "https://github.com/vendestine/vendestine.github.io/tree/main"
edit-url-template = "https://github.com/vendestine/vendestine.github.io/tree/main/{path}"
[output.html.playground]
editable = true
line-numbers = true
[output.html.code.hidelines]
python = "~"
[output.html.search]
limit-results = 20
use-boolean-and = true
boost-title = 2
boost-hierarchy = 2
boost-paragraph = 1
expand = true
heading-split-level = 2
[output.html.fold]
enable = true
[preprocessor.yml-header]
上传到github仓库
(1) 将cswiki/feishu-book/dist/docs的所有内容,拷贝的md-book目录下,拷贝之后打开cmd,执行mdbook serve --open 本地检查是否渲染成功,如果是自己想要的页面,那么就执行mdbook build 生成最后需要部署的页面
(2) 将md-book目录下的内容上传到main分支
(3) 将md-book/book目录下的内容上传到gh-pages分支
Github Pages
如果要部署到github pages,其实不需要什么github actions,因为github actions,每次要重装mdbook和相关插件很消耗资源,所以我们直接将mdbook的book目录下的所有内容上传到仓库就行了
(1) 创建远程仓库,username.github.io,注意一定要取名这个,后面会解释为什么
(2) 远程仓库,settings里设置github pages选型为,deploy from branch,然后选择gh-pages分支
(3) 有新内容push到gh-pages分支后,应该会自动触发github action,部署网站
更新网站
一般记录是在飞书的知识库下,那么我们要更新到网站的话只需要执行如下步骤
首先所有的操作都发生在C:/Users/ventu/Desktop/tmp/cswiki目录下
(1) 准备好markdown文件
cd feishu-book
yarn feishu-pages # 生成dist目录,里面存放的是飞书知识库导出的所有markdown文件和资源
(2) copy C:/Users/ventu/Desktop/tmp/cswiki/feishu-book/dist/docs所有文件到mdbook项目的根目录下,选择全部替换
(3) 查看网页渲染效果,没有问题就build
# md-book目录下
mdbook serve --open
mdbook build
(4) 上传到github仓库
md-book根目录,git push到main分支,这样之后可以在github上查看这些markdown文件
book目录,git push到gh-pages分支,将这些渲染后的pages文件上传到网站上
问题
为什么远程仓库取名username.github.io
因为我这边的markdown的资源引用都是使用的绝对路径(例如 base-url/assets/xxx.jpg)
base-url会拼接在请求的url里,site-url会拼接在存储的url里。
为了正确加载资源,我们必须保证,资源的存储url和请求url相等,才能正确访问资源。因为存储和请求的话,都是使用一样的prefix和path。所以简单来讲,我们只需要让site-url和base-url一致就可以正确加载
部署情况
prefix为域名
如果创建的仓库名是别的,例如blog,
那么site-url是/blog,存储url是username.github.io/blog/path;base-url默认是/,请求url是username.github.io/path,所以此时网站上请求资源就会失败。
解决方法有两种
要么改变site-url,要么改变base-url,这里选择改变site-url为/
(1) 仓库名使用username.github.io,这样site-url就是/
(2) 使用自定义的一级域名,site-url此时是/
为什么markdown文件放在mdbook项目的根目录下,而不是放在src目录下
按照上个问题的的解决方案,我们已经可以在自己的网站上正确加载markdown中的资源引用,但是在github上查看还是不行。
其实这个问题也是和上面的问题类似,为了正确加载资源,我们必须保证site-url和base-url一致。
非部署情况
如果在github查看或者本地查看,那么prefix为项目根目录
如果markdown文件存放在src目录下,那么存储url是root/src/path,此时site-url是/src,请求url是root/path,base-url是/,所以加载资源失败。
解决方法:
site-url改成/
将markdown文件存放在mdbook项目根目录下,这样子site-url和base-url就都是/,此时请求url和存储url都是root/path
飞书文档转化markdown测试
本文用于演示当前 feishu-docx 导出后能完美支持的格式,在下面列出的均可以有较好的支持。
由于 Feishu OpenAPI 数据给出有限,feishu-docx 导出并不能 100% 还原在飞书文档里面的格式,实际可以达到 99% 的效果。
已知不支持格式:
-
多维表格 - 飞书多维表格(电子表格)是独立的数据,且数据格式庞大复杂,暂时不支持,请编写文档的时候使用普通表格。
-
流程图 / UML 图 / 画板 / 思维导图 - 以上几种为飞书文档的子应用功能,目前对接的飞书文档未给出此类数据或图片,所以无法实现。
- 兼容方式:流程图、思维导图
-
图片尺寸、裁剪 - 图片将以原图的方式输出,由于飞书 OpenAPI 未给出图片的裁剪和缩放尺寸信息,所以导出内容不含宽度、高度,这项可以依据最终页面的设定图片 100% 宽度来实现。
-
公式 - 暂未支持,这个后面可能会支持。
-
其他飞书三方组件 - 这个无法支持,API 未提供数据。
-
文字颜色 - 文字的前景色、背景色,考虑到 Markdown 输出,暂时未做支持。
基于以上,建议在飞书侧编写文档的时候,尽量采用支持的格式,目前已经支持的格式能满足文档撰写(如帮助文档、博客)等场景的文档格式需要。
下面是完整格式演示
This is heading 2
This is heading 3
This is heading 4
This is heading 5
This is heading 6
This is a block quote. With a new line.
Paragraph
导出飞书知识库,并按相同目录结构生成 Static Page Generator 支持 Markdown 文件组织方式,用于发布为静态网站。
Generate Feishu Wiki into a Markdown for work with Static Page Generators.
Callout
Orange Callout
Yellow Callout
Green Callout
Blue Callout
Purple Callout
Gray Callout
Grid
Here is a 3 column grid example:
BlockQuote in Grid Line 1
This is line 2
List Item in Grid
- This is level 1.1
Level 2
- Level 2.1 as Ordered
- Level 2.2
Bullet List
-
Projects
- GitHub
- Twitter
- x.com
-
OpenSource
- feishu-pages
- feishu-docx
Ordered List
-
This is 1 item
- This is a item
- This is i
- This is b
- This c
- This is a item
-
This is 2 item
- This is 2.1
- This is 2.2
CodeBlock
A JSON example:
{
"name": "feishu-pages",
}
A TypeScript example:
const name = "feishu-pages";
TODO
-
This item is completed
-
This is imcomplete
Divider
There is a divider
To split contents.
Image
An example of an image with caption.
File
Table
Currently, feishu-docx only supports pure Table.
Name | Type | Website |
|---|---|---|
This is merge row. | ||
GitHub | Programming | |
Social Network | ||
Dribbble | Design | |
Equation
$$E = mc^2$$
Iframe
Artboard
GitBook
gitbook node使用
gitbook和github之间的同步
(1) 实验结果
gitbook和github之间的同步分为两种,gitbook -> github 和 github -> gitbook,结果测试后我发现,对于这两种方式都是如下的工作模式:
gitbook web端上,edit page后,merge,commit自动push到github仓库上去; github仓库,自己手动编辑后,gitbook web端的page会自动pull仓库的内容,进行更新;
(2) 结论
通过上述的实验,我们清楚了gitbook和github同步的本质
无论是在gitbook编辑,还是在github仓库编辑,实际都是把更新的内容add到索引里,然后再提交到commit,最后push到github仓库中。github仓库中,肯定是最新的commit,而gitbook web端会自动检测是否有新的commit,如果有就进行update,同步成最新的commit。所以同步源其实就是github仓库
那既然如此,为什么有给出了两种同步方式,这里的同步其实是说gitbook和github建立connection后,第一次同步的方式,因为建立connection的时候,要么是gitbook为空,github仓库有内容;要么是gitbook有内容,github仓库为空。
按照上述机制,如果初始情况是 gitbook有内容,github仓库为空,这个时候作为同步源的github仓库是空的,所以此时必须选择gitbook -> github,将gitbook的内容同步到github仓库中。之后可以随意切换同步模式,因为github仓库不为空了,它可以作为同步源,gitbook和github仓库都是自动同步。
环境配置
参考
环境配置,我真的踩了很多坑,这里记录一下有用的一些资料,防止以后换了新设备继续踩坑。
[1] https://zhuanlan.zhihu.com/p/343053359
[2] https://blog.csdn.net/m0_74239772/article/details/132710525
[3] https://www.cnblogs.com/hacv/p/14311409.html
卸载原来的node和npm
因为gitbook框架和高版本的node和npm适配,会出现很多奇怪的bug,所以我们使用较低版本version 10的node和配套的npm,但如果下载新的node和npm前,以前老版本的node和npm没有卸载干净,也会出一些奇奇怪怪的错误,所以首先一定要确保彻底卸载原来的node和npm。
要彻底卸载 Node.js 和 npm,具体步骤取决于你使用的操作系统。以下是针对 Windows 和 macOS/Linux 的详细卸载步骤。
在 Windows 上卸载 Node.js 和 npm
(1) 通过控制面板卸载:
- 打开“控制面板”。
- 点击“程序” > “程序和功能”。
- 在程序列表中找到 Node.js,右键点击并选择“卸载”。
- 按照提示完成卸载。
(2) 删除残留文件:
- 卸载后,检查以下目录并手动删除 Node.js 和 npm 的相关文件:
C:/Program Files/nodejs(Node.js 安装目录)C:/Users/<YourUsername>/AppData/Roaming/npm(全局 npm 模块)C:/Users/<YourUsername>/AppData/Roaming/npm-cache(npm 缓存)C:/Users/<YourUsername>/AppData/Local/Programs/Nodejs(如果存在)
(3) 检查环境变量:
- 右键点击“此电脑”或“计算机”,选择“属性”。
- 点击“高级系统设置” > “环境变量”。
- 在“系统变量”中找到
Path变量,确保删除与 Node.js 相关的路径(例如C:/Program Files/nodejs/)。
(4) 重启计算机(可选):
- 为确保所有更改生效,可以重启计算机。
在 macOS 上卸载 Node.js 和 npm
(1) 使用 Homebrew 卸载(如果是通过 Homebrew 安装的):
brew uninstall node
(2) 手动删除 Node.js 和 npm:
- 打开终端,运行以下命令删除 Node.js 和 npm 的相关文件:
sudo rm -rf /usr/local/bin/node
sudo rm -rf /usr/local/bin/npm
sudo rm -rf /usr/local/lib/node_modules
sudo rm -rf /usr/local/include/node
sudo rm -rf /usr/local/share/man/man1/node.1
sudo rm -rf /usr/local/share/man/man1/npm.1
(3) 检查和删除 npm 缓存:
sudo rm -rf ~/.npm
sudo rm -rf ~/.node-gyp
(4) 检查环境变量:
- 打开
~/.bash_profile、~/.bashrc或~/.zshrc文件,确保没有与 Node.js 相关的路径。如果有,手动删除。
(5) 重启终端:
- 为确保所有更改生效,可以重启终端。
在 Linux 上卸载 Node.js 和 npm
(1) 使用包管理器卸载:
如果是通过 apt 安装的(Debian/Ubuntu):sudo apt remove nodejs npm
如果是通过 yum 安装的(CentOS/RHEL):sudo yum remove nodejs npm
如果是通过 dnf 安装的(Fedora):sudo dnf remove nodejs npm
(2) 手动删除 Node.js 和 npm: 运行以下命令删除 Node.js 和 npm 的相关文件:
sudo rm -rf /usr/local/bin/node
sudo rm -rf /usr/local/bin/npm
sudo rm -rf /usr/local/lib/node_modules
sudo rm -rf /usr/local/include/node
sudo rm -rf /usr/local/share/man/man1/node.1
sudo rm -rf /usr/local/share/man/man1/npm.1
(3) 检查和删除 npm 缓存:
sudo rm -rf ~/.npm
sudo rm -rf ~/.node-gyp
(4) 检查环境变量:
- 打开
~/.bash_profile、~/.bashrc或~/.zshrc文件,确保没有与 Node.js 相关的路径。如果有,手动删除。
(5) 重启终端:
为确保所有更改生效,可以重启终端。
下载node和npm
版本选择node version10,验证node和npm的版本
官网下载 node v10:https://nodejs.org/en/about/previous-releases
根据不同的平台,选择对应的安装程序:https://nodejs.org/download/release/v10.24.1/
node -v
npm -v
确认是v10版本的node和配套的npm就可以了
安装GitBook
npm install -g gitbook-cli
gitbook -V
gitbook -V如果gitbook框架没有下载的话,会install,下载完成后再次执行gitbook -V验证是否安装成功
Gitbook node端和web端详细研究
gitbook node和gitbook web
Gitbook node端:
gitbook最开始只是node端的一个框架,一般都是在node端下载gitbook框架,然后我们自己组织markdown文件,最后上传到服务器上,其实就和之前使用的hexo框架差不多
Gitbook web端:
之后又出现了web端,web端其实可以看做一个云笔记,就是按照web page的方式编辑gitbook,然后编辑完后,会push到对应的gitbook仓库中
两者之间的关系:
gitbook web 和 gitbook node其实并不完全相同,gitbook web端实际上是原始的gitbook node + 定制插件和定制样式。直接把gitbook web的仓库clone下来,然后用gitbook node去打开的时候,两者的界面还是有一些区别的。
gitbook web:
gitbook node:
结论:gitbook node端,可定制化更高,可以下载很多插件,同时页面ui貌似更大气好看一些,所以之后还是使用gitbook node端为主,配上一些好用的插件,提高生产力。
gitbook的summary.md和README.md
gitbook渲染到web page,主要就是通过所有的md文件 + summary.md + README.md 其中README.md是gitbook的简介,而summary.md是gitbook的目录
gitbook的编辑方式:
(1) gitbook web端编辑:如果是采用gitbook web端编辑,那么会自动将第一个page的内容作为README.md,然后自动根据现有的page结构,生成目录写入summary.md里
(2) github仓库编辑:如果是直接在仓库边界,那么我们为了让仓库的md文件渲染到web page上,我们需要手动编辑summary.md文件,也就是说要自己手动组织目录结构。
gitbook page 和 github markdown
(1) markdown -> page
如果只有一个一级标题,自动提升,一级标题->page标题,二级->一级,依次类推; 如果有多个一级标题,不会自动提升,markdown文件名是page标题,一级->一级,二级->二级,依次类推
(2) page-> markdown
都是自动下降,page标题->一级标题,一级标题->二级标题,依次类推
(3) 示例
(4) 总结
为了保持gitbook上传的markdown,和最后同步到github的markdown的一致性。 编辑markdown文件,一级标题是文章名字,然后内容用二三四级标题即可。 编辑page,page标题是文章名字,然后内容用一二三级标题即可。
gitbook和github之间的同步
(1) 实验结果
gitbook和github之间的同步分为两种,gitbook -> github 和 github -> gitbook,结果测试后我发现,对于这两种方式都是如下的工作模式:
gitbook web端上,edit page后,merge,commit自动push到github仓库上去; github仓库,自己手动编辑后,gitbook web端的page会自动pull仓库的内容,进行更新;
(2) 结论
通过上述的实验,我们清楚了gitbook和github同步的本质
无论是在gitbook编辑,还是在github仓库编辑,实际都是把更新的内容add到索引里,然后再提交到commit,最后push到github仓库中。
github仓库中,肯定是最新的commit,而gitbook web端会自动检测是否有新的commit,如果有就进行update,同步成最新的commit。所以同步源其实就是github仓库。
那既然如此,为什么有给出了两种同步方式,这里的同步其实是说gitbook和github建立connection后,第一次同步的方式,因为建立connection的时候,要么是gitbook为空,github仓库有内容;要么是gitbook有内容,github仓库为空。
按照上述机制,如果初始情况是 gitbook有内容,github仓库为空,这个时候作为同步源的github仓库是空的,所以此时必须选择gitbook -> github,将gitbook的内容同步到github仓库中。之后可以随意切换同步模式,因为github仓库不为空了,它可以作为同步源,gitbook和github仓库都是自动同步。