【C++】总结10--C++11第二篇

文章目录

  • 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 实例间的循环引用。
  • 新增加容器——静态数组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

你可能感兴趣的:(C++,c++,开发语言)