bingc++(完美转发、线程库、特殊类、异常)
上一篇
目录标题
- 完美转发(隐含 完美折叠)
- 线程库
- 线程函数参数
- 原子性操作库
- 自定义[原子类型](https://www.cnblogs.com/ittinybird/p/4830834.html)
- lock_guard与unique_lock
- lock_guard
- unique_lock
- 特殊类
- 只能在堆上创建对象
- 只能在栈上创建对象
- 异常
- 异常的抛出和匹配原则
- setjmp和longjmp(c语言)
- c++异常
- 异常的重新抛出
- 异常安全
- 异常规范
- 自定义异常体系
完美转发(隐含 完美折叠)
所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值
void Fun(int &x)
{
cout << "lvalue ref" << endl;
}
void Fun(int &&x)
{
cout << "rvalue ref" << endl;
}
void Fun(const int &x)
{
cout << "const lvalue ref" << endl;
}
void Fun(const int &&x)
{
cout << "const rvalue ref" << endl;
}
template<typename T>
void PerfectForward(T &&t)
{
//Fun(t);
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10); // rvalue ref
int a;
PerfectForward(a); // lvalue ref
PerfectForward(std::move(a)); // rvalue ref
const int b = 8;
PerfectForward(b); // const lvalue ref
PerfectForward(std::move(b)); // const rvalue ref
return 0;
}
线程库
- 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。
- 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中包含了一个结构体
#include <thread>
int main()
{
thread t;
cout << t.get_id() << endl;
return 0;
}
typedef struct
{ /* thread identifier for Win32 */
void *_Hnd; /* Win32 HANDLE */
unsigned int _Id;
} _Thrd_imp_t;
- 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。线程函数一般情况下可按照以下三种方式提供:
- 函数指针
#include <thread>
void ThreadFunc(int n)
{
for (size_t i = 0; i < n; ++i)
{
cout << "thread!!!" << endl;
}
cout << "thread End" << endl;
}
int main()
{
thread t1(ThreadFunc, 10);
t1.join(); // 调用之后,主线程就会被阻塞,当t1对应的线程执行完成之后,main线程才继续往下执行
cout << "main thread " << endl;
return 0;
}
- 函数对象
class ThreadFunc
{
public:
void operator()(int x, int y)
{
cout << x + y << endl;
cout << "thread End!!!" << endl;
}
};
int main()
{
thread t1(ThreadFunc(), 10, 20);
t1.join(); // 调用之后,主线程就会被阻塞,当t1对应的线程执行完成之后,main线程才继续往下执行
cout << "main thread " << endl;
return 0;
}
- lambda表达式
int main()
{
thread t1([](){
for (int i = 0; i < 10; ++i)
{
cout << "thread!!!" << endl;
}
});
//t1.join(); // 调用之后,主线程就会被阻塞,当t1对应的线程执行完成之后,main线程才继续往下执行
t1.detach();
cout << "main thread " << endl;
return 0;
}
- thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。
- 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
- 采用无参构造函数构造的线程对象
- 线程对象的状态已经转移给其他线程对象
- 线程已经调用jion或者detach结束
线程函数参数
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改
后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。
测试
void ThreadFunc(int& ra)
{
ra = 100;
cout << &ra << endl;
}
int main()
{
int a = 10;
cout << &a << endl;
// 注意:线程函数参数的ra引用的不是创建线程时传递的实参a
// 创建线程时传递的a 将来被拷贝到线程私有的栈空间中了
// ra实际应用的是线程栈空间中的a的拷贝
thread t(ThreadFunc, a);
t.join();
cout << a << endl;
return 0;
}
如果想改变对象,参数传指针即可或者用下面的方法
void ThreadFunc(int& ra)
{
ra = 100;
cout << &ra << endl;
}
int main()
{
int a = 10;
cout << &a << endl;
thread t(ThreadFunc, std::ref(a));
t.join();
cout << a << endl;
return 0;
}
原子性操作库
#include <atomic>
#include <thread>
// 采用原子类型
atomic_uint sum = { 0 };
void ThreadFunc(int n)
{
for (size_t i = 0; i < n; ++i)
{
sum++;
}
}
int main()
{
thread t1(ThreadFunc, 10000000);
thread t2(ThreadFunc, 10000000);
t1.join();
t2.join();
cout << sum << endl;
return 0;
}
自定义原子类型
atmoic<T> t; // 声明一个类型为T的原子类型变量t
注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了。
#include <atomic>
int main()
{
atomic<int> a1(0);
//atomic<int> a2(a1); // 编译失败
atomic<int> a2(0);
//a2 = a1; // 编译失败
return 0; }
lock_guard与unique_lock
lock_guard
c++11中定义
template<class _Mutex>
class lock_guard
{
public:
// 在构造lock_gard时,_Mtx还没有被上锁
explicit lock_guard(_Mutex& _Mtx)
: _MyMutex(_Mtx)
{
_MyMutex.lock();
}
// 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁
lock_guard(_Mutex& _Mtx, adopt_lock_t)
: _MyMutex(_Mtx)
{}
~lock_guard() _NOEXCEPT
{
_MyMutex.unlock();
}
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
_Mutex& _MyMutex;
}
通过上述代码可以看到,lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock
使用
#include <mutex>
int number = 0;
mutex g_lock;
int ThreadProc1()
{
for (int i = 0; i < 100; i++)
{
lock_guard<mutex> Lock(g_lock);
++number;
cout << "thread 1 :" << number << endl;
}
return 0;
}
int ThreadProc2()
{
for (int i = 0; i < 100; i++)
{
lock_guard<mutex> Lock(g_lock);
--number;
cout << "thread 2 :" << number << endl;
break;
}
return 0;
}
int main()
{
thread t1(ThreadProc1);
thread t2(ThreadProc2);
t1.join();
t2.join();
cout << "number:" << number << endl;
system("pause");
return 0;}
unique_lock
与lock_gard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所有权的方式,管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。在构造(或移动(move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的 unique_lock 对象负责传入的 Mutex对象的上锁和解锁操作。使用以上类型互斥量实例化unique_lock的对象时,自动调用构造函数上锁,unique_lock对象销毁时自动调用析构函数解锁,可以很方便的防止死锁问题。 与lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:
上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock
修改操作:移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有权)、释放(release:返回它所管理的互斥量对象的指针,并释放所有权)
获取属性:owns_lock(返回当前对象是否上了锁)、operator bool()(与owns_lock()的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)。
特殊类
只能在堆上创建对象
class HeapOnly
{
public:
static HeapOnly* GetHeapOnly(int ho)
{
return new HeapOnly(ho);
}
~HeapOnly()
{
}
HeapOnly(const HeapOnly&) = delete;
HeapOnly(HeapOnly&&) = delete;
private:
HeapOnly(int ho)
: _ho(ho)
{}
private:
int _ho;
};
只能在栈上创建对象
class StackOnly
{
public:
static StackOnly CreateObject()
{
return StackOnly();
}
private:
StackOnly() {}
};
或者
class StackOnly
{
public:
StackOnly() {}
private:
void* operator new(size_t size);
void operator delete(void* p);
};
异常
异常的抛出和匹配原则
- 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。
- 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
- 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回)
- **catch(…)**可以捕获任意类型的异常,问题是不知道异常错误是什么。
- 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获,这个在实际中非常实用,我们后面会详细讲解这个。
#include <windows.h>
int main()
{
FILE* pf = fopen("2222.txt", "r");
if (nullptr == pf)
{
cout << "打开文件失败" << endl;
//1.
cout << errno << endl;
//2.
size_t errNo = GetLastError();
cout << errNo << endl;
return 0;
}
fclose(pf);
return 0;
}
setjmp和longjmp(c语言)
// setjmp 和 longjmp的使用
jmp_buf buff;
void func1()
{
FILE* pf = fopen("2222.txt", "r");
if (nullptr == pf)
{
// 此处并不对该错误进行处理
// 而是让程序执行流程跳转到专门处理错误的位置来处理该错误
// buff表示将来要跳转到的位置,在跳转之前必须将buff中要跳转到的位置设置好
// 1: 跳转之后直接赋值给setjmp左侧的变量,用来区分是那个函数中的longjmp发生了跳转
longjmp(buff, 1);
}
// ...
// 对文件进行操作
fclose(pf);
}
void func2()
{
int* p = (int*)malloc(sizeof(int)* 10);
if (nullptr == p)
{
// 此处并不对该错误进行处理
// 而是让程序执行流程跳转到专门处理错误的位置来处理该错误
longjmp(buff, 2);
}
// ...
free(p);
}
int main()
{
// setjmp(buff):设置跳转点 并且 setjmp第一次调用时返回的是0
// 将来longjmp(buff, num); 要跳转到buff标记的位置
int istate = setjmp(buff);
if (0 == istate)
{
func2();
func1();
}
else
{
// 程序遇到了错误
switch (istate)
{
case 1:
cout << "func1: 打开文件失败" << endl;
break;
case 2:
cout << "func2: malloc失败" << endl;
break;
default:
cout << "未知错误" << endl;
}
}
// 从该位置程序继续往下执行
cout << "..." << endl;
return 0;
}
c++异常
void func1()
{
FILE* pf = fopen("2222.txt", "r");
if (nullptr == pf)
{
throw 1; // throw用来抛出异常的
}
// ...
// 对文件进行操作
fclose(pf);
}
int main()
{
// 注意:对于可能会发生异常的函数调用时候,一定需要使用try-catch结构来尝试进行捕获并处理
// 否则:程序将会崩溃
// 注意:对于有可能会发生异常的代码一定要放在try中尝试进行捕获
// catch: 会根据抛出异常的类型进行捕获,捕获到之后,在catch中可以对异常进行处理
try
{
func1();
}
catch (int err)
{
cout << err << endl;
}
// 程序从此位置会继续往下执行...
cout << "继续往下执行..." << endl;
return 0;
}
异常的重新抛出
// 异常的重新抛出
void func1()
{
FILE* pf = fopen("2222.txt", "r");
if (nullptr == pf)
{
throw 1;
}
// ...
// 对文件进行操作
fclose(pf);
}
/*
func1想要将自己的异常交给main方法处理,即:func2不能捕获
但是func2不捕获又不行,因为如果不捕获,func2内部的空间就是泄漏
所以func2必须对func1抛出的异常捕获,注意:func2捕获的目的不是为了
处理该异常,而是为了释放自己内部的资源,因此func2最后还必须对func1中的
异常继续往出抛
*/
void func2()
{
int* p = new int[10];
try
{
func1();
}
catch (...)
{
delete[] p;
throw;
}
delete[] p;
}
int main()
{
try
{
func2();
}
catch (int err)
{
cout << err << endl;
}
_CrtDumpMemoryLeaks();
return 0;
}
异常安全
- 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化
- 析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
- C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题,
异常规范
- 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的后面接throw(类型),列出这个函数可能抛掷的所有异常类型。
- 函数的后面接throw(),表示函数不抛异常。
- 若无异常接口声明,则此函数可以抛掷任何类型的异常。
// 假设:func一定不会抛出异常
// 如果一个方法确定一定不会抛出异常,只需再该函数参数列表之后跟上throw()
// 万一再实现时抛出了异常,则编译报错
void func()throw()
{
// throw 1;
}
// 假和:一个方法中可能会抛出不同种类的异常,也可以对要抛出异常的类型来进行约束
// void func(int )throw(int, double, char): func只能抛出int、double、char类型的异常
void func(int)throw(int, double, char)
{
// 应该抛出
// throw 1;
// 非法抛出
throw "error";
}
自定义异常体系
// 一般情况,不会直接抛出int、double种类没有意义的异常,该种方式一般是学习时语法验证
// 在项目中,各个公司一般都会维护自己的异常体系结构,好处:捕获异常方便,而且对象可以携带更多的错误信息
#include <string>
class Excecption
{
public:
Excecption(const string& errInfo = "", size_t errId = 0)throw()
: _errInfo(errInfo)
, _errId(errId)
{}
// 纯虚函数
virtual void what()throw() = 0;
protected:
string _errInfo;
size_t _errId;
};
// 网络
class NetException : public Excecption
{
public:
NetException(const string& errInfo = "", size_t errId = 0)throw()
: Excecption(errInfo, errId)
{}
// 纯虚函数
virtual void what()throw()
{
cout << _errInfo << ":" << _errId;
}
};
// DBException
class DBException : public Excecption
{
public:
DBException(const string& errInfo = "", size_t errId = 0)throw()
: Excecption(errInfo, errId)
{}
// 纯虚函数
virtual void what()throw()
{
cout << _errInfo << ":" << _errId;
}
};
void func1()
{
// 操作数据库
DBException e("数据库连接失败", 301);
throw e;
}
void func2()
{
// 网络相关
NetException e("网络连接失败", 501);
throw e;
}
#include <exception>
#include <vector>
int main()
{
try
{
vector<int> v;
cout << v.at(100) << endl;
func1();
func2();
}
catch (Excecption& e)
{
e.what();
}
catch (exception& e)
{
e.what();
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}