C++11与14
C++11与14
ZEROKO14C++11到C++20的新特性解析
C++标准介绍
优秀的c++知识库:cppreference
C++标准演化
- C++98(1.0) 正式版本
- C++03(TR1)
- C++11(2.0) 正式版本
- C++14
- C++17
- C++20
- C++23
两个层面的新特性
- 语言
- Variadic Templates
- move Semantics
- auto
- Range-base for loop
- Initializer list
- Lambdas
- …
- 标准库
- type_traits
- unordered_set
- forward_list
- array
- tuple
- Con-currency
- regex
- thread
- bitset
- …
确认支持C++标准
程序内部由下面的宏标识C++标准
1 | //C++11 |
各版本新增特性
C++ 11
- auto关键字
- decltype关键字
- nullptr字面值
- constexpr关键字
- function相关
- 基于范围的for循环
- Lambda表达式
- initializer_list
- 标准库bind函数
- 智能指针shared_ptr,unique_ptr
- 右值引用&&
- STL容器
std::array,std::forward_list,std::unordered_map,std::unordered_set
C++ 14
- 拓展了lambda表达式,更加泛型:支持auto
- 拓展了类型推导至任意函数:C11只支持lambda返回类型的auto
- 弃用关键字
[[deprecated]]
C++ 17
拓展了constexpr至switch if等:C++11的constexpr函数只能包含一个表达式
typename 嵌套
inline 内联变量
模板参数推导
元组类
std::tuple:std::pair实现两个元素的组合,它实现多个类模板
std::variant表示一个类型安全的联合体。引用包装器 std::reference_wrapper
非类型模板参数可传入类的静态成员
初始化(如struct)对象时,可用花括号进行对其成员进行赋值
lambda表达式可捕获
*this的值,但this及其成员为只读十六进制的单精度浮点数
继承与改写构造函数
1
using B1::B1;//表示继承B1的构造函数
当模板参数为非类型时,可用auto自动推导类型
判断有没有包含某文件__has_include
[[fallthrough]]用于switch语句块内,表示会执行下一个case或default[[nodiscard]]表示函数的返回值没有被接收,在编译时会出现警告。[[maybe_unused]]即便没使用也不警告
C++ 20
concept用于声明具有特定约束条件的模板类型
1
2
3// 声明一个数值类型的concept
template<typename T>
concept number = std::is_arithmetic<T>::value;范围库(Ranges Library)
模块(modules)
新的基础类型
long long类型
以及unsigned long long类型
先后在C99加入和C++11加入
由于编译器兼容C/C++,因此C++11之前C99之后也能使用该类型
引入新的字面量后缀(LL以及ULL)
LL :
long long x=2147483647LL+1;(2147483647为$2^31-1$,int类型的最大值)编译器对字面量默认以32位进行处理,上面例子中不写LL的话,值会变成-2147483648,因此需要编译器按照64位处理字面量则需要用到
LL,此后结果为2147483648,恢复正常判断最大值最小值
- 最大值:
std::numeric_limits<long long>::max() - 最小值:
std::numeric_limits<long long>::min() - 针对unsigned long long是这样:
std::numeric_limits<unsigned long long>::max()
- 最大值:
新字符类型
char16_t/char32_t
char16_t对应UTF16 16位长度char32_t对应UTF32 32位长度
字符串前缀
1 | char16_t utf16c=u'好'; |
u16string/u32string
std::u16string 和 std::u32string 是 C++11 引入的用于存储 UTF-16 和 UTF-32 编码的字符串的类型
wchar_t
windows编程常用字符类型,因为windows的API大部分都有这个wchar的版本
但是该类型不常用的,原因是对于跨平台不友好,在windows上和linux上的字符长度是不确定的
对应的字符串是wstring
char8_t
对应utf8
C++20引入的新类型
c++20之前使用char
对应的字符串类型为u8string
字符串前缀
1 | //C++20之后的标准做法 |
在实际开发中,可以考虑使用现有的开源库,如 ICU(International Components for Unicode)库,它提供了丰富的 Unicode 支持,包括对不同编码的字符串操作。
[[字符编码#字符集转换|字符编码的转换参考此处]]
函数封装与绑定
STL标准库中提供了一些函数包装的模板,他们可以对函数或调用对象进行包装,方便在其他函数中调用
function类
头文件: <functional>中
用于代替函数指针,并且远比函数指针强大,可以作为所有函数的接口,包括函数,函数对象,成员函数等所有一切函数
定义如下:
1 | template< class R,class... Args> |
使用案例
1 | double multiply(double a, double b) |
function还可以用于封装类的成员函数,只需要第一个参数类型为类的引用
1 | // 线性函数类 |
function实现了一种叫做类型擦除的模式,即通过单个通用接口来使用各种具体的类型
1 | float add(float a, float b) |
mem_fn函数
如果是指向类成员,也可以使用mem_fn
1 | template< class M,class T> |
参数是指向类成员的指针,返回值是一个可调用的包装器
例子:
1 | struct Foo |
bind函数
在C++中,std::bind函数是一个函数模板,用于创建函数对象(也称为函数符或函数器)。std::bind函数的主要作用是将一个可调用对象(函数、函数指针、成员函数、函数对象等)和其参数绑定在一起,形成一个新的可调用对象。这种绑定的过程可以延迟函数调用,允许我们在稍后的时间点调用这个函数对象,并传递参数。
定义:
1 | template<class F,class... Args> |
下面是std::bind函数的一般用法和示例:
1 |
|
在这个示例中,std::bind函数将printSum函数和参数10、20绑定在一起,创建了一个新的函数对象func。通过调用func(),实际上会调用printSum(10, 20)函数。
std::bind函数的一些特点和用法包括:
- 可以绑定任意可调用对象,包括自由函数、成员函数、函数指针等。
- 可以绑定部分参数,即在创建函数对象时只传递部分参数,稍后再传递剩余的参数。
- 可以改变参数的顺序,通过占位符
std::placeholders::_1、std::placeholders::_2等指定参数的位置。 - 返回的函数对象可以拷贝、移动和赋值,可以存储和延迟调用。
std::bind函数在C++11标准中引入,并位于<functional>头文件中。它是实现函数绑定和延迟调用的重要工具,可以简化代码并提高灵活性。
也可以使用变量来传入
1 | auto f = bind(sum,1,2,n);//这是值传入,调用f的时候,n的值为此代码执行时候的n值 |
实现原理
1 | // 简化版的 bind 实现 |
实际的
std::bind函数会更加复杂,因为它支持更多的特性,如绑定成员函数、占位符、引用传递等。底层实现会涉及到更多的模板元编程技术,例如参数包展开、递归模板等。具体的实现细节可能会因不同的标准库而有所不同,但基本思想是相似的。如果你对
std::bind函数的底层实现原理感兴趣,可以查阅C++标准库实现的源代码,如GNU libstdc++或LLVM libc++等。这些源代码中会展示std::bind函数更复杂和完整的实现细节。
类型转换
C++11引入的针对string类型的转换函数如下:
int std::stoi(const std::string& str, size_t* pos = 0, int base = 10):将字符串转换为整数long std::stol(const std::string& str, size_t* pos = 0, int base = 10):将字符串转换为长整数long long std::stoll(const std::string& str, size_t* pos = 0, int base = 10):将字符串转换为长长整数unsigned long std::stou(const std::string& str, size_t* pos = 0, int base = 10):将字符串转换为无符号长整数unsigned long long std::stoull(const std::string& str, size_t* pos = 0, int base = 10):将字符串转换为无符号长长整数float std::stof(const std::string& str, size_t* pos = 0):将字符串转换为单精度浮点数double std::stod(const std::string& str, size_t* pos = 0):将字符串转换为双精度浮点数long double std::stold(const std::string& str, size_t* pos = 0):将字符串转换为长双精度浮点数
确定的表达式求值顺序
C++17才具体说明,此前由编译器确定
函数表达式中的参数会在函数体内的语句执行之前被求值
1
foo(a,b,c);//a,b,c都是表达式
但是要注意函数的参数之间的顺序依然是不确定的
后缀表达式和移位运算符求值总是从左往右
1
2
3
4
5
6E1[E2]
E1.E2
E1.*E2
E1->*E2
E1<<E2
E1>>E2赋值表达式求值总是从右往左的
= += -= *= /= 等等new表达式的内存分配总是会优先于构造函数中参数的求值
重载运算符的表达式的求值顺序应由与之相应内置运算符的求值顺序确定
字面量优化
十六进制浮点字面量
hexfloat和defaultfloat
C++17引入
二进制整数字面量
C++14引入
前缀0b和0B
单引号作为整数分隔符
C++14引入,目的是让数字看起来比较好辨识
单引号整数分隔符对于十进制,八进制,十六进制,二进制整数都是有效的
1 | constexpr int x = 123'456; |
原生字符串字面量
C++11引入
这种字符串字面值使用R"()"的语法,允许在字符串中包含特殊字符而无需转义,方便处理包含大量转义字符的字符串。这种特性在处理正则表达式、文件路径等场景中非常有用。
如果字符串中包含小括号和引号的组合,可能会导致编译器对原生字符串的解析出错。为了避免这种情况,可以在原始字符串字面值的开头和结尾添加自定义的定界符,以确保编译器能够正确解析字符串。这样即使字符串中包含小括号和引号的组合,编译器也能正确识别字符串的开始和结束。 语法为:
R"自定义定界符()自定义定界符"以下是一个示例,演示如何在原始字符串字面值中使用自定义定界符来避免编译器解析错误:
1
2
3
4
5
6
7
8
int main() {
const char* rawString = R"###(This is a raw string with "quotes" and (parentheses))###";
std::cout << rawString << std::endl;
return 0;
}
//输出为:
//This is a raw string with "quotes" and (parentheses)上述代码中的
###是自定义的,可以自己想写什么写什么
用户自定义字面量
C++11引入
允许用户自定义字面量,只需要定义一个字面量运算符函数
基本语法: return_type operator"" identifier(params)
- 双引号和identifier之间必须有空格,但改规则在C++14中被删除了
- 标准表示identifier应该以下划线
_开始,把没有下划线的标识符保留给标准库使用
1 |
|
字面量函数运算符的参数规则:
- 整形
unsigned long long/const char*或者没有参数,直接拿实参作为字面量 - 浮点
long double/const char*或者没有参数,直接拿实参作为字面量 - 字符串
const char* - 字符
char
数据对齐相关
alignas和alignof
首先需要先了解一下不可忽视的数据对齐问题 [[C语言入门#结构体字节对齐|结构体字节对齐(有关于设置结构体字节对齐长度的方式)]]
![]()
内存对齐的原因是因为硬件需要,首当其冲的就是cpu.我们的cpu对数据对齐有迫切的需要(提高性能),通常来说好的数据对齐长度和cpu访问数据总线的宽度有关系.比如cpu访问32位的数据总线,就会期待数据是按照32位对齐的.另外,对于数据对齐引发错误的情况,通常发生在arm架构上(arm架构对数据对齐更严格).除了cpu外,还有其他硬件也需求硬件对齐,比如通过DMA访问硬盘,就会要求数据必须是4k对齐的
alignof运算符
用于获取类型的对齐长度(不能用于对象)
先看看C++11之前有一些通过宏来获取类型的对齐长度(C++11之后也能用),各大厂商编译器提供了获取类型或对象对齐长度的函数,如:
1
2
3
4
5
6
7
8
9
10
11 //MSVC
auto x1=__alignof(int);
auto x2=__alignof(void(*)());
int a;
cout<<__alignof(a)<<endl;
//GCC
auto x3=__alignof__(int);
auto x4=__alignof__(void(*)());
int a;
cout<<__alignof__(a)<<endl;因此C++11标准引入alignof统一了用法
使用案例
1 | auto x1=alignof(int); |
注意:alignof只能处理类型,不能处理对象,即使使用decltype获得类型也不准确,因为使用decltype获取的类型是默认对齐长度的类型.因此这种情况还是需要使用编译器厂商提供的方法
alignas说明符
用于设置类型或者常量表达式的对齐长度
先看看C++11之前各大编译器提供的设置对齐长度的方法(C++11之后也能用),例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 //MSVC
short x1;
__declspec(align(8)) short x2;
cout<<__alignof(x1)<<endl;
cout<<__alignof(x2)<<endl;
//GCC
short x3;
__attribute__((aligned(8))) short x4;
cout<<__alignof(x3)<<endl;
cout<<__alignof(x4)<<endl;
//输出均为:
//2
//8
规则:
- 如果将alignas用于结构体,该结构体整体就会以该数值来对齐结构体
- 如果修改结构体成员的对齐长度,那么结构体整体的对齐长度也会发生变化,因为结构体整体的对齐长度总是大于或等于他最大的成员的对齐长度
使用案例
1 | //结构体大小: 16 结构体对齐长度: 8 |
标准库也提供了一些方法
其他关于对齐字节长度的支持
C++11引入
alignment_of获取类型的对齐字节长度
aligned_storage分配一块指定对齐字节长度和大小的内存
aligned_union获取参数类型中对齐字节长度最严格的(对齐字节数最大)作为分配内存的对齐字节长度
使用new分配指定对齐长度的对象
C++17支持new分配指定对齐长度的对象
new运算符接受一个align_val_t类型的参数来获得分配对象需要的对齐字节长度
1 | //new运算符的声明发生了变化 |
属性说明符和标准属性
属性说明符
C++11之前,GCC和MSVC提供的属性语法:
1 | //GCC |
C++11引入了属性说明符语法,C++17进一步扩展了这一特性
1 | //基本语法 |
基本规则
属性可以出现在整个声明之前,或者直接更在被声明的对象之后,组合起来的规则为:
属性说明符总是声明位于其之前的对象,而在整个声明之前的属性,则会声明语句中所有声明的对象
效果为: 若程序不符合属性要求,编译器将会发出警告
使用using打开属性的命名空间
C++17引入
1 | //有命名空间的情况 |
标准属性
- noreturn: 声明函数不会返回
- carries_dependency: 允许跨函数传递内存依赖项
- deprecated: 标记实体为弃用
- fallthrough: 在switch语句中提示编译器直落行为是故意的
- nodiscard: 声明函数的返回值不应该被舍弃
- maybe_unused: 声明实体可能不会被使用
- likely/unlikely: 表示某条路径更加有可能或没可能,用于优化,通常用于switch语句
- no_unique_address: 指示数据成员不需要唯一地址
- 等等
noreturn
声明函数不会返回
1 | [[noreturn]] void foo(){} |
carries_dependency
允许跨函数传递内存依赖项,它通常用于弱内存顺序架构平台上多线程程序优化,避免编译器生成不必要的内存栅栏指令
powerPC微处理器架构属于弱内存顺序架构平台,Intel和amd的x86和x64处理器系列不属于
deprecated
带有此属性实体被声明为弃用
1 | [[deprecated]] void foo(){} |
如果使用弃用的实体,编译会给出系统警告: deprecated("xxx was deprecated")
fallthrough
C++17中引入:在switch语句的上下文中提示编译器直落行为是故意的
1 | void bar(){} |
nodiscard
声明函数的返回值不应该被舍弃
可以声明在函数或类或枚举类型上,但是声明到类或枚举类型上时只有被当成函数的返回值时才会生效
1 | class [[nodiscar]] X{}; |
maybe_unused
C++17引入,声明实体可能不会被使用
1 | int foo(int a[[maybe_unused]],int b[[maybe_unused]]) |
likely/unlikely
C++20引入的,通常用于switch语句中,表示某条路径更加有可能或没可能让编译器可以进行优化
- 声明在标签或语句上
- likely属性允许编译器对该属性所在的执行路径相对于其他执行路径更可能的进行优化
- unlikely允许编译器对该属性所在的执行路径相对于其他执行路径更不可能的进行优化
no_unique_address
C++20引入,指示编译器该数据成员不需要唯一地址
通常用于数据成员类型只有成员函数,没有成员变量的类型
1 | struct Empty{}; |
新增预处理器功能和宏
头文件可用宏
__has_include(<头文件>)
判断某个头文件是否能被包含进来(注意他不关心头文件是否已经被导入)
1 |
特性测试宏
对于代码库的作者,因为有了特性测试宏,可以根据客户端开发环境适配不同的代码功能,让自己的代码库能更高效的使用在更多的环境上
属性测试运算宏
__has_cpp_attribute(属性);
指示编译器是否支持某种属性,该属性可以是标准属性,也可以是编译器特有的属性,前者展开为属性添加进标准的年份与月份,后者展开为非零值
语言功能特性测试宏
如果支持,将展开为引入特性的年月
标准库功能特性测试宏
参数不为空宏
__VA_OPT__
c+20引入
可变参数不为空时才展开
1 | //可变参数宏的问题:打印案例 |
协程
协程的理解
协程是一种可以被挂起和恢复的函数,它提供了一种创建异步代码的方法
如今协程已经成为大多数语言的标配,尽管名称可能不同,但它们都可以被划分为两大类
有栈(stackful)协程 可以任意嵌套函数中被挂起
每一个协程都会有自己的调用栈(一般的协程使用栈内存来存储数据)
无栈(stackless)协程,如async/await以及C++20中的协程 不可以任意嵌套函数中被挂起
无栈协程不具备数据栈
此处「有栈」和「无栈」的含义不是指协程在运行时是否需要栈,对于大多数语言来说,一个函数调用另一个函数,总是存在调用栈的;而是指协程是否可以在其任意嵌套函数中被挂起,此处的嵌套函数读者可以理解为子函数、匿名函数等。显然有栈协程是可以的,而无栈协程则不可以.这也决定了有栈协程被挂起时的自由度要比无栈协程高
协程的目的:一份不需要将生产者或是消费者重写为状态机就可以移植的代码,一个隐式的状态机
有栈协程
有栈协程是可以在其任意嵌套函数中被挂起的
实现一个协程的关键点在于如何保存、恢复和切换上下文。已知函数运行在调用栈上;如果将一个函数作为协程,我们很自然地联想到,保存上下文即是保存从这个函数及其嵌套函数的(连续的)栈帧存储的值,以及此时寄存器存储的值;恢复上下文即是将这些值分别重新写入对应的栈帧和寄存器;而切换上下文无非是保存当前正在运行的函数的上下文,恢复下一个将要运行的函数的上下文。有栈协程便是这种朴素思想下的产物。
切换上下文无非是保存当前正在运行的函数的上下文,恢复下一个将要运行的函数的上下文。于是我们可以基于上述两段汇编构造一个void swap_ctx(char **current, char **next)函数,分别传入char **init_ctx(char *func)构造好的上下文即可实现切换。为了方便使用,我们可以将swap_ctx()封装成yield()函数,在这个函数里简单实现了不同函数的调度逻辑。于是一个简单的例子便完成了 相关代码可以参考此处
有栈协程则是通过切换整个栈帧来实现上下文切换。每个协程都有自己独立的栈空间,允许它们在任何地方挂起和恢复执行。这些协程可以自由地执行递归或其他复杂的控制流。
[[进程与线程#模拟线程切换|曾经写过一段这样原理的代码用于通过单线程模拟多线程实际上就是有栈协程,相关部分可以参考此处]]
无栈协程
相比于有栈协程直接切换栈帧的思路,无栈协程在不改变函数调用栈的情况下,采用类似生成器(generator)的思路实现了上下文切换
无栈协程(如基于生成器的实现)则不使用独立的栈空间,而是利用生成器的特性来实现上下文切换。无栈协程的基本思路是将函数的状态(如局部变量和执行位置)保存在生成器的内部状态中,而不是在栈帧中。这样,切换协程时只需保存和恢复生成器的状态,而不需要处理复杂的栈帧结构。
无栈协程的一种实现,虽然有多线程以及可读性等诸多问题,但确实是无栈协程
有栈协程被挂起时的自由度要比无栈协程高,有栈协程在兼容现有的同步代码时异常方便;而无栈协程的兼容性基本为零,总不可能给所有同步代码都加上 async/await 吧
无栈协程原理
无栈协程其实现原理是将执行的方法编译为一个状态机,实现的时候不需要在临时栈和系统栈直接拷贝现场。因此无栈协程的效率和占用的资源更少。当然,有栈协程的代码会更加的简单易读。
C++20协程
协程(Coroutines)就是一个可以挂起执行,稍后再恢复执行的函数。只要一个函数包含 co_await、co_yield 或 co_return 关键字,则它就是协程。
协程是函数的泛化,协程允许函数被暂停(suspended),并在之后恢复(resumed)执行。
C++20 引入的协程属于无栈协程(stackless coroutine)。它们通过编译器的支持,使用状态机的方式在不改变函数调用栈的情况下进行协程切换。每个协程通过 co_await、co_yield 等关键字实现挂起和恢复,不会为每个协程分配独立的栈。
C++20协程TS(Technical Specification技术规范)带给了我们什么:
- 3个新的关键字:
co_await、co_yield和co_return- 几个新的类型(在
std::experimental命名空间中):
coroutine_handle<P>coroutine_traits<Ts...>suspend_alwayssuspend_never- 一个通用的机制,库的开发者可以用它和协程交互,并定制他们的行为。
- 一个语言设施,它使得编写异步代码更简单。
C++协程TS提供的设施可以看作是一个用于协程的低级汇编语言。这些设施很难以安全的方式直接使用,它更倾向于给库的开发者,让他们可以编写出应用程序开发者可以安全使用的高级抽象。
协程TS没有定义协程的语义。它没有定义如何产生返回给调用者的值。它没有定义传递给
co_return语句的返回值要做什么,以及如何处理传递出协程的异常。它没有定义协程应该在哪个线程上恢复。协程TS定义了两种类型的接口:Promise接口和Awaitable接口。
Promise接口规定了一些和协程自身行为相关的方法。库的开发者可以定制:当协程被调用时的行为,当协程返回时的行为(正常返回或未处理的异常),以及定制协程内任何
co_await和co_yield表达式的行为。Awaitable接口规定了一些控制
co_await表达式语义的方法。当一个值被co_await时,这部分代码将被翻译为一系列awaitable对象的方法,这使得可以规定:是否要暂停当前协程,在暂停协程后是否要执行一些逻辑,在协程恢复执行后是否要产生co_await表达式的结果。
- Promise 接口主要用于定制协程本身的行为,例如协程的调用、返回以及内部的 co_await 和 co_yield 表达式的行为。
- Awaitable 接口主要用于控制 co_await 表达式的语义,包括决定是否暂停协程、在暂停后和恢复后的相关逻辑处理。
它们共同作用于协程,以实现协程的各种功能和行为定制。
- co_await: 提供暂停协程的能力,允许协程被恢复执行
协程函数返回值的类型,必须是一个自定义类型,并且这个自定义类型需要按照一定的格式来定义
1 |
|
coroutine_handle也暴露出多个接口,用于控制协程的行为、获取协程的状态,与promise_type不同的是,promise_type里的接口需要我们填写实现,promise_type里的接口是给编译器调用的。coroutine_handle的接口不需要我们填写实现,我们可以直接调用。
| coroutine_handle接口 | 作用 |
|---|---|
| from_promise() | 从promise对象创建一个coroutine_handle |
| done() | 检查协程是否运行完毕 |
| operator bool | 检查当前句柄是否是一个coroutie |
| operator() | 恢复协程的执行 |
| resume | 恢复协程的执行(同上) |
| destroy | 销毁协程 |
| promise | 获取协程的promise对象 |
| address | 返回coroutine_handle的指针 |
| from_address | 从指针导入一个coroutine_handle |
基于MSVC的await编译选项进行讲解
- 拓展了标准库,提供了一些辅助库
下面代码使用了微软协程库的特性 co_await
命名空间相关
C++17引入内联命名空间与嵌套命名空间
内联命名空间
把子命名空间的元素导入到父命名空间中
注意: inline这个关键字不能用到第一层的命名空间中
1 | namespace Parent{ |
主要作用 : 方便库的开发者管理代码,升级代码后无缝地提供给使用者.当我们想去修改升级这个函数的时候,版本1换成版本2,最好的方式就是使用内联命名空间,本来inline是在版本1的namespace的,只需要将inline关键字转移到版本2的namespace上,则达成修改,其他代码无需修改,并且保留了原始版本代码
嵌套命名空间
主要是用来减少命名空间带来的代码冗余
1 | namespace A::B::C { |
嵌套内联命名空间
C++20才支持
1 | namespace A::B{ |
拓展的inline说明符
内联变量
C++17引入了内联变量
在C++中,内联变量是指使用
inline关键字声明的变量。使用inline关键字声明的变量会被视为内联变量,编译器会尝试将其直接嵌入到调用它的地方,而不是分配内存空间给该变量。这样可以减少函数调用的开销,提高程序的执行效率。需要注意的是,内联变量的定义必须在所有使用该变量的地方可见,否则会导致链接错误。
[[C++基础#内联函数(inline function)|内联变量知识点跳转]]
除了内联命名空间与嵌套命名空间中对inline的新拓展外,inline还拓展了用于定义非常量静态成员变量的用法
内联非常量静态成员变量
非常量静态成员变量的问题:声明和定义必须分开,即定义必须在类外
1 | class X{ |
常量静态数据成员可以一次性完成声明和定义
1
2
3
4 class X{
public:
static const int num{5};
};
从C++17开始标准引入了内联定义静态数据成员的方式:解决了C++中定义静态成员变量繁琐的问题
1 | //基本语法 |
即使将类X的定义作为头文件包含在多个源文件中也不会有任何问题
让编译器可以聪明的选择首次出现的变量进行定义和初始化,这种特性符合inline说明符提案文档中的一句话:inline说明符可以应用于变量和函数.声明为内联的变量和函数具有相同的语义,他们一方面可以在多个翻译单元中定义,另一方面又必须在每个使用他的翻译单元中定义,并且行为就像同一个变量
函数返回类型后置
基本语法:
一般用于推导函数模板返回类型
1 | auto test(int a,int b)->int |
默认实参
- 默认实参可以不写参数名
- 默认实参在声明中可传递(可能有点绕,看下面解释)
- 形参包前可以直接写默认实参
C++11版本引入了函数的默认实参可以不写参数名的特性。void f(int=3);
在函数声明中,所有在拥有默认实参的形参之后的形参必须拥有在这个或同一作用域中先前的声明中所提供的默认实参。你可能觉得很绕,其实说白了就是说,你可以给任何形参默认实参,但是,你需要在当前作用域提前给你已经声明了默认实参的形参后面的形参默认实参。比如:
1 | void f(int, int, int = 10); |
除非该形参是从某个形参包展开得到的或是函数形参包,如:
1 | template<class...Args> |
还有很多点,略,详情点击参考
右值引用
C++11提出的一个非常重要的概念,它的出现不仅完善了C++的语法,改善了C++在数据转移时的执行效率(减少了非必要复制),同时还增强了c++的模板能力
c++11中对C++影响最深远的特性就是右值引用
首先区分左值和右值
- 判断对象能否取地址 可以取地址的为左值,不可以取地址的为右值
- 所谓的左值一般是指一个指向特定内存具有名称的值(具名对象),它有一个相对稳定的内存地址,并且有一段较长的生命周期.而右值则是不指向稳定内存的地址匿名值(不具名对象),它的生命周期很短,通常都是暂时性的
通俗理解:
左值就是“可以找到的东西”,可以重复使用和修改
右值就是“用完就丢的东西”,在表达式中一旦计算出来就没有持久性
通常是表达式的结果
在 C++ 中引入左值和右值的概念,主要是为了优化和区分对象的生命周期。比如,如果编译器知道某个对象是右值,它可以安全地将其移动或优化,而不用保留它的完整生命周期。
简单区分:
1 | int a=9; |
1 | //原本对右值不能取reference |
当右值出现于赋值运算符右侧时,我们认为对其资源进行偷取/搬移(move),而非拷贝(copy)是合理的
因此
- 必须有语法让我们在调用端告诉编译器,这是个右值
- 必须有语法让我们在被调用端写出一个专门处理右值的所谓移动构造函数
于是乎,引入了右值引用
左值引用和右值引用
常量左值引用
1
2int &x1 = 7;//编译错误
const int &x1 = 11;//编译成功右值引用语法
1
int &&k = 11;
右值引用引入了移动语义
值类别
分为2种: 泛左值(glvalue)和右值(rvalue)
泛左值: 通过计算评估能够确定对象位域或函数的标识的表达式(简单理解就是具名对象)
分为3种: 左值,纯右值和将亡值
纯右值: 通过计算评估,能够用于初始化对象和位域或者能够计算运算符操作数的值的表达式(简单理解为是为了初始化其他对象的)
将亡值: 资源可以被重用的对象和位域,通常是因为它们接近生命周期的末尾,另外也有可能是经过右值引用的转换产生的
- 左值和将亡值统称为泛左值
- 纯右值和将亡值统称为右值
左值转换为右值
基本的转换方式
1 | int i=0; |
使用std::move
1 | static_cast<remove_reference<decltype(arg)>::type&&>(arg) |
remove_reference<>:remove_reference是一个模板元函数,是 C++11 标准引入的一个类型转换工具,位于<type_traits>头文件中,用于移除参数类型的引用修饰符。例如,如果arg的类型是int&,remove_reference<int&>::type将返回int
移动语义
当一个右值复制
拷贝构造的时候,若被拷贝对象是一个右值或者是一个临时对象的时候,原本的做法非常不聪明.更高效的做法是将马上要被销毁的临时对象的资源内容移动到目标对象中
1
2
3
4
5
6
7
8
9
10
11
12
13 string make_greeting(string &&name) // name说:"请尽管移动我!"
{
string result = "Hello, ";
result += std::move(name);
// 直接把name的内容"偷"过来
return result;
// result也会被移动返回,超高效!
}
// 使用示例
string greeting = make_greeting(std::move(name));
cout << greeting << endl;
// 现在原对象name变成"空壳子",不能再使用它的值
右值引用的语法是 T&&。右值引用允许我们获取对临时对象的引用,从而在不拷贝的情况下对它们进行操作
move函数
std::move 是一个函数,其主要目的是将任何类型的变量无条件地转化为右值。
用于实现移动语义,减少不必要的拷贝开销和内存开销。
例如,将一个左值传入
push_back时,可以使用std::move来实现真正的转移,避免额外的拷贝操作:std::vector<std::string> vec; std::string x = "abcd"; vec.push_back(std::move(x));//用move可以理解为x的资源将被"偷"取1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
总之要注意:
- 看到`T&&`参数,可以用`std::move`把它移走 ✅
- 被移动后的对象不要再使用它的值 ❌
- 如果对象后面还要用,就不要移动它 ⚠️
move函数的原理
> 学习的时候,可以利用:`-fno-elide-constructors`是一个编译 选项,用于告诉编译器不要优化构造函数。通常情况下,编译器会尝试对构造函数进行优化,例如通过返回值优化(Return Value Optimization)来避免不必要的拷贝操作。使用该选项可以禁用这种优化,强制执行构造函数的拷贝操作。这在某些情况下可能有用,例如在调试时需要确保每次构造函数都被调用。
通过观察发现:移动语义可以将函数中的局部变量返回出来,观察汇编代码会发现,实际上根本没有进入到函数内,而是在进入函数前就定义了局部变量.意思就是本应在最里层的局部变量定义到了外面,这样当然就可以返回了
### 移动构造函数以及移动赋值运算符函数
C++的类中因此新增的默认函数:
> 移动构造是一门既省内存又高效的绝学,但使用不当可能会导致"走火入魔"
>
> 使用场景
>
> 1. 当你的对象持有大量资源(内存、文件句柄等)
> 2. 当你知道源对象马上就要销毁时
> 3. 在容器操作中需要频繁移动对象时
- 移动构造函数
注意:移动构造函数必须标记为 noexcept,这样STL容器才敢放心大胆地使用它
- 移动赋值运算符函数
```cpp
myClass(myClass&& other) noexcept{}//移动构造函数
myClass& operator=(const myClass&& other) noexcept{}//移动赋值运算符函数
{
//...
return *this;
}
第一个图是浅拷贝流程,中间的图是原来的深拷贝流程,右图是移动流程,即偷(一个生动形象的动词,注意打断了原本对象和空间的联系)
加入了移动语义的类中,编译器隐式声明的特殊成员图一览:
规则总结:
- 默认构造,析构,赋值拷贝,赋值移动,拷贝构造,拷贝移动默认都会自动由编译器生成
- 用户定义了任何构造函数,则默认构造函数不会自动生成
- 用户定义了拷贝构造和拷贝赋值函数或析构函数任一,则移动语义两个函数不会自动生成
- 用户定义了移动语义两个函数任一,则复制语义两个函数不会自动生成
非常重要的一点: 只要是使用了构造函数,即使是使用的移动构造函数,也会在该对象生命周期结束的时候,自动调用析构函数(根据调用的构造函数的次数来决定)
一个使用移动构造函数的例子:(注意调用了两次析构函数)
一个案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class MyObject {
private:
int* data;
public:
MyObject() : data(nullptr) {
std::cout << "Default Constructor" << std::endl;
}
MyObject(int value) : data(new int(value)) {
std::cout << "Regular Constructor" << std::endl;
}
// 移动构造函数
MyObject(MyObject&& other)noexcept : data(other.data) {
other.data = nullptr;
std::cout << "Move Constructor" << std::endl;
}
// 移动赋值操作符
MyObject& operator=(MyObject&& other) noexcept {
if (this != &other) {
delete data;
data = other.data;
other.data = nullptr;
}
std::cout << "Move Assignment Operator" << std::endl;
return *this;
}
~MyObject() {
delete data;
std::cout << "Destructor" << std::endl;
}
void printData() const {
if (data != nullptr) {
std::cout << "Data: " << *data << std::endl;
} else {
std::cout << "Data is null" << std::endl;
}
}
};
int main() {
MyObject obj1(10);
obj1.printData();
MyObject obj2(std::move(obj1)); // 使用std::move调用移动构造函数
obj2.printData();
obj1.printData(); // obj1的data现在为null
return 0;
}
/*
Regular Constructor
Data: 10
Move Constructor
Data: 10
Data is null
Destructor <--调用了两次析构函数
Destructor
*/
万能引用
万能引用是针对模板而来的
最大的目的是为了让你的函数中传入的参数可以左值也可以是右值
常量左值引用既可以引用左值又可以引用右值,是一个几乎万能的引用,但可惜由于其常量性,导致它的使用范围收到一些限制.因此此处介绍的万能引用是真正意义上的”万能”的引用
右值引用 –模板-> 万能引用:
具体类型的&&符号表示右值引用
模板的&&符号表示万能引用
所谓的万能引用是因为发生了类型推导,在T&&和auto&&的初始化过程中都会发生类型的推导,如果已经有一个确定的类型,比如int &&,则是右值引用。在这个推导过程中,初始化的源对象如果是一个左值,则目标对象会推导出左值引用;反之如果源对象是一个右值,则会推导出右值引用,不过无论如何都会是一个引用类型。
万能引用能如此灵活地引用对象,实际上是因为在C++11中添加了一套引用叠加推导的规则——引用折叠。在这套规则中规定了在不同的引用类型互相作用的情况下应该如何推导出最终类型
1 | void foo(int &&i){}//i为右值引用 |
万能引用既可以是一个左值引用,也可以是一个右值引用,这个能力是通过模板形参的推导完成的
万能引用最重要的一个应用是完美转发
万能引用的规则
万能引用使用了一套叫做引用折叠的规则,即不同引用类型叠加后的推导结果
| 类模板型 | T实际类型 | 最终类型 |
|---|---|---|
| T& | R | R& |
| T& | R& | R& |
| T& | R&& | R& |
| T&& | R | R&& |
| T&& | R& | R& |
| T&& | R&& | R&& |
- 类模板型和T实际类型均有引用符,则按照数量少的引用符确定最终类型
- 类模板型和T实际类型有一个有引用符,则按照有引用符的数量来确定最终类型
或者另一个记忆方式:(按照优先级来罗列)
- 有左值引用,则最终类型为左值引用(优先级最高)
- 有右值引用,则最终类型为右值引用
值得一提的是万能引用的形式必须是T&&或者auto&&,而不能是vector<T>&&,必须在初始化的时候被直接推导出来,若在推导中出现中间过程,则不是一个万能引用
完美转发
为什么需要完美转发?
在 C++ 中,有时我们编写的模板函数只是为了“中转调用另一个函数”。但是,模板函数中的参数通常无法保持原始的左值或右值状态,这会导致性能问题或意外行为。因此,我们需要一种机制来让模板函数能正确地转发参数。
为了解决在函数模板中传递参数时保持参数的值类别(lvalue 或 rvalue)不变的问题
1 | //下面三种方式都不是完美转发,都有各自的问题: |
完美转发
原来是左值,转发后也是左值;原来是右值,转发后也是右值
1 | template<class T> |
std::forward用于在模板函数中完美转发参数。- 它有条件地将变量转化为右值,只有当输入的变量是右值时,才会将其转化为右值引用;如果输入的变量是左值,那么
forward将输入的变量转化成左值。 - 通常用于保留原始变量的左值和右值属性。
此处详解完美转发的必要性:
1 |
|
问题:无论传入的是左值还是右值,forward_to_process 都只调用了 process(const std::string&)。这是因为在模板函数中,参数 arg 被默认视为左值,即使我们传递的是右值,也失去了右值特性,导致效率低下(没有调用 process(std::string&&) 的移动版本)。
下面使用完美转发:
1 |
|
进一步泛华,将process也泛化
1 |
|
自定义支持移动的类
完整写法参考
1 | class MyString{ |
非静态成员默认初始化
从C++11开始,声明非静态数据成员的同时,可以直接对其使用等号或大括号进行初始化
以前只有类型为整形或者枚举类型的常量静态数据成员才可以进行这种默认初始化
1 | class X{ |
另外从C++20开始,允许我们对数据成员的[[C++基础#位域|位域]]进行默认初始化了
1 | struct S{ |
可变参数模板
Variadic Templates
[[C++基础#模板|基础模板知识点参阅]]
关键词: ...
...就是一个所谓的pack(包)
- 用于模版参数就是模版参数包
- 用于函数参数类型就是函数参数类型包
- 用于函数参数就是函数参数包
使用这种语法能兼容两个变化点,模板参数的两个点:
参数个数(variable number)
本质是利用参数个数逐一递减的特性,实现递归函数调用.使用函数模板完成
参数类型(different type)
本质是利用参数个数逐一递减导致参数类型也逐一递减的的特性,实现递归继承或递归复合,使用类模板完成
设计函数的时候
当希望函数参数是类型不同,个数也不同的时候,采用可变参数模板
当希望函数参数只有个数不同,类型相同的时候,可以采用initializer_list,但是调用的时候需要多写一对
{}这种情况也可以使用可变参数模板,只需要在定义 参数为[1+包]的函数时,使第一个参数的类型固定,而不是模板类型.
函数案例
针对下面的情况:
1 | void print() |
args可以为任意数量(包含0个)的参数,并且每个参数可以是任何类型的(args为一包类型的一包参数)
sizeof...(args)用于查看包中的个数
但print函数的参数为0个时,因为0个参数没办法拆分为1+0,所以会走void print(),即作为递归的终止条件
可以这么理解:
...在左边表示定义,...在后边表示使用
...T表示定义T类型包,T...表示使用类型包,T... t,使用类型包定义类型包的实例t,使用实例应该t...
更复杂的版本:
1 | //要求是传入什么类型就输出什么类型,用%符号来占位,%%表示%本身 |
可变参数模板求和案例
1 | // 超级sum |
类案例
下面的案例是tuple(元组),tuple可以任意指定多个不同类型的成员,构造出一个对象
递归继承
实现方式是通过可变参数模板实现的递归继承
流程可描述为:将tuple类模板的类型分为[1+一包],先定义一个类型为第一个参数的类型,使该类继承自类型为[一包]的类型的类.(下方代码下面有贴继承链)
1 | //下面案例是元组 |
参照如下的继承链理解:(下图使用 {} 代替 <>) (a->b表示a继承b)
1 | graph BT |
使用上面案例的情况如下:
1 | tuple<int,float,string> t(41,6.3,"nico"); |
递归复合
层层复合
1 | // 下面案例是元组 |
如下图罗列了每个类型的成员变量类型,层层组合关系
并存问题
1 | template <typename T,typename... Types> |
上面这种情况参数为[1+包]和[包]的两个重载函数可以并存
注意:当调用print函数传入多个参数时,参数为[1+包] 比 [包] 更特化,调用的是[1+包]那个函数,即 void print(const T& firstArg,const Types&... args).因此当他们共同存在的时候,参数为[包]的函数就永远不会被调用到了.
可变参数模版的作用:1.递归
模版表达式中的空格
1 | vector<list<int> >; //每个C++版本的都支持 |
nullptr
nullptr是C++11引入的空指针常量用于代替NULL或者0赋值给空指针
是一个nullptr_t类型的纯右值
例子如下:
1 | //有两个函数 |
nullptr_t
顾名思义,nullptr的类型
typedef decltype(nullptr)nullptr_t;
nullptr_t是一个与 nullptr 具有相同类型的类型。
用处:为了在代码中使用更具有语义的类型,例如:可以使用 nullptr_t 来声明接受空指针的函数参数或返回类型,以增加代码的清晰度和可读性。
三向比较
在C++20中引入了太空船运算符<=>,它是一种三向比较运算符,用于比较两个值。这个运算符返回一个特殊的值,表示两个值之间的关系。这个值可以是负整数、零或正整数,分别表示第一个值小于、等于或大于第二个值。 太空船运算符的语法如下:
1 | result = expression1 <=> expression2; |
其中,result的值为:
- 负整数:如果expression1小于expression2。
- 零:如果expression1等于expression2。
- 正整数:如果expression1大于expression2。
通过这个运算符,可以用一个单一的运算符来处理所有六种传统比较操作:小于、小于等于、等于、大于、大于等于和不等于。
这个运算符对于排序和比较操作非常有用,可以简化代码并提高可读性。
注意:运算符<=>的返回值只能与0和自身类型来比较,如果同其他数值比较,编译器会报错
1 | bool=7<=>11<0;//编译成功,b为true |
返回类型
<=>运算符的返回值有三种类型
- strong_ordering 严格相等:相等并且可替换算相等
- weak_ordering 非严格相等:相等但不可替换算相等,比如大小写不同的字符串
- partial_ordering 非严格相等:相等但不可替换算相等,多了一个可以毫无比较关系的选项
对基础类型的支持
- 对两个算数类型的操作数,对各操作数进行一般算术转换,然后进行比较,其中整形的比较结果为
strong_ordering,浮点型的比较结果为partial_ordering - 对于两个底层类型不同的枚举类型,则无法编译
- 对于其中一个操作数为bool类型的情况,另外一个操作数必须也是bool类型,否则无法编译,比较结果为
strong_ordering - 不支持比较的两个操作数为数组的情况,会导致编译错误
- 对于其中一个操作数为指针类型的情况,需要另一个操作数是同样类型的指针,或者是可以转换为相同类型的指针,最终比较结果为
strong_ordering
自动生成比较函数
C++20规定,当用户为自定义类型声明了三向比较运算符,那么编译器会为其自动生成<,>,<=,>=四种运算符函数(前提是使用 = default),减少了开发者的工作量
用户自定义类型中,如果实现了<,==运算符函数,该类的三向比较中将自动生成合适的比较代码
1 | strong_ordering operator<=>(const MyType&) const=default; |
auto关键字
auto表示任意类型
声明变量时根据初始化表达式自动推断该变量的类型,声明函数时函数返回值的占位符
auto一大用处是当类型非常长或非常复杂的时候使用,合理使用auto可以减少代码冗余
1 | //非常长,如:iterator |
auto关键字的特性
从左往右的推导
1
2auto x=1,y=4.2;
//从左往右x会先被推导为int类型,因此后半段会报错;使用表达能力更强的类型
1
2
3//此处会推导出x的类型为double而不是int类型,虽然这里的条件表达式最终返回的是1
auto x=true?1:4.2;
static_assert(std::is_same<decltype(x),double>::value);不能声明非静态成员变量
C++20之前无法声明形参
推导规则
按值初始化
忽略CV限定符,即const和volatile两种
1
2
3const int i = 5;
auto j = i;
//推导出j为int类型按引用初始化
忽略引用
1
2
3
4int i =5;
int &j=i;
auto m=j;
//m是int类型万能引用
1
2
3
4int i =5;
auto&& m=i;
auto&& j=5;
//auto推导为int类型,j为int&&数组或者函数
会推导为指向相应类型的指针
1
2
3int i[5];
auto m = i;
auto推导为指向int类型的指针-
该项在C++17标准和其之前的标准有区别,这里只提c++17之后的标准
直接使用列表初始化
列表里必须是单个元素,则auto推导为元素的类型
多个元素,编译无法通过
使用等号赋值的列表初始化
auto推导的类型是initializer_list
auto占位符使用
从C++14开始支持使用auto来推导函数的返回类型,此时不需要使用返回类型后置的语法
返回类型推导
要求统一返回类型,如果在if else中返回多个不同类型,编译无法通过
1
auto sum(int a1,int a2){return a1+a2;}
lambda表达式的形参中使用auto
C++14开始支持,给auto增加了一定的泛型能力
1
2auto l=[](auto a1,auto a2){return a1+a2;};
auto retval=l(5,5.0);非类型模板形参占位符
C++17开始支持
1
2
3
4template<auto N>
void f(){
cout<<N<<endl;
}
一致性初始化
Uniform Initialization
列表初始化的主要目的是让stl容器如同数组一般的被初始化
传统初始化方式主要是两种
使用括号初始化的方式叫做直接初始化
1
2 int x(8);
C x2(4);使用等号初始化的方式叫做拷贝初始化
1
2 int x=8;
C x2=4;
现在,任何变量的初始化都可以用一个共通语法设置初值: {}
他也分为直接初始化和拷贝初始化
1 | int values[]{1,2,3}; |
其实是利用一个事实:编译器看到{t1,t2…tn}便做出一个initializer_list<T>,它关联到一个array<T,n>.调用函数(例如ctor(构造函数))时该array内的元素可被编译器分解逐一传给函数.但若函数参数就是个initializer_list<T>,调用者不会分解逐一传给函数,而是作为一个initializer_list<T>传入
- 编号1代码:这形成一个
initializer_list<string>,背后有个array<string,6>.调用vector<string> ctors时编译器找到了一个vector<string>接受initializer_list<string>的构造函数.所有stl容器都有这种构造函数 - 编号2代码:这形成一个这形成一个
initializer_list<double>,背后有个array<double,2>.调用complex<double>构造函数时该array内的2个元素被分解传给构造函数.complex<double>并无任何构造函数接受initializer_list<double>参数
initializer_list<T>
Initializer Lists
初始化列表不填任何东西会被0初始化(若是指针则初始化为nullptr)
1 | int i;//i未被初始化 |
初始化列表不允许大空间到小空间的转换(narrowing:收缩)
1 | int x1(5.3); //OK x1=5 |
上面的ERROR在gcc中是warning
使用
为了支持用户定义类型的初始化列表概念,C++11提供了类模板std::initializer_list。它可用于支持通过值列表进行初始化,或者在任何其他只需要处理值列表的地方使用。
1 | void print(std::initializer_list<int> vals) |
优先级问题
如果同时有两个函数P(int a,int b)或者P(initializer_list<int> initlist)当执行 P q{77,5};会优先调用后者
对[[STL]]容器的影响:如今所有容器都接受指定任意数量的值用于构建或赋值或
insert()或assign();max()和min()也可以接受任意参数.
1
2
3
4
5
6
7
8
9
10 vector<int> v1{2,5,7,12,34,45,56};
vector<int> v2({2,5,7,69,83,50});
vector<int> v3;
v3 ={2,5,7,13,69,83,50};
v3.insert(v3.begin()+2,{0,1,2,3,4});
max({string("Ace"),string("Stacy"),string("Sabrina"),string("Barkley")});
min({54,16,48,5});
//要注意的是:
vector<int>(1)//创建临时的空间大小为1的vector<int>,里面的值是不确定的
vector<int>{1}//创建临时的空间里面的值是1的vector<int>
initializer_list原理
initializer_list内部的实现实际上是对array的引用(头指针和长度),没有包含array
因此如果复制initializer_list产生的另一个initializer_list,双方是同一个array的引用
初始化列表不支持隐式缩窄转换
列表初始化由宽类型转为窄类型编译无法通过
1 | int x=999; |
指定初始化
为了提高数据成员初始化的可读性和灵活性
C++20标准引入了指定初始化的特性
C语言在C99标准就已经支持该功能了
1 | //基本语法 |
语法要求
- 必须是一个聚合类型
- 数据成员必须是非静态数据成员
- 数据成员最多只能被初始化一次
- 非静态数据成员的初始化必须按照声明的顺序进行
- 针对联合体中的数据成员只能初始化一次,不能同时指定
- 不能嵌套指定初始化数据成员
- 一旦使用指定初始化就不能混用其他方法对数据成员初始化了
- 禁止对数组使用指定初始化
委托构造函数
C++11引入了委托构造函数的概念,允许一个构造函数调用同一个类的另一个构造函数来完成初始化。
为了简洁化冗余的构造函数
传统构造方式如下:
1 | class X2 |
委托构造方式:
1 | class x |
注意事项
每个构造函数都可以委托另一个构造为代理
不要递归循环委托
委托构造函数的执行顺序是:代理构造函数的初始化列表->代理构造函数体->委托构造函数体
若在代理构造函数执行完成后,委托构造函数主体抛出异常,则自动调用该类的析构函数
标准规定,代理构造函数执行完成就算构造完成,因此委托构造函数主体抛出异常必然会导致对象的析构
若构造函数为委托构造函数,那么其初始化列表不能对数据成员和基类进行初始化
委托模板构造函数
这种泛型能力可以有效减少构造函数的代码
1 | class x |
委托构造函数使用和初始化列表一样的[[C++基础#Function-try-block|function-try-block机制]]处理异常
若函数 try 块在委托构造函数上,而它调用的代理构造函数成功完成,但之后该委托构造函数的函数体抛出了异常,则将在进入函数 try 块的任何 catch 子句之前完成此对象的析构函数。
若异常在代理构造函数的初始化列表或函数主体中被抛出,委托构造函数主体将不再执行后序代码,交给catch执行.
1 | struct S { |
继承构造函数
C++11引入,用于解决继承关系中构造函数的冗余
1 | class Base{ |
基本语法
1 | class Base{ |
相关规则
派生类是隐式继承基类的构造函数,所以只有在程序中使用了这些构造函数,编译器才会为派生类生成继承构造函数的代码
派生类不会继承基类的默认构造函数和拷贝构造函数
继承构造函数不会影响派生类默认构造函数的隐式声明
在派生类中声明签名相同的构造函数会禁止继承相应的构造函数
1
2
3
4
5
6class Derive:public Base
{
public:
Derived(int x){...}
using Base::Base;//继承构造函数,但由于上一句,不会继承Base(int x):x(x),y(0){}代码
};派生类继承多个签名相同的构造函数会导致编译失败
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Base1{
public:
Base1(int){...}
};
class Base2{
public:
Base2(int){...}
};
class Derive:public Base1,Base2
{
public:
using Base1::Base1;
using Base2::Base2;//多继承,多个签名相同的构造函数导致编译失败
};继承构造函数的基类构造函数如果是私有,将不继承该函数
强枚举类型
[[C语言入门#枚举|枚举类型]]有两个问题
枚举类型可以隐式转换为整形
由于有隐式转换为整形,可以对枚举类型的值进行比较,不合理
无法指定枚举类型的底层类型
1
2
3
4
5
6
7
8
9
10
11
12enum E{
e1=1,
e2=2,
e3=0xfffffff0
}
int main()
{
bool b=e1<e3;
cout<<boolalpha<b<<endl;
//gcc中返回true;可以认为E的底层是unsigned int
//微软编译器返回false;可以认为E的底层是int,输出e3是-16
}
为了解决上述问题,C++11引入了强枚举类型
三个新特性
- 枚举标识符属于强枚举类型的作用域
- 枚举标识符不会隐式转换为整形
- 能指定强枚举类型的底层类型,底层类型默认为int类型
基本语法
关键词:enum class
1 | enum class HighSchool{ |
指定类型方式如下
1 | enum class E:unsigned int{//指定类型为unsigned int |
枚举类型的列表初始化
C++17标准开始,对有底层类型的枚举类型对象可以直接使用列表初始化
1 | enum class Color{ |
使用using打开强枚举类型
C++20标准拓展让using功能可以打开强枚举类型的命名空间
1 | enum class Color{ |
聚合类型
C++的聚合类型概念是在C++11标准中引入的。引入这一概念的目的是为了提供一种简洁的方法来初始化结构体和类的成员,以解决在早期版本中在初始化复杂数据结构时的冗长和不便。通过聚合类型,可以使用统一的初始化语法来初始化结构体和类的成员,使代码更加简洁和易读。
聚合类型需要满足的条件
没有用户提供的构造函数
C++20改成了没有用户声明的构造函数:区别在于:即使声明为显示删除:
构造函数=delete;或者显示默认:构造函数=default;,都会将结构体改变为非聚合类型没有私有和受保护的非静态数据成员
没有虚函数
在新标准的拓展中,如果类存在继承关系,额外满足条件:
- 必须是public的基类,不能是私有或者受保护的基类
- 必须是非虚继承
注意,聚合类型的要求没有要求基类必须是聚合类型
1 | class MyString:public std::string{}; |
聚合类型的初始化
可以直接使用尖括号初始化,总是假设基类是一种在所有数据成员之前声明的特殊成员
1 | class MyString:public std::string{} |
C++17开始,禁止受保护权限的构造函数在聚合类型初始化过程中被调用,因此会导致编译错误.解决方式很简单,提供一个公有权限的构造函数就可以了
1 | class BaseData{ |
小括号列表初始化
C++20后允许使用带小括号的列表初始化聚合类型对象
1 | struct X{ |
值得注意的是:聚合类型的小括号列表初始化支持缩窄转换,这是普通列表初始化所不支持的
基于范围的for循环
for的一种语法糖
这种语法只适用于支持[[STL#迭代器|迭代器]]访问的容器
本质上可以支持数组或对象,对于对象必须满足以下2个条件中的任意一个:
- 对象定义了begin和end成员函数
- 定义了以对象类型为参数的begin和end普通函数
1 | for(decl:coll) |
也由于其本质是属于隐式转换
1 | class C{ |
for语法糖的其他例子:
1 | for (int i : {12, 3, 5, 7, 9, 13, 17, 19}) |
如果在循环过程中确认不会修改引用对象,那么推荐在范围声明中加上const限定符,以帮助编译器生成更加高效的代码
begin和end函数不必返回相同类型
C++17规定了begin和end函数不必返回相同类型
C++11中编译器针对这种for特殊写法自动生成的伪代码
1
2
3
4
5auto&& range=range_expression;
for(auto begin=begin_expr,end=end_expr;begin!=end;++begin){//这一句就要求了begin和end必须是同一个返回类型
range_declaration=*begin;
//loop_statement
}C++17编译器针对这种for特殊写法自动生成的伪代码
1
2
3
4
5
6
7auto && range=range_expression;
auto begin=begin_expr;//分开接收了begin和end函数的返回值类型
auto end=end_expr;
for(;begin!=end;++begin){
range_declaration=*begin;
//loop_statement
}
意义
虽然标准容器(如 std::vector, std::map 等)的 begin() 和 end() 返回相同的类型,但并非所有的 STL 组件都如此
std::istream 和 std::ostream 迭代器
std::istream_iterator 的 begin() 返回普通的输入迭代器,但 end() 返回一个特殊的默认构造的迭代器,用于表示流结束。
更大的意义在于支持自定义范围类型
在某些自定义类型中,begin() 和 end() 返回不同类型是合理且必要的。例如:
- 只读范围:begin() 返回一个可变迭代器(mutable_iterator),而 end() 返回一个不可变迭代器(const_iterator)。
- 半开区间:begin() 返回一个普通迭代器,而 end() 返回一个哨兵值(sentinel),这种模式在处理流式数据或异步数据源时尤为常见。
1 | struct Range { |
临时范围表达式初始化语句
C++20允许在范围声明之前可以先执行初始化语句
1 | for(vector<int> ints={0,1,2};auto i:ints) |
实现一个支持基于范围的for循环的类
条件
- 该类型必须有一组和其类型相关的begin和end函数,他们可以是类的成员函数也可以是独立函数
- begin和end函数需要返回一组类似迭代器的对象,并且这组对象必须支持
operator*,operator!=和operator++运算符符号
1 |
|
在这个示例中,我们定义了一个名为Range的类,它包含了开始和结束范围。通过在Range类中实现begin()和end()方法,我们使得Range对象可以被用于基于范围的for循环。在主函数中,我们创建了一个Range对象r,并使用基于范围的for循环遍历了这个Range对象并输出了结果。
支持初始化语句的if和switch
if
从C++17开始,if和switch支持初始化语句
基本语法: if(init;condition){}
初始化语句中声明的变量生命周期将会伴随整个if-else代码块
1 | if(bool b=foo();b){ |
常用方法:
1 | mutex mx;//互斥锁 |
switch
基本语法:swtich(init;condition){}
static_assert声明
[[C++基础#断言|运行时断言(runtime assert)]]是在
<cassert>头文件中引入的,运行时断言是一种在运行时检查条件是否为真的机制,如果条件为假,程序将以错误消息终止。
运行时断言的缺点,只有在程序运行到断言出才能给出断言的判断
因此,C++11引入了static_assert声明 (编译阶段就能确定正确与否)
静态断言用于在程序编译阶段评估常量表达式,并对返回false的表达式断言
对静态断言的要求
- 所有处理必须在编译期间执行,不允许有空间或时间上的运行时成本
- 必须具有简单的语法
- 断言失败可以显示丰富的错误诊断信息
- 可以在命名空间,类或代码块内使用
- 失败的断言会在编译阶段报错
使用方法
基本语法: static_assert(常量表达式,诊断消息字符串);
1 | static_assert(sizeof(int)>=4,"sizeof(int)>=4"); |
常量表达式为假的时候,终止程序并打印诊断消息字符串
单参数static_assert
C++17标准支持单参数static_assert,即不用传参诊断消息字符串,函数内部默认为常量表达式的字符串本身
互斥锁
std::lock_guard和std::mutex都是C++11引入的内容,因此它们通常可以在<mutex>头文件中找到。因此,为了使用std::lock_guard和std::mutex,您需要包含<mutex>头文件。
=default/=delete
C++11引入=default,=delete
C++标准允许编译器为类自动添加一些函数,这些函数被称为:类的特殊成员函数,如下:
声明任何构造函数都会抑制默认构造函数的添加,上面除了析构函数之外,其余的都是构造函数(Big-Five)
如果你加上=default,编译器就会给你生成(如果有的话)一个默认版本.如果加上=delete表示不要编译器给我生成默认版本
1 | class Zoo |
C++类中会给函数的无参构造函数,拷贝构造函数,拷贝赋值构造函数自动生成默认版本的定义(浅拷贝),在C++2.0后多了两种会自动生成的构造函数,总共五种(俗称Big-Five).
p.s. 这种默认生成的构造函数全是public且inline的
=default用于Big-Five之外是无意义的=delete可用于任何函数身上(p.s.=0只能用于virtual函数
注意:不要同时使用explicit和=delete
1 | struct type |
非受限联合类型
[[C语言入门#共用体(联合体)|传统联合类型]]的成员类型不能是一个非平凡类型(成员类型不能有自定义的构造函数)
1 | union U |
于是C++20以后,非受限联合类型得到了支持,若联合类型中存在非平凡类型,则该联合体的构造和析构函数将被隐式删除,必须在联合体中定义构造和析构函数
1 | union U{ |
这样可以保证使用x3没有问题,但使用其他成员又怎么办呢?此处使用[[C++基础#placement new机制|placement new机制]]来初始化构造x3和x4
1 | union U{ |
C++中的联合类型使用率过低,了解即可
uncaught_exceptions
std::uncaught_exceptions() 在 C++17 中引入,用于获取当前存在的未捕获异常的数量。这个功能常在析构函数、资源管理类或执行清理操作时用于防止在异常处理中再抛异常。
1 |
|
noexcept
C++11 为了替代 throw() 用于函数声明的异常规范功能而提出的一个新的关键字noexcept
移动语义出来之前,throw就够用了,移动语义有个问题就是出现异常的时候会导致移动的对象和被移动的对象都出问题,解决方式
关键词: noexcept(常量表达式)
常量表达式的结果会被转换成一个 bool 类型的值,该值为 true,表示函数不会抛出异常,反之则能抛出异常。而不带常量表达式的 noexcept相当于声明了 noexcept(true),即不会抛出异常。
1 | void foo() noexcept; |
noexcept作为运算符的情况(noexcept不仅仅是说明符,也是运算符)
1 | int foo() noexcept{return 42;} |
noexcept 可以用来阻止异常的传播和扩散
noexcept 作用于模板时,则增强了 c++ 的泛型编程的能力
noexcept 更大的用处就是保证程序的安全。
因此出于安全考虑,C++11 标准中类的析构函数默认为
noexcept(true)。但是,如果程序员显式地为析构函数指定了noexcept(false)或者类的基类或成员有noexcept(false)的析构函数,析构函数就不会再保持默认值。提高效率
移动构造函数默认为
noexcept(true),可以去除一些异常处理机制,提高效率
带参数语法:
1 | template<class T> |
用noexcept解决移动构造问题
阻止会抛出异常的移动
1 | //简单粗暴:直接让移动有风险的对象代码直接编译阶段报错 |
更聪明的方法:让编译器自己选择更适合的版本
一个泛型
swap函数,它根据类型T是否支持无异常移动语义来决定采用不同的交换策略:优先采用移动语义以提高效率,同时确保在移动操作可能抛出异常的情况下能够回退到安全但可能较慢的拷贝交换方式。
1 | //通过移动交换(move-based swap) |
std::integral_constant是 C++ 标准库中定义的一个模板类,它主要用于模板元编程。这个类模板的主要作用是存储编译期已知的、类型为T的常量值,并且可以通过其value成员变量在运行时访问该常量。
noexcept和throw的差别
- C++11:相同的结果,不同的机制
- C++17:相同的结果和机制
- C++20:throw被移除
默认使用noexcept的函数
五大会自动生成的函数(big-five),都会默认使用noexcept
- 类型默认构造函数
- 默认拷贝构造函数
- 默认赋值函数
- 默认移动构造函数
- 默认移动赋值函数.
另外,上面对应的这些函数在类型的基类和成员中也具有noexcept声明
另外还有默认带有noexcept声明的函数:
- 类的析构函数
- delete运算符(用于释放
new运算符分配的内存空间)
使用noexcept的时机
- 一定不会出现异常的函数
- 函数虽然可能抛出异常,但是这是绝对不能接受的,否者程序应该直接终止(比如类中的移动相关函数)
异常规范作为类型的一部分
1 | void(*fp)() noexcept=nullptr;//定义noexcpt的函数指针变量为fp |
override/final
override
用于让编译器检查是否符合重写规则
应用在虚函数身上,表示对虚函数的覆写/改写/重写
1 | virtual void vfunc(int) override {} |
- 重载(overload),通常是指在同一个类中有两个或两个以上的函数,函数名相同,但函数签名不同,即有不同的形参
- 重写(override),意思更接近覆盖,指派生类覆盖了基类的虚函数
- 隐藏(overwrite),指基类成员函数,无论他是否为虚函数,当派生类出现同名函数时
- 若派生类函数签名不同于基类函数,则基类函数会被隐藏
- 若派生类函数签名与基类函数相同,则需要确认基类函数是否为虚函数
- 若为虚函数,就是重写
- 否则就是隐藏
1 | class Base{ |
final
可以用于声明虚函数,也可以用于声明类
声明类
阻止类被作为基类,表明为最终继承类
1 | struct Base1 final{}; |
声明虚函数
阻止派生类函数去继承此类的虚函数
表明为最终覆写虚函数,不能再往下覆写了
1 | class Base{ |
局部数组可用变量初始化大小
C++11之后使局部数组可以直接使用变量初始化大小
原本,**编译时常量(compile-time constant)**就可以为变量初始化大小
1
2
3
4
5
6
7
8
9
10
11
12 template<typename __Tp,size_t _Nm>
struct myArray
{
typedef _Tp
typedef __Tp*
typedef value_type*
value_type _M_instance[_Nm ? _Nm:1];
iterator begin()
{return iterator(&_M_instance[0];}
iterator end()
{return iterator(&_M_instance[_Nm]);}
};
_Nm是一个模板参数,它在编译时就已经确定了值,因此可以用来初始化类的成员变量的数组长度这样使用是合法的:
myArray<int, 5> arr; *// 使用编译时常量 5 初始化数组长度
1
2
3
4
5 //这样使用是合法的:
myArray<int, 5> arr; // 使用编译时常量 5 初始化数组长度`
//这样使用是非法的:
int n = 5;
myArray<int, n> arr; // 错误:n 不是一个编译时常量
现在支持如下语法:
1 | int main() |
但注意只能在函数内部定义的数组中使用,不能作为类的成员变量或全局变量使用。
decltype
用于获取表达式的类型
在c++1.0中可以通过typeid得到类型名的一串字符串
1 | const std::type_info& type = typeid(num); |
但是获取到的类型只是字符串,无法真正使用该类型,并且各编译器返回的字符串还不相同
虽然彼时C++有些编译器支持typeof可以实现此功能,但并非标准
1 | int a=0; |
decltype正是可以实现类似typeof的功能,并且同时具备完备的兼容性
使用方式
1 | map<string,float> coll; |
推导规则
decltype(e)(其中e的类型为T)的推导规则有五条:
如果e是一个未加括号的标识符表达式(结构化绑定除外)或者未加括号的类成员访问,则decltype(e)推断出的类型是e的类型T。如果并不存在这样的类型,或者e是一组重载函数,则无法进行推导。
潜台词就是下面其他规则都是带括号的e:
如果e是一个函数调用或者仿函数调用,那么
decltype(e)推断出的类型是其返回值的类型如果e是一个类型为T的左值,则
decltype(e)是T&如果e是一个类型为T的将亡值,则
decltype(e)是T&&除去以上情况,则
decltype(e)是T
CV限定符的推导
- 通常情况下,
decltype(e)所推导的类型会同步e的cv限定符 - 当e是未加括号的成员变量时,父对象表达式的cv限定符会被忽略
1 | const int&& foo(); |
decltype的类型如果想打印出来观察,可以配合typeid使用来观察
1
2
3
4
5
6
7
8 template <typename T>
void print_decltype(T x)
{
std::cout << "decltype(x): " << typeid(decltype(x)).name() << std::endl;
}
//比如int打印出来为i
//int*打印出来为pi
decltype和auto配合使用
用decltype的推导表达式规则来推导auto
1
2auto x1=(i);//按照auto的推导规则:x1推导为int
decltype(auto) x2=(i);//按照decltype的推导规则:x2推导为int&为非类型模板形参占位符
1
2
3
4
5templatye<decltype(auto) N>
void f()
{
cout<<N<<endl;
}
主要作用
声明返回类型
1 | template<typename T1,typename T2> |
上面这种情况想要实现的效果编译无法通过,因为编译器从左到右识别到x+y的时候,他还不知道x和y是什么,因此编译报错:
C++2.O又出现了一种新语法,如下:
1 | template<typename T1,typename T2> |
这种语法和lambdas定义的方式很像,下面是lambdas表达式的定义方式:
$$
\left[…\right]\left(…\right)\text{mutable}{opt}\ \textit{ throwSpec}{opt}\text{ -> retType}_{opt}\left{…\right}
$$
用于元编程
1 | //该函数针对容器 |
value_type是STL容器的一个成员类型别名,用于表示容器中存储的元素类型。在STL容器中,value_type通常用于定义容器内元素的类型,方便编写通用的泛型代码。另外针对map这种容器,还有key_type可以获取其键的类型
1 | vector<int> vecs{}; |
传递lambda表达式的类型
面对lambda,我们手上往往只有对象,没有类型,要获得其类型,就得借助于decltype
1 | auto cmp=[](const Person& p1,const Person& p2){ |
p.s. 如果传入的值不是lambda,报错会特别晦涩难懂(即上文中的coll括号中的cmp).对于自定义排序函数来说一个普通的函数还是更直观
类型别名与别名模板
类似typedef
关键字:using 对该关键词赋予了新的意义
基本语法 using identifier = type-id
1 | //定义一个func类型 |
这种语法超脱于typedef的地方就在于别名模板,也是他诞生的原因
别名模板
1 | //定义 |
使用#define和typedef无法达到类似的效果
用typedef只能达到这种程度
1 | template<class T> |
需要注意的是:别名模版不能[[C++基础#函数模特化|函数模板特化]]与[[C++基础#类模板特化|类模板特化]]
Lambda表达式
Lambda看起来像一个函数,实际上是一个对象
C++11中的Lambda表达式**用于定义并创建匿名的 函数对象**,以简化编程工作。
Lambda表达式最简形式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 //下面的函数是把lambda表达式写成内联函数(inline)
//其实是定义了一个类,定义该类的仿函数函数体如下代码,定义为这个类的对象,如果用变量接收,接收到的是一个匿名的函数对象
auto f=[]{
cout<<"hello lambda"<<endl;
}
//调用函数对象
f();
//=======上面写法与下面写法等同=========
//定义一个类并创建对象并且调用该函数对象的仿函数
[]{
cout<<"hello lambda"<<endl;
}();
Lambda表达式完整构成
1 | [capture](parameters) mutable ->return-type |
$$
\left[…\right]\left(…\right)\text{mutable}{opt}\ \textit{ throwSpec}{opt}\text{ -> retType}_{opt}\left{…\right}
$$
简单理解:
capture部分用于指定哪些变量应被捕获,并以何种方式捕获。捕获可以分为按值捕获(value capture)和按引用捕获(reference capture)。
最简单也最具代表性的两种用法:
- 按值捕获是将外部变量的副本存储在lambda中
[=]- 按引用捕获是将外部变量的引用存储在lambda中
[&]
opt表示该项可选,三个可选项只要有一个存在,就必须写小括号
① 函数对象参数;
[],捕获列表:标识一个Lambda的开始,这部分必须存在,不能省略。函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义Lambda为止时Lambda所在作用范围内可见的局部变量(包括Lambda所在类的this)(这里指明局部变量的原因是因为全局变量本来就可以在类中被使用,如果使用全局变量或者static变量,一般会被编译器警告)。函数对象参数有以下形式:
- 空。没有使用任何函数对象参数。
- =。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
- &。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是引用传递方式(相当于编译器自动为我们按引用传递了所有局部变量)。
- this。函数体内可以使用Lambda所在类中的成员变量。
- a。将a按值进行传递。按值进行传递时,函数体内不能修改传递进来的a的拷贝,因为默认情况下函数是const的。要修改传递进来的a的拷贝,可以添加mutable修饰符。
- &a。将a按引用进行传递。
- a, &b。将a按值进行传递,b按引用进行传递。
- =,&a, &b。除a和b按引用进行传递外,其他参数都按值进行传递。
- &, a, b。除a和b按值进行传递外,其他参数都按引用进行传递。
C++14标准开始定义了广义捕获,分为下面两种
简单捕获(上方介绍的为简单捕获)
初始化捕获
1
2
3int x=5;
auto foo=[r=x+1]{return r;};
//r的作用域为lambda表达式中
② 操作符重载函数参数;
标识重载的()操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如:(a,b))和按引用(如:(&a,&b))两种方式进行传递。注意是要带类型的,C++14开始类型还可以使用auto.
③ 可修改标示符;
mutable声明,这部分可以省略。按值传递函数对象参数时,加上mutable修饰符后,才可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)
1 | QPushButton * myBtn = new QPushButton (this); |
④ 函数返回值;
->返回值类型,标识函数返回值的类型,当返回值为void,或者函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。
⑤ 是函数体;
{},标识函数的实现,这部分不能省略,但函数体可以为空。
很重要的一点: 利用decltype获取lambda表达式的类型
Lambdas的一些注意点
无状态Lambda表达式
从C++20开始,对于无状态Lambda表达式是可以构造和赋值的
即可以隐式转换为函数指针
解决了无法就地编写内嵌函数的尴尬问题
捕捉上下文变量(即捕获列表
[]中是否有东西),不管实际函数体中有没有用到上下文变量,只要不活了上下文变量都属于有状态 lambda在需要函数回调为参数的函数中非常有用
1 | void f(void(*)()){} |
捕获[*this]和[=,this]
[*this]:拷贝this对象[=,this]:是为了区分[=,*this]
模板语法的泛型lambda表达式
从C++20开始,可以使用下面语法:
1 | auto f=[]<typename T>(vector<T> vector){}; |
constexpr lambda
从C++17开始,lambda表达式在条件允许的情况下都会隐式声明为constexpr
[详情跳转](#constexpr lambdas表达式)
Lambdas案例细节
mutable值传递,引用传递,以及值传递,三种情况的比较如下:
1 | //mutable值传递 |
Lambdas原理
lambda 表达式出现的契机,正是用来代替被临时定义使用的可调用对象,我们可以把 lambda 表达式理解为一个未命名的函数,但他又不同于一般的函数,他有一个很大的特点就是可以捕获状态,但又不需要声明一个新的类来保存状态,而其实在编译器内部对 lambda 表达式的处理就是生成了一个未命名的类,并通过 lambda 表达式生成该未命名类的未命名函数对象。因此对于那些我们只要用一次的简短的函数或函数对象来说,利用 lambda 表达式能极大的增强代码的封装性和可读性
lambda 表达式是一个函数对象,在定义一个 lambda 表达式的时候,相当于编译器为我们定义了一个临时的类,该类重载了函数调用运算符,同时对于引用捕获的变量,编译器无须在 lambda 产生的类中将其存储为数据成员,而只需要存储引用,而对于值捕获的变量,由于其需要被拷贝到 lambda 中,因此这种 lambda 产生的类就必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数。若将一个变量初始化为一个 lambda 表达式,则相当于初始化了一个该临时类的对象,而在调用的时候就相当于调用了该类的函数重载运算符
参考下面:
1 | class A |
函数对象(难点)
重载函数调用操作符的类,其对象常称为函数对象(function object),即它们是行为类似函数的对象,也叫仿函数(functor),其实就是重载“()”操作符,使得类对象可以像函数那样调用。
注意:
- 函数对象(仿函数)是一个类,不是一个函数。
- 函数对象(仿函数)重载了”() ”操作符使得它可以像函数一样调用。
分类:假定某个类有一个重载的operator(),而且重载的operator()要求获取一个参数,我们就将这个类称为“一元仿函数”(unary functor);相反,如果重载的operator()要求获取两个参数,就将这个类称为“二元仿函数”(binary functor)。
函数对象的作用主要是什么?STL提供的算法往往都有两个版本,其中一个版本表现出最常用的某种运算,另一版本则允许用户通过template参数的形式来指定所要采取的策略。
注意: 定义函数对象类的时候,不能使用小括号定义(无论构造函数任何情况都是错的),如:baz b();,这样是错的,而应该是:baz b;,但如果存在带参构造函数,可以使用baz b(123);来定义
1 | //函数对象是重载了函数调用符号的类 |
总结:
- 函数对象通常不定义构造函数和析构函数,所以在构造和析构时不会发生任何问题,避免了函数调用的运行时问题。
- 函数对象超出普通函数的概念,函数对象可以有自己的状态
- 函数对象可内联编译,性能好。用函数指针几乎不可能
- 模版函数对象使函数对象具有通用性,这也是它的优势之一
谓词
谓词是指普通函数或重载的operator()返回值是bool类型的函数对象(仿函数)。如果operator接受一个参数,那么叫做一元谓词,如果接受两个参数,那么叫做二元谓词,谓词可作为一个判断式。
一元谓词与二元谓词的案例:(含lambda表达式)
1 | //一元谓词 |
内建函数对象
STL内建了一些函数对象。分为:算数类函数对象,关系运算类函数对象,逻辑运算类仿函数。这些仿函数所产生的对象,用法和一般函数完全相同,当然我们还可以产生无名的临时对象来履行函数功能。使用内建函数对象,需要引入头文件#include
- 6个算数类函数对象,除了negate是一元运算,其他都是二元运算。
1 | template<class T> T plus<T>//加法仿函数 |
- 6个关系运算类函数对象,每一种都是二元运算。
1 | template<class T> bool equal_to<T>//等于 |
- 逻辑运算类运算函数,not为一元运算,其余为二元运算。
1 | template<class T> bool logical_and<T>//逻辑与 |
内建函数对象举例:
1 | //取反仿函数 |
函数对象适配器
函数对象适配器bind1st和bind2nd
现在我有这个需求 在遍历容器的时候,我希望将容器中的值全部加上用户输入的数之后显示出来,怎么做?我们直接给函数对象绑定参数 编译阶段就会报错for_each(v.begin(), v.end(), bind2nd(myprint(),100));
如果我们想使用绑定适配器,需要我们自己的函数对象继承 unary_function或者 binary_function根据我们函数对象是一元函数对象 还是二元函数对象
自己建的函数对象写bind1st bind2nd适配器要三个操作:
- 利用bind1st或bind2nd进行绑定
- 继承public:binary_function<参数1类型,参数2类型,返回值类型>或 unary_function
- 加const
【注意】内建的函数对象不需要写这些2,3(其实想写也没法写,内置的嘛)。利用函数指针适配器和成员函数适配器转换成的函数对象也不需要写2,3(同样也没法写…)。
1 | class MyPrint :public binary_function<int,int,void>//继承binary_function |
总结: bind1st和bind2nd区别?
- bind1st : 将参数绑定为函数对象的第一个参数
- bind2nd : 将参数绑定为函数对象的第二个参数
bind1st bind2nd作用:将二元函数对象转为一元函数对象
所以bind1st或bind2nd如果要和其他适配器嵌套,比如需要用的是not1,因为已经转换为一元函数对象
取反适配器not1和not2
1 | class GreaterThenFive:public unary_function<int,bool> |
函数指针适配器 ptr_fun
1 | void MyPrint03(int v,int v2) |
成员函数适配器 mem_fun_ref和mem_fun
- 如果容器存放的是对象指针, 那么用mem_fun
- 如果容器中存放的是对象实体,那么用mem_fun_ref
mem_fun_ref
1 | class Person |
mem_fun
1 | void test05(){ |
智能指针
原理:智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。这意味着智能指针本身是在栈上的,但它所管理的内存是在堆上分配的。当智能指针本身被回收以后,会将智能指针上管理的内存释放
优点:
- 自动内存管理:智能指针可以自动处理内存的分配和释放,避免了内存泄漏和悬空指针的问题。
- 避免手动释放内存:使用智能指针可以避免手动释放内存的麻烦,减少了出错的可能性。
- 安全性:智能指针通常会在其生命周期结束时自动释放内存,避免了因忘记释放内存而导致的内存泄漏。
- 方便性:智能指针提供了类似于原始指针的使用方式,但又具有自动内存管理的功能,使得代码更加简洁和易读。
头文件 #include <memory>
智能指针的用途: 帮助管理内存,可以做到自动释放内存,避免忘记释放而造成内存泄露
shared_ptr允许多个智能指针指向同一个空间,使用引用计数来跟踪资源的生命周期多个类共享指针存在这么一个问题:每个类都存储了指针地址的一个拷贝,如果其中一个类删除了这个指针,其它类并不知道这个指针已经失效,此时就会出现野指针的现象。为了解决这一问题,我们可以使用引用指针来计数,仅当检测到引用计数为0时,才主动删除这个数据
unique_ptr只能一个智能指针指向同一个空间,不能进行复制或共享资源,可以通过移动语义转移资源所有权weak_ptrweak_ptr是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。它是一种用于解决shared_ptr循环引用时产生死锁问题的智能指针。不拥有资源所有权,不能直接访问所管理的资源。
在功能上类似于普通指针,比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存
auto_ptr在C++11中移除,已被unique_ptr取代
使用 std::make_shared 和 std::make_unique:这两个函数提供了一种更安全和高效的方式来创建智能指针,避免了手动调用 new
auto_ptr
可以通过lock()函数获取一个有效的std::shared_ptr,如果原始的std::shared_ptr 已经释放了资源,则返回空指针。
1 | auto p = make_shared<int>(42); |
auto_ptr的局限性
复制或者赋值都会改变资源的所有权
1 | // auto_ptr 被C++11抛弃的主要原因 |
如果替换auto_ptr为unique_ptr,p1 = p2;在编译的时候会报错,因为unique_ptr中的拷贝赋值运算符是删除了的,若unique_ptr改为:p1 = move(p2);,那么会和上面案例一样报,内存访问越界问题,因为p2已经释放.
跟auto_ptr一样可以赋值,但是需要使用move修饰,使得程序员知道后果
不支持对象数组的内存管理
1 | auto_ptr<int[]> array(new int[5]); // 不能这样定义 |
unique_ptr
C++11用更严谨的unique_ptr 取代了auto_ptr
- auto_ptr不支持移动语义
- auto_ptr不支持对象数组的内存管理
unique_ptr支持移动语义,并且支持对象数组的内存管理
1 | // 会自动调用delete [] 函数去释放内存 |
除了上面指出的两点外,unique_ptr的其余用法都与auto_ptr用法一致
auto_ptr与unique_ptr智能指针的内存管理陷阱
可以通过下面的方式破坏排他性
1 | unique_ptr<string> p1; |
shared_ptr
三种初始化方式
以shared_ptr作为案例
1 | //直接使用构造函数(括号里是赋予的初始值) |
p.s. make_unique 是 C++14 中引入的一个函数,编译时需要指定-std=c++14
常用api
get(): 获取指针值虽然可以获得原始值,但我们不应该delete这一指针,也不应该用它赋值/初始化另一个智能指针;当将原生指针传给shared_ptr后,就应该让shared_ptr接管这一指针,而不再直接操作原生指针。
use_count(): 智能指向的内存的引用次数(有几个智能指针指向这块内存)reset(): 对智能指针进行重置操作,使智能指针原有的指向修改为新的指向,该函数会首先将原有的内存的引用计数减1,当减小到0的时候就会释放内存.其实就是重新赋值:p.reset(new int(1024));
使用swap可以交换两个智能指针管理的内存对象
构造函数
shared_ptr< T > sp1;空的shared_ptr,可以指向类型为T的对象shared_ptr< T > sp2(new T());定义shared_ptr,同时指向类型为T的对象shared_ptr<T[]> sp4;空的shared_ptr,可以指向类型为T[]的数组对象 C++17后支持shared_ptr<T[]> sp5(new T[] { … });指向类型为T的数组对象 C++17后支持shared_ptr< T > sp6(NULL, D());//空的shared_ptr,接受一个D类型的删除器,使用D释放内存p.s. 这种情况仅用于自动执行释放操作
shared_ptr< T > sp7(new T(), D());//定义shared_ptr,指向类型为T的对象,接受一个D类型的删除器,使用D删除器来释放内存
使用智能指针管理对象数组
1 | //AA是个类 |
智能指针可以像普通指针那样使用:
1 | //智能指针test |
可以像普通指针那样使用的原因在于:因为其里面重载了 * 和 -> 运算符, * 返回普通对象(return (*get())),而 -> 返回指针对象(return get())
shared_ptr工作原理
std::shared_ptr 是一种智能指针,它使用引用计数的方式来自动管理所指向对象的生命周期。当多个 shared_ptr 共享同一个对象时,它们会共同维护一个引用计数。主要规则如下:
- 创建一个
shared_ptr时,初始引用计数为 1。 - 当一个
shared_ptr被复制或拷贝赋值给另一个shared_ptr时,目标shared_ptr会增加源shared_ptr所指向对象的引用计数。 - 当一个
shared_ptr被销毁(如离开作用域)时,它会减少所指向对象的引用计数。 - 当一个对象的引用计数降至 0 时,
std::shared_ptr会自动释放该对象,调用其析构函数,并回收其占用的内存。
shared_ptr模拟代码
由于利用引用计数的方式: 因为引用计数算是临界资源, 所以对其的操作必须通过互斥量进行保护, 进行原子操作(保护临界资源)
1 | namespace test { |
shared_ptr使用陷阱
shared_ptr作为被管控的对象的成员时,小心因循环引用造成内存泄漏!
1 |
|
weak_ptr
为了解决shared_ptr循环引用的问题, 循环引用, 也就是相互之间都有引用计数关系,使得对象无法真正被释放的问题
- shared_ptr解决循环引用的原理: 在引用计数的时候将
_pre和_next指针修改成weak_ptr智能指针即可 - 原理就是,
node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加 node1和node2的引用计数。
结构化绑定
C++17标准引入。结构化绑定允许将一个结构体或元组的成员绑定到单独的变量中,以便更方便地访问和操作这些成员
接受多个返回值
关键词: auto[标识符列表]
绑定原生数组
1 | int a[3]{1,3,5}; |
绑定到结构体和类对象
结构体或类的非静态公有数据成员(包括继承来的成员)必须和标识符列表中的别名的个数相同(C++20有另外的修改)
1 | class BindBase{ |
绑定到元组和类元组的对象
需要满足条件(这是一种方向指导,而不是具体的代码)
- 首先需要满足
tuple_size<T>::value是一个符合语法的表达式,并且该表达式获得的整数值与标识符列表中的别名个数相同 - 其次,类型T还需要保证
tuple_elementM<i,T>::type也是一个符合语法的表达式,其中i是小于tuple_size<T>::value的整数,表达式代表了类型T中第i个元素的类型; - 最后,类型T必须存在合法的成员函数模板
get<i>()或者函数模板get<i>(t),其中i是小于tuple_size<T>::value的整数,t是类型T的实例,get<i>()和get<i>(t)返回的是实例t中第i个元素的值
结构化绑定是基于编译器能直接访问其公有非静态数据成员而实现的.如果想自己实现这种结构化绑定的机制,可能需要借助其他技术或等待未来C++标准对反射等特性更完善的支持。(也有说法是可以对任意对象做结构化绑定,前提是要编写额外代码,此处有存疑)
下面是一种间接模拟的方式,并非原生地为自定义类型提供了结构化绑定支持。C++20以及之前的标准并未直接支持对所有自定义类型的结构化绑定功能。
1 |
|
这种方式可以访问私有成员,避免了绑定的访问权限问题
很实用的一种情形案例如下:
1 | struct Out { |
目前这种做法可以做到让返回值更富有语意,并且可以很方便的扩展,如果要增加一个新的返回值,只需要扩展现有的结构体就可以了。正如上文所说,在 CppCoreGuidelines 中对于多返回值更建议使用 tuple 或 struct ,这样做能让返回值的语意更加明确
结构化绑定的原理
在结构化绑定中编译器会根据限定符生成一个等号右边对象的匿名副本,而绑定的对象正是这个副本而非原对象本身.另外这里的别名就真的是单纯的别名,别名的类型和绑定目标对象的子对象类型相同
绑定的访问权限问题
对于私有或保护成员,即使在友元函数内也无法直接通过结构化绑定访问它们
1 | struct A{ |
常量表达式
C++11以前没有一种方法可以保证编译期间确定常量和函数的计算结果的(详情参考下面视频)
无法保证编译器确定的常量
1
2
3
4
5
6
7 int get_index0(){
return 0;
}
int get_index1(){
return 1;
}
const int index0=get_index0();//触发编译错误,因为编译器认为index0必须是运行期才能确定下来的于是引入了
constexpr关键字
一个用constexpr说明符声明的变量或数据成员,要求该值必须在编译器计算
1 | constexpr int x=42; |
常量表达式函数
常量表达式函数的返回值可以在编译阶段那就计算出来
1 | //常量表达式函数 |
约束条件(C++11标准)
- 函数必须返回一个值,所以它的返回类型不能是void
- 函数体必须只有一条语句:
return expr,其中expr必须也是一个常量表达式 - 函数使用之前必须有定义
- 函数必须用constexpr声明
即使成功定义了常量表达式函数,也不一定最终就在编译器求值,因为传入的实参如果不是常量表达式,常量表达式函数会退化为普通函数
约束条件新标准(C++14标准)
- 函数体允许声明变量,除了没有初始化的,static和thread_local变量
- 函数允许出现if和switch语句,不能使用go语句
- 函数允许所有的循环语句,包括
for,while,do-while - 函数可以修改生命周期和常量表达式相同的对象
- 函数的返回值可以声明为void
- constexpr修饰的成员函数不再具有const属性
约束条件拓展(C++20)
c++20标准明确允许在常量表达式中使用虚函数
1
2
3
4
5
6
7
8struct X
{
constexpr virtual int f() const {return 1;}
};
int main(){
constexpr X x;
int i=x.f();
}允许在constexpr中进行平凡的默认初始化
允许在constexpr函数中出现try-catch
可以出现try-catch,但没有意义,因为 try-catch 语句用于处理运行时的异常,而 constexpr 函数在编译时就会被求值,不会引发运行时异常。因此,编译器通常会在编译时检查 constexpr 函数的代码是否会引发异常,并在有可能引发异常的情况下导致编译错误。因此,在 constexpr 函数中使用 try-catch 是没有意义的。
允许在constexpr中更改联合类型的有效成员
允许dynamic_cast和typeid出现在常量表达式中
C++23进一步拓展:暂略
constexpr lambdas表达式
从C++17开始,lambda表达式在条件允许的情况下都会隐式声明为constexpr:
1 | constexpr int foo() |
常量表达式构造函数
作用于自定义类型
1 | class X{ |
约束条件
- 构造函数必须用constexpr修饰
- 构造函数初始化列表里必须是常量表达式
- 构造函数的函数体必须为空
意义
常量表达式用于构造函数的意义主要体现在优化和安全性方面
- 编译时常量化:如果一个构造函数使用常量表达式初始化对象的某些成员变量,那么在编译时就可以计算出这些值。这有助于在编译时捕获潜在的错误,并减少运行时开销。
- 提高性能:通过使用常量表达式,程序在运行时需要做的计算减少了,因为某些结果已经在编译时得到。这降低了程序的执行时间,特别是在资源受限的系统中,这种优化尤为重要。
- 代码安全性:因为常量表达式在编译时计算,有助于确保代码的一致性和安全性。它可以防止某些类型的错误,例如未经初始化的数据使用。
- 不可变性:常量对象或只读数据成员可以用常量表达式进行初始化,这确保了对象一旦创建,其状态就不能再被改变,增加了对象的不可变性。这也使得代码更易于推理和测试。
- 元编程支持:在现代C++中,使用
constexpr关键字的能力支持了一些更高级的编程技巧,如模板元编程。这使得开发者可以编写出更通用、灵活和高效的代码。
if constexpr
if constexpr的条件必须是编译器能确定结果的常量表达式
条件结果一旦确定,编译器将只编译符合条件的代码块
1 | void check1(int i){ |
consteval
consteval是C++20引入的一个新关键字,用于强制声明一个编译时求值的函数
constexpr是非强制性的,由编译器决定是否在编译器就执行计算
而consteval是确保函数在编译器就执行计算,用法与constexpr一致
constinit
constinit是C++20引入的一个新关键字,用于声明在程序启动时初始化的const变量。它确保在程序启动时初始化const变量,以避免静态初始化顺序问题。使用constinit可以提高程序的可靠性和可移植性。当一个变量被声明为
constinit时,它要求该变量必须在编译时用常量表达式进行初始化,而且只能在声明时初始化,不能在运行时再次赋值。解决因为静态初始化顺序错误导致问题.例如:假设有两个静态对象x和y,分别存在两个不同的源文件中,其中一个对象x的构造函数依赖于对象y.现在我们有50%的可能性会出错.因为哪个对象先构造决定了是否有错,为了避免这种问题发生,我们通常希望使用常量初始化程序去初始化静态变量,不幸的是常量初始化的规则很复杂,需要一种方法帮我们完成检查工作.当不符合常量初始化时,可以在编译阶段报错.于是C++20引入了constinit关键词
使用constinit检查常量初始化规则
用于具有静态存储持续时间的变量声明上,它要求变量是常量初始化
1
2
3
4constinit int x=11;//编译成功,全局变量具有静态存储持续
int main(){
constinit static int y=42;//编译成功,静态变量具有静态存储持续
constinit int z=7;//编译失败,局部变量是动态分配的或者要求变量具有常量表达式初始化程序
1
2
3
4const char* f(){return "hello";}
constexpr const char* g(){return "hello";}//const可以不写的
constinit const char* str1 = f();//编译错误,f()不是一个常量表达式初始化程序
constinit const char* str2 = g();//编译成功
线程局部存储
将对象内存和线程关联起来,对象在线程开始时分配内存,在线程结束时释放内存.每个对象相对线程是独立的,并且不会相互干扰
线程局部存储(Thread Local Storage,TLS)原本是操作系统层面的概念
TLS使用的是和全局变量、静态变量等一样的存储空间,但是TLS的变量是线程私有的,每个线程都有自己的副本,而全局变量和静态变量则是所有线程共享的。
Windows和Linux都有各自的方法管理线程局部存储
- Windows
- TlsAlloc 分配未被使用的线程局部空间的索引
- TlsGetValue 获取索引指向的空间的值
- TlsSetValue 设置索引指向的空间的值
- TlsFree 释放索引指向的线程局部空间
- Linux
- pthread_key_create 分配未被使用的线程局部空间的索引
- pthread_getsepcific 获取索引指向的空间的值
- pthread_setspecific 设置索引指向的空间的值
- pthread_key_delete 释放索引指向的线程局部空间
编译器对线程局部存储的支持(C++11之前,方式不统一)
GCC&CLANG
__thread
MSVC
__declspec(thread)
C++11后统一了标准
关键词:thread_local 放在变量声明前表示为线程局部存储
基本语法:
1 | struct X{ |
被thread_local声明的变量在行为上非常像静态变量,只不过多了线程属性,他能够解决全局变量和静态变量在多线程程序中存在的问题
典型的例子为:errno的多线程安全问题.一个多线程程序A线程,正准备获取错误码,另一个线程B修改了错误码,这时候线程A获取到的错误码就不合理了,为了解决这个问题,C++11标准将errno重新定义为线程独立变量
线程局部存储的内存地址
- 线程局部存储只定义了生命周期,而没有定义访问性.即可以获取线程局部存储变量的地址,并将这个地址传递给其他线程,并且其他线程可以在变量生命周期内自由使用该变量.但这样做意义不大并且风险性大容易导致未定义行为造成程序崩溃
- 线程局部存储的地址是运行时被计算出来的(static和全局变量地址是编译时确定的),因此线程局部存储的地址不是一个常量,因此无法和常量表达式结合
线程局部存储对象初始化和销毁
对于同一个线程中线程局部存储对象只会被初始化一次,即使被多次调用
有点类似静态变量只会在全局初始化一次
线程局部存储对象的销毁通常发生在线程销毁的时刻
tuple
头文件: #include <tuple>
从C++11标准开始引入的内容,属于C++标准库的一部分,用于代表一个固定数量的异质对象的集合。
线程
语言级线程支持
c++11关于并发引入了好多新东西,这里按照如下顺序介绍:
- std::thread相关
- std::mutex相关
- std::lock相关
- std::atomic相关
- std::call_once相关
- volatile相关
- std::condition_variable相关
- std::future相关
- async相关
c++11之前可能使用pthread_xxx来创建线程,繁琐且不易读,c++11引入了std::thread来创建线程,支持对线程join或者detach
stl新容器
C++谷歌代码规范
Boost
filesystem
曾作为一个技术规范存在,并且在Boost库中有一个成熟的实现——Boost.Filesystem。随着C++17的发布,文件系统库成为标准库的一部分,使得C++程序员能够在不依赖第三方库的情况下进行跨平台的文件系统操作。
emplace
C++11针对顺序容器引入了三个新成员
emplace_front
在容器开头直接构造新元素
emplace
在指定位置直接构造新元素
代表了直接调用构造函数
直接不传参数,相当于调用无参构造函数,即在栈上就地构造
emplace_back
在容器末尾直接构造新元素
定义:
1 | template <class Container> |
这三个操作与push_front,insert,push_back操作相同,但这些操作是构造元素,而不是拷贝元素
insert在插入时,由于我们传的是右值,其调用了构造和移动构造,而emplace只调用了构造函数。也就是说,emplace是在插入位置直接构造元素,而不是和insert一样,先是构造好,再移动或复制到插入位置。这样做的优势就是能够减少一次移动构造或拷贝构造。
优势
性能优化:避免了不必要的临时对象创建和拷贝操作
直接构造:参数直接传递给对象的构造函数,如果是空的话相当于直接在栈上构造
完美转发:保持参数的值类型和引用类型
C++17 后,emplace_back 和 emplace_front 返回对新构造元素的引用
C++17三剑客
- optional 可存在可不存在包装器
- any 任意类型
- vairant 类型安全的联合体,可以存储指定类型中的一个
optional
头文件:<optional>
C++17引入,C++ 17 之前的版本可以通过 boost::optional 实现几乎相同的功能。
包装可以为空的类型
类模板 std::optional 管理一个可选 的容纳值,既可以存在也可以不存在的值。
一种常见的 optional 使用情况是一个可能失败的函数的返回值。与其他手段,如 std::pair<T, bool> 相比,optional 良好地处理构造开销高昂的对象,并更加可读,因为它显式表达意图
optional的内存管理是自动的,当 std::optional 的生命周期结束时,它所持有的值会被自动释放。 具体来说,当 std::optional 对象被销毁时,如果它包含一个值(即状态为“有值”),那么这个值的析构函数会被调用,从而释放相关资源。如果它没有值(状态为“无值”),则不会有任何资源释放操作。
1 |
|
std::nullopt 是 C++ 17 中提供的没有值的 optional 的表达形式,等同于 { }
创建一个 optional 的方法
1 | // 空 optiolal |
使用emplace(Args...)方法也可以将一个T类型对象置入一个已经存在的std::optional对象中。
1 | auto optVec = std::make_optional<std::vector<int>>(3, 22); |
访问 optional 对象中数据的方法
1 | // 跟迭代器的使用类似,访问没有 value 的 optional 的行为是未定义的 |
判断option对象是否有值
使用has_value()函数来询问std::optional此时是否有值
可以显式地转化为bool型变量来显示他此时是否拥有一个有意义的值
1 | std::optional<unsigned> opt = firstEvenNumberIn(text); |
销毁optino对象中的值
reset()方法销毁存储在std::optional中的值,并将其值为空
C++中的函数式编程
首先需要了解[[架构相关#函数式编程|函数式编程的思想]]
纯函数
在C++中实现一个纯函数,但又要递归,而递归的过程中需要利用一个共享空间来在递归函数中共用,同时需要保持纯函数的性质呢?比如说斐波那契数列求值
1 | // 带有缓存功能的递归斐波那契函数 |
虽然使用到了空间,但不依赖外部
高阶函数
applyFunction是一个高阶函数,因为它接受另一个函数作为参数。在main函数中,我们通过lambda表达式将一个简单的操作传递给applyFunction来处理向量中的每个元素
1 |
|
常用的高阶函数
transform
将给定范围内的每个元素通过某种转换函数转换为一个新的值,通常用于对容器中的元素进行批量操作。
1
2// 使用std::transform将numbers中的每个元素平方,导出为square
std::transform(numbers.begin(), numbers.end(), square.begin(), [](int x) { return x * x; });copy_if
复制满足某个条件的元素到一个新的容器,通常用于过滤操作。
1
2
3// 使用std::copy_if将numbers中所有偶数复制到evens
std::copy_if(numbers.begin(), numbers.end(), std::back_inserter(evens), [](int x) { return x % 2 == 0; });
//back_inserter是插入到末尾迭代器,需要<iterator>头文件accumulate
对指定范围内的元素进行累积求值,通常用于总和、乘积或其他累积操作。
1
2
3
// 使用std::accumulate计算所有元素的乘积
int product = std::accumulate(numbers.begin(), numbers.end(), 1, [](int accumulated, int current) { return accumulated * current; });reduce
对指定范围内的元素进行聚合运算
1
2
3
4
5
6
7
8
9
10
// 使用std::reduce计算元素总和
int sum = std::reduce(numbers.begin(), numbers.end(), 0);
// 使用自定义操作符(乘法)
int product = std::reduce(numbers.begin(), numbers.end(), 1, std::multiplies<>());
// 使用并行执行策略 (仅在支持并行的系统中有效)
int parallel_sum = std::reduce(std::execution::par, numbers.begin(), numbers.end(), 0);std::reduce接受一个可选的执行策略参数std::execution::seq: 顺序执行(默认)std::execution::par: 并行执行std::execution::par_unseq: 并行且无序执行
for_each
对指定范围内的每个元素执行某个操作
1
2// 使用std::for_each对每个元素执行操作
std::for_each(numbers.begin(), numbers.end(), [](int &n) { n *= 2; });any_of
检查指定范围内是否存在至少一个满足给定谓词条件的元素。返回
true或false1
2// 使用std::any_of检查是否存在偶数
bool has_even = std::any_of(numbers.begin(), numbers.end(), [](int n) { return n % 2 == 0; });all_of
检查指定范围内的所有元素是否都满足给定谓词条件。返回
true或false1
2// 使用std::all_of检查是否所有元素都是偶数
bool all_even = std::all_of(numbers.begin(), numbers.end(), [](int n) { return n % 2 == 0; });find_if
在指定范围内查找第一个满足给定谓词条件的元素。返回指向该元素的迭代器。如果未找到,返回结束迭代器。
1
2// 使用std::find_if查找第一个偶数
auto it = std::find_if(numbers.begin(), numbers.end(), [](int n) { return n % 2 == 0; });
组合函数与管道函数
组合函数的实现
compose函数可以将多个函数组合成一个新的函数,从右到左执行
1 |
|
也可以使用reduce实现:
1 | template <typename T, typename... Funcs> |
管道函数的实现
pipe函数将多个函数从左到右组合。可以认为它是compose的逆序实现
1 |
|
也可以使用reduce来实现:
1 | // pipe函数使用std::reduce |
效率不如递归方式的实现
偏函数与柯里化
实现柯里化
1 | // 辅助函数:检查是否可以调用函数F,参数类型为Args |
有了通用的柯里化函数后,就可以配合各种高阶函数,如组合使用
闭包
在C++中,闭包(closure)是一个能够捕获并存储其所在作用域中的变量的函数对象(function object)。闭包主要通过C++11引入的Lambda表达式来实现。
通过lambda表达式,C++提供了一种简洁且强大的方式来创建闭包,这使得编写高阶函数和处理回调变得更加方便。
高阶函数(Higher-Order Function)是指接受一个或多个函数作为参数,或返回一个函数作为结果的函数。在函数式编程中,高阶函数是一个核心概念,但它同样适用于其他编程范式,包括面向对象编程和过程式编程。
以下是高阶函数的一些特性和例子:
高阶函数的特性
- 接受函数作为参数:高阶函数可以接受一个或多个函数作为参数。这使得它能够对传入的函数进行操作,如在不同的上下文中调用它们。
- 返回函数作为结果:高阶函数可以返回一个函数作为结果。这样可以生成新的函数,或延迟计算。
高阶函数在很多场景下非常有用,例如:
- 回调函数:在事件驱动编程中,高阶函数可以用来注册和调用回调函数。
- 函数组合:高阶函数可以用来组合多个函数,使代码更加模块化和可重用。
- 装饰器:在Python中,装饰器是一种特殊的高阶函数,用于修改或增强另一个函数的行为。
在C++中当按引用捕获一个局部变量并在该变量的作用域之外访问它时,会导致未定义行为:
1 |
|
在这个例子中,当createLambda返回时,局部变量x已经被销毁,因此lambda捕获的引用不再有效,执行lambda会导致未定义行为。
使用智能指针解决生命周期问题
为了解决这个问题,可以使用std::shared_ptr或std::unique_ptr来管理捕获的变量,使其在lambda需要时仍然有效:
1 |
|
值得注意的是:
在JavaScript中,闭包不会产生类似于C++中按引用捕获局部变量在作用域之外执行导致未定义行为的问题。JavaScript的闭包捕获的是变量的引用,且JavaScript的垃圾回收机制会确保闭包中引用的变量在闭包存在期间不会被销毁。
如果希望在C++中实现类似JavaScript闭包的效果在使用时不需要额外的括号,我们可以通过返回一个对象来实现。这个对象可以重载赋值运算符和类型转换运算符,从而实现类似JavaScript闭包的行为。
1 |
|
C++23
C++20
std::span
统一所有连续内存的处理,搞定所有C++数组传参,无论是int[]还是vector<>,还是array





