文章目录
- RAII
- C++11新特性
RAII
- RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等)的简单技术,在对象的构造函数中获取资源,在对象的析构函数中释放资源,从而确保资源的正确获取和释放
- 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这样做的两大好处:
-
- 不需要显式的释放资源
- 采用这种方式,保证了对象所需的资源在其生命周期内始终保持有效
C++11新特性
-
部分新特性如列表初始化、变量类型推导(auto)、默认成员函数控制、右值引用等在这篇博文中详细介绍:【C++】C++11 第一篇_林深方见鹿的博客-CSDN博客
-
范围for循环
用于遍历容器中的元素或者数组中的元素,语法如下:
for (auto element : container) {// 循环体,使用element访问容器中的元素}
范围for循环会自动遍历容器或数组中的每一个元素,并将当前元素的值赋值给element
,然后执行循环体中的代码。循环会在容器或数组的每个元素上执行一次,直到遍历完所有元素为止。
范围for循环在遍历容器或数组时,避免了使用迭代器或下标的复杂语法,使得代码更加简洁和易读。它是C++11中非常实用和方便的特性之一。
-
final和override
-
智能指针,C++库的智能指针都定义在memory头文件中
-
-
智能指针原理:RAII特性;重载operator* 和 operator->,具有指针行为
-
unique_ptr:用unique_ptr替换已弃用的C++98的auto_ptr,auto_ptr的实现思想是管理权转移,auto_ptr弃用的原因是它非常容易造成访问空指针,引发程序崩溃,因为当对象拷贝或赋值后,前面的对象就悬空了,即原对象将失去资源的所有权,如果此时再意外访问原对象资源,就可能会导致意外的行为。例如,下面的代码可能会导致问题:
std::auto_ptr<int> ptr1(new int(42)); std::auto_ptr<int> ptr2 = ptr1; // ptr1将变为空指针 std::cout << *ptr1 << std::endl; // 未定义的行为,因为ptr1已经失去了对资源的所有权
unique_ptr的实现思想:简单粗暴的防拷贝,使用独占所有权语义,确保每个指针只能管理一个对象,并在拷贝时禁止所有权转移,从而避免了auto_ptr所产生的问题,模拟一份简单的unique_ptr:
template<class T> class UniquePtr {UniquePtr(T* ptr = nullptr) :_ptr(ptr){}~UniquePtr() {if (_ptr)delete _ptr;} private:UniquePtr(UniquePtr<T> const &) = delete;UniquePtr& operator=(UniquePtr<T> const &) = delete; private:T *_ptr; };
-
shared_ptr
shared_ptr的原理是通过引用计数的方式来实现多个share_ptr对象之间共享资源
shared_ptr在其内部,给每个资源都维护了一份计数,用来记录该份资源被几个对象共享
在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一
如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了
简单模拟share_ptr,代码如下:
template<class T> class SharePtr { public:SharePtr(T* ptr = nullptr):_ptr(ptr),_pRefCount(new int(1)),_pMutex(new mutex){}~SharePtr() {Release();}//拷贝构造SharePtr(const SharePtr<T>& sp) :_ptr(sp._ptr),_pRefCount(sp._pRefCount),_pMutex(sp._pMutex){AddRefCount();}//赋值语句SharePtr<T>& operator=(const SharePtr<T>& sp) {if (_ptr != sp._ptr) {//释放之前的管理资源Release();//共享管理新对象的资源,增加引用计数_ptr = sp._ptr;_pRefCount = sp._pRefCount;_pMutex = sp._pMutex;AddRefCount();}return *this;}T& operator*() { return *_ptr; }T* operator->() { return _ptr; }int UseCount() { return *_pRefCount; }T* Get() { return _ptr; }void AddRefCount() {//加锁_pMutex->lock();++(*_pRefCount);_pMutex->unlock();} private:void Release() {bool deleteflag = false;//释放资源时引用计数减一,如果减到0,就释放资源_pMutex->lock();if (--(*_pRefCount) == 0) {delete _ptr;delete _pRefCount;deleteflag = true;}_pMutex->unlock();//如果deleteflag为true了,则意味着_ptr、_pRefCount已经被释放了//我们就需要进行_pMutex的释放if (deleteflag == true) {delete _pMutex;}} private:T* _ptr;//指向所管理资源的指针int* _pRefCount;//引用计数mutex* _pMutex;//互斥锁 };
shared_ptr的线程安全问题:
shared_ptr的线程安全问题分为两方面,首先是引用计数是多个智能指针对象所共享的,两个线程中智能指针的引用计数同时加加或减减,这个操作不是原子的,可能会导致引用计数的错乱,最终导致资源未释放或程序崩溃的问题,所以智能指针中引用计数加加和减减是需要加锁的,也就是说引用计数的操作是线程安全的。
另一方面是智能指针管理的对象存放在堆上,两个线程同时去访问,可能会导致线程安全问题
shared_ptr的循环引用问题:
智能指针指向的空间中也保存有智能指针,且两个智能指针指向的空间中保存的智能指针还指向对方的空间,这称为循环引用。
为了解决这个问题。C++中增加了weak_ptr。可以使用
weak_ptr
来取得shared_ptr
的临时共享所有权,在引用计数的场景下,把节点中的 _prev 和 _next改成weak_ptr就可以了。weak_ptr只是单纯的赋值,不会使引用计数++,析构后也不负责释放空间。
-
-
- weak_ptr:结合
shared_ptr
使用的特例智能指针。weak_ptr
提供对一个或多个shared_ptr
实例拥有的对象的访问,但不参与引用计数。 如果你想要观察某个对象但不需要其保持活动状态,请使用该实例。 在某些情况下,需要断开shared_ptr
实例间的循环引用。
- weak_ptr:结合
-
新增加容器——静态数组array、forward_list以及unordered系列
-
lambda表达式
lambda表达式式实际是一个匿名函数
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
lambda表达式各部分说明:
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用
- (parameters):参数列表,与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)
- ->returntype:返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分 可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导
- {statement}:函数体,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量
注意: 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体不可以为空。 因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情
lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量
捕获列表说明:捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
注意:
- 父作用域指包含lambda函数的语句块
- 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量;[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
- 捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
- 在块作用域以外的lambda函数捕捉列表必须为空
- 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错
- lambda表达式之间不能相互赋值,即使看起来类型相同
函数对象与lambda表达式:
函数对象,又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的类对象
class Rate{ public:Rate(double rate) : _rate(rate){}double operator()(double money, int year){return money * _rate * year;} private:double _rate; }; int main(){// 函数对象double rate = 0.49;Rate r1(rate);r1(10000, 2);// lambda表达式auto r2 = [=](double monty, int year)->double {return monty * rate*year; };r2(10000, 2);return 0; }
从使用方式上来看,函数对象与lambda表达式完全一样,函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一 个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()
-
线程库
函数名 功能 thread() 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程 thread(fn, args1,…) 构造一个线程对象,并关联线程函数fn,args1,…为线程函数的参数 get_id() 获取线程id jionable() 线程是否还在执行,joinable代表的是一个正在执行中的线程 jion() 该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行 detach() 在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程 变为后台线程,创建的线程的"死活"就与主线程无关 注意:
-
线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态
-
当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程
-
get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中包含了一个 结构体:
typedef struct { /* thread identifier for Win32 */void *_Hnd; /* Win32 HANDLE */unsigned int _Id; } _Thrd_imp_t;
-
当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。线程函数一 般情况下可按照以下三种方式提供:函数指针、lambda表达式、函数对象
-
thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的执行
-
可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效:采用无参构造函数构造的线程对象、线程对象的状态已经转移给其他线程对象、线程已经调用jion或者detach结束
线程函数参数:
-
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。 如果想要通过形参改变外部实参时,必须借助std::ref()函数
-
注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数
join与detach:
启动了一个线程后,当这个线程结束的时候,如何去回收线程所使用的资源呢?
-
join()方式 :主线程被阻塞,当新线程终止时,join()会清理相关的线程资源,然后返回,主线程再继续向下执行,然后销毁线程对象。由于join()清理了线程的相关资源,thread对象与已销毁的线程就没有关系 了,因此一个线程对象只能使用一次join(),否则程序会崩溃。
-
detach()方式:该函数被调用后,新线程与线程对象分离,不再被线程对象所表达,就不能通过线程对象控制线程了,新线程会在后台运行,其所有权和控制权将会交给C++运行库。同时,C++运行库保证,当 线程退出时,其相关资源的能够正确的回收。
detach()函数一般在线程对象创建好之后就调用,因为如果不是jion()等待方式结束,那么线程对象可能会在新线程结束之前被销毁掉而导致程序崩溃。因为std::thread的析构函数中,如果线程的状态是 jionable,std::terminate将会被调用,而terminate()函数直接会终止程序。
线程对象销毁前,要么以jion()的方式等待线程结束,要么以detach()的方式将线程与线程对象分 离。
原子性操作库(atomic)
- C++11引入的原子操作类型,使得线程间数据的同步变得非常高效,所谓原子操作:即不可被中断的一个或一系列操作
- 在C++11中,我们不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问;可以使用atomic类模板,定义出需要的任意原子类型
- 注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了。
lock_guard与unique_lock
-
std::lock_gurad 是 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
-
与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所管理的互斥量的指针)
Mutex的种类:
- std::mutex:C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动
- std::recursive_mutex:其允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,释放互斥量时需要调用与该锁层次深度相同次数的unlock(),除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同
- std::timed_mutex 比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until()
- try_lock_for() :接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返 回false。
- try_lock_until() 接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false
-