文章目录
申明:
这是 C++2.0(C++11 和 C++14) 新特性的笔记,来自于观看过的一些视频教程、网上博文和C++方面的书籍,可能还缺少一些知识,这里只是对现阶段本人所掌握的知识进行整理归纳,日后还会补充。
笔记里的 ctor是构造函数,dctor是析构函数
一、C++版本及一些学习网站
1.1 C++ Standard(规格)的演化
- C++98 1.0版本
- C++03 TR1
- C++11 2.0版本
- C++14 2.0版本
C++2.0新特性包括语言和标准库两个层面
1.2 C++的学习网站
- Compiler Support for C++11 and C++14
- C++11 FAQ, from Stronstrup
- CPlusPlus.com
- CppReference.com
- gcc.gnu.org
二、关键字
2.1 __cplusplus
__cplusplus的作用是检测编译器支持的版本
使用方法:
#include <iostream>
using namespace std;
int main()
{
cout << "支持C++"<< __cplusplus << "版本" << endl;
return 0;
}
/*
#define __cplusplus 201103L (支持C++11)
199711L (C++98)
*/
2.2 alignas
alignas的作用是结构体的一个标识用来声明此结构体最少采用几字节的对齐方式,但若结构体内所包含的元素类型字节数对齐大于声明则采用包含的字节数对齐。
案例:
/*************************************************************************
> File Name: alignas.cpp
> Author: Nfh
> Mail: 1024222310@qq.com
> Created Time: 2020年07月02日 星期四 09时53分41秒
************************************************************************/
#include <iostream>
// alignas的作用是结构体的一个标识
// 用来声明此结构体最少采用几字节的对齐方式
struct alignas(8) S{
};
// 这里就是8字节对齐方式
struct alignas(1) S{
double d;
}
// 这里就是8字节
int main()
{
return 0;
}
2.3 alignof
alignof用来得到结构体的对齐方式,默认取值有:0 1 2 4 8 16 32 64 …
案例:
/*************************************************************************
> File Name: alignof.cpp
> Author: Nfh
> Mail: 1024222310@qq.com
> Created Time: 2020年07月02日 星期四 10时03分43秒
************************************************************************/
#include <iostream>
struct FOO{
int a;
char b;
};
struct alignas(1) S{
double d;
};
// alignof用来得到结构体的对齐方式
// 默认取值有:0 1 2 4 8 16 32 64 ...
int main()
{
std::cout << "char: " << alignof(char) << std::endl;
std::cout << "int* : " << alignof(int*) << std::endl;
std::cout << "FOO: " << alignof(FOO) << std::endl;
std::cout << "S: " << alignof(S) << std::endl;
return 0;
}
2.4 static_assert
static_assert与assert的作用大致相同都是断言。
注意:static_assert是在编译期间进行的断言,不满足则编译不过去。
案例:
/*************************************************************************
> File Name: static_assert.cpp
> Author: Nfh
> Mail: 1024222310@qq.com
> Created Time: 2020年07月03日 星期五 10时17分17秒
************************************************************************/
#include <iostream>
// static_assert与assert的作用大致相同
// 注意:static_assert是在编译期间进行的断言,不满足则编译不过去
void testAssert()
{
static_assert(sizeof(int) == 4, "errno sizeof(int):");
static_assert(sizeof(long) == sizeof(long long),
"errno sizeof: ");
}
int main()
{
return 0;
}
muduo中所用到的:
// 用来判断 epoll 和 poll 所监听的文件描述符的状态标志是否一致
static_assert(EPOLLIN == POLLIN, "errno EPOLLIN == POLLIN");
static_assert(EPOLLPRI == POLLPRI, "errno EPOLLPRI == POLLPRI");
static_assert(EPOLLOUT == POLLOUT, "errno EPOLLOUT == POLLOUT");
static_assert(EPOLLRDHUP == POLLRDHUP, "errno EPOLLRDHUP == POLLRDHUP");
static_assert(EPOLLERR == POLLERR, "errno EPOLLERR == POLLERR");
static_assert(EPOLLHUP == POLLHUP, "errno EPOLLHUP == POLLHUP");
2.5 enum的改进
传统的C语言的enum在当两个枚举中含有相同的属性时编译器是不知道的,
例如:
Color c = red; Hate a = red; 此时编译器就不知道从哪里找red;
所以传统的enum一般定义为:enum Color{C_red, C_yellow, C_blue},这样方便区分。
新的enum语法解决了这个问题
enum class NewColor{red, yellow, blue};
案例:
/*************************************************************************
> File Name: enum.cpp
> Author: Nfh
> Mail: 1024222310@qq.com
> Created Time: 2020年07月02日 星期四 21时26分56秒
************************************************************************/
// enum的扩展
#include <iostream>
// 传统的C的enum在当两个枚举中含有相同的属性时编译器是不知道的
// 例如:
// Color c = red; Hate a = red; 此时编译器就不知道从哪里找red
// 传统的enum一般定义为:enum Color{C_red, C_yellow, C_blue};
enum Color{red, yellow, blue};
enum Hate{red, good, yes};
void testC()
{
Color c = red;
switch(c){
case red:
std::cout << "red" <<"\n"; break;
case yellow:
std::cout << "yellow" << "\n"; break;
case blue:
std::cout << "blue" << "\n"; break;
default:
std::cout << "..." << "\n";
}
int a = c; //可以直接赋值
}
// 新的enum语法解决了这个问题
enum class NewColor{red, yellow, blue};
void testCjj()
{
NewColor c = NewColor::red;
switch(c){
case NewColor::red:
std::cout << "red" <<"\n"; break;
case NewColor::yellow:
std::cout << "yellow" << "\n"; break;
case NewColor::blue:
std::cout << "blue" << "\n"; break;
default:
std::cout << "..." << "\n";
}
int a = static_cast<int>(c); // 这里强转必须使用static_cast
}
int main()
{
return 0;
}
2.6 asm
asm的作用是在程序内内嵌入一部分汇编语言,这个关键字一般人用的很少。
案例:
/*************************************************************************
> File Name: asm.cpp
> Author: Nfh
> Mail: 1024222310@qq.com
> Created Time: 2020年07月02日 星期四 10时38分52秒
************************************************************************/
#include <iostream>
// asm的作用是在程序内内嵌入一部分汇编语言
void showAsm()
{
asm("movq $60, %rax\n\t"
"movq $2, %rdi\n\t"
"syscall");
// 这里的这句汇编等价于 exit(2)
}
int main()
{
return 0;
}
2.7 constexpr
constexpr的作用是 如果某个值在编译期间可以求出值那么就在编译期间求出。
案例:
/*************************************************************************
> File Name: constexpr.cpp
> Author: Nfh
> Mail: 1024222310@qq.com
> Created Time: 2020年07月02日 星期四 11时15分39秒
************************************************************************/
#include <iostream>
int fun(int n)
{
return (n <= 1) ? 1 : (n * fun(n - 1));
}
// constexpr的作用是
// 如果在编译期间可以求出值那么就在编译期间求出。
constexpr int fun0(int n)
{
return (n <= 1) ? 1 : (n * fun0(n - 1));
}
int main()
{
std::cout << "fun(): " << fun(4) << "\n"; //运行期间求出值
std::cout << "fun0():" << fun0(4) << "\n"; //编译期间求出值
char arr[fun0(5)]; //由于是在编译期间求出的值所以这里不会报错
return 0;
}
2.8 explicit
1、在C++2.0以前explicit是针对构造函数一个实参的,现在explicit是针对构造函数一个以上的实参的。
2、explicit的作用是:
不让编译器自动(隐式)调用ctor(构造函数),要在我明确调用时才调用;所以这个关键字一般 用在ctor之前。
例子1:
struct Complex
{
int real;
int imag;
explicit Complex(int re, int im = 0) : real(re), imag(im){}
Complex operator+(const Complex& x)
{
return Complex((real + x.real), (imag + x.imag));
}
};
Complex c1(12, 5);
Complex c2 = c1 + 5; // error, 这里不能隐式转换
/*
这个例子是针对在C++2.0以前explicit是针对构造函数一个实参的,在C++2.0及以后explicit是针对构造函数一个以上的实参的
*/
例子2:
/*************************************************************************
> File Name: explicit.cpp
> Author: Nfh
> Mail: 1024222310@qq.com
> Created Time: 2020年07月02日 星期四 21时43分46秒
************************************************************************/
#include <iostream>
struct A
{
A(int){}
A(int, int){}
// 表示A可以默认的转化为一个int整形数
operator int() const{ return 0; }
}
struct B
{
explicit B(int){}
explicit B(int, int){}
explicit operator int(){ return 0; }
}
void test()
{
A a1 = 1; //YES
B b1 = 1; //NO
A a2(2); //YES
B b2(2); //YES
//...
int na1 = a1; //YES
int nb1 = b2; //NO
int na2 = static_cast<int>(a1); //YES
int nb2 = static_cast<int>(b2); //YES
A a3 = (A)3; //YES
B b3 = (B)3; //YES
}
int main()
{
return 0;
}
2.9 auto
auto关键字:自动类型推倒。
- auto一般用在类型很长或很难记的地方,例如迭代器类型。
- auto也可用获取lambda表达式类型。
/*1.*/
std::list<std::string> stringList;
auto it = find(stringList.begin(), stringList.end(), ..,);
// std::list<std::string>::iterator it = find(stringList.begin(), stringList.end(), ..,);
/*2.*/
auto fun = []{
std::cout << "hello lambda" << std::endl;
}; // 这还是个类型不能使用后面加上()才可调用起来
fun(); // 打印出 hello lambda
2.10 =default =delete
从字面意思理解,default是默认,delete是禁止、删除。
如果你自己定义了一个ctor(构造函数),则编译器就不会给你默认的ctor。
如果你强制加上 =default,就可以重新获得并使用 default ctor。这里的default ctor表示默认构造函数(不带任何实参),可以有多个。
如果加上=delete则表示禁止该函数。
#include <iostream>
using namespace std;
class DataOnly {
public:
DataOnly () {}
~DataOnly () {}
DataOnly (const DataOnly & rhs) = delete; //禁止使用该函数
DataOnly & operator=(const DataOnly & rhs) = delete; //禁止使用该函数
DataOnly (const DataOnly && rhs) =default; //报错,因为默认移动构造不能有多个
DataOnly & operator=(DataOnly && rhs) {}
};
int main(int argc, char *argv[]) {
DataOnly data1;
DataOnly data2(data1); // error: call to deleted constructor of 'DataOnly'
DataOnly data3 = data1; // error: call to deleted constructor of 'DataOnly'
return 0;
}
//=delete 关键字可用于任何函数,不仅仅局限于类的成员函数
void func1() = default; // 报错,default不能用在普通函数上
void func2() = delete; // 可以,但此时无意义
2.11 using
1、using使用的三个地方:
-
命名空间的引用:using name std; using std::cout
-
二义性那里的手动调用: using A::f(使用A的f) using B::f(使用B的f)
-
C++11以后的类型化名:
/*例如*/ using func = void(*fun)(int, int); tmplate<typename T> using myString = std::basic_string<T, std::char_traits<T>>;
2、注意
在C++11以后的类型化名中,用typedef 或 using来表示类型化名没有任何不同:
//例如
typedef void(*func)(int, int);
using func = void(*)(int, int);
/*
以上两种表示情况是相同的。
*/
2.12 override
1、override的字面意思是复写,改写,应用在虚函数上。
2、override的作用是:
当我们在子类中重写虚函数时,如果我们不小心把其参数类型写错,而在原先,编译器是不会给我们报错的,在C++11出现了override关键字;当在虚函数后加上override时,编译器会提醒我们。
3、例子
struct Base
{
virtual void fun(float x){}
};
struct Derivaed : public Base
{
virtual void fun(int x) override // 会报错,编译器提醒
{}
};
2.13 final
1、final的作用:
- 对类修饰:表示此类是最后一个,不会再有下一个类继承此类。
- 对 虚函数修饰:表示不能再子类中对其重写。
2、例子
/*对类*/
struct Base final
{};
struct Derived : public Base
{}; //会报错
/*对虚函数*/
struct Base
{
virtual void fun() final;
};
struct Derived : public Base
{
virtual void fun(){} // 会报错
};
2.14 nullptr
C++让nullptr代替0或NULL。
C++11以前是 #define NULL 0
nullptr 的类型是: std::nullptr_t ;在头文件 #include <cstddef>中。
typedef decltype(nullptr) nullptr_t;
2.15 decltype
1、C++11引入decltype类型说明符,它的作用是选择并返回操作数的数据类型,在此过程中分析表达式并得到它的类型,却不实际计算表达式的值。
通俗来讲:decltype的作用其实就是想得到表达式的类型而不是值。
2、decltype的应用有几方面
-
用来声明一个返回类型。
template <typename T1, typename T2> decltype(x+y) add(T1 x, T2 y);
-
适用于元编程
template <typename T> void test18_(T obj) { typedef typename decltype(obj)::iterator iType;// 这里的decltype(obj)其实就是类型 T。 /* 注意上面的这行语句中在decltype(obj)前加了typename,原因是因为在编译这句话时obj类型是不知道的, 加上typename让编译器确定decltype(obj)::iterator就是一个类型。 */ }
-
用于表示lambda 的类型
auto cmp = [](const Person& p1, const Person& p2){ ... }; std::set<Person, decltype(cmp)> coll(cmp); /* 解释: 面对lambda,我们只有object,没有type,要得到lambda的type,必须借用decltype关键字。 */
2.16 noexcept
1、对于异常这里不做介绍,但简单说几句。
-
异常是一定要被处理的。
-
A调用B,若B抛出异常,且若B不处理,则继续往上面走,到A。(也就是说往源头走直到被处理)
-
若一直未被处理,贼会调用默认的异常处理函数 std::terminate(); 而这个函数又会调用std::abordt()使得程序中断。
2、noexcept的作用是在某种条件下函数不会抛出异常。
3、例子:
void foo()noexcept{} // foo()一定不丢异常
void foo2() noexcept(true){} // foo2()在true的情况向一定不丢异常
void swap(Type& x, Type&y) noexcept(noexcept(x.swap(y))){
x.swap(y);
} // 表示x.swap(y)不丢异常的情况下 ,swap(Type& x, Type&y)一定不丢异常。
2.17 for
C++11 for循环的特殊写法
1、for的新形式
for(decl : coll){
statement;
}
/*
decl: 变量
coll: 数组或容器
statement: 需要执行的语句
*/
作用:利用迭代器从coll每次拿出一个放到decl中。
2、编译器理解
for(auto pos_ = coll.begin(), end_ = coll.end(); pos_ != end_; ++pos_){
decl = *pos_;
statement
}
// or
for(auto pos_ = begin(coll), end_ = end(coll); pos_ != end_; ++pos_){
decl = *pos_;
statement
}
// 第二种用法中的begin()和end()是C++2.0新加的两个全局函数,科目可以放容器。
3、例子
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main()
{
vector<int> vc = {1, 5, 8, 4, 9};
for(auto it : vc)
{
cout << it << endl;
}
return 0;
}
4、一个注意的地方
当一个类的构造函数加上explicit的时候,有时候在使用for循环时就不适合用这种新方法。
class C
{
public:
explicit C(const string& s);
}
vector<string> vc;
for(const C& elem : vc){ // 这里会报错,因为C的ctor添加了explicit关键字,不会隐式调用。
...
}
以上这些关键字及这些关键字的用法是现在我所知的,后续有则继续加上。
三、 lambda表达式
3.1 lambda表达式的介绍
C++11引入了lambda,它能作为一个内嵌的函数,也能当做一个参数或对象来使用。Lambda改变了C++标准库的使用方式。仿函数或函数对象可以由Lambda来代替。例如我们以前在使用排序函数的时候会用到比较大小,我们 可以写一个函数对象,但现在我们可以用Lambda来代替。
3.2 最简单的Lambda
1、>
[]{
std::cout << "hello lambda!" << std::endl;
}; // 这是一个类型,还不能使用。
2、>
[]{
std::cout << "hello lambda!" << std::endl;
}(); // 在其后面加上()把它调用起来,打印出 hello lambda!
3、>一般写成这样
auto I = []{
std::cout << "hello lambda!" << std::endl;
};
I(); // 打印出 hello lambda!
/*
Lambda可以写在某个地方(声明或表达式中)突然、临时想写,就可以用,不用写一个函数来调用。
*/
3.3 完整的lambda表达式
[] () mutable throwSpec -> type{...}
/*
1、[]叫做捕获说明符,里面放的是截取外部变量的方式,表示一个lambda表达式的开始。
2、()普通参数列表,是函数传入的参数
3、mutable表示捕获的变量在函数体内部可否修改。
4、throwSpec表示抛出异常
5、->type表示返回类型,如果没有返回类型,则可以省略这部分。这涉及到c++11的另一特性,参见自动类型推导,最后就是函数体部分。
注意:
mutable throwSpec -> type这三个都是可写可不写,但三个有一个存在,则()必须写,若是三个一个 都没有,则()可写可不写。
*/
[]外部变量的捕获规则
默认情况下,即捕获字段为 [] 时,lambda表达式是不能访问任何外部变量的,即表达式的函数体内无法访问当前作用域下的变量。
[captures]中的“captures””称为“捕获列表”,可以捕获表达式外部作用域的变量,在函数体内部直接使用,这是与普通函数或函数对象最大的不同(C++里的包闭必须显示指定捕获,而lua语言里的则是默认直接捕获所有外部变量。)
捕获列表里可以有多个捕获选项,以逗号分隔,使用了略微“新奇”的语法,规则如下
[] :无捕获,函数体内不能访问任何外部变量
[=] :以值(拷贝)的方式捕获所有外部变量,函数体内可以访问,但是不能修改。
[&] :以引用的方式捕获所有外部变量,函数体内可以访问并修改(需要当心无效的引用);
[var] :以值(拷贝)的方式捕获某个外部变量,函数体可以访问但不能修改。
[&var] :以引用的方式获取某个外部变量,函数体可以访问并修改
[this] :捕获this指针,可以访问类的成员变量和函数,
[=,&var] :引用捕获变量var,其他外部变量使用值捕获。
[&,var] :只捕获变量var,其他外部变量使用引用捕获。
3.4 一些Lambda表达式例子
1、例1:
1、>
#include <iostream>
using namespace std;
int main()
{
int id = 0;
cout << id << endl; // 0
auto f = [id]()mutable{
cout << id << endl;
++id; // 可以++id,有mutable
};
cout << id << endl; // 0
id = 42;
cout << id << endl; // 42
f(); // 0
f(); // 1
return 0;
}
2、>
#include <iostream>
using namespace std;
int main()
{
int id = 0;
cout << id << endl; // 0
auto f = [&id](int p){
cout << id << endl;
++id; // 可以++id,因为是引用
};
cout << id << endl; // 0
id = 42;
cout << id << endl; // 42
f(1); // 42
f(2); // 43
return 0;
}
3、>
#include <iostream>
using namespace std;
int main()
{
int id = 0;
cout << id << endl; // 0
auto f = [id](){
cout << id << endl;
++id; // erron,会报错,因为没有mutable
};
return 0;
}
4、>
#include <iostream>
using namespace std;
int main()
{
int id = 0;
auto f = [id]()mutable->int{
cout << id << endl;
++id;
static int x = 5; // 内部可以声明变量
return x; // 可以返回值
};
int a = f();
cout <<a <<endl;
return 0;
}
2、例2: 编译器对Lambda的理解
编译器对Lambda的理解是这是一个未知的函数对象:
// 拿例1的第一个例子来说,其翻译为函数对象就是:
class Functor
{
public:
void operator()()
{
cout << id << endl;
id++;
}
private:
int id;
};
3、例3: 带有返回类型的
int t = 5;
auto lam = [t](int val)->bool{
return t == val;
};
bool b1 = lam(5); // b1 = 1
bool b2 = lam(7); // b2 = 0
4、 例4: 当做函数对象来使用
int arr[7] = {12, 8, 6, -1, 63, 3, 5};
auto cmp = [](int a1, int a2){
return a1 > a2;
};
sort(arr, arr + 7, cmp);
//或
sort(arr, arr + 7, [](int a1, int a2){
return a1 > a2;
});
5、 例5:通过decltype来获取lambda表达式的类型
auto cmp = [](const Person& p1, const Person& p2){
...
};
std::set<Person, decltype(cmp)> coll(cmp);
/*
解释:
面对lambda,我们只有object,没有type,要得到lambda的type,必须借用decltype关键字。
*/
四、Uniform Initialization(一致性的初始化)
4.1 {}的使用
1、任何初始化动作都可以用一种{} 来解决。
例如:
int values[] = {1, 2, 3} =====> int values[]{1, 2, 3};
complex<double> c(4.0, 3.0) =====> complex<double> c{4.0, 3.0}; //C++标准库的复数库
vector<string> city{"Beijing", "London", "Cologne"};
2、{}的注意
int j{}; // j = 0
int* q{}; // q = nullptr
int x1{5.0}; // 报错 errno
int x2 = {5.4}; // 报错 errno
char c{999}; // 报错 errno
vector<int> v{1, 2.3, 4, 5.6}; // 报错 errno
// 后面的报错原因是用 {} 不允许高向低的转。
4.2 解释{}这种使用方法的内部实现。
这种 {}(一致性初始化)其实是利用一个事实:编译器看到 {t1, t2, t3…tn} 便做出一个 initializer_list,它内部关联一个 array<T, n>(T表示类型,n表示个数)。调用函数(例如ctor)时array内的元素可被编译器分解逐一赋值给函数。但若函数参数是个 initializer_list,调用者应该将多个T参数组装成一个**initializer_list**付入。
所以再来看一下上面的两个案例:
complex<double> c(4.0, 3.0) =====> complex<double> c{4.0, 3.0}; //C++标准库的复数库
/*
因为此类的构造函数无 initializer_list<double> ,所以其内部的array(double, 2)会分开赋值给ctor
*/
vector<string> city{"Beijing", "London", "Cologne"};
/*
这个会形成一个 initializer_list<string> ,内部有个array<string, 3>,调用vector<string> 的
ctor时,编译器会找到其重载的构造函数中有 initializer_list<string>的,来接受这个赋值。
*/
这里说明一下:现在的STL库中大部分容器均用到 initializer_list 。
4.3 **initializer_list**的深入
例1:
void print(std::initializer_list<int> vals)
{
for(auto p = vals.begin(); p != vals.end(); ++p)
{
std::cout << *p << std::endl;
}
}
// 调用
print({12, 3, 5, 8});
/*
将内部{}作为一组传过去。付给initializer_list的一定是一个initializer_list 或者 {...}形式。
*/
例2:
class P
{
public:
P(int a, int b) 1.
{
cout << "a = " << a << " " << b << endl;
}
P(initializer_list<int> initlist) 2.
{
cout << "values = " << endl;
for(auto i : initlist)
{
cout << i << " ";
}
cout << endl;
}
};
P p(77, 5); // 调用1
P q{77, 5}; // 调用2
P r{75, 8, 6}; // 调用2
P s = {5, 8}; // 调用2
/*
若2不存在,则 q、r、s会调用1 ctor进行赋值(拆分赋值)
complex<T>由于没有类似于2ctor的存在,所以其用initializer_list<T>赋值时只能用1这种ctor进行拆分赋 值。
*/
4.4 initializer_list内部的构造函数
接下来我们看一下initializer_list的内部构造函数。
template<class _E>
class initializer_list
{
public:
...
private:
iterator _M_array;
size_type _M_len;
constexpr initializer_list(const_iterator _a, size_type _l)
: _M_array(_a), _M_len(_l)
{
}
public:
...
};
/*
我们分析上述类中的private下的这几行,可以发现其内部确实有个array。
但是当我们对initializer_list<>此类型的值进行拷贝时需要注意,因为其发生的是浅拷贝,也就是两个_M_array指向同一块内存,这是非常危险的,所以我们若是对其拷贝则需要慎重考虑。
*/
五、 左值引用和右值引用
5.1 左值引用
1、先看一下传统的左值引用。
int a = 10;
int &b = a; // 定义一个左值引用变量
b = 20; // 通过左值引用修改引用内存的值
*左值引用在汇编层面其实和普通的指针是一样的;*定义引用变量必须初始化,因为引用其实就是一个别名,需要告诉编译器定义的是谁的引用。
int &var = 10;
上述代码是无法编译通过的,因为10无法进行取地址操作,无法对一个立即数取地址,因为立即数并没有在内存中存储,而是存储在寄存器中,可以通过下述方法解决:
const int &var = 10;
使用常引用来引用常量数字10,因为此刻内存上产生了临时变量保存了10,这个临时变量是可以进行取地址操作的,因此var引用的其实是这个临时变量,相当于下面的操作:
const int temp = 10;
const int &var = temp;
2、根据上述分析,得出如下结论:
- 左值引用要求右边的值必须能够取地址,如果无法取地址,可以用常引用;
但使用常引用后,我们只能通过引用来读取数据,无法去修改数据,因为其被const修饰成常量引用了。
那么C++11 引入了右值引用的概念,使用右值引用能够很好的解决这个问题。
5.2 右值引用
1、C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:
- 可以取地址的,有名字的,非临时的就是左值;
- 不能取地址的,没有名字的,临时的就是右值;
2、左值和右值有哪些
-
立即数,函数返回的值,临时变量等都是右值;
-
非匿名对象(包括变量),函数返回的引用,const对象等都是左值。
从本质上理解,创建和销毁由编译器幕后控制,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象)。
定义右值引用的格式如下:
类型 && 引用名 = 右值表达式;
右值引用是C++ 11新增的特性,所以C++ 98的引用为左值引用。右值引用用来绑定到右值,绑定到右值以后本来会被销毁的右值的生存期会延长至与绑定到它的右值引用的生存期。
int &&var = 10;
int &&var = int(10);
在汇编层面右值引用做的事情和常引用是相同的,即产生临时量来存储常量。但是,唯一 一点的区别是,右值引用可以进行读写操作,而常引用只能进行读操作。
右值引用的存在并不是为了取代左值引用,而是充分利用右值(特别是临时对象)的构造来减少对象构造和析构操作以达到提高效率的目的。
3、用C++实现一个简单的顺序栈:
class Stack
{
public:
// 构造
Stack(int size = 1000)
:msize(size), mtop(0)
{
cout << "Stack(int)" << endl;
mpstack = new int[size];
}
// 析构
~Stack()
{
cout << "~Stack()" << endl;
delete[]mpstack;
mpstack = nullptr;
}
// 拷贝构造
Stack(const Stack &src)
: msize(src.msize), mtop(src.mtop)
{
cout << "Stack(const Stack&)" << endl;
mpstack = new int[src.msize];
for (int i = 0; i < mtop; ++i) {
mpstack[i] = src.mpstack[i];
}
}
// 赋值重载
Stack& operator=(const Stack &src)
{
cout << "operator=" << endl;
if (this == &src)
return *this;
delete[]mpstack;
msize = src.msize;
mtop = src.mtop;
mpstack = new int[src.msize];
for (int i = 0; i < mtop; ++i) {
mpstack[i] = src.mpstack[i];
}
return *this;
}
int getSize()
{
return msize;
}
private:
int *mpstack;
int mtop;
int msize;
};
Stack GetStack(Stack &stack)
{
Stack tmp(stack.getSize());
return tmp;
}
int main()
{
Stack s;
s = GetStack(s);
return 0;
}
运行结果如下:
Stack(int) // 构造s
Stack(int) // 构造tmp
Stack(const Stack&) // tmp拷贝构造main函数栈帧上的临时对象
~Stack() // tmp析构
operator= // 临时对象赋值给s
~Stack() // 临时对象析构
~Stack() // s析构
**为了解决浅拷贝问题,为类提供了自定义的拷贝构造函数和赋值运算符重载函数,并且这两个函数内部实现都是非常的耗费时间和资源(首先开辟较大的空间,然后将数据逐个复制),**我们通过上述运行结果发现了两处使用了拷贝构造和赋值重载,分别是tmp拷贝构造main函数栈帧上的临时对象、临时对象赋值给s,其中tmp和临时对象都在各自的操作结束后便销毁了,使得程序效率非常低下。
那么我们为了提高效率,是否可以把tmp持有的内存资源直接给临时对象?是否可以把临时对象的资源直接给s?
在C++11中,我们可以解决上述问题,方式是提供带右值引用参数的拷贝构造函数和赋值运算符重载函数.
// 带右值引用参数的拷贝构造函数
Stack(Stack &&src)
:msize(src.msize), mtop(src.mtop)
{
cout << "Stack(Stack&&)" << endl;
/*此处没有重新开辟内存拷贝数据,把src的资源直接给当前对象,再把src置空*/
mpstack = src.mpstack;
src.mpstack = nullptr;
}
// 带右值引用参数的赋值运算符重载函数
Stack& operator=(Stack &&src)
{
cout << "operator=(Stack&&)" << endl;
if(this == &src)
return *this;
delete[]mpstack;
msize = src.msize;
mtop = src.mtop;
/*此处没有重新开辟内存拷贝数据,把src的资源直接给当前对象,再把src置空*/
mpstack = src.mpstack;
src.mpstack = nullptr;
return *this;
}
运行结果如下:
Stack(int) // 构造s
Stack(int) // 构造tmp
Stack(Stack&&) // 调用带右值引用的拷贝构造函数,直接将tmp的资源给临时对象
~Stack() // tmp析构
operator=(Stack&&) // 调用带右值引用的赋值运算符重载函数,直接将临时对象资源给s
~Stack() // 临时对象析构
~Stack() // s析构
程序自动调用了带右值引用的拷贝构造函数和赋值运算符重载函数,使得程序的效率得到了很大的提升,因为并没有重新开辟内存拷贝数据。
mpstack = src.mpstack;
可以直接赋值的原因是临时对象即将销毁,不会出现浅拷贝的问题,我们直接把临时对象持有的资源赋给新对象就可以了。
所以,临时变量都会自动匹配右值引用版本的成员方法,旨在提高内存资源使用效率。
带右值引用参数的拷贝构造和赋值重载函数,又叫移动构造函数和移动赋值函数,这里的移动指的是把临时量的资源移动给了当前对象,临时对象就不持有资源,为nullptr了,实际上没有进行任何的数据移动,没发生任何的内存开辟和数据拷贝。
5.3 move构造与move赋值
1、实现MyString的move构造和move赋值
/*move构造*/
MyString(const MyString&& str)
: data_(str.data_), len_(str.len_)
{
str.len_ = 0;
str.data_ = nullptr;
}
/*move赋值*/
MyString& operator=(const MyString&& str)
{
if(this != &str)
{
if(data_ != nullptr)
delete data_;
len_ = str.len_;
data_ = str.data_;
str.len_ = 0;
str.data_ = nullptr;
}
return *this;
}
2、左值也可调用move ctor
若想让左值也可调用move ctor,则C++11 提供了: std::move() 函数。
MyString s1("hello");
MyString s2(std::move(s1));
六、C++11 bind和function用法
6.1 function的用法
function是一个template,定义于头文件functional中。function<int(int, int)>通过声明一个function类型,它是“接受两个int参数、返回一个int类型”的可调用对象,这里可调用对象可以理解为函数指针(指针指向一个函数,该函数有两个int类型参数,返回int类型,即:int (*p)(int, int) )。
可调用对象:对于一个对象或表达式,如果可以对其使用调用运算符,则称该对象或表达式为可调用对象。
C++语言中有几种可调用对象:
-
函数
-
函数指针
-
lambda表达式
-
bind创建的对象以及重载了函数调用运算符的类
和其他对象一样,可调用对象也有类型。例如,每个lambda有它自己唯一的(未命名)类类型;函数及函数指针的类型则由其返回值类型和实参类型决定。
function的用法:
1、》保存普通函数
void printA(int a)
{
cout << a << endl;
}
std::function<void(int a)> func;
func = printA;
func(2); //
2、》保存lambda表达式
std::function<void()> func_1 = [](){cout << "hello world" << endl;};
func_1(); //hello world
3、》保存成员函数
class Foo{
Foo(int num) : num_(num){}
void print_add(int i) const {cout << num_ + i << endl;}
int num_;
};
//保存成员函数
std::function<void(const Foo&,int)> f_add_display = &Foo::print_add;
Foo foo(2);
f_add_display(foo,1);
//在实际使用中,可使用auto关键字。
6.2 关于bind的用法
可将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。(我们可以理解bind函数的作用是接收一个可调用对象生成一个新的可调用对象)
调用bind的一般形式:auto newCallable = bind(callable,arg_list);
其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。即,当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。 from 《C++ primer》第五版
看代码:
#include <iostream>
#include <functional>
using namespace std;
class A
{
public:
void fun_3(int k,int m)
{
cout<<"print: k="<<k<<",m="<<m<<endl;
}
};
void fun_1(int x,int y,int z)
{
cout<<"print: x=" <<x<<",y="<< y << ",z=" <<z<<endl;
}
void fun_2(int &a,int &b)
{
a++;
b++;
cout<<"print: a=" <<a<<",b="<<b<<endl;
}
int main(int argc, char * argv[])
{
//f1的类型为 function<void()>
auto f1 = std::bind(fun_1,1,2,3); //表示绑定函数 fun 的第一,二,三个参数值为: 1 2 3
f1(); //print: x=1,y=2,z=3
auto f2 = std::bind(fun_1, placeholders::_1,placeholders::_2,3);
//表示绑定函数 fun 的第三个参数为 3,而fun 的第一,二个参数分别由调用 f2 的第一,二个参数指定
f2(1,2);//print: x=1,y=2,z=3
auto f3 = std::bind(fun_1,placeholders::_2,placeholders::_1,3);
//表示绑定函数 fun 的第三个参数为 3,而fun 的第一,二个参数分别由调用 f3 的第二,一个参数指定
//注意: f2 和 f3 的区别。
f3(1,2);//print: x=2,y=1,z=3
int m = 2;
int n = 3;
auto f4 = std::bind(fun_2, placeholders::_1, n); //表示绑定fun_2的第一个参数为n, fun_2的第二个参数由调用f4的第一个参数(_1)指定。
f4(m); //print: m=3,n=4
cout<<"m="<<m<<endl;//m=3 说明:bind对于不事先绑定的参数,通过std::placeholders传递的参数是通过引用传递的,如m
cout<<"n="<<n<<endl;//n=3 说明:bind对于预先绑定的函数参数是通过值传递的,如n
A a;
//f5的类型为 function<void(int, int)>
auto f5 = std::bind(&A::fun_3, &a,placeholders::_1,placeholders::_2); //使用auto关键字
f5(10,20);//调用a.fun_3(10,20),print: k=10,m=20
std::function<void(int,int)> fc = std::bind(&A::fun_3, &a,std::placeholders::_1,std::placeholders::_2);
fc(10,20); //调用a.fun_3(10,20) print: k=10,m=20
return 0;
}
七、C++11的thread
7.1 C++ 11 提供的 std::thread 类
1、无论是 Linux 还是 Windows 上创建线程的 API,都有一个非常不方便的地方,就是线程函数的签名必须是固定的格式(参数个数和类型、返回值类型都有要求)。C++11 新标准引入了一个新的类 std::thread(需要包含头文件),使用这个类的可以将任何签名形式的函数作为线程函数。以下代码分别创建两个线程,线程函数签名不一样:
#include <stdio.h>
#include <thread>
void threadproc1()
{
while (true)
{
printf("I am New Thread 1!\n");
}
}
void threadproc2(int a, int b)
{
while (true)
{
printf("I am New Thread 2!\n");
}
}
int main()
{
//创建线程t1
std::thread t1(threadproc1);
//创建线程t2
std::thread t2(threadproc2, 1, 2);
//利用lambda表达式传入
std::thread t3([=]{threadproc2(1, 2);});
while (true)
{
//Sleep(1000);
//权宜之计,让主线程不要提前退出
}
return 0;
}
2、当然, std::thread 在使用上容易犯一个错误,即在 std::thread 对象在线程函数运行期间必须是有效的。什么意思呢?我们来看一个例子:
#include <stdio.h>
#include <thread>
void threadproc()
{
while (true)
{
printf("I am New Thread!\n");
}
}
void func()
{
std::thread t(threadproc);
}
int main()
{
func();
while (true)
{
//Sleep(1000);
//权宜之计,让主线程不要提前退出
}
return 0;
}
上述代码在 func 中创建了一个线程,然后又在 main 函数中调用 func 方法,乍一看好像代码没什么问题,但是在实际运行时程序会崩溃。崩溃的原因是,当 func 函数调用结束后,func 中局部变量 t (线程对象)被销毁了,而此时线程函数仍然在运行。这就是我所说的,使用 std::thread 类时,必须保证线程函数运行期间,其线程对象有效。这是一个很容易犯的错误,解决这个问题的方法是,std::thread 对象提供了一个 detach 方法,这个方法让线程对象与线程函数脱离关系,这样即使线程对象被销毁,仍然不影响线程函数的运行。我们只需要在在 func 函数中调用 detach 方法即可,代码如下:
//其他代码保持不变,这里就不重复贴出来了
void func()
{
std::thread t(threadproc);
t.detach();
}
然而,在实际编码中,这也是一个不推荐的做法,原因是我们需要使用线程对象去控制和管理线程的运行和生命周期。所以,我们的代码应该尽量保证线程对象在线程运行期间有效,而不是单纯地调用 detach 方法使线程对象与线程函数的运行分离。
7.2 std::thread 获取线程id的方法
C++11的线程库可以使用 std::this_thread 类的 get_id 获取当前线程的 id,这是一个类静态方法。
当然也可以使用 std::thread 的 get_id 获取指定线程的 id,这是一个类实例方法。
但是 get_id 方法返回的是一个包装类型的 std::thread::id 对象,不可以直接强转成整型,也没有提供任何转换成整型的接口。所以,我们一般使用 std::cout 这样的输出流来输出,或者先转换为 std::ostringstream 对象,再转换成字符串类型,然后把字符串类型转换成我们需要的整型。案例代码如下:
#include <iostream>
#include <thread>
#include <unistd.h>
#include <sstream>
void func1()
{
while(true){
sleep(1);
std::cout << "I am thread 1" << std::endl;
}
}
void func2(int a)
{
while(true){
sleep(1);
std::cout << "I am thread 2" << std::endl;
}
}
int main()
{
//创建线程1
std::thread t1(func1);
//获取线程ID的方法1
std::thread::id work_id1 = t1.get_id(); //返回值是std::thread::id;
std::cout << "thread id1 = " << work_id1 << std::endl;
//创建线程2
std::thread t2(func2, 2);
//获取线程ID的方法2
std::thread::id main_id = std::this_thread::get_id(); //获取主线程的ID(当前线程)
//将生成的id转化为整形
std::ostringstream oss;
oss << main_id;
std::string st = oss.str();
std::cout << "main thread id = " << st << std::endl;
while(true){
}
/**
C++ 11 的 std::thread 统一了 Linux 和 Windows 的线程创建函数,提供等待线程退出的接口,
std::thread 的 join 方法就是用来等待线程退出的函数。使用这个函数时,必须保证该线程还处于运行中状态,
也就是说等待的线程必须是可以 “join”的,如果需要等待的线程已经退出,此时调用join 方法,程序会产生崩溃。
因此,C++ 11 的线程库同时提供了一个 joinable 方法来判断某个线程是否可以join,
如果不确定线程是否可以”join”,可以先调用 joinable 函数判断一下是否需要等待。
*/
if(t1.joinable()) //判断线程是否可以join
t1.join();
if(t2.joinable())
t2.join();
return 0;
}
7.3 std::thread的joinable方法的作用
C++ 11 的 std::thread 统一了 Linux 和 Windows 的线程创建函数,提供等待线程退出的接口,std::thread 的 join 方法就是用来等待线程退出的函数。使用这个函数时,必须保证该线程还处于运行中状态,也就是说等待的线程必须是可以 “join”的,如果需要等待的线程已经退出,此时调用join 方法,程序会产生崩溃。因此,C++ 11 的线程库同时提供了一个 joinable 方法来判断某个线程是否可以join,如果不确定线程是否可以”join”,可以先调用 joinable 函数判断一下是否需要等待。
案例代码如下:
if(t1.joinable()) //判断线程是否可以join
t1.join();
if(t2.joinable())
t2.join();
八、C++11的线程同步
在 C/C++ 语言中直接使用操作系统提供的多线程资源同步 API 虽然功能强大,但毕竟存在诸多限制,且同样的代码却不能同时兼容 Windows 和 Linux 两个平台;再者 C/C++ 这种传统语言的使用份额正在被 Java、python、go 等语言慢慢蚕食,很大一部分原因是 C/C++ 这门编程语言在一些功能上缺少“完备性”,如对线程同步技术的支持,而这些功能在像 Java、python、go 中是标配。因此 C++ 11 标准新加入了很多现代语言标配的东西,其中线程资源同步对象就是其中很重要的一部分。 C++ 11 标准中新增的用于线程同步的 std::mutex 和 std::condition_variable 对象的用法,有了它们我们就可以写出跨平台的多线程程序了。
8.1 std::mutex系列
C++ 11/14/17 中提供了如下 mutex 系列类型:
互斥量 | 版本 | 作用 |
---|---|---|
mutex | C++11 | 最基本的互斥量 |
timed_mutex | C++11 | 有超时机制的互斥量 |
recursive_mutex | C++11 | 可重入的互斥量 |
recursive_timed_mutex | C++11 | 结合 timed_mutex 和 recursive_mutex 特点的互斥量 |
shared_timed_mutex | C++14 | 具有超时机制的可共享互斥量 |
shared_mutex | C++17 | 共享的互斥量 |
这个系列的对象均提供了**加锁(lock)、尝试加锁(trylock)和解锁(unlock)**的方法,我们以 std::mutex 为例来看一段示例代码:
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex g_num_mutex;
int g_num = 0; //共享全局变量
void ThreadDeal(int id)
{
for(int i = 0; i < 3; i++){
g_num_mutex.lock(); //加锁
g_num++;
std::cout << id << " => " << g_num << std::endl;
g_num_mutex.unlock(); //解锁
//设置线程睡眠时间,类似于C语言的 sleep(1);
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
// 这里在介绍一个函数:
// std::this_thread::yield();
// 该函数的作用是把当前线程所占用的CPU释放出来。
int main()
{
std::thread t1(ThreadDeal, 0);
std::thread t2(ThreadDeal, 1);
t1.join();
t2.join();
return 0;
}
/*
注意:如果在 Linux 下编译和运行程序,在编译时你需要链接 pthread 库,否则能够正常编译但是运行时程序会崩溃,崩溃原因:
terminate called after throwing an instance of ‘std::system_error’
what(): Enable multithreading to use std::thread: Operation not permitted
*/
8.2 更灵活的互斥量管理
为了避免死锁, std::mutex.lock() 和 std::mutex::unlock() 方法需要成对使用,但是如上文介绍的如果一个函数中有很多出口,而互斥体对象又是需要在整个函数作用域保护的资源,那么在编码时因为忘记在某个出口处调用 std::mutex.unlock 而造成死锁,上文中推荐使用利用 RAII 技术封装这两个接口,其实 C++ 11 标准也想到了整个问题,因为已经为我们提供了如下封装:
互斥量管理 | 版本 | 作用 |
---|---|---|
lock_guard | C++11 | 基于作用域的互斥量管理 |
unique_lock | C++11 | 更加灵活的互斥量管理 |
shared_lock | C++14 | 共享互斥量的管理 |
scope_lock | C++17 | 多互斥量避免死锁的管理 |
8.3 C++ lock_guard 互斥锁
概述
根据对象的析构函数自动调用的原理,c++11推出了std::lock_guard自动释放锁,其原理是:声明一个局部的lock_guard对象,在其构造函数中进行加锁,在其析构函数中进行解锁。最终的结果就是:在定义该局部对象的时候加锁(调用构造函数),出了该对象作用域的时候解锁(调用析构函数)。
使用方法
1.首先需要包含mutex头文件
2.然后创建一个锁 std::mutex mutex
3.在需要被加锁的作用域内 将mutex传入到创建的std::lock_guard局部对象中
我们这里以 std::lock_guard 为例:
void func()
{
std::lock_guard<std::mutex> guard(mymutex);
//在这里放被保护的资源操作
}
/*
mymutex 的类型是 std::mutex,在 guard 对象的构造函数中,会自动调用 mymutex.lock() 方法加锁,当该函数出了作用域后,调用 guard 对象时析构函数时会自动调用 mymutex.unlock() 方法解锁。
*/
注意: mymutex 生命周期必须长于函数 func 的作用域,很多人在初学这个利用 RAII 技术封装的 std::lock_guard 对象时,可能会写出这样的代码:
//错误的写法,这样是没法在多线程调用该函数时保护指定的数据的。
void func()
{
std::mutex m;
std::lock_guard<std::mutex> guard(m);
//在这里放被保护的资源操作
}
示例代码:
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex g_num_mutex;
int g_num = 0;
void ThreadDeal(int id)
{
for(int i = 0; i < 3; i++){
{
//使用std::lock_guard防止忘记调用unlock()
std::lock_guard<std::mutex> guard(g_num_mutex);
g_num++;
std::cout << id << " => " << g_num << std::endl;
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
int main()
{
std::thread t1(ThreadDeal, 0);
std::thread t2(ThreadDeal, 1);
t1.join();
t2.join();
return 0;
}
另外,如果一个 std::mutex 对象已经调用了 lock() 方法,再次调用时,其行为是未定义的,这是一个错误的做法。所谓“行为未定义”即在不同平台上可能会有不同的行为。
#include <mutex>
int main()
{
std::mutex m;
m.lock();
m.lock();
m.unlock();
return 0;
}
/*
实际测试时,上述代码重复调用 std::mutex.lock() 方法在 Windows 平台上会引起程序崩溃。
上述代码在 Linux 系统上运行时会阻塞在第二次调用 std::mutex.lock() 处。
不管怎样,对一个已经调用 lock() 方法再次调用 lock() 方法的做法是错误的,我们实际开发中要避免这么做。
*/
8.4 C++11多线程 unique_lock详解
1、unique_lock取代lock_guard
unique_lock是个类模板,工作中,一般lock_guard(推荐使用);lock_guard取代了mutex的lock()和unlock();
unique_lock比lock_guard灵活很多,效率上差一点,内存占用多一点。
2、 unique_lock的第二个参数
lock_guard可以带第二个参数:
std::lock_guard<std::mutex> sbguard1(my_mutex1, std::adopt_lock);// std::adopt_lock标记作用;
-
std::adopt_lock
表示这个互斥量已经被lock了(你必须要把互斥量提前lock了 ,否者会报异常);std::adopt_lock标记的效果就是假设调用一方已经拥有了互斥量的所有权(已经lock成功了);通知lock_guard不需要再构造函数中lock这个互斥量了。
unique_lock也可以带std::adopt_lock标记,含义相同,就是不希望再unique_lock()的构造函数中lock这mutex。
用std::adopt_lock的前提是,自己需要先把mutex lock上;用法与lock_guard相同。
-
std::try_to_lock
我们会尝试用mutex的lock()去锁定这个mutex,但如果没有锁定成功,我也会立即返回,并不会阻塞在那里;
用这个try_to_lock的前提是你自己不能先lock。实例代码如下:
#include<iostream> #include<thread> #include<string> #include<vector> #include<list> #include<mutex> using namespace std; class A { public: void inMsgRecvQueue() { for (int i = 0; i < 10000; i++) { cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl; { std::unique_lock<std::mutex> sbguard(my_mutex, std::try_to_lock); if (sbguard.owns_lock()) { //拿到了锁 msgRecvQueue.push_back(i); //... //其他处理代码 } else { //没拿到锁 cout << "inMsgRecvQueue()执行,但没拿到锁头,只能干点别的事" << i << endl; } } } } bool outMsgLULProc(int &command) { my_mutex.lock();//要先lock(),后续才能用unique_lock的std::adopt_lock参数 std::unique_lock<std::mutex> sbguard(my_mutex, std::adopt_lock); std::chrono::milliseconds dura(20000); std::this_thread::sleep_for(dura); //休息20s if (!msgRecvQueue.empty()) { //消息不为空 int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在 msgRecvQueue.pop_front();//移除第一个元素。但不返回; return true; } return false; } //把数据从消息队列取出的线程 void outMsgRecvQueue() { int command = 0; for (int i = 0; i < 10000; i++) { bool result = outMsgLULProc(command); if (result == true) { cout << "outMsgRecvQueue()执行,取出一个元素" << endl; //处理数据 } else { //消息队列为空 cout << "inMsgRecvQueue()执行,但目前消息队列中为空!" << i << endl; } } cout << "end!" << endl; } private: std::list<int> msgRecvQueue;//容器(消息队列),代表玩家发送过来的命令。 std::mutex my_mutex;//创建一个互斥量(一把锁) }; int main() { A myobja; std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja); std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja); myOutMsgObj.join(); myInMsgObj.join(); cout << "主线程执行!" << endl; return 0; } 用std::defer_lock的前提是,你不能自己先lock,否则会报异常
-
std::defer_lock
用std::defer_lock的前提是,你不能自己先lock,否则会报异常
std::defer_lock的意思就是并没有给mutex加锁:初始化了一个没有加锁的mutex。
我们借着defer_lock的话题,来介绍一些unique_lock的重要成员函数
3、unique_lock的成员函数
lock() 加锁 unlock()解锁
defer_lock、lock()与unlock() 实例代码 如下 :
void inMsgRecvQueue()
{
for (int i = 0; i < 10000; i++)
{
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
std::unique_lock<std::mutex> sbguard(my_mutex, std::defer_lock);//没有加锁的my_mutex
sbguard.lock();//咱们不用自己unlock
//处理共享代码
//因为有一些非共享代码要处理
sbguard.unlock();
//处理非共享代码要处理。。。
sbguard.lock();
//处理共享代码
msgRecvQueue.push_back(i);
//...
//其他处理代码
sbguard.unlock();//画蛇添足,但也可以
}
}
try_lock()
尝试给互斥量加锁,如果拿不到锁,返回false,如果拿到了锁,返回true,这个函数是不阻塞的;实例代码如下:
void inMsgRecvQueue()
{
for (int i = 0; i < 10000; i++)
{
std::unique_lock<std::mutex> sbguard(my_mutex, std::defer_lock);//没有加锁的my_mutex
if (sbguard.try_lock() == true)//返回true表示拿到锁了
{
msgRecvQueue.push_back(i);
//...
//其他处理代码
}
else
{
//没拿到锁
cout << "inMsgRecvQueue()执行,但没拿到锁头,只能干点别的事" << i << endl;
}
}
}
release()
返回它所管理的mutex对象指针,并释放所有权;也就是说,这个unique_lock和mutex不再有关系。严格区分unlock()与release()的区别,不要混淆。
如果原来mutex对像处于加锁状态,你有责任接管过来并负责解锁。(release返回的是原始mutex的指针)。实例代码如下:
void inMsgRecvQueue()
{
for (int i = 0; i < 10000; i++)
{
std::unique_lock<std::mutex> sbguard(my_mutex);
std::mutex *ptx = sbguard.release(); //现在你有责任自己解锁了
msgRecvQueue.push_back(i);
ptx->unlock(); //自己负责mutex的unlock了
}
}
为什么有时候需要unlock();因为你lock()锁住的代码段越少,执行越快,整个程序运行效率越高。有人也把锁头锁住的代码多少成为锁的粒度,粒度一般用粗细来描述;
a)锁住的代码少,这个粒度叫细,执行效率高;
b)锁住的代码多,这个粒度叫粗,执行效率低;
要学会尽量选择合适粒度的代码进行保护,粒度太细,可能漏掉共享数据的保护,粒度太粗,影响效率。
选择合适的粒度是高级程序员能力和实力的体现;
4、unique_lock所有权的传递
std::unique_lockstd::mutex sbguard(my_mutex);//所有权概念
sbguard拥有my_mutex的所有权;sbguard可以把自己对mutex(my_mutex)的所有权转移给其他的unique_lock对象;
所以unique_lock对象这个mutex的所有权是可以转移,但是不能复制。
std::unique_lockstd::mutex sbguard1(my_mutex);
std::unique_lockstd::mutex sbguard2(sbguard1);//此句是非法的,复制所有权是非法的
std::unique_lock<std::mutex> sbguard2(std::move(sbguard));//移动语义,现在先当与sbguard2与my_mutex绑定到一起了
//现在sbguard1指向空,sbguard2指向了my_mutex
方法1 :std::move()
方法2:return std:: unique_lockstd::mutex 代码如下:
std::unique_lock<std::mutex> rtn_unique_lock()
{
std::unique_lock<std::mutex> tmpguard(my_mutex);
return tmpguard;//从函数中返回一个局部的unique_lock对象是可以的。三章十四节讲解过移动构造函数。
//返回这种举报对象tmpguard会导致系统生成临时unique_lock对象,并调用unique_lock的移动构造函数
}
void inMsgRecvQueue()
{
for (int i = 0; i < 10000; i++)
{
std::unique_lock<std::mutex> sbguard1 = rtn_unique_lock();
msgRecvQueue.push_back(i);
}
}
8.5 STD::SHARED_MUTEX(C++17的互斥量)
C++ 11 标准让很多开发者诟病的原因之一是,C++ 新标准借鉴 boost 库的 boost::mutex、boost::shared_mutex 而引入 std::mutex 和 std::shared_mutex,但是在 C++11 中只引入了 std::mutex,直到 C++ 17 才有 std::shared_mutex,这让只能使用仅支持 C++11 标准的编译器(例如 Visual Studio 2013,gcc/g++ 4.8)的开发者非常不方便。
商业项目中一般不会轻易升级编译器,因为商业项目一般牵涉的代码范围较大,升级编译器后可能导致大量旧的文件需要修改,例如对于被广泛使用的 CentOS 7.0,其自带的 gcc 编译器是 4.8,升级 gcc 的同时会导致系统自带的 glibc 库发生变化,导致系统中大量其他程序无法运行。因此,实际的商业项目中,升级旧的开发环境是非常慎重的。
std::shared_mutex 底层实现主要原理是操作系统提供的读写锁,也就是说,在存在多个线程对共享资源读、少许线程对共享资源写的情况下,std::shared_mutex 比 std::mutex 效率更高。
std::shared_mutex 提供了 lock() 和 unlock() 方法获取写锁和解除写锁,提供了 lock_shared() 和 unlock_shared() 方法获取读锁和解除读锁,写锁模式我们称为排他锁(Exclusive Locking),读锁模式我们称为共享锁(Shared Locking)。
另外,C++ 新标准中引入与 std::shared_mutex 配合使用的 std::unique_lock、std::shared_lock 两个对象用于出了锁进入作用域自动加锁、出了作用域自动解除锁,前者用于加解 std::shared_mutex 的写锁,后者用于加解 std::shared_mutex 的读锁。
注意:std::unique_lock 在 C++11 引入,std::shared_lock 在 C++14 引入。
下面是对共享资源存在多个读线程和一个写线程,分别使用 std::mutex 和 std::shared_mutex 做的一个性能测试,测试代码如下:
/**
* std::shared_mutex与std::mutex的性能对比
* zhangyl 2016.11.10
*/
//读线程数量
#define READER_THREAD_COUNT 8
//最大循环次数
#define LOOP_COUNT 5000000
#include <iostream>
#include <mutex>
#include <shared_mutex>
#include <thread>
class shared_mutex_counter {
public:
shared_mutex_counter() = default;
~shared_mutex_counter() = default;
//使用std::shared_mutex,同一时刻多个读线程可以同时访问value_值
unsigned int get() const {
//注意:这里使用std::shared_lock
std::shared_lock<std::shared_mutex> lock(mutex_);
return value_;
}
//使用std::shared_mutex,同一个时刻仅有一个写线程可以修改value_值
void increment() {
//注意:这里使用std::unique_lock
std::unique_lock<std::shared_mutex> lock(mutex_);
value_++;
}
//使用std::shared_mutex,同一个时刻仅有一个写线程可以重置value_值
void reset() {
//注意:这里使用std::unique_lock
std::unique_lock<std::shared_mutex> lock(mutex_);
value_ = 0;
}
private:
mutable std::shared_mutex mutex_;
//value_是多个线程的共享资源
unsigned int value_ = 0;
};
class mutex_counter {
public:
mutex_counter() = default;
~mutex_counter() = default;
//使用std::mutex,同一时刻仅有一个线程可以访问value_的值
unsigned int get() const {
std::unique_lock<std::mutex> lk(mutex_);
return value_;
}
//使用std::mutex,同一时刻仅有一个线程可以修改value_的值
void increment() {
std::unique_lock<std::mutex> lk(mutex_);
value_++;
}
private:
mutable std::mutex mutex_;
//value_是多个线程的共享资源
unsigned int value_ = 0;
};
//测试std::shared_mutex
void test_shared_mutex()
{
shared_mutex_counter counter;
int temp;
//写线程函数
auto writer = [&counter]() {
for (int i = 0; i < LOOP_COUNT; i++) {
counter.increment();
}
};
//读线程函数
auto reader = [&counter, &temp]() {
for (int i = 0; i < LOOP_COUNT; i++) {
temp = counter.get();
}
};
//存放读线程对象指针的数组
std::thread** tarray = new std::thread*[READER_THREAD_COUNT];
//记录起始时间
clock_t start = clock();
//创建READER_THREAD_COUNT个读线程
for (int i = 0; i < READER_THREAD_COUNT; i++)
{
tarray[i] = new std::thread(reader);
}
//创建一个写线程
std::thread tw(writer);
for (int i = 0; i < READER_THREAD_COUNT; i++)
{
tarray[i]->join();
}
tw.join();
//记录起始时间
clock_t end = clock();
printf("[test_shared_mutex]\n");
printf("thread count: %d\n", READER_THREAD_COUNT);
printf("result: %d cost: %dms temp: %d \n", counter.get(), end - start, temp);
}
//测试std::mutex
void test_mutex()
{
mutex_counter counter;
int temp;
//写线程函数
auto writer = [&counter]() {
for (int i = 0; i < LOOP_COUNT; i++) {
counter.increment();
}
};
//读线程函数
auto reader = [&counter, &temp]() {
for (int i = 0; i < LOOP_COUNT; i++) {
temp = counter.get();
}
};
//存放读线程对象指针的数组
std::thread** tarray = new std::thread*[READER_THREAD_COUNT];
//记录起始时间
clock_t start = clock();
//创建READER_THREAD_COUNT个读线程
for (int i = 0; i < READER_THREAD_COUNT; i++)
{
tarray[i] = new std::thread(reader);
}
//创建一个写线程
std::thread tw(writer);
for (int i = 0; i < READER_THREAD_COUNT; i++)
{
tarray[i]->join();
}
tw.join();
//记录结束时间
clock_t end = clock();
printf("[test_mutex]\n");
printf("thread count:%d\n", READER_THREAD_COUNT);
printf("result:%d cost:%dms temp:%d \n", counter.get(), end - start, temp);
}
int main() {
//为了排除测试程序的无关因素,测试时只开启一个
test_mutex();
//test_shared_mutex();
return 0;
}
/*
如果条件允许,建议读者认真甄别实际场景,可以使用 std::shared_mutex 去替代部分 std::mutex,以提高程序执行效率。
*/
8.6 std::condition_variable(C++11的条件变量)
C++ 11 提供了 std::condition_variable 这个类代表条件变量,与 Linux 系统原生的条件变量一样,同时提供了等待条件变量满足的 wait 系列方法(wait、wait_for、wait_until 方法),发送条件信号使用 notify 方法(notify_one 和 notify_all 方法),当然使用 std::condition_variable 对象时需要绑定一个 std::unique_lock 或 std::lock_guard 对象。
//C++ 11 中 std::condition_variable 不再需要显式调用方法初始化和销毁。
采用C++11的生产者消费者案例:
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
#include <list>
#include <condition_variable>
class Task
{
public:
Task(int id) : m_id(id){}
void PrintThreadTask()
{
std::cout << "ThreadID:[" << std::this_thread::get_id() << "], TaskID: [" << m_id << "]" << std::endl;
}
private:
int m_id;
};
std::mutex g_task_mutex;
std::list<Task*> TaskList;
std::condition_variable g_task_cv;
void *ProductThread()
{
int id = 0;
Task *ts = nullptr;
while(true){
ts = new Task(id);
//加作用域缩小锁的粒度
{
std::unique_lock<std::mutex> unique(g_task_mutex);
TaskList.push_back(ts);
std::cout << "productThread -> " << id << std::endl;
}
//释放信号量,通知消费者线程
g_task_cv.notify_one();
id ++;
//休眠1秒
std::this_thread::sleep_for(std::chrono::seconds(1));
}
return nullptr;
}
void *ConsumerThread()
{
Task *ts = nullptr;
while(true){
//加作用域缩小锁的粒度
{
std::unique_lock<std::mutex> unique(g_task_mutex);
while(TaskList.empty()){
g_task_cv.wait(unique);
}
ts = TaskList.front();
TaskList.pop_front();
}
if (ts == nullptr)
continue;
ts->PrintThreadTask();
delete ts;
ts = nullptr;
//std::this_thread::sleep_for(std::chrono::seconds(1));
}
return nullptr;
}
int main()
{
std::thread ct1(ConsumerThread);
std::thread ct2(ConsumerThread);
std::thread ct3(ConsumerThread);
std::thread ct4(ConsumerThread);
std::thread ct5(ConsumerThread);
std::thread pt(ProductThread);
pt.join();
ct1.join();
ct2.join();
ct3.join();
ct4.join();
ct5.join();
return 0;
}
九、智能指针类
C/C++ 语言最为人所诟病的特性之一就是存在内存泄露问题,因此后来的大多数语言都提供了内置内存分配与释放功能,有的甚至干脆对语言的使用者屏蔽了内存指针这一概念。这里不置贬褒,手动分配内存与手动释放内存有利也有弊,自动分配内存和自动释放内存亦如此,这是两种不同的设计哲学。有人认为,内存如此重要的东西怎么能放心交给用户去管理呢?而另外一些人则认为,内存如此重要的东西怎么能放心交给系统去管理呢?在 C/C++ 语言中,内存泄露的问题一直困扰着广大的开发者,因此各类库和工具的一直在努力尝试各种方法去检测和避免内存泄露,如 boost,智能指针技术应运而生。
9.1 C++98/03的尝试——STD::AUTO_PTR
在 2019 年讨论 std::auto_ptr 不免有点让人怀疑是不是有点过时了,确实如此,随着 C++11 标准的出现(最新标准是 C++20),std::auto_ptr 已经被彻底废弃了,取而代之是 std::unique_ptr。然而,之所以还 介绍一下 std::auto_ptr 的用法以及它的设计不足之处是想让你了解 C++ 语言中智能指针的发展过程,一项技术如果我们了解它过去的样子和发展的轨迹,我们就能更好地掌握它,不是吗?
- std::auto_ptr 的基本用法如下代码所示:
#include <memory>
int main()
{
//初始化方式1
std::auto_ptr<int> sp1(new int(8));
//初始化方式2
std::auto_ptr<int> sp2;
sp2.reset(new int(8));
return 0;
}
/*
智能指针对象 sp1 和 sp2 均持有一个在堆上分配 int 对象,其值均是 8,这两块堆内存均可以在 sp1 和 sp2 释放时得到释放。这是 std::auto_ptr 的基本用法。
*/
- std::auto_ptr 真正让人容易误用的地方是其不常用的复制语义,即当复制一个 std::auto_ptr 对象时(拷贝复制或 operator = 复制),原对象所持有的堆内存对象也会转移给复制出来的对象。示例代码如下:
#include <iostream>
#include <memory>
///所有的智能指针类(包括 std::unique_ptr)均包含于头文件 <memory> 中。
int main()
{
//测试拷贝构造
std::auto_ptr<int> sp1(new int(8));
std::auto_ptr<int> sp2(sp1);
///get() 函数获取原始指针, std::weak_ptr,std::unique_ptr 和 std::shared_ptr 都提供了
if (sp1.get() != NULL)
{
std::cout << "sp1 is not empty." << std::endl;
}
else
{
std::cout << "sp1 is empty." << std::endl;
}
if (sp2.get() != NULL)
{
std::cout << "sp2 is not empty." << std::endl;
}
else
{
std::cout << "sp2 is empty." << std::endl;
}
//测试赋值构造
std::auto_ptr<int> sp3(new int(8));
std::auto_ptr<int> sp4 = sp3;
if (sp3.get() != NULL)
{
std::cout << "sp3 is not empty." << std::endl;
}
else
{
std::cout << "sp3 is empty." << std::endl;
}
if (sp4.get() != NULL)
{
std::cout << "sp4 is not empty." << std::endl;
}
else
{
std::cout << "sp4 is empty." << std::endl;
}
return 0;
}
///运行结果
/*
sp1 is empty.
sp2 is not empty.
sp3 is empty.
sp4 is not empty.
*/
- 由于 std::auto_ptr 这种不常用的复制语义,我们应该避免在 stl 容器中使用 std::auto_ptr,例如我们绝不应该写出如下代码:
std::vector<std::auto_ptr<int>> myvectors;
/*
当用算法对容器操作的时候(如最常见的容器元素遍历),很难避免不对容器中的元素实现赋值传递,这样便会使容器中多个元素被置为空指针,这不是我们想看到的,会造成很多意想不到的错误。
正因为存在上述设计上的缺陷,在 C++11及后续语言规范中 std::auto_ptr 已经被废弃,你的代码不应该再使用它。
*/
9.2 STD::UNIQUE_PTR
- std::unique_ptr 对其持有的堆内存具有唯一拥有权,也就是说引用计数永远是 1,std::unique_ptr 对象销毁时会释放其持有的堆内存。可以使用以下方式初始化一个 std::unique_ptr 对象:
///智能指针最初设计的目的就是为了管理堆对象的(即那些不会自动释放的资源)。
//初始化方式1
std::unique_ptr<int> sp1(new int(123));
//初始化方式2
std::unique_ptr<int> sp2;
sp2.reset(new int(123));
//初始化方式3
//make_unique的初始化方法在C++14才加上
std::unique_ptr<int> sp3 = std::make_unique<int>(123);
/*
你应该尽量使用初始化方式 3 的方式去创建一个 std::unique_ptr 而不是方式 1 和 2,因为形式 3 更安全,原因 Scott Meyers 在其《Effective Modern C++》中已经解释过了,有兴趣的读者可以阅读此书相关章节。
*/
- 重置unique_ptr对象
在 unique_ptr 对象上调用**reset()**函数将重置它,即它将释放delete关联的原始指针并使unique_ptr 对象为空。
std::unique_ptr<int> up(new int(10));
up.reset();
- 释放关联的原始指针
在 unique_ptr 对象上调用 release()
将释放其关联的原始指针的所有权,并返回原始指针。这里是释放所有权,并没有delete原始指针,reset()
会delete原始指针。
std::unique_ptr<Task> taskPtr5(new Task(55));
// 不为空
if(taskPtr5 != nullptr)
std::cout<<"taskPtr5 is not empty"<<std::endl;
// 释放关联指针的所有权
Task * ptr = taskPtr5.release();
// 现在为空
if(taskPtr5 == nullptr)
std::cout<<"taskPtr5 is empty"<<std::endl;
- 令很多人对 C++11 规范不满的地方是,C++11 新增了 std::make_shared() 方法创建一个 std::shared_ptr 对象,却没有提供相应的 std::make_unique() 方法创建一个 std::unique_ptr 对象,这个方法直到 C++14 才被添加进来。当然,在 C++11 中你很容易实现出这样一个方法来:
template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&& ...params)
{
return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}
- 鉴于 std::auto_ptr 的前车之鉴,std::unique_ptr 禁止复制语义,为了达到这个效果,std::unique_ptr 类的拷贝构造函数和赋值运算符(operator =)被标记为 delete。
template <class T>
class unique_ptr
{
//省略其他代码...
//拷贝构造函数和赋值运算符被标记为delete
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
};
//因此,下列代码是无法通过编译的:
std::unique_ptr<int> sp1(std::make_unique<int>(123));;
//以下代码无法通过编译
//std::unique_ptr<int> sp2(sp1);
std::unique_ptr<int> sp3;
//以下代码无法通过编译
//sp3 = sp1;
- 禁止复制语义也存在特例,即可以通过一个函数返回一个 std::unique_ptr:
#include <memory>
std::unique_ptr<int> func(int val)
{
std::unique_ptr<int> up(new int(val));
return up;
}
int main()
{
std::unique_ptr<int> sp1 = func(123);
return 0;
}
//上述代码从 func 函数中得到一个 std::unique_ptr 对象,然后返回给 sp1。
- 既然 std::unique_ptr 不能复制,那么如何将一个 std::unique_ptr 对象持有的堆内存转移给另外一个呢?答案是使用移动构造(move ctor),示例代码如下:
#include <memory>
int main()
{
std::unique_ptr<int> sp1(std::make_unique<int>(123));
std::unique_ptr<int> sp2(std::move(sp1)); //move构造
std::unique_ptr<int> sp3;
sp3 = std::move(sp2); //move赋值
return 0;
}
- 以上代码利用 std::move 将 sp1 持有的堆内存(值为 123)转移给 sp2,再把 sp2 转移给 sp3。**最后,sp1 和 sp2 不再持有堆内存的引用,变成一个空的智能指针对象。**并不是所有的对象的 std::move 操作都有意义,只有实现了移动构造函数(Move Constructor)或移动赋值运算符(operator =)的类才行,而 std::unique_ptr 正好实现了这二者,以下是实现伪码:
template<typename T, typename Deletor>
class unique_ptr
{
//其他函数省略...
public:
unique_ptr(unique_ptr&& rhs)
{
this->m_pT = rhs.m_pT;
//源对象释放
rhs.m_pT = nullptr;
}
unique_ptr& operator=(unique_ptr&& rhs)
{
this->m_pT = rhs.m_pT;
//源对象释放
rhs.m_pT = nullptr;
return *this;
}
private:
T* m_pT;
};
//这是 std::unique_ptr 具有移动语义的原因
- std::unique_ptr 不仅可以持有一个堆对象,也可以持有一组堆对象,示例如下:
#include <iostream>
#include <memory>
int main()
{
//创建10个int类型的堆对象
//形式1
std::unique_ptr<int[]> sp1(new int[10]);
//形式2
std::unique_ptr<int[]> sp2;
sp2.reset(new int[10]);
//形式3
std::unique_ptr<int[]> sp3(std::make_unique<int[]>(10));
for (int i = 0; i < 10; ++i)
{
sp1[i] = i;
sp2[i] = i;
sp3[i] = i;
}
for (int i = 0; i < 10; ++i)
{
std::cout << sp1[i] << ", " << sp2[i] << ", " << sp3[i] << std::endl;
}
return 0;
}
//输出结果
0, 0, 0
1, 1, 1
2, 2, 2
3, 3, 3
4, 4, 4
5, 5, 5
6, 6, 6
7, 7, 7
8, 8, 8
9, 9, 9
9.3 自定义智能指针对象持有的资源的释放函数
- 默认情况下,智能指针对象在析构时只会释放其持有的堆内存(调用 delete 或者 delete[]),但是假设这块堆内存代表的对象还对应一种需要回收的资源(如操作系统的套接字句柄、文件句柄等),我们可以通过自定义智能指针的资源释放函数。假设现在有一个 Socket 类,对应着操作系统的套接字句柄,在回收时需要关闭该对象,我们可以如下自定义智能指针对象的资源析构函数,这里以 std::unique_ptr 为例:
#include <iostream>
#include <memory>
class Socket
{
public:
Socket()
{
}
~Socket()
{
}
//关闭资源句柄
void close()
{
}
};
int main()
{
auto deletor = [](Socket* pSocket) {
//关闭句柄
pSocket->close();
//TODO: 你甚至可以在这里打印一行日志...
delete pSocket;
};
std::unique_ptr<Socket, void(*)(Socket * pSocket)> spSocket(new Socket(), deletor);
//也可以利用decltype关键字反向推导deletor的类型
std::unique_ptr<Socket, decltype(deletor)> spSocket(new Socket(), deletor);
return 0;
}
- **注意:**自定义 std::unique_ptr 的资源释放函数其规则是:
std::unique_ptr<T, DeletorFunPtr>
/*
其中 T 是你要释放的对象类型,DeletorFunPtr 是一个自定义函数指针。
*/
9.4 STD::SHARED_PTR
-
std::unique_ptr 对其持有的资源具有独占性,而 std::shared_ptr 持有的资源可以在多个 std::shared_ptr 之间共享,每多一个 std::shared_ptr 对资源的引用,资源引用计数将增加 1,每一个指向该资源的 std::shared_ptr 对象析构时,资源引用计数减 1,最后一个 std::shared_ptr 对象析构时,发现资源计数为 0,将释放其持有的资源。多个线程之间,递增和减少资源的引用计数是安全的。(注意:这不意味着多个线程同时操作 std::shared_ptr 引用的对象是安全的)。
-
std::shared_ptr 提供了一个 use_count() 方法来获取当前持有资源的引用计数。除了上面描述的,std::shared_ptr 用法和 std::unique_ptr 基本相同。
下面是一个初始化 std::shared_ptr 的示例:
//初始化方式1
std::shared_ptr<int> sp1(new int(123));
//初始化方式2
std::shared_ptr<int> sp2;
sp2.reset(new int(123));
//初始化方式3
std::shared_ptr<int> sp3;
sp3 = std::make_shared<int>(123);
- 注意:和 std::unique_ptr 一样,应该优先使用 std::make_shared 去初始化一个 std::shared_ptr 对象,因为这样的初始化是安全的。
再来看另外一段代码:
#include <iostream>
#include <memory>
class A
{
public:
A()
{
std::cout << "A constructor" << std::endl;
}
~A()
{
std::cout << "A destructor" << std::endl;
}
};
int main()
{
{
//初始化方式1
std::shared_ptr<A> sp1(new A());
///调用use_count()方法返回shared_ptr的引用计数
std::cout << "use count: " << sp1.use_count() << std::endl;
//初始化方式2
std::shared_ptr<A> sp2(sp1);
std::cout << "use count: " << sp1.use_count() << std::endl;
//调用 sp2 的 reset() 方法,sp2 释放对资源对象 A 的引用
sp2.reset();
std::cout << "use count: " << sp1.use_count() << std::endl;
{
std::shared_ptr<A> sp3 = sp1;
std::cout << "use count: " << sp1.use_count() << std::endl;
}
//作用域结束后use_count减一。
std::cout << "use count: " << sp1.use_count() << std::endl;
}
return 0;
}
/*
程序执行结果:
use count: 1
use count: 2
use count: 1
use count: 2
use count: 1
*/
- swap函数
typedef std::shared_ptr<int> intPtr;
intPtr ap(new int(10));
intPtr bp(new int(9));
// swap函数的作用是对两个智能指针所管理的资源的交换
ap.swap(bp);
// std::swap(ap, bp);
- 自定义删除器 Deleter
下面将讨论如何将自定义删除器与 std :: shared_ptr 一起使用。
当 shared_ptr 对象超出范围时,将调用其析构函数。在其析构函数中,它将引用计数减1,如果引用计数的新值为0,则删除关联的原始指针。
析构函数中删除内部原始指针,默认调用的是delete()函数。
有些时候在析构函数中,delete函数并不能满足我们的需求,可能还想加其他的处理。
- 当 shared_ptr 对象指向数组
std::shared_ptr<int[]> p3(new int[12]);
像这样申请的数组,应该调用delete []
释放内存,而shared_ptr析构函数中默认delete
并不能满足需求。
- 给shared_ptr添加自定义删除器
在上面在这种情况下,我们可以将回调函数传递给 shared_ptr 的构造函数,该构造函数将从其析构函数中调用以进行删除,即:
// 自定义删除器
void deleter(Sample * x)
{
std::cout << "DELETER FUNCTION CALLED\n";
delete[] x;
}
// 构造函数传递自定义删除器指针
std::shared_ptr<Sample> p3(new Sample[12], deleter);
完整示例:
#include <iostream>
#include <memory>
struct Sample
{
Sample() {
std::cout << "Sample\n";
}
~Sample() {
std::cout << "~Sample\n";
}
};
void deleter(Sample * x)
{
std::cout << "Custom Deleter\n";
delete[] x;
}
int main()
{
std::shared_ptr<Sample> p3(new Sample[3], deleter);
return 0;
}
/*
输出:
Sample
Sample
Sample
Custom Deleter
~Sample
~Sample
~Sample
*/
- 使用Lambda 表达式 / 函数对象作为删除器
class Deleter
{
public:
void operator() (Sample * x) {
std::cout<<"DELETER FUNCTION CALLED\n";
delete[] x;
}
};
// 函数对象作为删除器
std::shared_ptr<Sample> p3(new Sample[3], Deleter());
// Lambda表达式作为删除器
std::shared_ptr<Sample> p4(new Sample[3], [](Sample * x){
std::cout<<"DELETER FUNCTION CALLED\n";
delete[] x;
});
具体案例:
#include <iostream>
#include <memory>
class Demo
{
public:
Demo()
{
std::cout << "Demo()" << std::endl;
}
~Demo()
{
std::cout << "~Demo()" << std::endl;
}
};
void DeleteDemo(Demo *p)
{
std::cout << "void *DeleteDemo(Demo *p)" << std::endl;
delete []p;
}
int main()
{
std::shared_ptr<Demo> sp(new(Demo[3]), DeleteDemo);
auto spt = [](Demo *p){
std::cout << "[](Demo *p){}" << std::endl;
delete []p;
};
std::shared_ptr<Demo> sp2(new(Demo[2]), spt);
return 0;
}
/*
输出:
Demo()
Demo()
Demo()
Demo()
Demo()
[](Demo *p){}
~Demo()
~Demo()
void *DeleteDemo(Demo *p)
~Demo()
~Demo()
~Demo()
*/
- shared_ptr 相对于普通指针的优缺点
缺少 ++, – – 和 [] 运算符
与普通指针相比,shared_ptr仅提供->
、*
和==
运算符,没有+
、-
、++
、--
、[]
等运算符。
示例:
#include<iostream>
#include<memory>
struct Sample {
void dummyFunction() {
std::cout << "dummyFunction" << std::endl;
}
};
int main()
{
std::shared_ptr<Sample> ptr = std::make_shared<Sample>();
(*ptr).dummyFunction(); // 正常
ptr->dummyFunction(); // 正常
// ptr[0]->dummyFunction(); // 错误方式
// ptr++; // 错误方式
//ptr--; // 错误方式
std::shared_ptr<Sample> ptr2(ptr);
if (ptr == ptr2) // 正常
std::cout << "ptr and ptr2 are equal" << std::endl;
return 0;
}
struct Base
{
Base(int id, std::string name)
: m_id(id), m_name(name){}
int m_id;
std::string m_name;
};
int main()
{
typedef std::shard_ptr<Base> BasePtr;
BasePtr obj(new Base(1001, "xixi"));
if(obj){ //operator bool
std::cout << "obj->id: " << obj->id << "\n"; // operator ->
std::cout << "*(obj).id" << *(obj).id << "\n"; // operator *
}
return 0;
}
- NULL检测
当我们创建 shared_ptr 对象而不分配任何值时,它就是空的;普通指针不分配空间的时候相当于一个野指针,指向垃圾空间,且无法判断指向的是否是有用数据。
shared_ptr 检测空值方法
std::shared_ptr<Sample> ptr3;
if(!ptr3)
std::cout<<"Yes, ptr3 is empty" << std::endl;
if(ptr3 == NULL)
std::cout<<"ptr3 is empty" << std::endl;
if(ptr3 == nullptr)
std::cout<<"ptr3 is empty" << std::endl;
- 创建 shared_ptr 时注意事项
不要使用同一个原始指针构造 shared_ptr
创建多个 shared_ptr 的正常方法是使用一个已存在的shared_ptr 进行创建,而不是使用同一个原始指针进行创建。
示例:
int *num = new int(23);
std::shared_ptr<int> p1(num);
std::shared_ptr<int> p2(p1); // 正确使用方法
std::shared_ptr<int> p3(num); // 不推荐
std::cout << "p1 Reference = " << p1.use_count() << std::endl; // 输出 2
std::cout << "p2 Reference = " << p2.use_count() << std::endl; // 输出 2
std::cout << "p3 Reference = " << p3.use_count() << std::endl; // 输出 1
/*
假如使用原始指针num创建了p1,又同样方法创建了p3,当p1超出作用域时会调用delete释放num内存,此时num成了悬空指针,当p3超出作用域再次delete的时候就可能会出错。
*/
- 不要用栈中的指针构造 shared_ptr 对象
shared_ptr 默认的构造函数中使用的是delete
来删除关联的指针,所以构造的时候也必须使用new
出来的堆空间的指针。
示例:
#include<iostream>
#include<memory>
int main()
{
int x = 12;
std::shared_ptr<int> ptr(&x); //错误
return 0;
}
/*
当 shared_ptr 对象超出作用域调用析构函数delete 指针&x时会出错。
*/
- 建议使用 make_shared创建shared_ptr
为了避免以上两种情形,建议使用make_shared()<>
创建 shared_ptr 对象,而不是使用默认构造函数创建。
std::shared_ptr<int> ptr_1 = std::make_shared<int>();
std::shared_ptr<int> ptr_2 (ptr_1);
- get()函数
std::shard_ptr<int> ap(new int(10));
// get()函数的作用是获取原始指针
int *p = ap.get();
另外不建议使用get()
函数获取 shared_ptr 关联的原始指针,因为如果在 shared_ptr 析构之前手动调用了delete
函数,同样会导致类似的错误。
- std::enable_shared_from_this
实际开发中,有时候需要在类中返回包裹当前对象(this)的一个 std::shared_ptr 对象给外部使用,C++ 新标准也为我们考虑到了这一点,有如此需求的类只要继承自 std::enable_shared_from_this 模板对象即可。用法如下:
#include <iostream>
#include <memory>
class A : public std::enable_shared_from_this<A>
{
public:
A()
{
std::cout << "A constructor" << std::endl;
}
~A()
{
std::cout << "A destructor" << std::endl;
}
std::shared_ptr<A> getSelf()
{
return shared_from_this();
}
};
int main()
{
std::shared_ptr<A> sp1(new A());
std::shared_ptr<A> sp2 = sp1->getSelf();
std::cout << "use count: " << sp1.use_count() << std::endl;
return 0;
}
/*
上述代码中,类 A 的继承 std::enable_shared_from_this<A> 并提供一个 getSelf() 方法返回自身的 std::shared_ptr 对象,在 getSelf() 中调用 shared_from_this() 即可。
*/
- std::enable_shared_from_this 用起来比较方便,但是也存在很多不易察觉的陷阱。
陷阱一:不应该共享栈对象的 this 给智能指针对象
假设我们将上面代码 main 函数 生成 A 对象的方式改成一个栈变量,即:
//其他相同代码省略...
int main()
{
A a;
std::shared_ptr<A> sp2 = a.getSelf();
std::cout << "use count: " << sp2.use_count() << std::endl;
return 0;
}
/*
运行修改后的代码会发现程序在 std::shared_ptr<A> sp2 = a.getSelf(); 产生崩溃。这是因为,智能指针管理的是堆对象,栈对象会在函数调用结束后自行销毁,因此不能通过 shared_from_this() 将该对象交由智能指针对象管理。切记:智能指针最初设计的目的就是为了管理堆对象的(即那些不会自动释放的资源)。
*/
陷阱二:避免 std::enable_shared_from_this 的循环引用问题
再来看另外一段代码:
// test_std_enable_shared_from_this.cpp : This file contains the 'main' function. Program execution begins and ends there.
//
#include <iostream>
#include <memory>
class A : public std::enable_shared_from_this<A>
{
public:
A()
{
m_i = 9;
//注意:
//比较好的做法是在构造函数里面调用shared_from_this()给m_SelfPtr赋值
//但是很遗憾不能这么做,如果写在构造函数里面程序会直接崩溃
std::cout << "A constructor" << std::endl;
}
~A()
{
m_i = 0;
std::cout << "A destructor" << std::endl;
}
void func()
{
m_SelfPtr = shared_from_this();
}
public:
int m_i;
std::shared_ptr<A> m_SelfPtr;
};
int main()
{
{
std::shared_ptr<A> spa(new A());
spa->func();
}
return 0;
}
//乍一看上面的代码好像看不出什么问题,让我们来实际运行一下看看输出结果:
A constructor
/*
我们发现在程序的整个生命周期内,只有 A 类构造函数的调用输出,没有 A 类析构函数的调用输出,这意味着 new 出来的 A 对象产生了内存泄漏了!
我们来分析一下为什么 new 出来的 A 对象得不到释放。当程序执行到 42 行后,spa 出了其作用域准备析构,在析构时其发现仍然有另外的一个 std::shared_ptr 对象即 A::m_SelfPtr 引用了 A,因此 spa 只会将 A 的引用计数递减为 1,然后就销毁自身了。现在留下一个矛盾的处境:必须销毁 A 才能销毁其成员变量 m_SelfPtr,而销毁 m_SelfPtr 必须先销毁 A。这就是所谓的 std::enable_shared_from_this 的循环引用问题。我们在实际开发中应该避免做出这样的逻辑设计,这种情形下即使使用了智能指针也会造成内存泄漏。也就是说一个资源的生命周期可以交给一个智能指针对象,但是该智能指针的生命周期不可以再交给整个资源来管理。
*/
- unique()函数
struct Base
{
Base(int id, std::string name)
: m_id(id), m_name(name){}
int m_id;
std::string m_name;
};
int main()
{
typedef std::shard_ptr<Base> BasePtr;
BasePtr obj(new Base(1001, "xixi"));
if(obj.unique())
std::cout << "只有一个人在管理这个指针" << std::endl;
// unique()函数的作用是判断这个指针当前是否只有一个人在管理
// obj.unique() 等价于 obj.use_count() == 1
// 调用unique效率会更高一些
return 0;
}
9.5 STD::WEAK_PTR
std::weak_ptr 是一个不控制资源生命周期的智能指针,是对对象的一种弱引用,只是提供了对其管理的资源的一个访问手段,引入它的目的为协助 std::shared_ptr 工作。
std::weak_ptr 可以从一个 std::shared_ptr 或另一个 std::weak_ptr 对象构造,std::shared_ptr 可以直接赋值给 std::weak_ptr ,也可以通过 std::weak_ptr 的 lock() 函数来获得 std::shared_ptr。它的构造和析构不会引起引用计数的增加或减少。std::weak_ptr 可用来解决 std::shared_ptr 相互引用时的死锁问题(即两个std::shared_ptr 相互引用,那么这两个指针的引用计数永远不可能下降为 0, 资源永远不会释放)。
示例代码如下:
#include <iostream>
#include <memory>
int main()
{
//创建一个std::shared_ptr对象
std::shared_ptr<int> sp1(new int(123));
std::cout << "use count: " << sp1.use_count() << std::endl;
//通过构造函数得到一个std::weak_ptr对象
std::weak_ptr<int> sp2(sp1);
std::cout << "use count: " << sp1.use_count() << std::endl;
//通过赋值运算符得到一个std::weak_ptr对象
std::weak_ptr<int> sp3 = sp1;
std::cout << "use count: " << sp1.use_count() << std::endl;
//通过一个std::weak_ptr对象得到另外一个std::weak_ptr对象
std::weak_ptr<int> sp4 = sp2;
std::cout << "use count: " << sp1.use_count() << std::endl;
return 0;
}
/*
程序执行结果如下:
use count: 1
use count: 1
use count: 1
use count: 1
*/
无论通过何种方式创建 std::weak_ptr 都不会增加资源的引用计数,因此每次输出引用计数的值都是 1。
既然,std::weak_ptr 不管理对象的生命周期,那么其引用的对象可能在某个时刻被销毁了,如何得知呢?std::weak_ptr 提供了一个 expired() 方法来做这一项检测,返回 true,说明其引用的资源已经不存在了;返回 false,说明该资源仍然存在,这个时候可以使用 std::weak_ptr 的 lock() 方法得到一个 std::shared_ptr 对象然后继续操作资源,以下代码演示了该用法:
//tmpConn_ 是一个 std::weak_ptr<TcpConnection> 对象
//tmpConn_引用的TcpConnection已经销毁,直接返回
if (tmpConn_.expired())
return;
//通过 std::weak_ptr 的 lock() 函数来获得 std::shared_ptr
std::shared_ptr<TcpConnection> conn = tmpConn_.lock();
if (conn)
{
//对conn进行操作,省略...
}
有读者可能对上述代码产生疑问,既然使用了 std::weak_ptr 的 expired() 方法判断了对象是否存在,为什么不直接使用 std::weak_ptr 对象对引用资源进行操作呢?实际上这是行不通的,std::weak_ptr 类没有重写 operator-> 和 operator* 方法,因此不能像 std::shared_ptr 或 std::unique_ptr 一样直接操作对象,同时 std::weak_ptr 类也没有重写 operator! 操作,因此也不能通过 std::weak_ptr 对象直接判断其引用的资源是否存在:
#include <memory>
class A
{
public:
void doSomething()
{
}
};
int main()
{
std::shared_ptr<A> sp1(new A());
std::weak_ptr<A> sp2(sp1);
//正确代码
if (sp1)
{
//正确代码
sp1->doSomething();
(*sp1).doSomething();
}
//正确代码
if (!sp1)
{
}
//错误代码,无法编译通过
//if (sp2)
//{
// //错误代码,无法编译通过
// sp2->doSomething();
// (*sp2).doSomething();
//}
//错误代码,无法编译通过
//if (!sp2)
//{
//}
return 0;
}
之所以 std::weak_ptr 不增加引用资源的引用计数来管理资源的生命周期,是因为,即使它实现了以上说的几个方法,调用它们也是不安全的,因为在调用期间,引用的资源可能恰好被销毁了,这会造成棘手的错误和麻烦。
因此,std::weak_ptr 的正确使用场景是那些资源如果可能就使用,如果不可使用则不用的场景,它不参与资源的生命周期管理。例如,网络分层结构中,Session 对象(会话对象)利用 Connection 对象(连接对象)提供的服务工作,但是 Session 对象不管理 Connection 对象的生命周期,Session 管理 Connection 的生命周期是不合理的,因为网络底层出错会导致 Connection 对象被销毁,此时 Session 对象如果强行持有 Connection 对象与事实矛盾。
std::weak_ptr 的应用场景,经典的例子是订阅者模式或者观察者模式中。这里以订阅者为例来说明,消息发布器只有在某个订阅者存在的情况下才会向其发布消息,而不能管理订阅者的生命周期。
class Subscriber
{
};
class SubscribeManager
{
public:
void publish()
{
for (const auto& iter : m_subscribers)
{
if (!iter.expired())
{
//TODO:给订阅者发送消息
}
}
}
private:
std::vector<std::weak_ptr<Subscriber>> m_subscribers;
};
9.6 智能指针的大小
一个 std::unique_ptr 对象大小与裸指针大小相同(即 sizeof(std::unique_ptr) == sizeof(void*)),而 std::shared_ptr 的大小是 std::unique_ptr 的一倍。以下是在 gcc/g++ 4.8 上(二者都编译成 x64 程序)的测试结果:
#include <iostream>
#include <memory>
#include <string>
int main()
{
std::shared_ptr<int> sp0;
std::shared_ptr<std::string> sp1;
sp1.reset(new std::string());
std::unique_ptr<int> sp2;
std::weak_ptr<int> sp3;
std::cout << "sp0 size: " << sizeof(sp0) << std::endl;
std::cout << "sp1 size: " << sizeof(sp1) << std::endl;
std::cout << "sp2 size: " << sizeof(sp2) << std::endl;
std::cout << "sp3 size: " << sizeof(sp3) << std::endl;
return 0;
}
/*
输出结果:
sp0 size: 16
sp1 size: 16
sp2 size: 8
sp3 size: 16
*/
/*
在 32 位机器上,std_unique_ptr 占 4 字节,std::shared_ptr 和 std::weak_ptr 占 8 字节;在 64 位机器上,std_unique_ptr 占 8 字节,std::shared_ptr 和 std::weak_ptr 占 16 字节。也就是说,std_unique_ptr 的大小总是和原始指针大小一样,std::shared_ptr 和 std::weak_ptr 大小是原始指针的一倍。
*/
9.7 智能指针使用注意事项
**C++ 新标准提倡的理念之一是不应该再手动调用 delete 或者 free 函数去释放内存了,而应该把它们交给新标准提供的各种智能指针对象。**C++ 新标准中的各种智能指针是如此的实用与强大,在现代 C++ 项目开发中,读者应该尽量去使用它们。智能指针虽然好用,但稍不注意,也可能存在许多难以发现的 bug,这里我根据经验总结了几条:
- 一旦一个对象使用智能指针管理后,就不该再使用原始裸指针去操作;
一段代码:
#include <memory>
class Subscriber
{
};
int main()
{
Subscriber* pSubscriber = new Subscriber();
std::unique_ptr<Subscriber> spSubscriber(pSubscriber);
delete pSubscriber;
return 0;
}
这段代码利用创建了一个堆对象 Subscriber,然后利用智能指针 spSubscriber 去管理之,可是却私下利用原始指针销毁了该对象,这让智能指针对象 spSubscriber 情何以堪啊?
记住,一旦智能指针对象接管了你的资源,所有对资源的操作都应该通过智能指针对象进行,不建议再通过原始指针进行操作了。当然,除了 std::weak_ptr,std::unique_ptr 和 std::shared_ptr 都提供了获取原始指针的方法——get() 函数。
int main()
{
Subscriber* pSubscriber = new Subscriber();
std::unique_ptr<Subscriber> spSubscriber(pSubscriber);
//pTheSameSubscriber和pSubscriber指向同一个对象
Subscriber* pTheSameSubscriber= spSubscriber.get();
return 0;
}
- 分清楚场合应该使用哪种类型的智能指针;
通常情况下,如果你的资源不需要在其他地方共享,那么应该优先使用 std::unique_ptr,反之使用 std::shared_ptr,当然这是在该智能指针需要管理资源的生命周期的情况下;如果不需要管理对象的生命周期,请使用 std::weak_ptr。
- 认真考虑,避免操作某个引用资源已经释放的智能指针;
前面的例子,一定让你觉得非常容易知道一个智能指针的持有的资源是否还有效,但是还是建议在不同场景谨慎一点,有些场景是很容易造成误判。例如下面的代码:
#include <iostream>
#include <memory>
class T
{
public:
void doSomething()
{
std::cout << "T do something..." << m_i << std::endl;
}
private:
int m_i;
};
int main()
{
std::shared_ptr<T> sp1(new T());
const auto& sp2 = sp1;
sp1.reset();
//由于sp2已经不再持有对象的引用,程序会在这里出现意外的行为
sp2->doSomething();
return 0;
}
/*
上述代码中,sp2 是 sp1 的引用,sp1 被置空后,sp2 也一同为空。这时候调用 sp2->doSomething(),sp2->(即 operator->)在内部会调用 get() 方法获取原始指针对象,这时会得到一个空指针(地址为 0),继续调用 doSomething() 导致程序崩溃。
*/
- 作为类成员变量时,应该优先使用前置声明(forward declarations)
我们知道,为了减小编译依赖加快编译速度和生成二进制文件的大小,C/C++ 项目中一般在 *.h 文件对于指针类型尽量使用前置声明,而不是直接包含对应类的头文件。例如:
//Test.h
//在这里使用A的前置声明,而不是直接包含A.h文件
class A;
class Test
{
public:
Test();
~Test();
private:
A* m_pA;
};
同样的道理,在头文件中当使用智能指针对象作为类成员变量时,也应该优先使用前置声明去引用智能指针对象的包裹类,而不是直接包含包裹类的头文件。
//Test.h
#include <memory>
//智能指针包裹类A,这里优先使用A的前置声明,而不是直接包含A.h
class A;
class Test
{
public:
Test();
~Test();
private:
std::unique_ptr<A> m_spA;
};
9.8 智能指针总结
1、前提:绝对不要自己手动管理资源。
2、要用weak_ptr指针打破循环引用。
3、在类的内部接口中,如果需要将this指针作为智能指针来使用,需要用该类派生自enable_shared_from_this。
4、使用shared_ptr作为函数的接口如果有可能用const shared_ptr&的形式。(采用常引用的方式传递,防止其生命 期的延长)
5、shared_ptr、weak_ptr与裸指针相比空间占用会大很多,并且效率上会有影响。尤其是在多线程下。
6、
class Object;
typedef std::shard_ptr<Object> ObjectPtr;
ObjectPtr obj = std::make_share<Object>(3);
// 它等价于 Object obj(new Object(3));
// 但是它却只new了一次,将Object已经引用计数等放到一起new了出来。
// 所以这种初始化的方法有很高的效率。
7、对于enable_shared_from_this的 这个函数:shared_from_this(),它在构造和析构中是不能使用的,要注意。
8、如果有可能,优先使用类的实例,其次万不得已使用std::unique_ptr,在万不得已在使用std::shard_ptr。
十、原子操作
10.1 介绍
在 C++ 98/03 标准中,如果想对整型变量进行原子操作,要么利用操作系统提供的相关原子操作 API,要么利用对应操作系统提供的锁对象来对变量进行保护,无论是哪种方式,编写的代码都无法实现跨平台操作,例如上一小介绍的 Interlocked 系列 API 代码仅能运行于 Windows 系统,无法移植到 Linux 系统。C++ 11 新标准发布以后,改变了这种困境,新标准提供了对整型变量原子操作的相关库,即 std::atomic ,这是一个模板类型:
template<class T>
struct atomic;
10.2 stl 库也提供的实例化的模板类型
类型别名 | 定义 |
---|---|
std::atomic_bool | std::atomic |
std::atomic_char | std::atomic |
std::atomic_schar | std::atomic |
std::atomic_uchar | std::atomic |
std::atomic_short | std::atomic |
std::atomic_ushort | std::atomic |
std::atomic_int | std::atomic |
std::atomic_uint | std::atomic |
std::atomic_long | std::atomic |
std::atomic_ulong | std::atomic |
std::atomic_llong | std::atomic |
std::atomic_ullong | std::atomic |
std::atomic_char16_t | std::atomic<char16_t> |
std::atomic_char32_t | std::atomic<char32_t> |
std::atomic_wchar_t | std::atomic<wchar_t> |
std::atomic_int8_t | std::atomicstd::int8_t |
std::atomic_uint8_t | std::atomicstd::uint8_t |
std::atomic_int16_t | std::atomicstd::int16_t |
std::atomic_uint16_t | std::atomicstd::uint16_t |
std::atomic_int32_t | std::atomicstd::int32_t |
std::atomic_uint32_t | std::atomicstd::uint32_t |
std::atomic_int64_t | std::atomicstd::int64_t |
std::atomic_uint64_t | std::atomicstd::uint64_t |
上表中仅列出了 C++ 11 支持的常用的整型原子变量,完整的列表读者可以参考这里:https://zh.cppreference.com/w/cpp/atomic/atomic。
10.3 使用案例
有了 C++ 语言本身对原子变量的支持以后,我们就可以“愉快地”写出跨平台的代码了,我们来看一段代码:
#include <atomic>
#include <stdio.h>
int main()
{
std::atomic<int> value;
value = 99;
printf("%d\n", (int)value);
//自增1,原子操作
value++;
printf("%d\n", (int)value);
return 0;
}
以上代码可以同时在 Windows 和 Linux 平台上运行,但是有读者可能会根据个人习惯将上述代码写成如下形式:
#include <atomic>
#include <stdio.h>
int main()
{
std::atomic<int> value = 99;
printf("%d\n", (int)value);
//自增1,原子操作
value++;
printf("%d\n", (int)value);
return 0;
}
代码仅仅做了一点简单的改动,这段代码在 Windows 平台上运行良好,但是在 Linux 平台上会无法编译通过(这里指的是在支持 C++ 11语法的 g++ 编译中编译),提示错误是:
error: use of deleted function ‘std::atomic<int>::atomic(const std::atomic<int>&)’
//产生这个错误的原因是 “std::atomic value = 99;” 这一行代码调用的是 std::atomic 的拷贝构造函数,对于 int 型,其形式一般如下:
atomic& operator=( const atomic& ) = delete;
// 所以 Linux 平台上编译器会提示错误,而 Windows 的 VC++ 编译器没有遵循这个规范。而对于代码:
value = 99;
//g++ 和 VC++ 同时实现规范中的:
T operator=( T desired );
/*
因此,如果读者想利用 C++ 11 提供的 std::atomic 库编写跨平台的代码,在使用 std::atomic 提供的方法时建议参考官方 std::atomic 提供的接口说明来使用,而不是想当然地认为一个方法在此平台上可以运行,在另外一个平台也能有相同的行为,避免出现上面说的这种情形。
*/
10.4 std::atomic的一些重载的方法
上述代码中之所以可以对 value 进行自增(++)操作是因为 std::atomic 类内部重载了 operator = 运算符,除此以外, std::atomic 提供了大量有用的方法,这些方法您一定会觉得似曾相似:
方法名 | 方法说明 |
---|---|
operator= | 存储值于原子对象 |
store | 原子地以非原子对象替换原子对象的值 |
load | 原子地获得原子对象的值 |
exchange | 原子地替换原子对象的值并获得它先前持有的值 |
compare_exchange_weak compare_exchange_strong | 原子地比较原子对象与非原子参数的值,若相等则进行交换,若不相等则进行加载 |
fetch_add | 原子地将参数加到存储于原子对象的值,并返回先前保有的值 |
fetch_sub | 原子地从存储于原子对象的值减去参数,并获得先前保有的值 |
fetch_and | 原子地进行参数和原子对象的值的逐位与,并获得先前保有的值 |
fetch_or | 原子地进行参数和原子对象的值的逐位或,并获得先前保有的值 |
fetch_xor | 原子地进行参数和原子对象的值的逐位异或,并获得先前保有的值 |
operator++ operator++(int) operator– operator–(int) | 令原子值增加或减少一 |
operator+= operator-= operator&= operator竖杠= operator^= | =加、减,或与原子值进行逐位与、或、异或 |
十一、新增的STL
C++11新增的STL容器这里只写了array,还有几个未写。
11.1 array
void arrayPart()
{
// array实际上是对C/C++中原生数组的封装
/*原型:
namespace std{
template<typename T, size_t N>
class array;
}
*/
// array的特点:内存在栈上分配,绝不会重新分配,随机访问元素。
std::array<int, 5>a = {1, 2, 3, 4, 5};
std::array<int, 5>b = {1} //默认补0
// array的接口
a.empty();
a.size();
a.max_size();
// operator == < != > >= <=
// 拷贝赋值 auto aa = a;
aa.swap(a);
swap(aa, a);
//访问元素方式
a[1];
a.at(1);
a.front();
a.back();
checkSize(a);
// 和C接口互用
std::array<char, 100> carr;
strcpy(&carr[0], "hello world\n");
printf("%s", &carr[0]);
// 错误的用法
printf("%s", carr.begin());
// array特殊的地方
auto info = std::get<1>(carr); // 这个函数是 获取下标为1的元素 等价于 auto info = carr[1];
carr.fill(0); //这个成员函数的作用是给数组所有元素赋值为(这里是0)
}
十二、C++11实现简单线程池案例
/*threadpool.h*/
#ifndef THREADPOOL_H
#define THREADPOOL_H
#include <thread>
#include <memory>
#include <iostream>
#include <list>
#include <vector>
#include <mutex>
#include <condition_variable>
class Task
{
public:
void doIt()
{
std::cout << "handle a task ..." << std::endl;
}
~Task()
{
std::cout << "a task delete..." << std::endl;
}
};
class ThreadPool
{
public:
ThreadPool();
~ThreadPool();
ThreadPool(const ThreadPool &pool) = delete;
ThreadPool operator =(const ThreadPool &pool) = delete;
public:
void initThreadPool(int threadNum = 5);
void stopThreadPool();
void addTask(Task *task);
void removeAllTasks();
private:
void threadFunc();
private:
std::list<std::shared_ptr<Task>> m_taskList;
std::vector<std::shared_ptr<std::thread>> m_threadVec;
std::mutex m_mutexList;
std::condition_variable m_cv;
bool m_poolIsRunning;
};
#endif // THREADPOOL_H
/*threadpool.cpp*/
#include "threadpool.h"
ThreadPool::ThreadPool()
{
m_poolIsRunning = false;
}
ThreadPool::~ThreadPool()
{
removeAllTasks();
}
void ThreadPool::initThreadPool(int threadNum)
{
if(threadNum <= 0)
threadNum = 5;
m_poolIsRunning = true;
for(int i = 0; i < threadNum; i++){
std::shared_ptr<std::thread> spThread;
spThread.reset(new std::thread(std::bind(&ThreadPool::threadFunc, this)));
m_threadVec.push_back(spThread);
}
}
void ThreadPool::stopThreadPool()
{
m_poolIsRunning = false;
m_cv.notify_all();
//等待所有线程退出
for (auto& iter : m_threadVec)
{
if (iter->joinable())
iter->join();
}
}
void ThreadPool::addTask(Task *task)
{
std::shared_ptr<Task> spTask;
spTask.reset(task);
{
std::lock_guard<std::mutex> guard(m_mutexList);
m_taskList.push_back(spTask);
}
m_cv.notify_one();
}
void ThreadPool::removeAllTasks()
{
{
std::lock_guard<std::mutex> guard(m_mutexList);
for (auto& iter : m_taskList)
{
iter.reset();
}
m_taskList.clear();
}
}
void ThreadPool::threadFunc()
{
std::shared_ptr<Task> spTask;
while(true){
std::unique_lock<std::mutex> unique(m_mutexList);
while(m_taskList.empty()){
if(!m_poolIsRunning)
break;
m_cv.wait(unique);
}
if(!m_poolIsRunning)
break;
spTask = m_taskList.front();
m_taskList.pop_front();
if (spTask == NULL)
continue;
spTask->doIt();
spTask.reset();
}
std::cout << "exit thread, threadID: " << std::this_thread::get_id() << std::endl;
}
/*main.cpp*/
#include "threadpool.h"
#include <chrono>
int main()
{
ThreadPool pool;
pool.initThreadPool(5);
Task *task = nullptr;
for(int i = 0; i < 10; i++){
task = new Task;
pool.addTask(task);
}
std::this_thread::sleep_for(std::chrono::seconds(5));
pool.stopThreadPool();
return 0;
}