1. C++简介
在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个重点去学习。C++11增加的语法特性非常篇幅非常多,我们这里没办法一 一讲解,所以本节课程主要讲解实际中比较实用的语法。
2. 统一列表初始化
2.1 {} 初始化
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。
struct Point
{
int _x;
int _y;
};
int main()
{
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
Point p = { 1, 2 };
return 0;
}
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加
struct Point
{
int _x;
int _y;
};
class A
{
public:
// 多参数构造函数
//explicit A(int x, int y) explict关键字,则将他不具有隐式类型转换
A(int x, int y)
:_x(x)
, _y(y)
{}
// 单参数构造函数
A(int x)
:_x(x)
, _y(x)
{}
private:
int _x;
int _y;
};
// 列表初始化
// 一切皆可以用列表初始化
// 可以将列表前的 = 去掉
void TestMode11()
{
// c语言中数组列表初始化
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
int array3[5]{ 0 };// C++11
// 单参数的隐式类型转换
A aa2 = 1;
A aa4 = { 1 };
A aa5{ 1 };
// 多参数的隐式类型转换
A aa1 = { 2,2 };
A aa6{ 2,2 };
const A& aa3{ 2,2 };
const A& aa3 = { 2,2 };// 临时变量具有常性
int i{ 1 };// 内置类型列表初始化
}
2.2 std::initializer_list
initializer_list实质上就是两个指针,一个指针常量数组的开始,一个指针指向常量数组的结束位置(末尾位置的下一个位置)
initializer_list实际上就是两个指针,则容量为8字节(x86)
//2. initializer_list的初始化
// 这个构造一劳永逸的解决了问题,不用像上面那样麻烦的方式解决
// vector(initializer_list<T> il);
// 容器想用不固定的{}数据个数初始化,initializer_list支持
void TestModel2()
{
vector<int> v1;
vector<int> v2(10, 1);
// 构造函数初始化
vector<int> v5({ 1,2,3,4,5 });
// X自定义 = Y类型 ->隐式类型转换 X(Y mm) X支持Y为参数类型构造就可以直接变化为以下类型
vector<int> v3 = { 1,2,3,4,5 };
vector<int> v4{ 10,20,30 };
auto il1 = { 10, 20, 30 };
initializer_list<int> il2 = { 10, 20, 30 };
cout << typeid(il1).name() << endl;
// 两个指针,一个指针常量数组的开始,一个指针指向常量数组的结束位置(末尾位置的下一个位置)
cout << sizeof(il1) << endl;// initializer_list实际上就是两个指针,则容量为8字节(x86)
pair<string, string> kv1("sort", "排序");
pair<string, string> kv2("insert", "插入");
map<string, string> dict1 = { kv1, kv2 };
// 1、pair多参数隐式类型转换
// 2、initializer_list<pair>的构造
map<string, string> dict2 = { {"sort", "排序"}, {"insert", "插入"} };
}
3. 声明
3.1 auto
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型腿断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型
自动去推断类型
// 声明
// auto:自动推断类型
void TestModel3()
{
int i = 0;
auto x = i;
//auto& x = i;
x++;
int& j = i;
auto y = j;
auto& y = j;
//
}
3.2 decltype
关键字decltype将变量的类型声明为表达式指定的类型。
和typeid比较类似
区别:
- typeid只是推出一个类型的名字的单纯的字符串,不可以用typeid来定义这个类型的对象
- 可以用decltype来定义这个类型的对象
template<class T>
class B
{
public:
T* New(int n)
{
return new T[n];
}
};
auto func1()
{
list<int> lt;
auto ret = lt.begin();
return ret;
}
// decltype
// 与typeid的区别:
// 1.typeid只是推出一个类型的名字的单纯的字符串,不可以用typeid来定义这个类型的对象
// 2.可以用decltype来定义这个类型的对象
void TestModel4()
{
list<int>::iterator it1;
// typeid推出时一个单纯的字符串
cout << typeid(it1).name() << endl;
// 不能用来定义对象
//typeid(it1).name() it2;
// 可以用来定义对象
decltype(it1) it2;
cout << typeid(it2).name() << endl;
auto it3 = it1;
cout << typeid(it3).name() << endl;
auto ret3 = func1();
B<decltype(ret3)> bb1;
map<string, string> dict2 = { {"sort", "排序"}, {"insert", "插入"} };
auto it4 = dict2.begin();
B<decltype(it4)> bb2;
B<std::map<std::string, std::string>::iterator> bb2;
// auto和decltype有些地方增加代码读起来难度
}
3.3 nullptr
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
4. 范围for循环
这个一般会进行与迭代器相关的内容,用迭代器遍历容器中的数据。
map<string, string> dict2 = { {"sort", "排序"}, {"insert", "插入"} };
for (auto& [x, y] : dict2)
{
cout << x << ":" << y << endl;
y += '2';
}
5.STL中的一些变化
新容器
用橘色圈起来是C++11中的一些几个新容器,但是实际最有用的是unordered_map和unordered_set。
容器中的一些新方法
如果我们再细细去看会发现基本每个容器中都增加了一些C++11的方法,但是其实很多都是用得比较少的。
比如提供了cbegin和cend方法返回const迭代器等等,但是实际意义不大,因为begin和end也是可以返回const迭代器的,这些都是属于锦上添花的操作。
6. 右值引用和移动语义
6.1 左值引用和右值引用
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。
无论左值引用还是右值引用,都是给对象取别名。
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
最通俗的来讲:左值就是可以被取地址的表达式
void TestModel6()
{
//左值是一个表达式,可以取地址的
int a = 10;
int b = a;
const int c = 10;
int* p = &a;
vector<int> v(10, 1);
v[1];
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &(*p) << endl;
cout << &(v[1]) << endl;
}
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
右值引用就是对右值的引用,给右值取别名
右值引用,给右值取别名
右值也分为两种
纯右值 (内置类型的)
将亡值 (自定义类型的)
// 右值引用
// 右值中也分为:
// 1. 纯右值(内置类型的)
// 2. 将亡值(自定义类型的)匿名对象(生命周期就只在所在的那一行)
void TestModel7()
{
//10、string("1111")、to_string(123), x + y
// 右值引用不可以取地址
/*cout << &10 << endl;
cout << &string("1111") << endl;
cout << &to_string(123) << endl;*/
int x = 1, y = 2;
/*cout << &(x + y) < endl;*/
/* 右值引用,给右值取别名
纯右值 (内置类型的)
将亡值 (自定义类型的)*/
int&& rref1 = (x + y);
string&& rref2 = string("1111");
string&& rref3 = to_string(123);
int&& rref4 = 10;
}
但是右值和左值也有一些特殊点
左值能否给右值取别名?
不可以,但是const左值引用可以
/*左值引用能否给右值取别名 -- 不可以,但是const左值引用可以*/
const string & ref1 = string("1111");
const int& ref2 = 10;
右值引用能否给左值取别名
** 不可以,但是可以给move以后的左值区别**
/*右值引用能否给左值取别名 -- 不可以,但是可以给move以后的左值区别*/
string s1("1111");
string&& rref5 = move(s1);
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。
int main()
{
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x + y;
rr1 = 20;
rr2 = 5.5; // 报错
return 0;
}
6.2 左值引用与右值引用比较
左值引用总结:
- 左值引用只能引用左值,不能引用右值。
- 但是const左值引用既可引用左值,也可引用右值
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
右值引用总结:
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以move以后的左值
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
现在所要应用的右值指的都是自定义类型的。
6.3 右值引用的使用场景和意义
前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引
用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!
// 拷贝构造
// 左值
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
// 移动构造 -- 移动将亡值对象的资源
// 右值(将亡值)
string(string&& s)
:_str(nullptr)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
左值引用的短板:
但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如:bit::string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。
由于左值就是直接用创痛的方式开辟空间,创建一个更大的空间,然后将原本对象的数据拷贝到其中,然后将这个空间在给string的当前对象中。
移动构造则是直接将这个自定义对象的资源转移到main函数中的哪个对象中
在没有移动构造时,左值和右值都可以去作为拷贝构造的参数
to_string函数
a.没有移动构造时,需要一次拷贝构造构成一个临时对象,然后销毁str,和一次赋值构造,将临时对象拷贝给ret1
b.有了移动构造和移动赋值,将str指向的资源直接先给到临时对象,然后ret1指向的资源与临时对象指向的资源进行交换,过一会,ret1原本则资源就会销毁了,刚刚交换过来的资源会好好保存到ret1中。
右值是移动将亡值的资源,使将要销毁的资源保存在其他对象中。
移动构造彻底解决了传值返回的场景,提高了传值返回的效率。
// 拷贝构造
// 左值
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
// 移动构造 -- 移动将亡值对象的资源
// 右值(将亡值)
string(string&& s)
:_str(nullptr)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
// 赋值重载
// s3 = 左值
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
return *this;
}
// s3 = 将亡值
string& operator=(string&& s)
{
cout << "string(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
ret1引用右值,与this 进行交换,而交换的参数s如果是右值则就是不能交换,则swap的函数参数要加上const,但是就不可以进行交换了,所以右值引用的本身对象是左值,而资源是右值。
s1是属于左值,右值只用本身是左值,可以取地址。
通俗一点,右值引用时,等于号左边是左值,等于号右边是右值。
int main()
{
std::string s3("22222222222");
// 左值引用
std::string& s4 = s3;
cout << &s4 << endl;
// 右值引用
std::string&& s1 = std::string("111111111");
// s1是左值(右值引用本身是左值)
cout << &s1 << endl;
//cout << &std::string("111111111") << endl;
std::string& s5 = s1;
return 0;
}
1. 左值右值都是有地址的,只是左值用名字地址去存储的,而右值是编译器空间存储,但是左值可以取地址,右值不能取地址。其实两个底层都是指针
2. 只有右值引用本身处理成左值,才能实现移动构造和移动赋值转移资源
3. 右值引用的属性如果是右值,那么移动构造和移动赋值,要转移资源的语法逻辑是矛盾的;右值的资源是不可以被改变的(可以理解为右值带有const属性)
4. 右值引用本身是左值的意义是为了移动构造移动赋值,转移资源的逻辑是自洽的
5.调用了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了
右值可以强转成左值。
std::string&& s6 = std::string("111111111111111111111111111111111111111");
std::string& s7 = s6;
STL容器插入接口函数也增加了右值引用版本
链表中右值引用版本接口函数
vector中右值引用的版本接口函数
void push_back (value_type&& val);
int main()
{
list<bit::string> lt;
bit::string s1("1111");
// 这里调用的是拷贝构造
lt.push_back(s1);
// 下面调用都是移动构造
lt.push_back("2222");
lt.push_back(std::move(s1));
return 0;
}
运行结果:
// string(const string& s) -- 深拷贝
// string(string&& s) -- 移动语义
// string(string&& s) -- 移动语义
注意
- 上面效率提升,针对的是自定义类型的深拷贝的类,因为深拷贝的类才有转移资源的移动系列函数
- 对于内置类型,和浅拷贝自定义类型,没有移动系列函数
6.4 完美转发
模板中的&& 万能引用
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 右值引用,引用后,右值引用本身属性变成左值
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
// 上面的模板函数可以直接传哪种参数,就生成下面那种函数方法
//void PerfectForward(int&& t)
//{
// Fun(t);
//}
//
//void PerfectForward(int& t)
//{
// Fun(t);
//}
//
//void PerfectForward(const int&& t)
//{
// Fun(t);
//}
//
//void PerfectForward(const int& t)
//{
// Fun(t);
//}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
当前情况下,无论怎么调用他都回去调用左值引用,这是有用Fun的函数方法参数传的是左值
但是,参数改为右值后,则调用的全是右值引用的Fun函数
为应对以上情况
引用完美转发
std::forward 完美转发在传参的过程中保留对象原生类型属性
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 右值引用,引用后,右值引用本身属性变成左值
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
/*函数模版里面,这里可以叫万能引用
实参传左值,就推成左值引用
实参传右值,就推成右值引用*/
template<typename T>
void PerfectForward(T&& t)
{
Fun((T&&)t);
Fun(forward<T>(t));
}
// 上面的模板函数可以直接传哪种参数,就生成下面那种函数方法
//void PerfectForward(int&& t)
//{
// Fun((int&&)t);
//}
//
//void PerfectForward(int& t)
//{
// Fun((int&)t);
//}
//
//void PerfectForward(const int&& t)
//{
// Fun((const int&&)t);
//}
//
//void PerfectForward(const int& t)
//{
// Fun((const int&)t);
//}
注意:
- 函数模版里面,这里可以叫万能引用
- 实参传左值,就推成左值引用
- 实参传右值,就推成右值引用
7. 新的类功能
默认成员函数
原来C++类中,有6个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载(不重要)
- const 取地址重载(不重要)
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
C++11 新增了两个:移动构造函数和移动赋值运算符重载。-》移动拷贝构造
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
// 类的新功能
class Person
{
public:
Person(const char* name = "张三", int age = 18)
:_name(name)
, _age(age)
{}
private:
bit::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1; // 默认拷贝构造
Person s3 = std::move(s1); // 默认移动构造
Person s4;
s4 = std::move(s2); // 默认移动赋值
return 0;
}
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
// 类的新功能
class Person
{
public:
Person(const char* name = "张三", int age = 18)
:_name(name)
, _age(age)
{}
// 强制生成
Person(const Person& p) = default;
Person& operator=(const Person & p) = default;
Person(Person&& p) = default;
Person& operator=(Person&& p) = default;
~Person()
{}
private:
bit::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1; // 默认拷贝构造
Person s3 = std::move(s1); // 默认移动构造
Person s4;
s4 = std::move(s2); // 默认移动赋值
return 0;
}
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
// 强制生成
Person(const Person& p) = default;
Person& operator=(const Person & p) = default;
Person(Person&& p) = default;
Person& operator=(Person&& p) = default;
如果要写移动拷贝构造和移动赋值,则必须要有写拷贝构造和赋值重载。反之可直接写拷贝构造和赋值,不用写移动拷贝和移动赋值
类成员变量初始化
C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化,这个我们在雷和对象默认就讲了,这里就不再细讲了。
强制生成默认函数的关键字default:
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
禁止生成默认函数的关键字delete:
**如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。**在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
用一个例题来理解
一个类只能在堆上生成对象
// 这个类只能在堆上生成对象
class HeapOnly
{
public:
static HeapOnly* CreateObj()// 类外没有对象无法覅用,所以定义成静态,用类域调用
{
return new HeapOnly;
}
// C++11
HeapOnly(const HeapOnly&) = delete;
// C++98 私有+只声明不实现
private:
//HeapOnly(const HeapOnly&);
HeapOnly()
{}
int _a = 1;
};
int main()
{
//HeapOnly ho1;
//HeapOnly* p1 = new HeapOnly;
HeapOnly* p2 = HeapOnly::CreateObj();
// 不能被拷贝,才能禁止
//HeapOnly obj(*p2);
return 0;
}
- 构造函数私有化,就不能用对象构造,不用随意创建对象,但是obj还是栈上的对象(拷贝构造)
- 让拷贝构造私有,只声明不实现(或者公有加delete)
继承和多态中的final与override关键字
- finally修饰一个类,类无法被继承
- override:判断派生类的虚函数是否完成重写
8.可变参数模板
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们点到为止,以后大家如果有需要,再可以深入学习。
下面就是一个基本可变参数的函数模板
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
template <class ...Args>
void Cpp_Printf(Args... args)
{
// 计算参数包的数据个数
cout << sizeof...(args) << endl;
}
int main()
{
Cpp_Printf(1);
Cpp_Printf(1, 'A');
Cpp_Printf(1, 'A', std::string("sort"));
return 0;
}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。
// 编译时,参数推到递归
void _Cpp_Printf()
{
cout << endl;
}
template <class T, class ...Args>
void _Cpp_Printf(const T& val, Args... args)
{
cout << val << endl;
_Cpp_Printf(args...);
}
template <class ...Args>
void Cpp_Printf(Args... args)
{
_Cpp_Printf(args...);
}
int main()
{
Cpp_Printf(1, 'A', std::string("sort"));
return 0;
}
上面的参数包推导,编译时递归调用,每次调用_Cpp_Printf方法,每次调用完就减少一个参数,直到无参时调用到_Cpp_Printf无参方法。
逗号表达式展开参数包
这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。
expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)…}将会展开成((printarg(arg1),0),
(printarg(arg2),0), (printarg(arg3),0), etc… ),最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
改进以后的代码如下:
template <class T>
int PrintArg(T t)
{
cout << t << " ";
return 0;
}
template <class ...Args>
void Cpp_Printf(Args... args)
{
// 编译时推导,args...参数有几个值,PrintArg就调用几次,就有几个返回值,arr就开多大
int arr[] = { PrintArg(args)... };
cout << endl;
}
void Cpp_Printf(int x, char y, std::string z)
{
int arr[] = { PrintArg(x),PrintArg(y),PrintArg(z) };
cout << endl;
}
int main()
{
/*Cpp_Printf(1.1);
Cpp_Printf(1.1, 'x');*/
Cpp_Printf(1, 'A', std::string("sort"));
return 0;
}
STL容器中的empalce相关接口函数:
vector的emplace_back的接口函数
list中的emplace_back的接口函数
template <class... Args>
void emplace_back (Args&&... args)
首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。
push_back和emplace_back
相同的使用方式下,没有区别
int main()
{
// 没区别
list<bit::string> lt;
bit::string s1("xxxxxxxxxxxx");
lt.push_back(s1);
lt.push_back(move(s1));
cout << endl;
bit::string s2("xxxxxxxxxxxx");
lt.emplace_back(s2);
lt.emplace_back(move(s2));
return 0;
}
多参数时
int main()
{
// 没区别
list<pair<bit::string, int>> lt;
pair<string, int> kv1("xxxxx", 1);
//lt.push_back(kv1);
lt.push_back(move(kv1));
cout << endl;
// 直接传pair的对象效果跟push_back系列是一样的
pair<string, int> kv2("xxxxx", 1);
lt.emplace_back(kv2);
lt.emplace_back(move(kv2));
// 直接传构造pair的参数包,参数包一直往下传,底层直接构造
lt.emplace_back("xxxxx", 1);
return 0;
}
模拟实现emplace
struct ListNode
{
ListNode<T>* _next;
ListNode<T>* _prev;
T _data;
ListNode(const T& x = T())
:_next(nullptr)
,_prev(nullptr)
,_data(x)
{}
ListNode(T&& x)
:_next(nullptr)
, _prev(nullptr)
, _data(forward<T>(x))
{}
template <class... Args>
ListNode(Args&&... args)
:_next(nullptr)
, _prev(nullptr)
, _data(forward<Args>(args)...)
{}
/*ListNode(const char* str, int val)
: _next(nullptr)
, _prev(nullptr)
, _data(str, val)
{}*/
};
template<class T>
class list
{
typedef ListNode<T> Node;
public:
void push_back(const T& x)
{
insert(end(), x);
}
void push_back(T&& x)
{
insert(end(), forward<T>(x));
}
template <class... Args>
void emplace_back(Args&&... args)
{
emplace(end(), forward<Args>(args)...);
}
/*void emplace_back(const char* str, int val)
{
emplace(end(), str, val);
}*/
void insert(iterator pos, const T& val)
{
Node* cur = pos._node;
Node* newnode = new Node(val);
Node* prev = cur->_prev;
// prev newnode cur;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
_size++;
}
void insert(iterator pos, T&& val)
{
Node* cur = pos._node;
Node* newnode = new Node(forward<T>(val));
Node* prev = cur->_prev;
// prev newnode cur;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
_size++;
}
template <class... Args>
void emplace(iterator pos, Args&&... args)
{
Node* cur = pos._node;
Node* newnode = new Node(forward<Args>(args)...);
Node* prev = cur->_prev;
// prev newnode cur;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
_size++;
}
//void emplace(iterator pos, const char* str, int val)
//{
// Node* cur = pos._node;
// Node* newnode = new Node(str, val);
// Node* prev = cur->_prev;
// //...
//}
private:
Node* _head;
size_t _size;
};
传参数时,用完美转发 forward
方法就相当于被编译器实例化成这样了
9. lambda表达式
9.1 举一个平常的例子
通常完美在自定义类型比较大小,需要用一个规则来定义大小,而这个就可以用仿函数来定义怎么样来定义相应的规则比较大小
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
// ...
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
struct ComparePriceLess
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price < gr._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。
9.2 lambda表达式
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
auto priceGreater = [](const Goods& g1, const Goods& g2) {return g1._price > g2._price; };
auto priceLess = [](const Goods& g1, const Goods& g2)
{
return g1._price < g2._price;
};
sort(v.begin(), v.end(), priceLess);
cout << typeid(priceLess).name() << endl;
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)
{
return g1._price > g2._price;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)
{
return g1._evaluate < g2._evaluate;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)
{
return g1._evaluate > g2._evaluate;
});
return 0;
}
9.3 lambda表达式语法
lambda表达式书写格式:
[capture-list] (parameters) mutable -> return-type { statement}
- lambda表达式各部分说明
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用,不可以被省略。
- (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
- mutable:**默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。**使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意:
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
lambda表达式:相对于我们而言是匿名函数对象,类型是未知的
- 返回值可以省略,编译器堆返回类型进行推导
- 参数列表为空时,可以省略
int main()
{
// lambda
auto add1 = [](int a, int b)->int {return a + b; };
// 返回值可以省略
auto add2 = [](int a, int b) {return a + b; };
// 没有参数,参数列表可以省略
auto func1 = [] {cout << "hello world" << endl; };
cout << typeid(add1).name() << endl;
cout << typeid(add2).name() << endl;
cout << typeid(func1).name() << endl;
cout << add1(1, 2) << endl;
func1();
return 0;
}
lambda表达式本质上就是仿函数,其仿函数的类名就是UUID,这样类名不会冲突。
- 捕获列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
注意:
a. 父作用域指包含lambda函数的语句块
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。
比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
d. 在块作用域以外的lambda函数捕捉列表必须为空。
e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
f. lambda表达式之间不能相互赋值,即使看起来类型相同
捕捉列表:传值捕捉,引用捕捉,混合捕捉
- 传值捕捉:默认捕捉的变量是const类型,mutable可以修改传值捕捉的对象,也是传值捕捉
- 引用捕捉:里面和外面都会改变
int main()
{
int a = 1, b = 2;
auto swap1 = [](int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
};
swap1(a, b);
// 捕捉a b对象给lambda
// mutable可以修改传值捕捉对象(日常一般不需要)
// 因为a b是拷贝过来,虽然修改也不改变外面的a b
auto swap2 = [a, b]() mutable
{
int tmp = a;
a = b;
b = tmp;
};
swap2();
// 引用方式捕捉
auto swap3 = [&a, &b]()
{
int tmp = a;
a = b;
b = tmp;
};
swap3();
// 捕捉地址
int* pa = &a, * pb = &b;
auto swap4 = [pa, pb]()
{
int tmp = *pa;
*pa = *pb;
*pb = tmp;
};
swap4();
return 0;
}
- 混合捕捉:就相当于运用多参数进行捕捉。
int main()
{
int a = 1, b = 2, c = 3, d = 4, e = 5;
// 传值捕捉所有对象
auto func1 = [=]()
{
return a + b + c * d;
};
cout << func1() << endl;
// 传引用捕捉所有对象
auto func2 = [&]()
{
a++;
b++;
c++;
d++;
e++;
};
func2();
cout << a << b << c << d << e << endl;
// 混合捕捉,传引用捕捉所有对象,但是d和e传值捕捉
auto func3 = [&, d, e]()
{
a++;
b++;
c++;
//d++;
//e++;
};
func3();
cout << a << b << c << d << e << endl;
// a b传引用捕捉,d和e传值捕捉
auto func4 = [&a, &b, d, e]() mutable
{
a++;
b++;
d++;
e++;
};
func4();
cout << a << b << c << d << e << endl;
return 0;
}
9.4 函数对象与lambda表达式
函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的类对象。
class Rate
{
public:
Rate(double rate) : _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
int main()
{
// 函数对象
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
// lamber
auto r2 = [=](double monty, int year)->double {return monty * rate * year;
};
r2(10000, 2);
return 0;
}
从使用方式上来看,函数对象与lambda表达式完全一样。
函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
10. 包装器
10.1 function
头文件:#include
可调用对象:
- 函数指针 -》 类型定义复杂
- 仿函数对象 -》 要定义一个类,用的时候有点麻烦,不适合同一类型
- lambda表达式 -》 没有类型概念
function包装器
function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器
包装器不是定义可调用对象
包装器的定义
std::function在头文件<functional>
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参
// 使用方法
#include<functional>
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator() (int a, int b)
{
return a + b;
}
};
// 不是定义可调用对象,包装可调用对象
int main()
{
function<int(int, int)> fc1;
// 包装对象
//function<int(int, int)> fc2(f);
function<int(int, int)> fc2 = f; // 函数名(函数指针)
function<int(int, int)> fc3 = Functor();// 仿函数对象
function<int(int, int)> fc4 = [](int x, int y) {return x + y;};// lambda表达式
// 调用
// 实际上还是一个仿函数
cout << fc2(1, 2) << endl;
//cout << fc2.operator()(1, 2) << endl;
cout << fc3(1, 2) << endl;
cout << fc4(1, 2) << endl;
return 0;
}
看如下的程序
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 函数名
cout << useF(f, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl;
// lambda表达式
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
cout << endl << endl;
return 0;
}
通过上面的程序验证,我们会发现useF函数模板实例化了三份。也就说有三个函数指针
包装器可以很好的解决上面的问题
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 函数名
cout << useF(f, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl;
// lambda表达式
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
cout << endl << endl;
// 把上面的函数方法用包装器包装一下
std::function<double(double)> func1 = f;
cout << useF(func1, 11.11) << endl;
// 函数对象
std::function<double(double)> func2 = Functor();
cout << useF(func2, 11.11) << endl;
// lamber表达式
std::function<double(double)> func3 = [](double d)->double { return d /
4; };
cout << useF(func3, 11.11) << endl;
return 0;
}
包装器包装以后,useF就只用实例化一份,包装器可以统一类型
包装成员函数指针
成员函数的函数指针 &类型(类名):函数名
// 包装成员函数指针
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
// 成员函数的函数指针 &类型::函数名
function<int(int, int)> fc1 = &Plus::plusi;
cout << fc1(1, 2) << endl;
function<double(Plus*, double, double)> fc2 = &Plus::plusd;
Plus plus;
cout << fc2(&plus, 1.1, 2.2) << endl;
function<double(Plus, double, double)> fc3 = &Plus::plusd;
cout << fc3(Plus(), 1.1, 2.2) << endl;
return 0;
}
注意:
- 静态成员函数可以直接用类名 :函数名调用。
- 非静态成员函数由于里面遗憾着this指针,则在调用时参数列表应该加上类的指针,之后在定义一个类的对象,之后调用进行取地址接收。
- 非静态成员函数也可以直接在参数列表用类名,则就不用创建对象,调用时直接用一个类的匿名对象既可。
10.2 bind
std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。
// 原型如下:
// 无参
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
// 有参
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
可以将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为第二个参数,以此类推。
主要作用:调整可调用对象的参数个数和顺序
用placeholders这个类来调用函数中的参数,在bind中将参数随意调返顺序,达到挑中可调用对象的参数顺序的结果
int Sub(int a, int b)
{
return a - b;
}
int main()
{
auto f1 = Sub;
cout << f1(10, 5) << endl;
// 调整顺序(参数顺序没有变化)
auto f2 = bind(Sub, placeholders::_1, placeholders::_2);
cout << f2(10, 5) << endl;
// 调整顺序(参数顺序有变化)
auto f2 = bind(Sub, placeholders::_2, placeholders::_1);
cout << f2(10, 5) << endl;
cout << typeid(f1).name() << endl;
cout << typeid(f2).name() << endl;
return 0;
}
绑定参数,使参数无法被调整顺序
将要绑定的参数直接显示写在相应的参数列表的位置,就完成绑定
class Sub
{
public:
Sub(int x)
:_x(x)
{}
int sub(int a, int b)
{
return (a - b) * _x;
}
private:
int _x;
};
int main()
{
// 成员函数包装时会有this指针,与function那里差不多
auto f3 = bind(&Sub::sub, placeholders::_1, placeholders::_2, placeholders::_3);
cout << f3(Sub(1), 10, 5) << endl;
Sub sub(1);
cout << f3(&sub, 10, 5) << endl;
// 绑定,调整参数个数
// 将参数直接显示写出时,就不会在去调整其位置,就时相当于绑定
auto f4 = bind(&Sub::sub, Sub(1), placeholders::_1, placeholders::_2);
cout << f4(10, 5) << endl;
auto f5 = bind(&Sub::sub, &sub, placeholders::_1, placeholders::_2);
cout << f5(10, 5) << endl;
return 0;
}
用一个示例表示bind
在通常情况下,调用函数则要这样书写,而名字可以不用写多次,造成不必要的书写
void fx(const string& name, int x, int y)
{
cout << name << "->[" << "血量:" << x << ",蓝:" << y << ']' << endl;
}
int main()
{
fx("王昭君", 80, 20);
fx("王昭君", 85, 10);
fx("王昭君", 99, 0);
fx("王昭君", 99, 80);
fx("亚瑟", 99, 80);
fx("亚瑟", 91, 80);
fx("亚瑟", 5, 80);
return 0;
}
用bind可以把name那一参数绑定,让他每次不要改变,完美也就不用多次书写
void fx(const string& name, int x, int y)
{
cout << name << "->[" << "血量:" << x << ",蓝:" << y << ']' << endl;
}
int main()
{
/*fx("王昭君", 80, 20);
fx("王昭君", 85, 10);
fx("王昭君", 99, 0);
fx("王昭君", 99, 80);
fx("亚瑟", 99, 80);
fx("亚瑟", 91, 80);
fx("亚瑟", 5, 80);*/
// 将上述代码改为下面,绑定姓名
auto f6 = bind(fx, "王昭君", placeholders::_1, placeholders::_2);
f6(80, 20);
f6(85, 10);
f6(99, 0);
f6(99, 80);
auto f7 = bind(fx, "亚瑟", placeholders::_1, placeholders::_2);
f7(80, 20);
f7(85, 10);
f7(99, 0);
f7(99, 80);
return 0;
}
_1永远代表第一个实参
_2永远代表第二个实参
…
也可以随意绑定参数
auto f8 = bind(fx, placeholders::_1, 80, placeholders::_2);
f8("武则天", 50);
f8("韩信", 40);
也可以传给function
bind本来就是一个binder类中,可以直接传给function,但是参数要对应的是未绑定的参数
//auto f8 = bind(fx, placeholders::_1, 80, placeholders::_2);
function<void(std::string, int)> f8 = bind(fx, placeholders::_1, 80, placeholders::_2);
f8("武则天", 50);
f8("韩信", 40);
也可以调用参数不同的bind
class Sub
{
public:
Sub(int x)
:_x(x)
{}
int sub(int a, int b)
{
return (a - b) * _x;
}
private:
int _x;
};
template<class T>
void fy(int n)
{
T* p = new T[n];
}
int main()
{
map<string, function<int(int, int)>> opFuncMap = {
{"+", [](int a, int b) {return a + b; }},
{"-", bind(&Sub::sub, Sub(10), placeholders::_1, placeholders::_2)},
{"*", [](int a, int b) {return a * b; }},
{"/", [](int a, int b) {return a / b; }}
};
fy<int>(10);
return 0;
}
11. 线程库
11.1 thread类的介绍
在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件。
函数名 | 功能 |
---|---|
thread() | 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程 |
thread(fn,args1, args2,…) | 构造一个线程对象,并关联线程函数fn,args1,args2,…为线程函数的参数 |
get_id() | 获取线程id |
jionable() | 线程是否还在执行,joinable代表的是一个正在执行中的线程。 |
jion() | 该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行 |
detach() | 在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关 |
注意:
- 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。
- 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
#include <thread>
int main()
{
std::thread t1;
cout << t1.get_id() << endl;
return 0;
}
get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中
包含了一个结构体:
// vs下查看
typedef struct
{ /* thread identifier for Win32 */
void *_Hnd; /* Win32 HANDLE */
unsigned int _Id;
} _Thrd_imp_t;
- 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。
线程函数一般情况下可按照以下三种方式提供:
函数指针
lambda表达式
函数对象
#include <iostream>
using namespace std;
#include <thread>
void ThreadFunc(int a)
{
cout << "Thread1" << a << endl;
}
class TF
{
public:
void operator()()
{
cout << "Thread3" << endl;
}
};
int main()
{
// 线程函数为函数指针
thread t1(ThreadFunc, 10);
// 线程函数为lambda表达式
thread t2([] {cout << "Thread2" << endl; });
// 线程函数为函数对象
TF tf;
thread t3(tf);
t1.join();
t2.join();
t3.join();
cout << "Main thread!" << endl;
return 0;
}
- thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。
- 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
采用无参构造函数构造的线程对象
线程对象的状态已经转移给其他线程对象
线程已经调用jion或者detach结束
面试题:并发与并行的区别?
- 并发(Concurrency):
并发是指在计算机系统中,多个任务看起来是同时执行的,但实际上可能是交替执行的。
并发通常涉及到时间片轮转或多任务处理,操作系统会将CPU时间分配给不同的任务,使得它们可以快速交替执行,从而给用户一种它们同时进行的错觉。
并发可以在单核处理器上实现,通过操作系统调度任务。- 并行(Parallelism):
并行是指在计算机系统中,多个任务或操作在同一时刻真正地同时执行。
并行需要硬件支持,如多核处理器或多处理器系统,每个核心或处理器可以独立执行不同的任务。
并行可以显著提高计算效率,特别是在处理可以分解为多个独立子任务的大规模问题时。简单来说,并发是操作系统层面的概念,它通过任务切换来实现多任务的"同时"执行;而并行是硬件层面的概念,它通过多个处理器或核心来实现真正的同时执行。两者都是为了提高系统资源的利用率和计算效率。
11.2 线程函数参数
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。
void Print(int n,int i,int& rx,mutex& rmtx)
{
mtx.lock();// 执行完线程1,在执行线程2
for (; i < n; i++)
{
/*cout << i << endl;*/
++rx;
}
//cout << endl;
mtx.unlock();
}
int main()
{
int x = 0;
mutex mtx;
// 如果想要通过形参改变外部实参时,必须借助std::ref()函数
//thread t1(Print, 100, x, mtx);
//thread t2(Print, 200, x, mtx);
thread t1(Print, 100, ref(x), ref(mtx));
thread t2(Print, 200, ref(x), ref(mtx));
t1.join();// 阻塞线程t1
t2.join();// 阻塞线程t2
cout << this_thread::get_id() << endl;// 打印主线程id
return 0;
}
注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数
11.3 原子性操作库(atomic)
多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,** **。比如:
#include <iostream>
using namespace std;
#include <thread>
unsigned long sum = 0L;
void fun(size_t num)
{
for (size_t i = 0; i < num; ++i)
sum++;
}
int main()
{
cout << "Before joining,sum = " << sum << std::endl;
thread t1(fun, 10000000);
thread t2(fun, 10000000);
t1.join();
t2.join();
cout << "After joining,sum = " << sum << std::endl;
return 0;
}
两个线程一起运行时,会形成线程的并行操作,一起去做sum++操作,可能会导致以下问题:没有达到预期的结果
C++98中传统的解决方式:可以对共享修改的数据可以加锁保护。
可以进行加锁操作
#include <iostream>
using namespace std;
#include <thread>
#include <mutex>
std::mutex m;
unsigned long sum = 0L;
void fun(size_t num)
{
// 锁放在循环外面,一个进程执行完后,下一个进程才能进入锁
m.lock();
for (size_t i = 0; i < num; ++i)
{
// 由于线程操作太快,将锁加在循环里面,会不安全,也会影响结果
//m.lock();
sum++;
//m.unlock();
}
m.unlock();
}
int main()
{
cout << "Before joining,sum = " << sum << std::endl;
thread t1(fun, 10000000);
thread t2(fun, 10000000);
t1.join();
t2.join();
cout << "After joining,sum = " << sum << std::endl;
return 0;
}
虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻
塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。
并且如果在锁内进行try catch操作,有可能出现在锁内出错直接抛异常,而这个锁没有被解锁,会造成风险,造成死锁。
因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入的原子操作类型,使得线程间数据的同步变得非常高效。
注意:需要使用以上原子操作变量时,必须添加头文件
#include <iostream>
using namespace std;
#include <thread>
#include <atomic>
// 原子操作
// 对这个多线程执行的变量进行原子操作,保证不可被中断的一个或一系列操作
atomic_long sum{ 0 };
void fun(size_t num)
{
for (size_t i = 0; i < num; ++i)
sum++; // 原子操作
}
int main()
{
cout << "Before joining, sum = " << sum << std::endl;
thread t1(fun, 1000000);
thread t2(fun, 1000000);
t1.join();
t2.join();
cout << "After joining, sum = " << sum << std::endl;
return 0;
}
在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问。
更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型。
atmoic<T> t; // 声明一个类型为T的原子类型变量t
注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了。
#include <atomic>
int main()
{
atomic<int> a1(0);
//atomic<int> a2(a1); // 编译失败
atomic<int> a2(0);
//a2 = a1; // 编译失败
return 0;
}
10.4 lock_guard和unique_lock
在多线程环境下,如果想要保证某个变量的安全性,只要将其设置成对应的原子类型即可,即高效又不容易出现死锁问题。但是有些情况下,我们可能需要保证一段代码的安全性,那么就只能通过锁的方式来进行控制
#include <iostream>
using namespace std;
#include <thread>
#include <mutex>
void Print(int n,int i,int& rx,mutex& rmtx)
{
mtx.lock();// 执行完线程1,在执行线程2
for (; i < n; i++)
{
/*cout << i << endl;*/
++rx;
}
//cout << endl;
mtx.unlock();
}
int main()
{
int x = 0;
mutex mtx;
thread t1(Print, 100, ref(x), ref(mtx));
thread t2(Print, 200, ref(x), ref(mtx));
t1.join();// 阻塞线程t1
t2.join();// 阻塞线程t2
cout << this_thread::get_id() << endl;// 打印主线程id
return 0;
}
上述代码的缺陷:锁控制不好时,可能会造成死锁,最常见的比如在锁中间代码返回,或者在锁的范围内抛异常。因此:C++11采用RAII的方式对锁进行了封装,即lock_guard和unique_lock。
11.4.1 mutex的种类
在C++11中,Mutex总共包了四个互斥量的种类:
- std::mutex
C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用
的三个函数:
函数名 | 函数功能 |
---|---|
lock() | 上锁:锁住互斥量 |
unlock() | 解锁:释放对互斥量的所有权 |
try_lock() | 尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞 |
注意,线程函数调用lock()时,可能会发生以下三种情况:
- 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁
- 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
- 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
线程函数调用try_lock()时,可能会发生以下三种情况:
可以手动解锁
- 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock释放互斥量
- 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉
- 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
- std::recursive_mutex
其允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),除此之外,
std::recursive_mutex 的特性和 std::mutex 大致相同。
- std::timed_mutex
比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until() 。
- try_lock_for()
接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。- try_lock_until()
接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
- std::recursive_timed_mutex
11.4.2 lock_guard
std::lock_gurad 是 C++11 中定义的模板类。定义如下:
template<class _Mutex>
class lock_guard
{
public:
// 在构造lock_gard时,_Mtx还没有被上锁
explicit lock_guard(_Mutex& _Mtx)
: _MyMutex(_Mtx)
{
_MyMutex.lock();
}
// 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁
lock_guard(_Mutex& _Mtx, adopt_lock_t)
: _MyMutex(_Mtx)
{}
~lock_guard() _NOEXCEPT
{
_MyMutex.unlock();
}
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
_Mutex& _MyMutex;
};
通过上述代码可以看到,lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。
class LockGuard
{
public:
LockGuard(mutex& mtx)
:_mtx(mtx)
{
_mtx.lock();
}
~LockGuard()
{
_mtx.unlock();
}
private:
mutex& _mtx;
};
int main()
{
vector<thread> vthd;
int n;
cin >> n;
vthd.resize(n);
atomic<int> x = 0;
//atomic<int> x{ 0 };
//int x = 0;
mutex mtx;
auto func = [&](int n) {
//mtx.lock();
// 局部域
{
//LockGuard lock(mtx);
//lock_guard<mutex> lock(mtx);
for (size_t i = 0; i < n; i++)
{
++x;
}
//mtx.unlock();
}
/*for (size_t i = 0; i < n; i++)
{
cout << i << endl;
}*/
};
for (auto& thd : vthd)
{
// 移动赋值
thd = thread(func, 100000);
}
for (auto& thd : vthd)
{
thd.join();
}
cout << x << endl;
printf("%d\n", x.load());
return 0;
}
lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock。
11.4.3 unique_lock
与lock_gard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。在构造(或移动(move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的unique_lock 对象负责传入的 Mutex 对象的上锁和解锁操作。使用以上类型互斥量实例化unique_lock的对象时,自动调用构造函数上锁,unique_lock对象销毁时自动调用析构函数解锁,可以很方便的防止死锁问题。
与lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:
- 上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock
- 修改操作:移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有权)、释放(release:返回它所管理的互斥量对象的指针,并释放所有权)
- 获取属性:owns_lock(返回当前对象是否上了锁)、operator bool()(与owns_lock()的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_id(int id) {
std::unique_lock<std::mutex> lck(mtx);
while (!ready)
cv.wait(lck);
std::cout << "thread " << id << '\n';
}
void go() {
std::unique_lock<std::mutex> lck(mtx);
ready = true;
cv.notify_all();
}
int main()
{
std::thread threads[10];
// spawn 10 threads:
for (int i = 0; i < 10; ++i)
threads[i] = std::thread(print_id, i);
std::cout << "10 threads ready to race...\n";
this_thread::sleep_for(std::chrono::milliseconds(100));
go(); // go!
for (auto& th : threads)
th.join();
return 0;
}
11.5 支持两个线程交替打印,一个打印奇数,一个打印偶数
int main()
{
std::mutex mtx;
condition_variable c;
int n = 100;
bool flag = true;
thread t2([&]() {
int j = 1;
while (j < n)
{
unique_lock<mutex> lock(mtx);
// 只要flag == true t2一直阻塞'
// 只要flag == false t2不会阻塞
while (flag)
c.wait(lock);
cout << j << endl;
j += 2; // 奇数
flag = true;
c.notify_one();// 全部锁都唤醒
}
});
this_thread::sleep_for(std::chrono::milliseconds(1000));
// 第一个打印的是t1打印0
thread t1([&]() {
int i = 0;
while (i < n)
{
unique_lock<mutex> lock(mtx);
// flag == false t1一直阻塞
// flag == true t1不会阻塞
while (!flag)
{
c.wait(lock);
}
cout << i << endl;
flag = false;
i += 2; // 偶数
c.notify_one();// 全部锁都唤醒
}
});
t1.join();
t2.join();
return 0;
}
通过flag的变换,让进程进行拥塞。
由于打印从0开始,falg为true,所以t2线程阻塞,t1进入锁打印0,之后全部唤醒,即t2也被解除阻塞,所以打印1,之后以此类推打印完100.