C++笔记

构造函数

构造函数的初始化顺序即成员定义顺序。
拷贝构造函数,又称复制构造函数。由编译器调用来完成一些基于同一类的其他对象的构建及初始化。

class 类名{
  类名(const 类名 &对象名)
};
  • 其形参必须是引用。
  • 通常加const限制,但不是必须的。
  • 如果在类中没有显式的声明上面一个拷贝构造函数,那么编译器会自动生成一个来进行对象之间,非static成员的位拷贝。

this指针

一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。

this指针相当于是类的一个自动生成、自动隐藏的私有成员,它存在于类的非静态成员函数中,指向被调用函数所在的对象。全局仅有一个this指针,当一个对象被创建时,this指针就存放指向对象数据的首地址。

奇异递归模板模式-多态

奇异递归模板模式(curiously recurring template pattern,CRTP)来实现多态,把派生类作为基类的模板参数,避免虚函数庞大的开销影响性能。

一般形式

// The Curiously Recurring Template Pattern (CRTP)
template<class T>
class Base
{
    // methods within Base can use template to access members of Derived
};
class Derived : public Base<Derived>
{
    // ...
};

静态多态

C++语言的多态,原本是用虚函数来实现的,属于动态多态。奇异递归模板模式称之为静态多态(static polymorphism)。

template <class T> 
struct Base
{
    void interface()
    {
        // ...
        static_cast<T*>(this)->implementation();
        // ...
    }

    static void static_func()
    {
        // ...
        T::static_sub_func();
        // ...
    }
};

struct Derived : Base<Derived>
{
    void implementation();
    static void static_sub_func();
};

CRTP利用了基类模板的成员函数体(即成员函数的实现)在声明之后不会被快速实例化,实际上只有被调用的模板类的成员函数才会被实例化,并在基类成员函数实现中利用了派生类的成员函数(通过类型转化)。

在上例中,Base::interface(),虽然是在struct Derived之前就被声明了,但未被编译器实例化直至它被实际调用,这发生于Derived声明之后,此时Derived::implementation()的声明是已知的。

这种技术获得了类似于虚函数的效果,并避免了动态多态的代价。也有人把CRTP称为“模拟的动态绑定”。

不通过虚函数机制,基类访问派生类的私有或保护成员,需要把基类声明为派生类的友元(friend)。如果一个类有多个基类都出现这种需求,声明多个基类都是友元会很麻烦。一种解决技巧是在派生类之上再派生一个accessor类,显然accessor类有权访问派生类的保护函数;如果基类有权访问accessor类,就可以间接调用派生类的保护成员了。

template<class DerivedT> class Base
{
  private:
    struct accessor : DerivedT  
    {                                      // accessor类没有数据成员,只有一些静态成员函数
        static int foo(DerivedT& derived)
        {
            int (DerivedT::*fn)() = &DeriveT::do_foo; //获取DerivedT::do_foo的成员函数指针  
            return (derived.*fn)();        // 通过成员函数指针的函数调用
        }
    };                                     // accessor类仅是Base类的成员类型,而没有实例化为Base类的数据成员。
  public:
    DerivedT& derived()                    // 该成员函数返回派生类的实例的引用
    {
       return static_cast<DerivedT&>(*this);
    }
    int foo()
   {                                       //  该函数具体实现了业务功能
        return accessor::foo( this->derived());
    }
};
 
struct Derived : Base<Derived>             //  派生类不需要任何特别的友元声明
  protected:
    int do_foo() 
    {
         // ... 具体实现 
         return 1; 
     }
};

函数

函数封装模板

std::function是C++中用于封装函数或函数对象的通用多态函数包装器。主要的功能和用法如下:

定义函数指针包装器

std::function<int(int,int)> func;

封装普通函数

int add(int a, int b) {
  return a + b;
}
func = add;

封装lambda表达式

func = [](int a, int b) {
  return a + b; 
};

封装函数对象

struct Adder {
  int operator()(int a, int b) {
    return a + b;
  }
}
Adder adder;
func = adder; // 隐式转换

封装成员函数

class MyClass {
public:
  int add(int a, int b) {...} 
};

MyClass obj;
func = std::bind(&MyClass::add, &obj, 
                  std::placeholders::_1, std::placeholders::_2);

std::function提供类型安全的函数封装,可以持有任意可调用对象,拥有函数指针的便利性和函数对象的灵活性。

std::function的设计动机:

  • C++需要一个通用的函数封装器,像函数指针那样简单易用,又像函数对象那样灵活多态
  • 可以封装任意可调用对象 - 普通函数、lambda表达式、函数对象、成员函数等
  • 提供类型安全的函数调用,编译时检查参数和返回值类型
  • 作为函数对象的通用抽象,支持传递和存储函数

std::function的实现机制:

  • 使用类型擦除技术,通过void指针存储任意函数对象
  • 用函数签名的类型traits提取调用签名信息
  • 通过函数指针进行实际调用,进行参数检查和转换
  • 存储小对象 Optimization,避免堆内存分配
  • 支持移动语义,避免复制开销

std::function的主要用法:

  • 定义函数对象包装器,指定函数签名
  • 封装并存储各种可调用对象,如函数、lambda、函数对象等
  • 作为回调函数参数,在需要函数对象的场景下使用
  • 通过bind绑定成员函数使用
  • 支持函数对象的作用域生命周期和多态
  • 避免直接使用函数指针带来的类型安全问题

所以std::function通过类型擦除和 Optimization 提供了一个既通用又高效的 C++ 函数封装器,很好地统一了函数指针和函数对象的特性。

匿名函数

在 C++ 11 和更高版本中,Lambda 表达式(通常称为 Lambda)是一种在被调用的位置或作为参数传递给函数的位置定义匿名函数对象(闭包)的简便方法。lambda表达式与任何函数类似,具有返回类型、参数列表和函数体。与函数不同的是,lambda能定义在函数内部。lambda表达式具有如下形式

[ capture list ] (parameter list) mutable(可选) 异常属性-> return type { function body }
  • capture list,捕获列表,局部变量对于lambda函数体是不可见的,需要通过捕获的方式获得。捕获只针对于lambda函数的作用域内可见的非静态局部变量。 lambda表达式可以直接使用静态变量,而不需要被捕获。lambda函数可以无条件访问全局变量、作用域内的静态变量。捕获可以分为按值捕获和按引用捕获。可以使用默认捕获模式来指示如何捕获 Lambda 体中引用的任何外部变量:[&] 表示通过引用捕获引用的所有变量,而 [=] 表示通过值捕获它们。 可以使用默认捕获模式,然后为特定变量显式指定相反的模式。使用默认捕获时,只有 Lambda 体中提及的变量才会被捕获。

    • []:默认不捕获任何变量;
    • [=]:默认以值捕获所有变量;
    • [&]:默认以引用捕获所有变量;
    • [x]:仅以值捕获x,其它变量不捕获;
    • [&x]:仅以引用捕获x,其它变量不捕获;
    • [=, &x]:默认以值捕获所有变量,但是x是例外,通过引用捕获;
    • [&, x]:默认以引用捕获所有变量,但是x是例外,通过值捕获;
    • [this]:通过引用捕获当前对象(其实是复制指针);
    • [*this]:通过传值方式捕获当前对象
  • parameter list,参数列表。从C++14开始,支持默认参数,并且参数列表中如果使用auto的话,该lambda称为泛化lambda(generic lambda);

  • return type,返回类型,这里使用了返回值类型尾序语法(trailing return type synax)。可以省略,这种情况下根据lambda函数体中的return语句推断出返回类型,就像普通函数使用decltype(auto)推导返回值类型一样;如果函数体中没有return,则返回类型为void。

  • function body,与任何普通函数一样,表示函数体

lambda表达式提供了两种默认捕获模式:按引用(&)和按值(=)。
默认按引用捕获会隐式的捕获所有局部变量的引用,容易导致访问悬空引用。相比之下,显式的写出需要捕获的变量可以更容易的检查对象生命周期,减小犯错可能。
默认按值捕获会隐式的捕获this指针,实际等同于按引用捕获了成员变量。如果存在静态变量,还会让阅读者误以为lambda复制了一份静态变量。从C++20开始,通过[=]默认捕获this将变为deprecated的。所以,当lambda表达式中使用了类成员变量或静态变量时,不宜使用按值默认捕获模式。
因此,通常应当明确写出lambda需要捕获的变量,而不是使用默认捕获模式。
【反例】

auto Fun()
{
    int addend = 0;
    static int baseValue = 0;
    return [=]() {                                 // 实际上只复制了addend
        ++baseValue;                               // 修改会影响静态变量的值
        return baseValue + addend;
    };
}

【正例】

auto Fun()
{
    int addend = 0;
    static int baseValue = 0;
    return [addend, value = baseValue]() mutable { // 使用C++14的捕获初始化一个变量
        ++value;                                   // 不会影响Fun函数中的静态变量
        return value + addend;
    };
}

函数返回引用

通过使用引用来替代指针,会使 C++ 程序更容易阅读和维护。C++ 函数可以返回一个引用,方式与返回一个指针类似。

当函数返回一个引用时,则返回一个指向返回值的隐式指针。这样,函数就可以放在赋值语句的左边
返回指向函数调用前就已经存在的对象的引用是正确的。当不希望返回的对象被修改时,则返回const引用。

返回const引用

由于返回值直接指向了一个生命期尚未结束的变量,因此,对于函数返回值(或者称为函数结果)本身的任何操作,都在实际上,是对那个变量的操作,这就是引入const类型的返回的意义。当使用了const关键字后,即意味着函数的返回值不能立即得到修改!如下代码,将无法编译通过,这就是因为返回值立即进行了++操作(相当于对变量z进行了++操作),而这对于该函数而言,是不允许的。如果去掉const,再行编译,则可以获得通过。

const int& plus(int a, int b, int& res){
   res = a + b;
   return res;
}
int main(){
   int a=1,b=2,c;
   plus(a, b, c)++;//wrong: returning a const reference
   return 0;
}

返回局部变量的引用

千万不要返回局部对象的引用。当函数执行完毕时,将释放分配给局部对象的存储空间。此时,对局部对象的引用就会指向不确定的内存。

const int& plus(int a, int b){
	int res = a + b;
	return res;//wrong:returning reference to a local object
}

联合体

联合体内部只能是结构体(struct)和普通变量类型(如int等),然后以其中一个占内存最大的变量的大小S就是该union所占内存的大小。意味着union联合体内所有变量共享这块内存S。

union U {
    unsigned short int a;
    struct {
        unsigned int b : 7;//(bit 0-6)
        unsigned int c : 6;//(bit 7-12)
        unsigned int d : 3;//(bit 13-15)
    }; 
}u;

联合U包含两个成员,一个是unsigned short int类型的变量,其大小为2个字节;另一个是一个自定义结构,该自定义结构中包含了3个unsigned int类型的变量。需要注意的是,每个unsigned int类型的变量的大小并不是默认的4个字节,而是通过冒号操作符指定了其大小,该大小的单位是比特。所以,联合u的大小是2个字节。
对联合体u进行赋值:

u.a=0;
//or
u.b=1;
u.c=2;
u.d=3;

联合可以为其成员指定public、protected和private等访问权限,默认情况下,其成员的访问权限为public。

智能指针

共享指针强制转换运算符允许将其中包含的指针强制转换为其他类型指针;

只能使用智能指针特定的强制转换运算符:

  • static_pointer_cast
  • dynamic_pointer_cast
  • const_pointer_cast
share_ptr<void> point(new int(1)); //共享指针内部保存void型指针
share_ptr<int> point(static_cast<int *>(point.get())); //compile error,undefined pointer
static_pointer_cast<int *>(point);    // OK

判断数据类型

typeid(T).name();

decltype是一个c++关键字,用于在编译时确定表达式的类型。它用于声明与另一个变量或表达式具有相同类型的变量,而不实际初始化变量。

decltype(expression)

这里expression 可以是任何有效的C++表达式,而 decltype(expression) 就是表达式的类型.

int x = 5;
decltype(x) y; // y is declared as an int variable

STL

算法

排序算法

//只针对可以随机访问的容器,例如vector、array等。
sort(begin,end,compare);
//而对于不可随机访问的容器,例如list,需要使用专用类成员函数的sort函数
list.sort(compare);

优先级队列

#include<queue>

priority_queue<int> q;//默认是大顶堆,队首是最大元素
priority_queue<int, vector<int>, less<int> > q;//第二个参数vector<>是用来承载底层数据结构堆(heap)的容器;
priority_queue<double, vector<double>, greater<double> > q;//第三个参数less<>或者greater<>是对第一个参数的比较类
//less<int>表示数字越大优先级越大,greater<int>表示数字越小,优先级越大。
q.top();
q.push();
q.pop();
q.size();
q.empty();

哈希表

自定义键值哈希表

#include <unordered_map>
#define HASH_P 116101
#define MAX_N 10000000000
class VOXEL_LOC{
 public:
    int64_t x, y, z;
    VOXEL_LOC(int64_t vx=0, int64_t vy=0, int64_t vz=0):x(vx), y(vy), z(vz){}
    bool operator==(const VOXEL_LOC &other) const {
        return (x==other.x && y==other.y && z==other.z);
    }
};
// Hash Value
namespace std{
    template<> struct hash<VOXEL_LOC> {
        int64_t operator()(const VOXEL_LOC &s)const{
            return (( ((s.z) * HASH_P) % MAX_N + (s.y)) * HASH_P) % MAX_N + (s.x);
        }        
    };
};

移动语义与完美转发

左值(lvalue)、右值(rvalue)、左值引用(lvalue reference)和右值引用(rvalue reference)

左值和右值

凡是真正的存在内存当中,而不是寄存器当中的值就是左值,其余的都是右值。其实更通俗一点的说法就是:凡是取地址(&)操作可以成功的都是左值,其余都是右值。

最常见的右值就是没有变量名的临时变量:

ClassName();

左值引用和右值引用

对于左值的引用就是左值引用,而对于右值的引用就是右值引用

class Test {
    int * arr{nullptr};
public:
    Test():arr(new int[5000]{1,2,3,4}) { 
    	cout << "default constructor" << endl;
    }
    Test(const Test & t) {//拷贝构造函数,需要数据拷贝操作
        cout << "copy constructor" << endl;
        if (arr == nullptr) arr = new int[5000];
        memcpy(arr, t.arr, 5000*sizeof(int));
    }
    Test(Test && t): arr(t.arr) {//移动构造函数,无需拷贝
        cout << "move constructor" << endl;
        t.arr = nullptr;
    }
    ~Test(){
        cout << "destructor" << endl;
        delete [] arr;
    }
};
template <typename T>
void func(T t) {
    cout << "in func" << endl;
}

拷贝构造函数

int main() {
	Test reusable;//调用默认构造函数
	Test t(reusable);//调用拷贝构造函数
}

输出结果:

default constructor
copy constructor
destructor
destructor

调用拷贝构造函数,进行大量的拷贝草坪做

移动构造函数

Test createTest() {
    return Test();//编译器
}
int main() {
    Test t(createTest());
}

输出结果:

default constructor
move constructor
destructor
move constructor
destructor
destructor

实例在createTest()函数中被使用默认构造函数(default constructor)构造一次之后,调用的全部都是移动构造函数,因为我们发现其实所有的这些值都是右值。这极大地节省了开支。

移动语义

完美转发

场景一:无法完美转发

template <typename T>
void relay(T&& t) {//虽然接收的是右值参数,但是进入函数之后就拥有了名字t,
    cout << "in relay" << endl;
    func(t);//这样调用函数无法实现完美转发
}
 
int main() {
    relay(Test());
}

运行结果为:

default constructor
in relay
copy constructor
in func
destructor
destructor

在relay当中转发的时候,调用了复制构造函数,也就是说编译器认为这个参数t并不是一个右值,而是左值。因为它有一个名字。区别左值和右值的唯一方法就是其定义,即能否取到地址。在这里,我们明显可以对t进行取地址操作,所以它是一个左值。也就是说,但凡有名字的“右值”,其实都是左值。
那么如果我们想要实现我们所说的,如果传进来的参数是一个左值,则将它作为左值转发给下一个函数;如果它是右值,则将其作为右值转发给下一个函数,我们应该怎么做呢?

场景二:实现完美转发

这时,我们需要std::forward<T>()std::forward<T>()std::move()相区别的是,move()会无条件的将一个参数转换成右值,而forward()则会保留参数的左右值类型。所以我们的代码应该是这样:

template <typename T>
void func(T t) {
    cout << "in func " << endl;
}
 
template <typename T>
void relay(T&& t) {
    cout << "in relay " << endl;
    func(std::forward<T>(t));
}

现在,运行结果就变为

default constructor
in relay
move constructor
in func
destructor
destructor

通用引用(universal reference)

现在一定有同学感到奇怪了,既然我刚才讲的完美转发就是怎么传进来怎么传给别人,那么也就是说在后面这个例子当中我们传进来的这个参数t竟然是一个左值!可是我们的参数表里不是写着T&&,要求接受一个右值吗?其实不是这样的。这里就牵扯到一个新的概念,叫做通用引用。

通用引用(universal reference)是Scott Meyers在C++ and Beyond 2012演讲中自创的一个词,用来特指一种引用的类型。构成通用引用有两个条件:

  1. 必须满足T&&这种形式
  2. 类型T必须是通过推断得到的

所以,在我们完美转发这个部分的例子当中,我们所使用的这种引用,其实是通用引用,而不是所谓的单纯的右值引用。因为我们的函数是模板函数,T的类型是推断出来的,而不是指定的。那么相应的,如果有一段这样的代码:

template <typename T>
class TestClass {
	public:
		void func(T&& t) {} //这个T&&是不是一个通用引用呢
}

上面的这个T是不是通用引用呢?答案是不是。因为当这个类初始化的时候这个T就已经被确定了,不需要推断。

所以,可以构成通用引用的有如下几种可能:

  1. 函数模板参数(function template parameters)
     template <typename T>
     void f(T&& param);
    
  2. auto声明(auto declaration)
     auto && var = ...;
    
  3. typedef声明(typedef declaration)
  4. decltype声明(decltype declaration)

多线程

C++11 this_thread

get_id()

调用命名空间 std::this_thread 中的 get_id() 方法可以得到当前线程的线程 ID,函数原型如下:

std::this_thread::id get_id() noexcept;

sleep_for()

为了能够实现并发处理,多个线程都是分时复用CPU时间片,快速的交替处理各个线程中的任务。因此多个线程之间需要争抢CPU时间片,抢到了就执行,抢不到则无法执行
(因为默认所有的线程优先级都相同,内核也会从中调度,不会出现某个线程永远抢不到 CPU 时间片的情况)。
命名空间 this_thread 中提供了一个休眠函数 sleep_for(),调用这个函数的线程会马上从运行态变成阻塞态并在这种状态下休眠一定的时长,因为阻塞态的线程已经让出了 CPU 资源,代码也不会被执行,所以线程休眠过程中对 CPU 来说没有任何负担。这个函数是函数原型如下,参数需要指定一个休眠时长,是一个时间段.

template <class Rep, class Period>
  void sleep_for (const chrono::duration<Rep,Period>& rel_time);
std::this_thread::sleep_for(std::chrono::nanoseconds(1));//阻塞1ns
std::this_thread::sleep_for(std::chrono::microseconds(1));//阻塞1us
std::this_thread::sleep_for(std::chrono::milliseconds(1));//阻塞1ms
std::this_thread::sleep_for(std::chrono::seconds(1));//阻塞1s
std::this_thread::sleep_for(std::chrono::minutes(1));//阻塞1min
std::this_thread::sleep_for(std::chrono::hours(1));//阻塞1h

sleep_until()

命名空间 this_thread 中提供了另一个休眠函数 sleep_until(),和 sleep_for() 不同的是它的参数类型不一样.

template <class Clock, class Duration>
  void sleep_until (const chrono::time_point<Clock,Duration>& abs_time);
  • sleep_until():指定线程阻塞到某一个指定的时间点 time_point类型,之后解除阻塞
  • sleep_for():指定线程阻塞一定的时间长度 duration 类型,之后解除阻塞
// 获取当前系统时间点
auto now = chrono::system_clock::now();
// 时间间隔为2s
chrono::seconds sec(2);
// 当前时间点之后休眠两秒 **时间点加时间段还是时间点**
this_thread::sleep_until(now + sec);

yield()

线程调用了 yield () 之后会主动放弃 CPU 资源,从运行态变为就绪态,就绪态的线程会马上参与到下一轮 CPU 的抢夺战中,不排除它能继续抢到 CPU 时间片的情况,这是概率问题。

  • std::this_thread::yield() 是让当前线程让渡出自己的CPU时间片,给其他线程使用
  • std::this_thread::sleep_for() 是让当前休眠”指定的一段”时间.

和sleep_for的区别是睡眠必然等到指定时间后才重新调度运行,但yield只在其他人有运行需求的情况下才出让。

参考链接

  • mutex 独占的互斥量,不能递归使用 C++11
  • timed_mutex 有超时功能的独占互斥量,不能递归使用 C++11
  • recursive_mutex 递归互斥量,能递归使用 C++11
  • recursive_timed_mutex 有超时功能的递归互斥量 C++11
  • shared_timed_mutex 具有超时机制的可共享互斥量 C++14
  • shared_mutex 可共享的互斥量 C++17

互斥锁(std::mutex)

std::mutex 是C++11 中最基本的互斥量,std::mutex 对象提供了独占所有权的特性——即不支持递归地对 std::mutex 对象上锁,而 std::recursive_lock 则可以递归地对互斥量对象上锁。

  • 构造函数,std::mutex不允许拷贝构造,也不允许移动构造,最初产生的 mutex 对象是处于 unlocked状态的。
  • lock(),调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:
    1. 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。
    2. 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
    3. 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
  • unlock(), 解锁,释放对互斥量的所有权。
  • try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况,
    1. 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用unlock释放互斥量。
    2. 如果当前互斥量被其他线程锁住,则当前调用线程返回false,而并不会被阻塞掉。
    3. 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
std::time_mutex

std::time_mutex 比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until()。

  • try_lock_for 函数接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

  • try_lock_until 函数则接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

递归锁(std::recursive_mutex)

std::recursive_mutex 与 std::mutex 一样,也是一种可以被上锁的对象,但是和 std::mutex 不同的是,std::recursive_mutex 允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,std::recursive_mutex 释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),可理解为 lock() 次数和 unlock() 次数相同,除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。

读写锁(std::shared_mutex)

读写锁相比互斥锁,读写锁允许更高的并行性,互斥量要么锁住要么不加锁,而且一次只有一个线程可以加锁。
读写锁可以有三种状态:

  • 读模式加锁状态;
  • 写模式加锁状态;
  • 不加锁状态;

读写锁也叫做“共享-独占锁”,当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。

  1. 若一个线程已经通过locktry_lock获取独占锁(写锁),则无其他线程能获取该锁(包括共享的)。尝试获得读锁的线程也会被阻塞。
  2. 仅当任何线程均未获取独占性锁时,共享锁(读锁)才能被多个线程获取(通过 lock_sharedtry_lock_shared )。
  3. 在一个线程内,同一时刻只能获取一个锁(共享或独占)。
  • 排他性锁定
    • lock : 锁定独占互斥量,若无法获得独占互斥量(有线程持有独占互斥量或共享互斥量)则阻塞
    • try_lock : 尝试锁定独占互斥量,若无法获得独占互斥量(有线程持有独占互斥量或共享互斥量)则返回
    • unlock : 解锁独占互斥量,释放对独占互斥量的所有权
  • 共享锁定
    • lock_shared : 锁定共享互斥量,若独占互斥量被锁定则阻塞
    • try_lock_shared : 尝试锁定共享互斥量,若独占互斥量被锁定则阻塞
    • unlock_shared : 解锁共享互斥量

shared_mutex常用于日志系统,多线程读,单线程写。

对于shared_mutex的独占互斥量所有权的获得可以使用std::unique_lock(shared_mutex),对于共享互斥量所有权的获取可以使用std::shared_lock(shared_mutex)

Scoped Locking

Scoped Locking 是将RAII手法应用于locking的并发编程技巧。其具体做法就是在构造时获得锁,在析构时释放锁,目前Scoped Locking技巧在C++中有以下4种实现:

  • std::lock_guard (c++11): 单个std::mutex(或std::shared_mutex)
  • std::unique_lock (c++11): 单个std::mutex(或std::shared_mutex), 用法比std::lock_guard更灵活
  • std::shared_lock (c++14): 单个std::shared_mutex
  • std::scoped_lock (c++17): 多个std::mutex(或std::shared_mutex)

RAII(Resource Acquisition Is Initialization)是由c++之父Bjarne Stroustrup提出的,中文翻译为资源获取即初始化,他说:使用局部对象来管理资源的技术称为资源获取即初始化;这里的资源主要是指操作系统中有限的东西如内存、网络套接字等等,局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入;

资源的使用一般经历三个步骤 : 1.获取资源 2.使用资源 3. 销毁资源
但是资源的销毁往往是程序员经常忘记的一个环节,所以程序界就想如何在程序员中让资源自动销毁呢?c++之父给出了解决问题的方案:RAII,它充分的利用了C++语言局部对象自动销毁的特性来控制资源的生命周期。

lock_guard

lock_guard就是基于RAII原理实现的在作用域内控制可锁对象所有权的类型
lock_guard在构造时对互斥量加锁,在析构时对互斥量解锁。

lock_guard有两种构造方法:

template< class mutex_type >
class lock_guard{
	lock_guard(mutex_type& m);//m是互斥量;构造时会调用m.lock()
	lock_guard(mutex_type& m, std::adopt_lock_t t);//t=std::adopt_lock表示假定调用线程已经获得互斥量的所有权,不再需要调用m.lock()
}

unique_lock

unique_lock可以实现与lock_guard相同的功能,同时提供了更加灵活的用法,但是效率会低一点,内存的占用也会大一点。unique_lock也是一个类模板,但是比起lock_guard,它有自己的成员函数来更加灵活进行锁的操作。

成员函数
void lock();//锁定互斥量
bool try_lock();//尝试锁定互斥量,锁定成功则返回true,否则返回false
bool try_lock_for( const std::chrono::duration<Rep,Period>& timeout_duration );//尝试锁定互斥量,锁定成功则返回true,否则等待timeout_duration时间返回false
bool try_lock_until(const std::chrono::time_point<Clock,Duration>& timeout_time);//尝试锁定互斥量,锁定成功则返回true,否则等待到timeout_time时间点返回false
void unlock();//解锁互斥量
void swap(unique_lock& other);//交换互斥量
mutex_type* release();//解除与互斥量的联系,但是不会改变互斥量的状态(不会unlock),返回指向mutex的指针
mutex_type* mutex();//返回指向mutex的指针
bool owns_lock();//判断*this是否获得了相关联的mutex的所有权(调用成功m.lock)
构造函数
//这样定义与lock_guard没区别,最终也是通过析构函数来unlock
std::unique_lock<std::mutex> munique(mutex);
std::unique_lock<std::mutex> munique(mutex, std::adopt_to_lock);//假定调用线程已经获得互斥量的所有权,不再需要调用m.lock()

std::unique_lock<std::mutex> munique(mutex, std::try_to_lock);//判断当前mutex能否被lock,如果不能被lock,则可以去执行其他代码
if (munique.owns_lock() == true) {
	// 执行共享内存的代码
}
else {
	// 执行一些没有共享内存的代码
}

std::unique_lock<std::mutex> munique(mutex, std::defer_lock);//表示暂时先不lock,之后手动去lock,但是使用之前也是不允许去lock。一般用来搭配unique_lock的成员函数去使用。
if (munique.try_lock() == true) {
	// 执行共享内存的代码
}
else {
	// 处理一些没有共享内存的代码
}

对unique_lock的对象来说,一个对象只能和一个mutex锁唯一对应,不能存在一对多或者多对一的情况,不然会造成死锁的出现。所以如果想要传递unique_lock对象对mutex的权限,需要运用到移动语义或者移动构造函数两种方法。

std::unique_lock<std::mutex> munique2(std::move(munique1));
// 此时munique1失去mlock的权限,并指向空值,munique2获取mlock的权限
scoped_lock

对多个mutex上锁可能会出现死锁,最常见的例子是ABBA死锁。假设存在两个线程(线程1和线程2)和两个锁(锁A和锁B),这两个线程的执行情况如下:

线程1线程2
获得锁A获得锁B
尝试获得锁B尝试获得锁A
等待获得锁B等待获得锁A

线程1在等待线程2持有的锁B,线程2在等待线程1持有的锁A,两个线程都不会释放其获得的锁; 因此,两个锁都不可用,将陷入死锁。

解决死锁最终都要打破资源依赖的循环,一般来说有两种思路:

  • 预防死锁:预防就是想办法不让线程进入死锁
  • 检测死锁和从死锁中恢复:如果预防死锁的代价比较高,而死锁出现的几率比较小,不如就先让其自由发展,在这个过程中提供一个检测手段来检查是否已经出现死锁,当检查出死锁之后就想办法破坏死锁存在的必要条件,让线程从死锁中恢复过来。

std::scoped_lock采用了和std::lock相同的死锁预防算法来对多个mutex上锁。

std::scoped_lock的预防死锁策略很简单,假设要对n个mutex(mutex1, mutex2, …, mutexn)上锁,那么每次只尝试对一个mutex上锁,只要上锁失败就立即释放获得的所有锁(方便让其他线程获得锁),然后重新开始上锁,处于一个循环当中,直到对n个mutex都上锁成功。这种策略是基本上是有效的,虽然有极小的概率出现“活锁”,例如上面的ABBA死锁中,线程1释放锁A的同一时刻时线程2又释放了锁B,然后这两个线程又同时分别获得了锁A和锁B,如此循环。

条件变量

参考链接
条件变量是利用线程间共享的变量进行同步的一种机制,是在多线程程序中用来实现等待–>唤醒逻辑常用的方法,线程可以使用条件变量来等待某个条件为真,注意理解并不是等待条件变量为真。

当条件不满足时,线程将自己加入等待队列,同时释放持有的互斥锁; 当一个线程唤醒一个或多个等待线程时,此时条件不一定为真(虚假唤醒),则继续等待,直到被唤醒且判断条件为真。

两个线程利用条件变量及互斥锁实现同步。条件变量和互斥锁对两个线程来说是全局的。

  • 一个线程利用条件变量实现等待,同时释放锁;
  • 一个线程获取锁后利用该条件变量唤醒等待的线程。

condition_variable类型的条件变量总是使用unique_lock<mutex>:如果要使用其他类型的锁,则需要使用condition_variable_any类型的条件变量。

用法:

等待
  • std::condition_variable::wait
    //线程挂起进入等待队列,并释放锁。一旦被唤醒则尝试获得锁,继续执行代码
    void wait(unique_lock<mutex>& lck);
    
    //若pred为false则对应线程挂起并释放锁,直到被唤醒,唤醒后再判断pred,若为false则继续挂起,直到被唤醒同时条件为true。
    //相当于: while (!pred()) wait(lck);
    template <class Predicate> 
    void wait(unique_lock<mutex>& lck, Predicate pred);
    
  • std::condition_variable::wait_until
    template <class Clock, class Duration>  
    cv_status wait_until (unique_lock<mutex>& lck, const chrono::time_point<Clock,Duration>& abs_time);
    template <class Clock, class Duration, class Predicate>       
    bool wait_until (unique_lock<mutex>& lck, const chrono::time_point<Clock,Duration>& abs_time, Predicate pred);
    
    如果指定谓语pred,则相当于
    while (!pred())
      if ( wait_until(lck,abs_time) == cv_status::timeout)
        return pred();
    return true;
    
  • std::condition_variable::wait_for
    //等待直到被唤醒或等待时间超过rel_time,然后尝试获得锁,成功获得锁后返回,返回值为超时与否
    template <class Rep, class Period>  
    cv_status wait_for(unique_lock<mutex>& lck, const chrono::duration<Rep,Period>& rel_time);
    template <class Rep, class Period, class Predicate>       
    bool wait_for (unique_lock<mutex>& lck, const chrono::duration<Rep,Period>& rel_time, Predicate pred);
    
    如果指定谓语pred,则与wait_util类似,如果超时则返回pred()状态。
唤醒
  • 唤醒一个等待线程
    std::condition_variable::notify_one
  • 唤醒所有等待的线程
    std::condition_variable::notify_all
    发送通知以唤醒等待队列中的线程。
    注意:一般在唤醒之前要解锁,防止线程被唤醒后因为无法获得锁而继续阻塞
    // Manual unlocking is done before notifying, to avoid waking up the waiting thread only to block again 
   lck.unlock();
   cv.notify_one();
  1. 注意区分条件变量与条件:条件变量是用于同步的机制,条件是条件变量控制函数执行或等待的判断依据。
  2. wait的两个重载方法,区别是等待时是否判断条件。

协程

C++协程是一种新的编程语言特性,引入了协程(coroutine)的概念,可以用来简化异步编程和多任务编程。C++20引入了协程库,使得C++程序员能够使用协程。
协程是一种比线程更轻量级的多任务编程模型。协程可以在一个线程内实现多个协程的切换,而不需要线程上下文切换的开销。协程可以用于实现异步编程模型,例如基于事件的编程模型和基于回调的编程模型。
在C++中,协程是一个特殊的函数,它可以在函数执行过程中暂停和恢复。协程在暂停和恢复时会保留函数的所有局部变量和状态,因此在协程恢复时可以继续执行之前的状态。C++20引入了协程库,使得协程可以更加方便地使用。协程库提供了一组函数和类型,用于创建和管理协程。
使用C++协程可以大大简化异步编程和多任务编程。协程使得编写高效的异步代码更加容易,也可以更方便地编写复杂的多任务编程逻辑。

IO

输入

std::ios::good()

Check whether state of stream is good
Returns true if none of the stream’s error state flags (eofbit, failbit and badbit) is set.
You can call it to verify if something isn’t going well and then verify the other bits to check what is wrong. For example :

  • End-of-File reached on input operation (eofbit)
  • Logical error on i/o operation (failbit)
  • Read/writing error on i/o operation (badbit)

输出

  1. cout非科学计数法输出
cout.setf(ios::fixed);
cout.precision(3); // 精度为输出小数点后3位
  1. fstream文件流非科学计数法输出
double test=3.1415926;
ofstream file(fileName);
file.setf(ios::fixed);
file.precision(5);//精度为输出小数点后5位
file<<test;
file.close();
double test=3.1415926;
ofstream file(fileName);
file.setf(ios::fixed);
file.precision(5);//精度为输出小数点后5位
file<<test;
file.close();
  1. fstream中几个函数
 file.precision(8);        
 file.flags(ios::left|ios::fixed);
 file.fill('0' );
 file.width(14);

前三个函数是一次设定始终有效,而第四个只对下一次输入有效。依次解释这四个函数的意义:

  • file.precision(3): 设定精度,小数点后有效数的位数,若输出0.32456,结果为0.324,;若输出0.3,结果为0.3。也就是对缺少的位数该函数不会去补充;
  • file.fill(‘0’ ):该函数的作用就是把空出来的位数用某一字符来补充。但注意仅设置precision的情况下是不会补充的。因为precision只负责精度,而不会限定具体位数。
  • file.width(14):则是限定输出的位数。但在系统优先满足精度,输出位数可能无法保证。例如321.45678,若设定8位宽度,5位精度,最终结果是321.45678。即优先满足精度要求,其次满足宽度要求。
  • 同样上例中若精度为2位,结果为321.45.剩下的可以用fill来补充

但设定以上三种条件也无法得出满意的结果。

因为系统默认数字右对齐,也就是填充字符会填充在数字的左边!
此时必须进步设定file.flags(ios::left|ios::fixed);其中ios::left是令字符左对齐,而ios::fixed,该参数指定的动作是以带小数点的形式表示浮点数,并且在允许的精度范围内尽可能的把数字移向小数点右侧;

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Shilong Wang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值