- 语法:使用类(范围for, lambda表达式, 包装器, 异常等),关键字类(override, final, 类型转换等)
- 右值引用: 增加移动语义, 避免不必要的拷贝(复用将亡值)
- 智能指针: 用对象生命周期管理资源
- 多线程: 并发编程, 线程安全(锁, 条件变量, 原子变量)
- 新增容器: foward_list, arry, unordered_map, unordered_set
右值引用
左值引用和右值引用
左值和左值引用
- 左值是一个数据表达式 (可出现在赋值表达式的左边/右边) ~~>可以取地址的就是左值
- 左值引用: 给左值取别名
右值和右值引用
- 右值 : 如字面常量, 表达式的返回值 (右值不能出现在赋值表达式的左边) ~~>右值不能取地址
- 右值引用: 给右值取别名
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
左值引用与右值引用比较
- 左值引用只能引用左值,但是const左值引用都可以引用
- 右值引用只能引用右值,但是可以引用move后的左值
- 左值引用: 直接减少了拷贝 1.引用传参 2.传引用返回
- 右值引用: 间接减少了拷贝 1.解决传值返回(将将亡值的资源转移即移动拷贝)
注意
-
引用的本质是指针, 右值也需要空间存储 (它怎么移动构造? 交换指针)
-
右值引用本身是左值, 为了实现移动构造语义, 才能转移资源
-
所以左值引用能够引用右值引用(本质还是左值)
-
右值引用的应用场景
对于深拷贝的自定义类型的将亡值进行移动构造
1.传值返回的优化. 用一个函数的返回值构造对象(右值直接移动, 左值需要拷贝)
2. 深拷贝的类,做参数
-
容器的push, 若是左值需要拷贝, 若是右值直接移动构成
push和emplace的区别
-
push是函数, emplace是参数包
-
场景:多参数构造对象---push先构造对象再push, emplace直接构造
完美转发
- 万能引用:使用模板,使其可以接收左值,右值
- 完美转发:std::forward,在传参得过程中,保留对象原生属性
- 为什么会属性丢失? 因为右值引用要被识别为左值, 才能被移动构造, 若存在继续往下传的情况, 右值会丢失
-
解决: 使用forward 接收参数,在传参得过程中,保留对象原生属
万能引用T&&(模板参数)
看起来是右值, 但由于模板的原因是万能引用(都可以传)
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
智能指针
多线程
线程函数
#include <thread> //头文件
void Function() {cout << "Hello from thread!\n";} //执行函数
int main() {
thread t1(Function); //创建线程并执行相关函数, 可以给其传参数
t1.join(); //等待线程
return 0;
}
- thread(): 创建一个线程对象, 无关联函数
- thread(fn,args1, args2,...): 创建一个线程对象, 并执行相关函数, args是传递的参数
- join(): 等待线程,阻塞式
- joinable() : 判断线程是否还在执行
- deatch() : 若不关心返回值, 调用后线程执行完毕后,自动回收
线程安全函数
1.锁
- mutex
- recursive_mutex: 允许同一个线程对互斥量多次上锁
- timed_mutex : 支持一段时间内阻塞式获取锁(增加: try_lock_for(), try_lock_until())
相关函数
- lock(): 锁住互斥量(阻塞)
- unlock(): 释放对互斥量的所有权
- try_lock(): 尝试锁住互斥量(非阻塞)
lockguard(RAII思想)
- lock_guard(_Mutex& _Mtx)
- 构造函数获取锁, 析构函数释放锁(比较单一)
unique_lock(RAII思想)
- 相比于lockguard, 提供了更多的成员函数
- 上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock
- 修改操作:移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有
权)、释放(release:返回它所管理的互斥量对象的指针,并释放所有权) - 获取属性:owns_lock(返回当前对象是否上了锁)、operator bool()(与owns_lock()的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)。
2.条件变量
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_id(int id) {
std::unique_lock<std::mutex> lck(mtx);
while (!ready) { // 循环等待,防止虚假唤醒
cv.wait(lck);
}
// 唤醒后继续执行
std::cout << "Thread " << id << '\n';
}
void go() {
std::unique_lock<std::mutex> lck(mtx);
ready = true;
cv.notify_all(); // 唤醒所有等待的线程
}
int main() {
std::thread threads[10];
// 启动10个线程
for (int i = 0; i < 10; ++i) {
threads[i] = std::thread(print_id, i);
}
std::cout << "10 threads ready to race...\n";
go(); // 设置 ready 为 true 并通知所有线程
// 等待所有线程完成
for (auto& th : threads) {
th.join();
}
return 0;
}
3.原子性操作库(atomic)
使用atomic类模板,定义出需要的任意原子类型(线程安全).
atmoic<T> t; // 声明一个类型为T的原子类型变量t
原子类型数据
- 是线程安全
- 属于"资源型数据", 禁止拷贝,赋值,移动构造,移动赋值
- 原子性理解: CAS操作即 一种比较后数据若无改变则交换数据的一种无锁操作(乐观锁)
补充:线程函数参数
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的.
注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数
#include <thread>
void ThreadFunc1(int& x){x += 10;}
void ThreadFunc2(int* x){*x += 10;}
int main()
{
// 在线程函数中对a修改,不会影响外部实参
// 原因:线程函数参数虽然是引用方式,但其实际引用的是线程栈中的拷贝
int a = 10;
thread t1(ThreadFunc1, a);
t1.join();
cout << a << endl;
// 如果想要通过形参改变外部实参时,必须借助std::ref()函数
thread t2(ThreadFunc1, std::ref(a));
t2.join();
cout << a << endl;
// 地址的拷贝
thread t3(ThreadFunc2, &a);
t3.join();
cout << a << endl;
return 0;
}
语法
lambda表达式
语法: lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement}
- [capture-list] : 捕捉列表,默认捕捉的是外面对象的拷贝. 该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- (parameters): 参数列表. 与普通函数的参数列表一致,如果不需要参数传递,则可省略
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)
- ->returntype:返回值类型. 可省略: 无返回值 , 返回值类型明确时(编译器推导返回类型)
- {statement}:函数体.
lambda表达式的使用
1.传值捕捉
int x = 2, y = 3;
auto func = [x,y]() mutable {//...};
2.传引用捕捉
int x = 2, y = 3;
auto func = [&x,&y](){//...};
3.混合捕捉
auto func = [&x,y]() {//...};
4.全部引用捕捉
auto func = [&]() {};
5.全部传值捕捉
auto func = [=]() {};
6.全部引用捕捉, x传值捕捉
auto func1 = [&,x]() {};
lambda表达式原理
- 本质: 是一个匿名类, 重载了(), 捕获列表式中的内容实际就是该类的成员变量
- lambda的大小是1, lambda是生成仿函数的对象的类型, 并且这个仿函数是一个空类
lamba会被编译器处理成仿函数, 编译器会生成一个仿函数的类
lamba就相当于是仿函数, 它的所有参数都会作为哪个仿函数类的参数
注意
- 针对每个lambda生成的类名不一样(属于不同的类)~~>不能互相赋值
- lambda表达式实际上可以理解为无名函数,该函数无法直接调用. 但可以使用auto
- 在块作用域以外的lambda函数捕捉列表必须为空
包装器
1.function包装器: 是个类模板, 可以对函数指针, 仿函数, lamba表达式进行包装, 提供统一的类型
std::function在头文件<functional>
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参
使用
function<int(int, int)> f1 = f;
function<int(int, int)> f2 = Functor();
function<int(int, int)> f3 = [](int a, int b) {return a + b; };
map<string, function<int(int, int)>> FuncMap;
FuncMap["函数指针"] = f;
FuncMap["仿函数"] = Functor();
FuncMap["lamba表达式"] = [](int a, int b) {return a + b; };
cout << FuncMap["lamba表达式"](3, 4) << endl;
成员函数的包装
- 非静态的成员函数, 需要加上& (其参数列表有this指针)
- 调用的时候, 加一个对象(一般使用匿名对象)
class Plus
{
public:
Plus(int rate = 2) :_rate(rate) {}
static int plusi(int a, int b) { return a + b; }//静态成员函数
double plusd(int a, int b) { return (a + b) * _rate; }//非静态成员函数
private:
int _rate = 2;
};
int main()
{
function<int(int, int)> f1 = Plus::plusi;//静态成员函数
function<double(Plus,int, int)> f2 = &Plus::plusd;//非静态成员函数
f1(3, 4);
f2(Plus(), 3, 4);
return 0;
}
2.bind包装器:是一个函数模板, 可以调整参数 ~~> 参数的顺序,个数
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
调整参数的顺序,个数
void Print(int a, int b){ cout << a <<" "<< b << endl;}
int main(){
auto f1 = bind(Print, placeholders::_2, placeholders::_1);
auto f2 = bind(Print, 1,2);
f1(10, 20); //打印20 10
f2(); //打印1 2
return 0;
}
绑定成员函数
class Sub {
public:
Sub(int x = 3) {}
int func(int a,int b){return a - b;}
};
int main()
{
Sub s;
function<int(Sub, int, int)> f1 = &Sub::func;
cout << f1(Sub(), 4, 3)<< endl;
function<int(int, int)> f2 = bind(&Sub::func, s, placeholders::_1, placeholders::_2);
cout << f2(4,3)<< endl;
function<int(Sub, int)> f3 = bind(&Sub::func, placeholders::_1, 100, placeholders::_2);
cout << f3(s, 4) << endl;
return 0;
}
类型转换
1. 静态转换(static_cast)
- 主要用于常见的隐式转换,比如将较小的整数类型转换为较大的整数类型、基类指针转换为派生类指针等。
- 可以用于转换没有相关性的指针类型。
- 在转换指针类型时,如果类型不相关,编译器可能无法进行类型检查。
2. 动态转换(dynamic_cast)
- 主要用于在继承关系中进行安全的向下转型(一般是指向子类的父类指针转换为子类指针)
- 只能用于含有虚函数的类层次结构中,因为动态转换会在运行时进行类型检查。
- 如果指针不能转换为目标类型,则返回nullptr(对指针)或抛出std::bad_cast异常(对引用)
3. 常量转换(const_cast)
- 主要用于去除变量的const属性或volatile属性,以便进行修改。
4. 重新解释转换(reinterpret_cast)
- 主要用于低级别的类型转换,将一个指针转换为另一种类型的指针,或将任意类型转换为void指针。
- reinterpret_cast通常不进行类型检查,因此可能会导致不安全的行为。
异常
概念
异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的
直接或间接的调用者处理这个错误
- throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
- catch: 在您想要处理问题的地方,通过异常处理程序捕获异常.catch 关键字用于捕获异常,可以有多个catch进行捕获。
- try: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。
如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛
出异常的代码,try 块中的代码被称为保护代码。使用 try/catch 语句的语法如下所示:
try
{
// 保护的标识代码
}catch( ExceptionName e1 )
{
// catch 块
}catch( ExceptionName e2 )
{
// catch 块
}catch( ExceptionName eN )
{
// catch 块
}
异常的使用
2.1异常的抛出和捕获
异常的抛出和匹配原则:
- 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。
- 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
- 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回)
- catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么。
- 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获
在函数调用链中异常栈展开匹配原则:
- 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则调到catch的地方进行处理。
- 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
- 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(...)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
- 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。
double Division(int a, int b)
{
if (b == 0)
throw "Division by zero condition!";
else
return ((double)a / (double)b);
}
void Func()
{
int x, y;
cin >> x >> y;
cout << Division(x, y) << endl;
}
int main()
{
try
{
Func();
}
catch (const char* str)
{
cout << str << endl;
}
catch (...)
{
cout << "未知错误" << endl;
}
return 0;
}
2.2异常的重新抛出
有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用
链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Division by zero condition!";
}
return (double)a / (double)b;
}
void Func()
{
// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。
// 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再
// 重新抛出去。
int* array = new int[10];
try {
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
catch (...)
{
cout << "delete []" << array << endl;
delete[] array;
throw;
}
// ...
cout << "delete []" << array << endl;
delete[] array;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
return 0;
}
2.3异常安全
- 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化
- 析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
- C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题
自定义异常体系
抛出的都是继承的派生类对象,捕获一个基类就可以了
class Exception
{
public:
Exception(int errid,const string& errmsg)
:_errid(errid)
,_errmsg(errmsg)
{}
int GetMid() const
{
return _errid;
}
const string& GetMsg() const
{
return _errmsg;
}
private:
int _errid;
string _errmsg;
};
double Division(int a , int b)
{
if (b == 0)
throw Exception(1, "除0错误");
else
return ((double)a / (double)b);
}
void Func()
{
int x, y;
cin >> x >> y;
cout << Division(x,y)<<endl;
}
int main()
{
try
{
Func();
}
catch (const Exception& e)
{
cout << e.GetMsg() << endl;
}
catch(...)
{
cout << "未知错误" << endl;
}
return 0;
}
异常的优缺点
异常的优点
- 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug
- 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那
么我们得层层返回错误,最外层才能拿到错误 - 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如
T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回
值表示错误
异常的缺点
- 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。
- 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。
- C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高。
- C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。
- 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:一、抛出异常类型都继承自一个基类。二、函数是否抛异常、抛什么异常,都使用 func() throw();的方式规范化