1. 原始自变量 定义原始字符串的方式为:R “xxx(原始字符串)xxx”,其中()两边的字符串可以省略(不省略的时候就要求一样)。原始字面量R可以直接表示字符串的实际含义,而不需要额外对字符串做转义或连接等操作。
用途:主要是防止一些特殊字符,如\t,\n等对想输出的字符串产生影响。
测试程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int main () { string str1 = R"hello(D:\hello\world\test.text)hello" ; cout << str1 << endl; string str2 = R"(<html> <head> <title> 海贼王 </title> </head> <body> <p> 我是要成为海贼王的男人 </p> </body> </html>)" ; cout << str2 << endl; }
输出结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 D:\hello\world\test.text <html> <head> <title> 海贼王 </title> </head> <body> <p> 我是要成为海贼王的男人 </p> </body> </html>
2. 指针空值类型nullptr nullptr无法隐式转换为整形,但是可以隐式匹配指针类型。在c++11标准下,相比NULL和0,使用nullptr初始化空指针可以令编写的程序更加健壮。
测试程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void func (char * p) { cout << "void func(char *p)" << endl; } void func (int p) { cout << "void func(int p)" << endl; } int main () { int * ptr1 = NULL ; char * ptr2 = NULL ; void * ptr3 = nullptr ; func (10 ); func (NULL ); func (nullptr ); }
输出结果:
1 2 3 void func(int p) void func(int p) void func(char *p)
3. constexpr修饰常量表达式 C++ 程序从编写完毕到执行分为四个阶段:预处理、 编译、汇编和链接4个阶段,得到可执行程序之后就可以运行了。需要额外强调的是,常量表达式和非常量表达式的计算时机不同,非常量表达式只能在程序运行阶段计算出结果,但是常量表达式的计算往往发生在程序的编译阶段,这可以极大提高程序的执行效率,因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间。
注:const和constexpr是等价的,都可以在程序的编译阶段计算出结果。
测试程序:
1 2 3 4 5 6 7 8 9 struct T { int a; }; int main () { constexpr int a = 13 ; constexpr T t{ 13 }; }
4. 自动类型推导 当变量不是指针或者引用类型时,推导的结果中不会保留const、volatile关键字。 当变量是指针或者引用类型时,推导的结果中会保留const、volatile关键字。
测试程序:
1 2 3 4 5 6 7 8 9 10 11 12 int main () { auto a = 3 ; auto b = 3.14 ; auto c = 'f' ; int tmp = 250 ; const auto a1 = tmp; auto a2 = a1; auto & a3 = a1; auto * pt1 = &a1; }
不允许使用auto的四个场景:
1.不能作为函数参数使用,因为只有在函数调用的时候才会给函数参数传递实参,auto要求必须要给修饰的变量赋值,因此二者矛盾。
1 2 3 int func (auto x, auto y) { cout << "x: " << x << "y: " << y << endl; }
2.不能用于类的非静态成员变量的初始化
1 2 3 4 5 class Test { auto v1 = 0 ; static auto v2 = 0 ; static const auto v3 = 10 ; };
3.不能使用auto关键字定义数组
1 2 3 4 5 6 int func () { int array[] = { 1 ,2 ,3 ,4 ,5 }; auto t1 = array; auto t2[] = array; auto t3[] = { 1 ,2 ,3 ,4 ,5 }; }
4.无法使用auto推导出模板参数
1 2 3 4 5 6 7 8 template <typename T>struct Test {};int func () { Test<double > t; Test<auto >t1 = t; return 0 ; }
5. decltype类型推导 它的作用是在编译器编译的时候推导出一个表达式的类型,如decltype (表达式);
测试程序:
1 2 3 4 int a = 10 ;decltype (a) b = 99 ; decltype (a+3.14 ) c = 52.13 ; decltype (a+b*c) d = 520.1314 ;
decltype的应用多数出现在泛型编程中,下面编写一个类模板,在里边添加遍历容器的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <list> #include <iostream> using namespace std;template <class T >class Container { public : void func (T& c) { for (m_it = c.begin (); m_it != c.end (); ++m_it) { cout << *m_it << " " ; } cout << endl; } private : decltype (T ().begin ()) m_it; }; int main () { list<int > st1{ 1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 }; Container<list<int >> c; c.func (st1); const list<int > st2{ 1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 }; Container<const list<int >> c2; c2.func (st2); return 0 ; }
decltype返回值类型后置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 template <typename R,typename T, typename U> R add1 (T t, U u) { return t + u; } template <typename T, typename U> auto add2 (T t, U u) -> decltype (t + u) { return t + u; } int main () { int t = 3 ; double u = 3.14 ; auto a = add1 <decltype (t + u), int , double >(t , u); cout << a << endl; auto b = add2 (t, u); cout << b << endl; }
6. final和overrid关键字的使用 6.1 final final
关键字来限制某个类不能被继承,或者某个虚函数不能被重写。
如果用final来修饰函数,只能修饰虚函数,这样就能阻止子类重写父类的这个函数了。
解释:当test()是基类中的一个虚函数时,在子类中重写了这个方法,但是不希望孙子类中继续重写这个方法了,因此在子类中将test()方法标记为final,孙子类中对这个方法就只能使用,而不能进行重写了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Base { public : virtual void test () { cout << "Base class..." ; } }; class Child : public Base{ public : void test () final { cout << "Child class..." ; } }; class GrandChild : public Child{ public : void test () { cout << "GrandChild class..." ; } };
如果使用final关键字来修饰类的话,表示该类是不允许被继承的,也就是说这个类不能有派生类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Base { public : virtual void test () { cout << "Base class..." ; } }; class Child final : public Base { public : void test () { cout << "Child class..." ; } }; class GrandChild : public Child { public :};
6.2 override override
关键字确保在派生类中声明的重写函数与基类的虚函数有相同的名字,同时也明确表明将会重写基类的虚函数,这样就可以保证重写的虚函数的正确性,也提高了代码的可读性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Base { public : virtual void test () { cout << "Base class..." ; } }; class Child : public Base{ public : void test () override { cout << "Child class..." ; } };
7.委托构造函数和继承构造函数 7.1 委托构造函数 委托构造函数允许使用同一个类中的一个构造函数调用其它的构造函数,从而简化相关变量的初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class test {public : test () {}; test (int num) { cout << "一个参数:" << num << endl; } test (int num, int sum) :test (num){ cout << "两个参数:" << sum << endl; } test (int num, int sum, int tum) :test (num, sum) { cout << "三个参数:" << tum << endl; } };
7.2 继承构造函数 继承构造函数可以让派生类直接使用基类的构造函数,而无需自己再写构造函数,尤其是在基类有很多构造函数的情况下,可以极大地简化派生类构造函数的编写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class base {public : base (int num) :a (num){} base (int num, int sum) :a (num), b (sum) {} base (int num, int sum, int tum) :a (num),b (sum),c (tum) {} int a; int b; int c; }; class child :public base {public : using base::base; }; int main () { child x (10 , 20 , 30 ) ; cout << x.a << x.b << x.c << endl; }
另外如果在子类中隐藏了父类中的同名函数,也可以通过using的方式在子类中使用基类中的这些父类函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class base {public : base (int num) :a (num) {} base (int num, int sum) :a (num), b (sum) {} base (int num, int sum, int tum) :a (num), b (sum), c (tum) {} void func (int x) { cout << x << endl; } void func (int x, int y) { cout << x << "and" << y << endl; } int a; int b; int c; }; class child :public base {public : using base::base; using base::func; void func () { cout << "lxx" << endl; } }; int main () { child c (250 ) ; c.base::func (39 ,93 ); c.func (39 ,93 ); }
在C++中,如果基类没有默认构造函数,子类也不能自动生成默认构造函数。这是因为子类对象的构造需要先构造基类的部分,如果基类没有默认构造函数,子类在构造时就无法调用基类的构造函数来初始化基类部分。
8 .可调用对象包装器和绑定器 8.1 可调用对象 在c++中有四种可调用对象的定义
1.是一个函数指针
1 2 3 4 5 6 int print (int x, double y) { cout << x << y << endl; return 0 ; } int (*func)(int , double ) = &print;
2.是一个具有operator()成员函数的类对象(仿函数)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct Test { void operator () (string msg) { cout << "msg: " << msg << endl; } }; int main (void ) { Test t; t ("lxxlxxlxx" ); return 0 ; }
3.是一个可被转换为函数指针的类对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 using func_ptr = void (*)(int , string);struct Test { static void print (int a, string b) { cout << "name: " << b << ", age: " << a << endl; } operator func_ptr () { return print; } }; int main (void ) { Test t; t (19 , "Monkey D. Luffy" ); return 0 ; }
4.是一个类成员函数指针或者类成员指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 struct Test { void print (int a, string b) { cout << "name: " << b << ", age: " << a << endl; } int m_num; }; int main (void ) { void (Test::*func_ptr)(int , string) = &Test::print; int Test::*obj_ptr = &Test::m_num; Test t; (t.*func_ptr)(19 , "Monkey D. Luffy" ); t.*obj_ptr = 1 ; cout << "number is: " << t.m_num << endl; return 0 ; }
8.2 可调用对象包装器 可调用对象的包装器是std::function
。它是一个类模板,可以容纳除了类(非静态)成员(函数)指针之外的所有可调用对象。使用std::function
,必须包含头文件functional
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 int add (int a, int b) { cout << a << " + " << b << " = " << a + b << endl; return a + b; } class T1 { public : static int sub (int a, int b) { cout << a << " - " << b << " = " << a - b << endl; return a - b; } }; class T2 { public : int operator () (int a, int b) { cout << a << " * " << b << " = " << a * b << endl; return a * b; } }; int main (void ) { function<int (int , int )> f1 = add; function<int (int , int )> f2 = T1::sub; T2 t; function<int (int , int )> f3 = t; f1 (9 , 3 ); f2 (9 , 3 ); f3 (9 , 3 ); return 0 ; }
因为回调函数本身就是通过函数指针实现的,使用对象包装器可以取代函数指针的作用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 void print (int num, string name) { cout << num << "\t" << name << endl; } using funcptr = void (*)(int , string); class Test {public : void operator () (string msg) { cout << "仿函数:" << msg << endl; } static void world (int a, string s) { cout << a << "\t" << s << endl; } }; class A {public : A (const function<void (int , string)>& f) :callback (f) {} void notify (int id, string name) { callback (id, name); } private : function<void (int , string)>callback; }; int main () { A aa (print) ; aa.notify (1 , "ace" ); A ab (Test::world) ; ab.notify (2 , "sabo" ); }
如果在编写程序的时候要用到多种类型的可调用对象,那么就可以通过可调用对象包装器对它们进行打包。因为只要给参数指定为可调用对象包装器类型,只要传进任意一种可调用对象类型(参数和返回值要求是相同类型),它都会进行隐式的类型转换。通过可调用对象的包装器把这些不同类型的可调用对象封装成一种类型,这样程序就显得更加简洁和灵活。
8.3 绑定器 std::bind
用来将可调用对象与其参数一起进行绑定。绑定后的结果可以使用std::function
进行保存,并延迟调用到任何我们需要的时候。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void output (int x, int y) { cout << x << " " << y << endl; } int main (void ) { bind (output, 1 , 2 )(); bind (output, placeholders::_1, 2 )(3 ); bind (output, 2 , placeholders::_1)(9 ); bind (output, 2 , placeholders::_2)(10 , 20 ); bind (output, placeholders::_1, placeholders::_2)(10 , 20 ); bind (output, placeholders::_2, placeholders::_1)(10 , 20 ); return 0 ; }
可调用对象包装器std::function
是不能实现对类成员函数指针或者类成员指针的包装的,但是通过绑定器std::bind
的配合之后,就可以完美的解决这个问题了。
在下面程序中,使用绑定器函数bind()绑定了某一个可调用对象,最终得到一个仿函数f或f1,其实这个仿函数对应的还是绑定的那个可调用对象(参数1),绑定的时候可以给它指定固定的参数(参数2和参数3),固定的参数可以是一个变量也可以是一个常量。如果绑定的时候不给它指定一个固定的数值,可以指定占位符,当指定占位符后,它就需要从仿函数调用时候的参数列表里面去读对应的数值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void testFunc (int x, int y, const function<void (int , int )>& f) { if (x % 2 == 0 ) { f (x, y); } } void output_add (int x, int y) { cout << "x=" << x << "\t y=" << y << "\t x+y=" << x + y << endl; } int main (void ) { for (int i=0 ; i<10 ; i++){ auto f = bind (output_add, i+100 , i+200 ); testFunc (i,i,f); auto f1 = bind (output_add, placeholders::_1, placeholders::_2); testFunc (i,i,f1); } }
输出的结果为:
1 2 3 4 5 6 7 8 9 10 x=100 y=200 x+y=300 x=0 y=0 x+y=0 x=102 y=202 x+y=304 x=2 y=2 x+y=4 x=104 y=204 x+y=308 x=4 y=4 x+y=8 x=106 y=206 x+y=312 x=6 y=6 x+y=12 x=108 y=208 x+y=316 x=8 y=8 x+y=16
下面程序是对类的成员函数以及类的成员变进行绑定和封装。因为可调用包装器std::function
不能对类成员函数和变量进行包装,但可以对仿函数进行包装,所以可以先通过绑定器bind来对类成员函数和变量进行绑定,然后可以得到对应的一个仿函数。这样包装器就可以间接的对其进行包装了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Test {public : void output (int x, int y) { cout << x << "\t" << y << endl; } int m_number = 100 ; }; int main () { Test t; auto f2 = bind (&Test::output, &t, 520 , placeholders::_1); function<void (int , int )>f22 = bind (&Test::output, &t, 520 , placeholders::_1); f2 (1314 ); auto f3 = bind (&Test::m_number, &t); function<int & (void )>f33 = bind (&Test::m_number, &t); cout << f3 () << endl; f3 () = 666 ; cout << f3 () << endl; }
上面程序中,f3和f33的类型是不一样的,绑定器bind绑定完后得到的是一个仿函数,而f33它是把仿函数进行了包装,得到一个包装器类型,所以这两个不是等价的。需要了解的是,f33这一行是做了隐式的类型转换,f3这一行是做了自动的类型推导。
9. lambda表达式 lambda
表达式的捕获列表可以捕获一定范围内的变量,具体如下:
[] 不捕抓任何变量
[&] 捕获外部作用域所有的变量,并作为引用在函数体内使用(按引用捕获)
[=] 捕获外部作用域所有的变量,并作为副本在函数体内使用(按值捕获) —>拷贝的副本在匿名函数体内部是只读的
[=,&foo] 按值捕获外部作用域中的所有变量,并按引用捕获外部变量foo
[bar] 按值捕获bar变量,同时不捕获其他变量
[&bar] 按引用捕获bar变量,同时不捕获其他变量
[this] 捕获当前类中的this指针
lambda
表达式的使用:
1 2 3 4 5 6 7 8 9 10 11 void func (int x, int y) { int a = 7 ; int b = 9 ; [=, &x]() mutable { int c = a; int d = x; b++; cout << b << endl; }(); cout << b << endl; }
关于通过值拷贝的方式捕获的外部变量是只读的原因:
lambda表达式的类型在C++11中会被看做是一个带operator()的类,即仿函数。
按照C++标准,lambda表达式的operator()默认是const的,一个const成员函数是无法修改成员变量值的。
所以mutable
选项的作用就在于取消operator()的const属性。
因为lambda表达式在C++中会被看做是一个仿函数,因此可以使用std::function和std::bind来存储和操作lambda表达式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void func (int x, int y) { int a; int b; using ptr = void (*)(int ); ptr p1 = [](int x){ cout << "x: " << x << endl; }; p1 (3 ); function<void (int )> fff = [=](int x){ cout << "x: " << x << endl; } fff (10 ); function<void (int )> fff1 = bind ([=](int x){ cout << "x: " << x << endl; },placeholders::_1); fff1 (9 ); }
10. 右值引用 在C++11中增加了一个新的类型,即右值引用,标记为 &&。
区别方法:可以对表达式取地址(&)就是左值,否则为右值 。所有有名字的变量或对象都是左值,而右值是匿名的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int main () { int num = 9 ; int & a = num; int && b = 8 ; const int && d = 6 ; const int & c = num; const int & f = b; const int & g = d; const int & h = d; }
通过上面程序可以得出结论:右值引用只能通过右值来初始化;常量的左值引用是一个万能的引用类型,可以通过同类型的各种引用来初始化左值引用。
在下面程序中,是通过右值引用来模拟浅拷贝。移动构造(右值引用)是把临时对象的指针成员移动走了,临时对象析构的时候析构了一个空指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 class Test { public : Test () : m_num (new int (100 )) { cout << "construct: my name is jerry" << endl; cout << "m_num的地址: " << &m_num << endl; } Test (const Test& a) : m_num (new int (*a.m_num)) { cout << "copy construct: my name is tom" << endl; } Test (Test&& a) : m_num (a.m_num) { a.m_num = nullptr ; cout << "move construct..." << endl; } ~Test () { cout << "destruct Test class ....." << endl; delete m_num; } int *m_num; }; Test getObj () { Test t; return t; } int main () { Test t = getObj (); Test&& t1 = getObj (); cout << "m_num的地址: " << &t1.m_num << endl; return 0 ; }
使用不带参数的构造函数和移动构造都可以实现浅拷贝,区别在于:不带参的构造函数使用浅拷贝,指针资源不会转移,是两个对象的指针指向同一块内存,析构就会出问题。而使用移动构造是实实在在的对资源进行了转移,转移完了之后,原来这个对象就不拥有这块资源了。
在上面程序中,getObj()
函数里面创建了一个临时的Test
对象并返回它。由于t
是一个左值(即它有一个持久的名字),编译器会调用Test
类的拷贝构造函数来创建t
。而t1是一个右值引用,因此,编译器会调用Test
类的移动构造函数来创建t1
。
右值可以分为两种:一个是将亡值,另一个是纯右值。
纯右值:非引用返回的临时变量,运算表达式产生的临时变量、原始字面量和lambda表达式等。
将亡值:与右值引用相关的表达式,如T&&类型函数的返回值、std::move的返回值等。
10.1 &&的特性 在C++中,并不是所有情况下&&
都代表是一个右值引用,具体的场景体现在模板和自动类型推导中,如果是模板参数需要指定为T&&
,如果是自动类型推导需要指定为auto &&
,在这两种场景下&&
被称作未定的引用类型。另外还有一点需要额外注意const T&&
表示一个右值引用,不是未定引用类型(是不需要推导的)。
在C++11中引用折叠的规则如下:
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 template <typename T>void f (T&& param) ; void f1 (const T&& param) ; f (10 ); int x = 10 ;f (x); f1 (10 ); int main () { int x = 520 , y = 1314 ; auto && v1 = x; auto && v2 = 250 ; decltype (x)&& v3 = y; cout << "v1: " << v1 << ", v2: " << v2 << endl; return 0 ; }; int && a1 = 5 ; auto && bb = a1; auto && bb1 = 5 ; int a2 = 5 ; int &a3 = a2; auto && cc = a3; auto && cc1 = a2; const int & s1 = 100 ; const int && s2 = 100 ; auto && dd = s1; auto && ee = s2; const auto && x = 5 ;
还有一种情况需要注意,例如在下面程序中,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void printValue (int &i) { cout << "l-value: " << i << endl; } void printValue (int &&i) { cout << "r-value: " << i << endl; } void forward (int &&k) { printValue (k); } int main () { int i = 520 ; printValue (i); printValue (1314 ); forward(250 ); return 0 ; };
输出的结果:
1 2 3 l-value: 520 r-value: 1314 l-value: 250
结论:
左值和右值是独立于他们的类型的,右值引用类型可能是左值也可能是右值。
编译器会将已命名的右值引用视为左值,将未命名的右值引用视为右值。
auto&&或者函数参数类型自动推导的T&&是一个未定的引用类型,它可能是左值引用也可能是右值引用类型,这取决于初始化的值类型(上面有例子)。
通过右值推导 T&& 或者 auto&& 得到的是一个右值引用类型,其余都是左值引用类型。
11. 转移和完美转发 11.1 move方法 std::move可以给右值引用进行初始化,把一些左值转换为右值;还有就是可以进行资源的转移,如果某一个对象后面不再被使用了,并且需要拷贝这个对象里面的数据到另一个对象中,这种情况下就可以进行资源转移,减少拷贝的次数,提高析构的允许效果。
程序如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Test { public : Test (){} ...... } int main () { Test t; Test && v1 = t; Test && v2 = move (t); return 0 ; } int main () { int a = 39 ; int b = move (a); }
11.2 forward方法 std::forward()函数实现的功能称之为完美转发。因为当一个右值引用
作为函数参数的形参时,在函数内部转发该参数给内部其他函数时,它就变成一个左值,并不是原来的类型了。如果需要按照参数原来的类型转发到另一个函数,就可以使用forward()方法。
std::forward(t);
当T为左值引用类型时,t将被转换为T类型的左值 当T不是左值引用类型时,t将被转换为T类型的右值
测试程序如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 template <typename T>void printValue (T& t) { cout << "l-value: " << t << endl; } template <typename T>void printValue (T&& t) { cout << "r-value: " << t << endl; } template <typename T>void testForward (T && v) { printValue (v); printValue (move (v)); printValue (forward<T>(v)); cout << endl; } int main () { testForward (520 ); int num = 1314 ; testForward (num); testForward (forward<int >(num)); testForward (forward<int &>(num)); testForward (forward<int &&>(num)); return 0 ; }
结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 1. 对于testForward (520 ); void testForward (T && v) { printValue (v); printValue (move (v)); printValue (forward<T>(v)); } 2. 对于testForward (num); void testForward (T && v) { printValue (v); printValue (move (v)); printValue (forward<T>(v)); } 3. 对于testForward (forward<int >(num)); void testForward (T && v) { printValue (v); printValue (move (v)); printValue (forward<T>(v)); } 4. 对于testForward (forward<int &>(num)); void testForward (T && v) { printValue (v); printValue (move (v)); printValue (forward<T>(v)); } 5. testForward (forward<int &&>(num)); void testForward (T && v) { printValue (v); printValue (move (v)); printValue (forward<T>(v)); }
12. 智能指针 智能指针能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。它的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。它的头文件是#include <memory>
。
C++11中提供了如下三种智能指针:
std::shared_ptr:共享的智能指针
std::unique_ptr:独占的智能指针
std::weak_ptr:弱引用的智能指针,它不共享指针,不能操作资源,是用来监视shared_ptr的。
12.1 共享智能指针 共享智能指针shared_ptr 是一个模板类,它可以让多个智能指针同时管理同一块有效的内存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int main () { shared_ptr<int > ptr1 (new int (520 )) ; cout << "ptr1管理的内存引用计数: " << ptr1.use_count () << endl; shared_ptr<char > ptr2 (new char [12 ]) ; cout << "ptr2管理的内存引用计数: " << ptr2.use_count () << endl; shared_ptr<int > ptr3; cout << "ptr3管理的内存引用计数: " << ptr3.use_count () << endl; shared_ptr<int > ptr4 (nullptr ) ; cout << "ptr4管理的内存引用计数: " << ptr4.use_count () << endl; return 0 ; }
当一个智能指针被初始化之后,就可以通过这个智能指针初始化其他新对象。在创建新对象的时候,对应的拷贝构造函数或者移动构造函数就被自动调用了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int main () { shared_ptr<int > ptr1 (new int (520 )) ; cout << "ptr1管理的内存引用计数: " << ptr1.use_count () << endl; shared_ptr<int > ptr2 (ptr1) ; cout << "ptr2管理的内存引用计数: " << ptr2.use_count () << endl; shared_ptr<int > ptr3 = ptr1; cout << "ptr3管理的内存引用计数: " << ptr3.use_count () << endl; shared_ptr<int > ptr4 (std::move(ptr1)) ; cout << "ptr4管理的内存引用计数: " << ptr4.use_count () << endl; std::shared_ptr<int > ptr5 = std::move (ptr2); cout << "ptr5管理的内存引用计数: " << ptr5.use_count () << endl; return 0 ; }
也可以通过reset方法,它既可以初始化,也可以重置:对于一个未初始化的共享智能指针,可以通过reset方法来初始化,当智能指针中有值的时候,调用reset会使引用计数减1。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int main () { shared_ptr<int > ptr1 = make_shared <int >(520 ); shared_ptr<int > ptr2 = ptr1; shared_ptr<int > ptr3 = ptr1; shared_ptr<int > ptr4 = ptr1; cout << "ptr1管理的内存引用计数: " << ptr1.use_count () << endl; cout << "ptr2管理的内存引用计数: " << ptr2.use_count () << endl; cout << "ptr3管理的内存引用计数: " << ptr3.use_count () << endl; cout << "ptr4管理的内存引用计数: " << ptr4.use_count () << endl; ptr4.reset (); cout << "ptr1管理的内存引用计数: " << ptr1.use_count () << endl; cout << "ptr2管理的内存引用计数: " << ptr2.use_count () << endl; cout << "ptr3管理的内存引用计数: " << ptr3.use_count () << endl; cout << "ptr4管理的内存引用计数: " << ptr4.use_count () << endl; shared_ptr<int > ptr5; ptr5.reset (new int (250 )); cout << "ptr5管理的内存引用计数: " << ptr5.use_count () << endl; return 0 ; }
初始化和使用方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 class Test {public : Test () { cout << "construct Test..." << endl; } Test (int x) : m_num (x) { cout << "construct Test, x = " << x << endl; } Test (string str) { cout << "construct Test, str = " << str << endl; } ~Test () { cout << "destruct Test..." << endl; } void setValue (int v) { m_num = v; } void print () { cout << "m_num:" << m_num << endl; } private : int m_num; }; int main () { shared_ptr<int > ptr1 (new int (3 )) ; cout << "ptr1 use_count:" << ptr1.use_count () << endl; shared_ptr<int >ptr2 = move (ptr1); cout << "ptr1 use_count:" << ptr1.use_count () << endl; cout << "ptr2 use_count:" << ptr2.use_count () << endl; shared_ptr<int >ptr3 = ptr2; cout << "ptr2 use_count:" << ptr2.use_count () << endl; cout << "ptr3 use_count:" << ptr3.use_count () << endl; shared_ptr<int >ptr4 = make_shared <int >(8 ); shared_ptr<Test>ptr5 = make_shared <Test>(8 ); shared_ptr<Test>ptr6 = make_shared <Test>("hello, world" ); ptr6.reset (); cout << "ptr6 use_count:" << ptr6.use_count () << endl; ptr5.reset (new Test ("hello" )); cout << "ptr5 use_count:" << ptr5.use_count () << endl; Test* t = ptr5.get (); t->setValue (1000 ); t->print (); ptr5->setValue (999 ); ptr5->print (); }
指定删除器:当智能指针管理的内存对应的引用计数变为0的时候,这块内存就会被智能指针析构掉了。当然我们在初始化智能指针的时候也可以自己指定删除动作,这个删除操作对应的函数被称之为删除器
,这个删除器函数本质是一个回调函数,我们只需要进行实现,其调用是由智能指针完成的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class Test {public : Test () { cout << "construct Test..." << endl; } Test (int x) : m_num (x) { cout << "construct Test, x = " << x << endl; } Test (string str) { cout << "construct Test, str = " << str << endl; } ~Test () { cout << "destruct Test..." << endl; } void setValue (int v) { m_num = v; } void print () { cout << "m_num:" << m_num << endl; } private : int m_num; }; int main () { shared_ptr<Test> pp (new Test(39 ),[](Test* t){ cout<<"-----------------------" <<endl; delete t; }) ; shared_ptr<Test[]> p1 (new Test[5 ]) ; shared_ptr<Test> p1 (new Test[5 ], [](Test* t) { delete [] t; }) ; shared_ptr<Test> p2 (new Test[5 ], default_delete<Test[]>()) ; }
注意:shared_ptr在通过指针对象去管理一块数组内存的时候,必须手动添加删除器,如果不是数组,智能指针默认提供的删除器就会删除这块内存。
12.2 独占智能指针 std::unique_ptr
是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,可以通过它的构造函数初始化一个独占智能指针对象,但是不允许通过赋值将一个unique_ptr
赋值给另一个unique_ptr
。
初始化和使用的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int main () { unique_ptr<int > ptr1 (new int (9 )) ; unique_ptr<int > ptr2 = move (ptr1); ptr2.reset (new int (8 )); unique_ptr<Test> ptr3 (new Test(1 )) ; Test* pt = ptr3.get (); pt->setValue (3 ); ptr3->setValue (9 ); }
删除器:unique_ptr
指定删除器和shared_ptr
指定删除器是有区别的,unique_ptr
指定删除器的时候需要确定删除器的类型,所以它不能像shared_ptr
那样直接指定删除器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 shared_ptr<int > ptr1 (new int (10 ), [](int *p) {delete p; }) ; unique_ptr<int > ptr1 (new int (10 ), [](int *p) {delete p; }) ; int main () { using func_ptr = void (*)(int *); unique_ptr<int , func_ptr> ptr1 (new int (10 ), [](int *p) {delete p; }) ; unique_ptr<int , func_ptr> ptr1 (new int (10 ), [=](int *p) {delete p; }) ; unique_ptr<int , function<void (int *)>> ptr1 (new int (10 ), [](int *p) {delete p; }); unique_ptr<int []> ptr5 (new int [3 ]) ; shared_ptr<int []> ptr5 (new int [3 ]) ; return 0 ; }
回忆:对于lambda表达式,[]里面为空,则对应的lambda表达式为函数指针类型;否则为仿函数类型。
12.3 弱引用智能指针 std::weak_ptr
可以看做是shared_ptr
的助手,它不管理shared_ptr
内部的指针。weak_ptr
没有重载操作符*和->,因为它不共享指针,不能操作资源,所以它的构造不会增加引用计数,析构也不会减少引用计数,它的主要作用就是作为一个旁观者监视shared_ptr
中管理的资源是否存在。
初始化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int main () { shared_ptr<int > sp (new int ) ; weak_ptr<int > wp1; weak_ptr<int > wp2 (wp1) ; weak_ptr<int > wp3 (sp) ; weak_ptr<int > wp4; wp4 = sp; weak_ptr<int > wp5; wp5 = wp3; return 0 ; }
其他常用方法:
通过调用weak_ptr
类提供的use_count()
方法可以获得当前所观测资源的引用计数。
通过调用weak_ptr
类提供的expired()
方法来判断观测的资源是否已经被释放,如果观察的资源的引用计数为0了,返回的就是true,否则返回false。
通过调用weak_ptr
类提供的lock()
方法来获取管理所监测资源的shared_ptr
对象。
通过调用weak_ptr
类提供的reset()
方法来清空对象,使其不监测任何资源。
演示程序如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int main () { shared_ptr<int > sp1, sp2; weak_ptr<int > wp; sp1 = std::make_shared <int >(520 ); wp = sp1; sp2 = wp.lock (); cout << "use_count: " << wp.use_count () << endl; sp1.reset (); cout << "use_count: " << wp.use_count () << endl; sp1 = wp.lock (); cout << "use_count: " << wp.use_count () << endl; cout << "*sp1: " << *sp1 << endl; cout << "*sp2: " << *sp2 << endl; return 0 ; }
12.4 智能指针的注意事项 shared_ptr
使用的注意事项:
1.不能使用一个原始地址初始化多个共享智能指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct Test { shared_ptr<Test> getSharedPtr () { return shared_ptr <Test>(this ); } ~Test (){ cout<<"class Test is disstruct..." <<endl; } }; int main () { Test* t = new Test; shared_ptr<Test>ptr1 (t); shared_ptr<Test>ptr2 = ptr1; }
2.函数不能返回管理了this的共享智能指针对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct Test { shared_ptr<Test> getSharedPtr () { return shared_ptr <Test>(this ); } ~Test () { cout << "class Test is disstruct..." << endl; } }; int main () { shared_ptr<Test>ptr1 (new Test); cout << ptr1.use_count () << endl; shared_ptr<Test>ptr2 = ptr1->getSharedPtr (); cout << ptr2.use_count () << endl; }
在这个例子中使用同一个指针this构造了两个智能指针对象ptr1和ptr2,这二者之间是没有任何关系的,因为ptr2并不是通过ptr1初始化得到的实例对象。在离开作用域之后this将被构造的两个智能指针各自析构,导致重复析构的错误。
这个问题可以通过一个模板类叫做std::enable_shared_from_this<T>
来解决,这个类中有一个方法叫做shared_from_this()
,通过这个方法可以返回一个共享智能指针,在该函数的底层就是使用weak_ptr
来监测this对象,并通过调用weak_ptr
的lock()
方法返回一个shared_ptr
对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct Test : enable_shared_from_this<Test>{ shared_ptr<Test> getSharedPtr () { return shared_from_this (); } ~Test () { cout << "class Test is disstruct..." << endl; } }; int main () { shared_ptr<Test>ptr1 (new Test); cout << ptr1.use_count () << endl; shared_ptr<Test>ptr2 = ptr1->getSharedPtr (); cout << ptr2.use_count () << endl; }
3.共享智能指针不能循环引用
13. POD类型 POD指的就是普通的旧数据 。它通常用于说明一个类型的属性,尤其是用户自定义类型的属性。
在C++11中将 POD划分为两个基本概念的合集,即∶平凡的(trivial)
和标准布局的(standard layout)
。
## 13
.1 平凡类型
一个平凡的类或者结构体应该符合以下几点要求:
拥有平凡的默认构造函数(trivial constructor)和析构函数(trivial destructor)。
拥有平凡的拷贝构造函数(trivial copy constructor)和移动构造函数(trivial move constructor)。
拥有平凡的拷贝赋值运算符(trivial assignment operator)和移动赋值运算符(trivial move operator)。
不包含虚函数以及虚基类。
13.2 非受限联合体 在C++11之前我们使用的联合体是有局限性的,主要有以下三点:
不允许联合体拥有非POD类型的成员
不允许联合体拥有静态成员
不允许联合体拥有引用类型的成员
在新的C++11标准中,取消了关于联合体对于数据成员类型的限定,规定任何非引用类型
都可以成为联合体的数据成员,这样的联合体称之为非受限联合体(Unrestricted Union)。
13.3 非受限联合体中静态成员的使用 补充知识:
如果在联合体里面出现了静态成员变量,那么它的初始化要放到联合体的外面(类和结构体也一样的)
静态成员是属于类的,而不是属于对象(类和结构体也一样的)
在联合体里面的静态成员和非静态成员使用的不是同一块内存(类和结构体也一样的)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 union Test { static int num; static void print () {} int num1; }; int Test::num = 3 ;int main () { Test t1; t1.num1 = 100 ; cout << "static num value: " << t1.num << endl; cout << "num1 value: " << t1.num1 << endl; Test t2; t2.num = 50 ; cout << "static num value: " << t2.num << endl; cout << "static num value: " << t1.num << endl; }
13.4 非受限联合体中非POD类型成员的使用 在c++11里面规定,如果在非受限联合体中使用了非POD里面的成员,编译器就会自动的删除这个联合体的构造函数和析构函数
placement new:一般情况下,使用new申请空间时,是从系统的堆(heap)中分配空间,申请所得的空间的位置是根据当时的内存的实际使用情况决定的。但是,在某些特殊情况下,可能需要在已分配的特定内存创建对象,这种操作就叫做placement new
即定位放置 new。
使用定位放置new申请内存空间:ClassName* ptr = new (定位的内存地址)ClassName;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Teacher { public : Teacher () {} void setText (string s) { name = s; } private : string name; }; union Student { Student () { new (&name)string; } ~Student () {} int id; Teacher t; string name; }; int main () { Student s; s.name = "lxx" ; s.t.setText ("hello" ); cout << "s.name=" << s.name << endl; }
13.5 标准布局的 标准布局类型主要主要指的是类或者结构体的结构或者组合方式。
标准布局类型的类应该符合以下五点定义,最重要的为前两条:
1.所有非静态成员有相同 的访问权限(public,private,protected)
类成员拥有不同的访问权限(非标准布局类型)
类成员拥有相同的访问权限(标准布局类型)
2.在类或者结构体继承时,满足以下两种情况之一∶
派生类中有非静态成员,基类中包含静态成员(或基类没有变量)。
基类有非静态成员,而派生类没有非静态成员。
1 2 3 4 5 6 7 8 9 10 struct Base { static int a;};struct Child : public Base{ int b;}; struct Base1 { int a;};struct Child1 : public Base1{ static int c;}; struct Child2 :public Base, public Base1 { static int d;); struct Child3 :public Base1{ int d;}; struct Child4 :public Base1, public Child { static int num; };
结论:非静态成员只要同时出现在派生类和基类间,即不属于标准布局。对于多重继承,一旦非静态成员出现在多个基类中,即使派生类中没有非静态成员变量,派生类也不属于标准布局。
3.子类中第一个非静态成员的类型与其基类不同
1 2 3 4 5 struct Base { static int a;};struct Child : public Base{ Base p; int b; };
4.没有虚函数和虚基类
5.所有非静态数据成员均符合标准布局类型,其基类也符合标准布局,这是一个递归的定义
14. 扩展的 friend 语法 friend关键字用于声明类的友元,友元可以无视类中成员的属性(public、protected 或是 private),友元类或友元函数都可以访问,这样虽然完全破坏了面向对象编程中封装性的概念。但有的时候friend关键字确实会让程序少写很多代码。
14.1 语法改进 声明一个类为另外一个类的友元时,不再需要使用class关键字,并且还可以使用类的别名(使用 typedef 或者 using 定义)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class Tom ; using Honey = Tom; class Jack { friend Tom; private : string name = "jack" ; void print () { cout << "my name is " << name << endl; } }; class Lucy { friend Honey; protected : string name = "lucy" ; void print () { cout << "my name is " << name << endl; } }; class Tom {public : void print () { cout << j.name << endl << l.name << endl; j.print (); l.print (); } private : Jack j; Lucy l; }; int main () { Tom t; t.print (); }
14. 命名空间this_thread std::this_thread
是关于线程的命名空间,在这个命名空间中提供了四个公共的成员函数,通过这些成员函数就可以对当前线程进行相关的操作了。
14. 1 get_id() std::this_thread中的get_id()方法可以得到当前线程的线程ID。测试程序如下:
1 2 3 4 5 6 7 8 9 10 11 void func () { cout << "子线程: " << this_thread::get_id () << endl; } int main () { cout << "主线程: " << this_thread::get_id () << endl; thread t (func) ; t.join (); }
14.2 sleep_for() 与进程一样,线程被创建后也有五种状态:创建态
,就绪态
,运行态
,阻塞态(挂起态)
,退出态(终止态)
。
众所周知的,在计算机中启动的多个线程都需要占用CPU资源,但是CPU的个数是有限的并且每个CPU在同一时间点不能同时处理多个任务。为了能够实现并发处理,多个线程都是分时复用
CPU时间片,快速的交替处理各个线程中的任务。因此多个线程之间需要争抢CPU时间片,抢到了就执行,抢不到则无法执行(因为默认所有的线程优先级都相同,内核也会从中调度,不会出现某个线程永远抢不到CPU时间片的情况)。
而sleep_for()是用于让线程休眠的函数,调用这个函数的线程会马上从运行态
变成阻塞态
并在这种状态下休眠一定的时长,因为阻塞态的线程已经让出了CPU资源,代码也不会被执行,所以线程休眠过程中对CPU来说没有任何负担。这个函数的参数需要指定一个休眠时长,是一个时间段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void func () { for (int i = 0 ; i < 10 ; ++i) { this_thread::sleep_for (chrono::seconds (1 )); cout << "子线程: " << this_thread::get_id () << ", i = " << i << endl; } } int main () { thread t (func) ; t.join (); }
需要注意的是:程序休眠完成之后,会从阻塞态
重新变成就绪态
,就绪态
的线程需要再次争抢CPU时间片,抢到之后才会变成运行态
,这时候程序才会继续向下运行。
14.3 sleep_until() sleep_until()
也是一个休眠函数,它是指定线程阻塞到某一个指定的时间点(time_point类型),之后解除阻塞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void func () { for (int i = 0 ; i < 10 ; ++i) { auto now = chrono::system_clock::now (); chrono::seconds sec (2 ) ; this_thread::sleep_until (now + sec); cout << "子线程: " << this_thread::get_id () << ", i = " << i << endl; } } int main () { thread t (func) ; t.join (); }
14.4 yield() 当线程中调用这个函数yield()
之后,如果它正处于运行态,那么它会主动让出自己已经抢到的CPU时间片,最终变为就绪态,这样其它的线程就有更大的概率能够抢到CPU时间片了。需要注意的是,线程调用了yield()
之后会主动放弃CPU资源,但是这个变为就绪态的线程会马上参与到下一轮CPU的抢夺战中,不排除它能继续抢到CPU时间片的情况,这是概率问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void func () { for (int i = 0 ; i < 100000000000 ; ++i) { cout << "子线程: " << this_thread::get_id () << ", i = " << i << endl; this_thread::yield (); } } int main () { thread t (func) ; thread t1 (func) ; t.join (); t1.join (); }
结论:
yield() 的目的是避免一个线程长时间占用CPU资源,从而导致多线程处理性能下降
yield() 是让当前线程主动放弃了当前自己抢到的CPU资源,但是在下一轮还会继续抢
15. 线程类
可执行文件的运行过程:如果在磁盘上有一个可执行的文件a.exe(占用的是硬盘资源),双击该文件,就可以得到一个应用程序(占用内存和cpu资源),将该应用程序称为进程。这个进程在物理内存里面占一块虚拟地址空间,相对于应用程序来说只能算一个中转站,是我们把程序启动起来之后,程序里面所有的数据被加载到这块虚拟地址空间里面,然后通过cpu里面的MMU(cpu里面的内存管理单元),它会把虚拟地址空间里面的数据映射到物理内存的另外一个位置。
每一个启动起来的进程都对应一块虚拟空间,其由两部分组成,一部分是内核区,一部分是用户区。如果是多进程,每个进程都对应一个虚拟地址空间。比如说进程1创建了一个新的进程2,在操作系统里面就会额外的再次分配一个虚拟地址空间。每个进程都对应一个虚拟地址空间。如果是线程的话,不会创建额外的虚拟地址空间,即多个线程是共用同一个虚拟地址空间的。
虚拟内存空间是虚拟的,一个进程存储的数据都是存储在虚拟内存映射的那块物理内存中,作用就是保护数据,确保进程之间的隔离,提高系统的安全性和稳定性,其中磁盘作为辅助。具体来说,当进程访问虚拟地址时,操作系统会将这些地址映射到实际的物理内存地址中,物理内存满了的话,就会将物理内存的一些不常用的数据存到磁盘,从而空出多余的内存空间。我们通过代码打印出来的地址都是物理内存里面分配的虚拟空间地址。
虚拟内存地址到物理内存地址的映射是通过分页或分段等机制来实现的,当通过虚拟地址空间写入数据时,如果对应的虚拟页在物理内存中,则直接在物理内存中进行修改。如果物理内存满了,操作系统会将不常用的页面换出到磁盘,确保有足够的内存用于当前的写入操作。
15.1 线程初始化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 void func () { cout << "我是子线程,叫做lxx..." << this_thread::get_id () << endl; } void func1 (string name, int age) { cout << "我是子线程,叫做" << name << ",age:" << age << this_thread::get_id () << endl; } int main () { cout << "主线程id: " << this_thread::get_id () << endl; thread t1; thread t2 (func) ; thread t3 (func1, "罗罗诺亚" , 19 ) ; thread t4 ([=](int id) { cout << "arg id:" << id << "thread id:" << this_thread::get_id() << endl; },100 ) ; thread t5 = move (t4); t2.join (); t3.join (); t5.join (); return 0 ; }
15.2 常用的成员函数 1.get_id()
:每个被创建出的线程实例都对应一个线程ID,这个ID是唯一的。通过该函数可用获取线程的ID。
2.join()
:调用这个函数的线程被阻塞,但是子线程对象中的任务函数会继续执行,当任务执行完毕之后join()会清理当前子线程中的相关资源然后返回,同时,调用该函数的线程解除阻塞继续向下执行。
3.detach()
:因为在线程分离之后,主线程退出也会一并销毁创建出的所有子线程,在主线程退出之前,它可以脱离主线程继续独立的运行,任务执行完毕之后,这个子线程会自动释放自己占用的系统资源。
4.joinable()
:该函数用于判断主线程和子线程是否处于关联(连接)状态,一般情况下,二者之间的关系处于关联状态,会返回一个true,否则返回一个false。
5.thread::hardware_concurrency()
:用于获取当前计算机的CPU核心数,根据这个结果在程序中创建出数量相等的线程。
15.3 类的成员函数作为子线程的任务函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Base {public : void showMsg (string name, int age) { cout << "我的名字是:" << name << ",今年 " << age << "岁了..." << endl; } static void message () { cout << "我是罗罗诺亚!!!" << endl; } }; void func () { cout << "hello,欢迎来到新世界!!!" << endl; } int main () { thread t1 (func) ; thread t2 (&Base::message) ; Base b; thread t3 (&Base::showMsg, b, "卡卡罗特" , 23 ) ; thread t4 (bind(&Base::showMsg, b, "卡卡罗特" , 23 )) ; t1.join (); t2.join (); t3.join (); t4.join (); }
16. call_once 在某些特定情况下,某些函数只能在多线程环境下调用一次,比如:要初始化某个对象,而这个对象只能被初始化一次,就可以使用std::call_once()
来保证函数在多线程环境下只能被调用一次。特别注意的是,在使用call_once()
的时候,需要一个once_flag
作为call_once()
的传入参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 once_flag g_flag; void do_once (int a, string b) { cout << "name: " << b << ", age: " << a << endl; } void do_something (int age, string name) { static int num = 1 ; call_once (g_flag, do_once, 19 , "luffy" ); cout << "do_something() function num = " << num++ << endl; } int main () { thread t1 (do_something, 20 , "ace" ) ; thread t2 (do_something, 20 , "sabo" ) ; thread t3 (do_something, 19 , "luffy" ) ; t1.join (); t2.join (); t3.join (); return 0 ; }
结果:
1 2 3 4 name: luffy, age: 19 do_something() function num = 1 do_something() function num = 2 do_something() function num = 3
应用:实现单例模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 once_flag g_flag; class Base {public : Base (const Base& obj) = delete ; Base& operator =(const Base& obj) = delete ; static Base* getInstance () { call_once (g_flag, [&]() { obj = new Base; cout << "实例对象被创建....." << endl; }); return obj; } void setName (string name) { this ->name = name; } string getName () { return name; } private : Base () {}; static Base* obj; string name; }; Base* Base::obj = nullptr ; void myFunc (string name) { Base::getInstance ()->setName (name); cout << "my name is: " << Base::getInstance ()->getName () << endl; } int main () { thread t1 (myFunc, "路飞" ) ; thread t2 (myFunc, "艾斯" ) ; thread t3 (myFunc, "萨博" ) ; thread t4 (myFunc, "索隆" ) ; t1.join (); t2.join (); t3.join (); t4.join (); return 0 ; }
17. C++线程同步之互斥锁 解决多线程数据混乱的方案就是进行线程同步,最常用的就是互斥锁,也可以称为互斥量,其头文件是#include <mutex>
,在C++11中一共提供了四种互斥锁:
std::mutex
:独占的互斥锁,不能递归使用
std::timed_mutex
:带超时的独占互斥锁,不能递归使用
std::recursive_mutex
:递归互斥锁,不带超时功能
std::recursive_timed_mutex
:带超时的递归互斥锁
17.1 std::mutex 成员函数:
1.lock()
:函数用于给临界区加锁,并且只能有一个线程获得锁的所有权,它有阻塞线程的作用。
2.try_lock()
:获取互斥锁的所有权并对互斥锁加锁,它与lock()区别在于try_lock()不会阻塞线程,lock()会阻塞线程。
如果互斥锁是未锁定状态,得到了互斥锁所有权并加锁成功,函数返回true
如果互斥锁是锁定状态,无法得到互斥锁所有权加锁失败,函数返回false
3.unlock()
:给锁定的互斥锁解锁,只有拥有互斥锁所有权的线程也就是对互斥锁上锁的线程才能将其解锁,其它线程是没有权限做这件事情的。
线程同步的目的是让多线程按照顺序依次执行临界区代码,这样做线程对共享资源的访问就从并行访问变为了线性访问,访问效率降低了,但是保证了数据的正确性。
特别需要注意的是:当线程对互斥锁对象加锁,并且执行完临界区代码之后,一定要使用这个线程对互斥锁解锁,否则最终会造成线程的死锁。死锁之后当前应用程序中的所有线程都会被阻塞,并且阻塞无法解除,应用程序也无法继续运行。
测试程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Base {public : void increment (int count) { for (int i = 0 ; i < count; i++) { mx.lock (); ++number; cout << "+++current number: " << number << endl; mx.unlock (); this_thread::sleep_for (chrono::milliseconds (500 )); } } void decrement (int count) { for (int i = 0 ; i < count; i++) { mx.lock (); --number; cout << "---current number: " << number << endl; mx.unlock (); this_thread::yield (); } } private : int number = 999 ; mutex mx; }; int main () { Base b; thread t1 (&Base::increment, &b, 10 ) ; thread t2 (&Base::decrement, &b, 10 ) ; t1.join (); t2.join (); }
17.2 std::lock_guard lock_guard
在使用上面提供的这个构造函数构造对象时,会自动锁定互斥量,而在退出作用域后进行析构时就会自动解锁,从而保证了互斥量的正确操作,避免忘记unlock()
操作而导致线程死锁。lock_guard
使用了RAII
技术,就是在类构造函数中分配资源,在析构函数中释放资源,保证资源出了作用域就释放。
使用:
1 2 3 4 5 6 7 8 9 10 void decrement (int count) { for (int i = 0 ; i < count; i++) { { lock_guard<mutex> guard (mx) ; --number; cout << "---current number: " << number << endl; } this_thread::yield (); } }
17.3 std::recursive_mutex 递归互斥锁std::recursive_mutex
允许同一线程多次获得互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题
测试程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Base {public : void increment (int count) { for (int i = 0 ; i < count; i++) { mx.lock (); ++number; cout << "+++current number: " << number << endl; mx.unlock (); this_thread::sleep_for (chrono::milliseconds (500 )); } } void decrement (int count) { for (int i = 0 ; i < count; i++) { { lock_guard<recursive_mutex>lock_guard (mx); increment (2 ); --number; cout << "---current number: " << number << endl; } this_thread::yield (); } } private : int number = 999 ; recursive_mutex mx; }; int main () { Base b; thread t1 (&Base::increment, &b, 10 ) ; thread t2 (&Base::decrement, &b, 10 ) ; t1.join (); t2.join (); }
17.4 std::timed_mutex std::timed_mutex
是超时独占互斥锁,主要是在获取互斥锁资源时增加了超时等待功能,因为不知道获取锁资源需要等待多长时间,为了保证不一直等待下去,设置了一个超时时长,超时后线程就可以解除阻塞去做其他事情了。
18. 原子变量atomic C++11提供了一个原子类型std::atomic<T>
,通过这个原子类型管理的内部变量就可以称之为原子变量,我们可以给原子类型指定bool
、char
、int
、long
、指针
等类型作为模板参数。
测试程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Base {public : Base (int n, string s) : age (n), name (s) {}; int age; string name; }; int main () { atomic<char >c ('a' ); atomic_char cc ('b' ) ; atomic<int > b; atomic_init (&b, 9 ); cc = 'd' ; b.store (88 ); char ccc = c.exchange ('e' ); Base base (123 , "luffy" ) ; atomic<Base*>atc_base (&base); cout << "c value: " << c << endl; cout << "ccc value: " << ccc << endl; cout << "b value: " << b.load () << endl; Base* tmp = atc_base.load (); cout << "name: " << tmp->name << ",age: " << tmp->age << endl; return 0 ; }
局限性:使用原子变量的时候,它不能够保护复合类型,比如说atomic<Base*> base
,现在base里面存储的就是Base类的指针,Base原子变量里面现在不能保护base这块指针指向的内存块里面的数据安全,它只能保护这个指针做算术运算时的线程安全,即将地址做移动时安全。
原子变量处理线程同步:使用原子变量或互斥量对公共资源进行处理,可以达到数据同步的效果,即以下程序最后可以刚好将number加到200,否则,可能小于200。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Base {public : void increment () { for (int i = 0 ; i < 100 ; i++) { number++; cout << "+++ increment thread id: " << this_thread::get_id () << ",number: " << number << endl; this_thread::sleep_for (chrono::milliseconds (100 )); } } void increment1 () { for (int i = 0 ; i < 100 ; i++) { number++; cout << "*** increment1 thread id: " << this_thread::get_id () << ",number: " << number << endl; this_thread::sleep_for (chrono::milliseconds (50 )); } } private : atomic_int number = 0 ; }; int main () { Base b; thread t1 (&Base::increment, &b) ; thread t2 (&Base::increment1, &b) ; t1.join (); t2.join (); }