【软件开发】C++使用笔记
【软件开发】C++使用笔记
数据类型
值类型
存放在栈空间中的一段内存。
- T:左值,最普通的变量,是具有变量名且可取地址的值。
- \(~\) :右值,常量或不具备名称的值,无变量名不可取地址。通常都是一些生命周期极短的临时值,例如未接收的函数返回值。
备注:
右值及其相关的一系列操作都是 C++11 才新增的功能。
指针类型
一种用于保存内存地址的值类型,可以利用特定的访问运算符访问其指向的内存。
- T*
- T[]
引用类型
一种封装的指针类型,像指针一样存储着内存地址信息,但像值类型一样直接访问该内存。
- T&:引用左值的引用,针对普通变量的引用。
- T&&:引用右值的引用,使右值类型得以被标识和传递,可借此区分左值来实现一些高性能代码。
- const T&:可用右值赋值的,引用常量左值的引用
- T&&(当 T 是一个由编译器推导的类型):万能引用,既可能作为左值引用也可能作为右值引用。
关于引用值类型的重点说明
要注意的是引用类型也是值类型,若有变量名就是左值类型,无关其引用对象的类型。所以无论是右值引用还是万能引用,只要它有变量名,那它就是一个左值。
由于引用类型的使用就相当于引用对象的值类型,因此引用的左值特性会影响到引用对象的值类型,只要其是左值类型的引用,那该引用的对象也就是左值。
-
右值的本质:
所以应将右值引用看成一种特殊的左值引用,该引用的目的仅仅是为了声明引用的对象是一个尚无人认领的临时值,以鼓励使用者按需处理。
-
右值的传递:
因此传递右值引用的本质就是如何将一个左值引用转为右值引用传递出去。非常简单,强制转换就行。若想泛型化,则可借助模板自动推导返回值万能引用的特性,自动强制转换为所需的引用类型。
不同引用值类型间的转换
- 左值转右值:直接强制转换即可,其标准函数为
std::move
(很多函数对右值有特殊处理,因此转换后原本的左值引用不该再继续使用)。 - 右值转左值:使用变量承接即可(因此当一个右值传入函数时,由于函数参数有名称,该右值将被转换为左值)。
- 完美转发:当不希望传递的引用因规则导致类型转换时,可以利用万能引用加传递时强制类型转换实现,其标准函数为
std::forward
。
基本类型转换
- 整数之间相互转换:直接按内存复制,受小端模式的内存布局影响,高精度直接掐断多余内存。
- 任何数与小数转换:按数学逻辑转换,精度不同时,整数受限于其表示范围,小数则可以表示无穷非数值等数。
函数匹配顺序
- 参数完全匹配的普通函数
- 参数满足要求的模板函数
- 类型转换后可以匹配的普通函数
需要注意的是,自动推导的模板函数不支持自动类型转换,必须要输入值与函数参数类型完全匹配才行。
模板
使用场景
- 函数模板
- 变量模板
- 类模板
需求与概念
需求(requires)和概念(concept)是从 C++20 开始新增的两个关键字,可借此轻松实现对模板参数的约束。
两者相关功能简述如下:
- 需求
- 一种语法符号,表示接下来要声明模板约束。
- 一种返回布尔值的运算,支持多种运算方法,包括表达式有效性。
- 概念
- 一种基于模板的编译时推导的常量布尔值。
- 一种具有限制要求的模板参数类型。
- 一种模板限制要求。
创建概念和需求
template <class T> //概念必须使用模板
concept userConcept = requires(T t) //requires表达式中要用的变量
{
t + t; //有效性验证:表达式是合法可编译的
{ t + t } noexcept; //异常验证:表达式是不会抛出异常的
{ t + t } -> std::same_as<int>; //返回类型验证:表达式计算结果满足特定要求
typename T::x; //类型要求:目标类型结构满足的指定的要求
requires std::is_same_v<T, int>; //条件验证:基于布尔值的验证要求
};
使用概念和需求
template <userConcept T> //将概念作为模板参数类型
requires //通过requires语法引入约束
userConcept //将概念作为布尔值条件
&& requires(T t) {} //通过requires计算的布尔值
&& std::is_same_v<T, int> //其他返回布尔值的手段
void Func()
{
}
其他说明
- 这些条件和运算本质都是布尔值,因此也支持布尔值的
&&
和||
运算。 - 建议将计算布尔值的表达式用
()
括起来,不然有时会因计算顺序问题导致编译失败。
可变参数模板
展开方式
- 递归函数展开:通过传参的方式逐渐取出可变参数依次处理。
- 逗号表达式展开:部分场景下省略号会将可变参数展开并用逗号分隔,可借此配合逗号表达式对每个参数执行自定义代码。
- 折叠表达式展开:可以自定义分隔符号和执行顺序的展开方式,相比默认省略号展开更加强大。
默认成员函数
创建一个空的类/结构时 C++ 编译器会默认生成如下 9 个成员函数:
- 构造函数
- 默认构造函数
- 默认拷贝构造函数
- 默认移动构造函数(C++ 11)
- 默认初始化列表构造函数(C++ 11)
- 析构函数
- 默认析构函数
- 赋值运算符
- 默认拷贝赋值运算符
- 默认移动赋值运算符(C++ 11)
- 取址运算符
- 默认取址运算符
- 默认常量取址运算符
失效条件
以下 5 个默认成员函数由于功能特殊,允许在特定环境下取消生成。
- 默认构造函数:
- 定义了任何构造函数。(可控)
- 成员无法被默认构造。
- 默认拷贝/移动构造函数:
- 定义了拷贝或移动构造函数中的任何一个。(可控)
- 成员或父类无法被拷贝 / 移动构造。
- 默认拷贝/移动赋值运算符:
- 定义了拷贝或移动赋值运算符中的任何一个。(可控)
- 成员是引用类型。(可控)
- 成员或父类无法被拷贝 / 移动赋值。
生成逻辑
-
默认构造函数
一个无参,对成员使用默认值或其默认构造函数进行初始化的构造函数。
-
默认列表初始化构造函数
实现基于大括号初始化功能,能更方便的构造对象。
- 没有定义显式的构造函数时
- 生成一个与成员对应并自带默认参数的初始化列表构造函数。
- 有父类时,首个参数用于初始化父物体。
- 有定义显式的构造函数时
- 始终生成与对应构造函数参数一致的初始化列表构造函数。
- 没有定义显式的构造函数时
-
默认拷贝/移动构造函数
自动对成员执行拷贝或移动操作,并调用父类的拷贝/移动构造函数来实现初始化。
-
默认拷贝/移动赋值运算符
自动对成员执行拷贝或移动操作,并调用父类的拷贝/移动赋值运算符来实现赋值。
-
默认析构函数
会自动对成员进行析构并调用父类析构函数。默认析构函数是强制性的,即使自定义了析构函数,默认析构函数依然会被随后调用。
宏
预定义宏
__FILE__
:当前代码所在文件的名称__LINE__
:当前代码所在位置的行数__func__
:当前代码所在函数的名称__COUNTER__
:一个计数器,每次调用返回数字加一
部分预定义宏可以用 C++20 的std::source_location
代替。
内联
内联可以实现宏的效果,通过函数体替代函数调用来提高程序效率。内联对应的关键字是inline
,但其真实作用并非是声明函数需要内联。实际上无论是否声明inline
,函数都有可能内联。inline
的真实用途是为了解决实现内联过程中导致的重定义问题。
- 要实现内联函数,其声明和定义必须放在同一个文件,否则肯定不会内联。
- 但定义放在头文件会导致引用该头文件的翻译单元重复包含该定义而导致重定义错误。
- 不过只要通过定义
inline
,便可让编译器特殊处理,将这些定义视作同一个而避免该错误。 - 重定义问题同样会出现在变量上,所以
inline
也可以对头文件定义的变量使用。 - 成员函数或变量隐式自带
inline
关键字,所以无需声明即可在头文件中直接定义。
二进制库
C++支持将部分代码单独编译成库文件,从而实现编译结果的复用。
库类型
静态库
静态库的本质就像是提前编译好的中间文件(.obj),而且确实有将中间文件当静态库来链接的办法,所以说静态库最终还是要合并到可执行文件的代码中的。在合并过程中编译器还会进行优化,自动剥离静态库中那些没有使用的代码部分。
动态库
动态库是一个可以独立使用的文件,编译过程中与可执行文件完全无关。所以为了实现动态链接,需要用户手动选择需要导出的符号,否则很多编辑器会默认全部导出,因此不会出现静态库那种部分代码被剥离的情况。
但要注意的是,在 MSVC 中可执行文件是通过一个静态库做中间人来连接动态库的,由于静态库会受剥离影响,因此相关的动态库可能会被整个无视而不被加载。
编译方法
MSVC 编译动态库的方法
https://stackoverflow.com/questions/225432/export-all-symbols-when-creating-a-dll/54067711
简单总结为三种方法:
- 手动给每个符号定义
__declspec(dllexport)
(太麻烦,而且代码不跨平台) - 创建一个模块定义文件(.def)提供给 VS(麻烦,一样要自己整理导出函数名)
- 利用 CMake 的
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
功能,自动生成模块定义文件(无需额外操作,跨平台,呱呱好用)
处理库依赖
所有的库都只包含自身项目的源文件的编译结果,所以单独一个库是无法处理链式依赖问题的。若使用到的代码不直接涉及其他库的源文件,那不会有事,否则必须要手动指定被链式依赖的库文件。
全局变量初始化
当全局变量和可执行文件在同一个项目中构建时可以正常使用,但放在其他需要链接的项目中时就会出现问题。
-
放在静态库
静态库中的全局变量合并到可执行文件后,是和项目中的全局变量完全一样的,但问题是静态库合并过程中会进行优化,如果编译器检测认为这个静态库中的全局变量不会使用,那将被剥离,最终就无法触发该全局变量的初始化函数。
-
放在动态库
动态库本身不会剥离任何代码,所以当动态库加载时,其中的全局变量也是可以正常初始化的。但在 MSVC 中因为是通过静态库连接的动态库,所以当该静态库被完全剥离时,相关动态库也会被整个无视。另外若全局变量的初始化放在了头文件,则这两个库都将生成该全局变量,但地址不同。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现