c++11标准(1)
一、long long类型
-
新增了类型long long和unsigned long long,以支持64位(或更宽)的整型。
-
在VS中,int和long都是4字节,long long是8字节。
-
在Linux中,int是4字节,long和long long是8字节。
二、char16_t和char32_t类型
新增了类型char16_t和char32_t,以支持16位和32位的字符。意义不大,好像没什么人用,连demo程序都找不到。
三、原始字面量
略。
四、统一的初始化(列表)
C++11丰富了大括号的使用范围,用大括号括起来的列表(统一的初始化列表)可以用于所有内置类型和用户自定义类型。
- 使用统一的初始化列表时,可以添加等号(=),也可以不添加:
int x={5};
double y{2.75};
short quar[5]{4,5,2,76,1};
- 统一的初始化列表也可以用于new表达式中:
int *ar=new int[4]{2,4,6,7};
- 创建对象时,也可以使用大括号(而不是圆括号)来调用构造函数:
class Girl
{
private:
int m_bh;
string m_name;
public:
Girl(int bh,string name) : m_bh(bh),m_name(name) {}
};
Girl g1(3, "西施"); // C++98的风格。
Girl g2={5, "冰冰"}; // C++11的风格。
Girl g3{8, "幂幂"}; // C++11的风格。
STL容器提供了将initializer_list模板类作为参数的构造函数:
vector<int> v1(10); // 把v1初始化为10个元素。
vector<int> v2{10}; // 把v2初始化为1个元素,这个元素的值是10。
vector<int> v2{3,5,8}; // 把v3初始化为3个元素,值分别是3、5、8。
- 头文件<initializer_list>提供了对模板类initializer_list的支持,这个类包含成员函数begin()和end()。除了用于构造函数外,还可以将initializer_list用于常规函数的参数:
#include <iostream>
#include <initializer_list>
double sum(std::initializer_list<double> il)
{
double total = 0;
for (auto it = il.begin(); it != il.end(); it++)
total = total + *it;
return total;
}
int main()
{
// double total = sum( 3.14, 5.20, 8 ); // 错误,如果没有大括号,这是三个参数。
double total = sum({ 3.14, 5.20, 8 }); // 正确,有大括号,这是一个参数。
std::cout << "total=" << total << std::endl;
}
五、自动推导类型auto
略。
六、decltype关键字
略。
七、函数后置返回类型
略
八、模板的别名
略
九、空指针nullptr
-
空指针是不会指向有效数据的指针。以前,C/C++用0表示空指针,这带来了一些问题,这样的话0既可以表示指针常量,又可以表示整型常量。
-
C++11新增了关键字nullptr,用于表示空指针;它是指针类型,不是整型类型。
-
为了向后兼容,C++11仍允许用0来表示空指针,因此表达式nullptr==0为true。
-
使用nullptr提供了更高的类型安全。例如,可以将0传递给形参为int的函数,但是,如果将nullptr传递给这样的函数,编译器将视为错误。
-
因此,出于清晰和安全考虑,请使用nullptr。
十、智能指针
略。
十一、异常规范方面的修改
略。
十二、强类型枚举(枚举类)
-
传统的C++枚举提供了一种创建常量的方式,但类型检查比较低级。还有,如果在同一作用域内定义的两个枚举,它们的成员不能同名。
-
针对枚举的缺陷,C++11 标准引入了枚举类,又称强类型枚举。
-
声明强类型枚举非常简单,只需要在enum后加上关键字 class。
例如∶
enum e1{ red, green };
enum class e2 { red, green, blue };
enum class e3 { red, green, blue, yellow };
-
使用强类型枚举时,要在枚举成员名前面加枚举名和::,以免发生名称冲突,如:e2::red,e3::blue
-
强类型枚举默认的类型为int,也可以显式地指定类型,具体做法是在枚举名后面加上:type,type可以是除wchar_t以外的任何整型。
例如:
enum class e2:char { red, green, blue };
十三、explicit关键字
- C++支持对象自动转换,但是,自动类型转换可能导致意外。为了解决这种问题,C++11引入了explicit关键字,用于关闭自动转换的特性。
十四、类内成员初始化
在类的定义中初始化成员变量。
class Girl
{
private:
int m_bh=20; // 年龄。
string m_name="美女"; // 姓名。
char m_xb = 'X'; // 性别。
public:
Girl(int bh, string name) : m_bh(bh), m_name(name) {}
};
十五、基于范围的for循环
略
十六、新的STL容器
-
array(静态数组)
-
array的大小是固定的,不像其它的模板类,但array有begin()和end()成员函数,程序员可以array对象使用STL算法。
-
forward_list(单向链表)
-
unordered_map、unordered_multimap、unordered_set、unordered_multiset(哈希表)
十七、新的STL方法(成员函数)
-
C++11新增了的方法cbegin()、cend()、crbegin()、crend(),这些方法将元素视为const。
-
iterator emplace (iterator pos, …); // 在指定位置插入一个元素,…用于构造元素,返回指向插入元素的迭代器。
-
更重要的是,除了传统的拷贝构造函数和赋值函数,C++11新增了移动构造函数和移动赋值函数。
十八、摒弃export
- C++98新增了export关键字,C++11不再使用,但仍保留它作为关键字,供以后使用。
十九、嵌套模板的尖括号
-
为了避免与运算符>>混淆,C++要求在声明嵌套模板时使用空格将尖括号分开:
-
vector<list
> v1; // 两个>之间必须加空格。
C++11不再这样要求:
vector<list<int>> v2; // 两个>之间不必加空格。
二十、final关键字
-
final关键字用于限制某个类不能被继承,或者某个虚函数不能被重写。
-
final关键字放在类名或虚函数名的后面。
示例:
class AA
{
public:
virtual void test()
{
cout << "AA class...";
}
};
class BB : public AA
{
public:
void test() final // 如果有其它类继承BB,test()方法将不允许重写。
{
cout << "BB class...";
}
};
class CC : public BB
{
public:
void test() // 错误,BB类中的test()后面有final,不允许重写。
{
cout << "CC class...";
}
};
二十一、override关键字
-
在派生类中,把override放在成员函数的后面,表示重写基类的虚函数,提高代码的可读性。
-
在派生类中,如果某成员函数不是重写基类的虚函数,随意的加上override关键字,编译器会报错。
示例:
class AA
{
public:
virtual void test()
{
cout << "AA class...";
}
};
class BB : public AA
{
public:
void test() override
{
cout << "BB class...";
}
};
二十二、数值类型和字符串之间的转换
传统方法用sprintf()和snprintf()函数把数值转换为char字符串;用atoi()、atol()、atof()把char字符串转换为数值。
C++11提供了新的方法,在数值类型和string字符串之间转换。
1、数值转换为字符串
使用to_string()函数可以将各种数值类型转换为string字符串类型,这是一个重载函数,在头文件
string to_string (int val);
string to_string (long val);
string to_string (long long val);
string to_string (unsigned val);
string to_string (unsigned long val);
string to_string (unsigned long long val);
string to_string (float val);
string to_string (double val);
string to_string (long double val);
2、字符转换为串数值
在C++中,数值类型包括整型和浮点型,针对于不同的数值类型提供了不同的函数在头文件
int stoi( const string& str, size_t* pos = nullptr, int base = 10 );
long stol( const string& str, size_t* pos = nullptr, int base = 10 );
long long stoll( const string& str, size_t* pos = nullptr, int base = 10 );
unsigned long stoul( const string& str, size_t* pos = nullptr, int base = 10 );
unsigned long long stoull( const string& str, size_t* pos = nullptr, int base = 10 );
float stof( const string& str, size_t* pos = nullptr );
double stod( const string& str, size_t* pos = nullptr );
long double stold( const string& str, size_t* pos = nullptr );
形参说明:
str:需要要转换的string字符串。
pos:传出参数,存放从哪个字符开始无法继续解析的位置,例如:123a45, 传出的位置将为3。
base:若base为0,则自动检测数值进制:若前缀为0,则为八进制,若前缀为0x或0X,则为十六进制,否则为十进制。
注意:string字符串转换为数值的函数可能会抛出异常,在《209、C++异常》中有详细介绍。
示例:
string str="123a45";
size_t pos;
int val = stoi(str, &pos, 10);
cout << "val=" << val << endl; // 输出123
cout << "pos=" << pos << endl; // 输出3
二十三、静态断言static_assert
在《210、C++断言》中有详细介绍。
二十四、常量表达式constexpr关键字
const关键字从功能上来说有双重语义:只读变量和修饰常量。
示例:
void func(const int len1)
{
// len1是只读变量,不是常量。
int array1[len1]={0}; // VS会报错,Linux平台的数组长度支持变量,不会报错。
const int len2 = 8;
int array2[len2]={0}; // 正确,len2是常量。
}
C++11标准为了解决const关键字的双重语义问题,保留了const表示“只读”的语义,而将“常量”的语义划分给了新添加的constexpr关键字。
所以,C++11 标准中,建议将const和constexpr的功能区分开,表达“只读”语义的场景用const,表达“常量”语义的场景用constexpr。
二十五、默认函数控制=default与=delete
在C++中自定义的类,编译器会默认生成一些成员函数:
无参构造函数
拷贝构造函数
拷贝赋值函数
移动构造函数
移动赋值函数
析构函数
=default表示启用默认函数。
=delete表示禁用默认函数。
示例:
include
using namespace std;
class Girl
{
private:
int m_bh = 20; // 年龄。
string m_name = "美女"; // 姓名。
char m_xb = 'X'; // 性别。
public:
Girl() = default; // 启用默认构造函数。
Girl(int bh, string name) : m_bh(bh), m_name(name) {}
Girl(const Girl& g) = delete; // 删除拷贝构造函数。
void show() { cout << "bh=" << m_bh << ",m_name=" << m_name << endl; }
};
int main()
{
Girl g1;
g1.show();
// Girl g2 = g1; // 错误,拷贝构造函数已删除。
}
221、委托构造和继承构造
C++11标准新增了委托构造和继承构造两种方法,用于简化代码。
一、委托构造
在实际的开发中,为了满足不同的需求,一个类可能会重载多个构造函数。多个构造函数之间可能会有重复的代码。例如变量初始化,如果在每个构造函数中都写一遍,这样代码会显得臃肿。
委托构造就是在一个构造函数的初始化列表中调用另一个构造函数。
注意:
不要生成环状的构造过程。
一旦使用委托构造,就不能在初始化列表中初始化其它的成员变量。
示例:
include
using namespace std;
class AA
{
private:
int m_a;
int m_b;
double m_c;
public:
// 有一个参数的构造函数,初始化m_c
AA(double c) {
m_c = c + 3; // 初始化m_c
cout << " AA(double c)" << endl;
}
// 有两个参数的构造函数,初始化m_a和m_b
AA(int a, int b) {
m_a = a + 1; // 初始化m_a
m_b = b + 2; // 初始化m_b
cout << " AA(int a, int b)" << endl;
}
// 构造函数委托AA(int a, int b)初始化m_a和m_b
AA(int a, int b, const string& str) : AA(a, b) {
cout << "m_a=" << m_a << ",m_b=" << m_b << ",str=" << str << endl;
}
// 构造函数委托AA(double c)初始化m_c
AA(double c, const string& str) : AA(c) {
cout << "m_c=" << m_c << ",str=" << str << endl;
}
};
int main()
{
AA a1(10, 20, "我是一只傻傻鸟。");
AA a2(3.8, "我有一只小小鸟。");
}
二、继承构造
在C++11之前,派生类如果要使用基类的构造函数,可以在派生类构造函数的初始化列表中指定。在《126、如何构造基类》中有详细介绍。
C++11推出了继承构造(Inheriting Constructor),在派生类中使用using来声明继承基类的构造函数。
示例:
include
using namespace std;
class AA // 基类。
{
public:
int m_a;
int m_b;
// 有一个参数的构造函数,初始化m_a
AA(int a) : m_a(a) { cout << " AA(int a)" << endl; }
// 有两个参数的构造函数,初始化m_a和m_b
AA(int a, int b) : m_a(a), m_b(b) { cout << " AA(int a, int b)" << endl; }
};
class BB :public AA // 派生类。
{
public:
double m_c;
using AA::AA; // 使用基类的构造函数。
// 有三个参数的构造函数,调用A(a,b)初始化m_a和m_b,同时初始化m_c
BB(int a, int b, double c) : AA(a, b), m_c(c) {
cout << " BB(int a, int b, double c)" << endl;
}
void show() { cout << "m_a=" << m_a << ",m_b=" << m_b << ",m_c=" << m_c << endl; }
};
int main()
{
// 将使用基类有一个参数的构造函数,初始化m_a
BB b1(10);
b1.show();
// 将使用基类有两个参数的构造函数,初始化m_a和m_b
BB b2(10,20);
b2.show();
// 将使用派生类自己有三个参数的构造函数,调用A(a,b)初始化m_a和m_b,同时初始化m_c
BB b3(10,20,10.58);
b3.show();
}
222、lambda函数
lambda函数是C++11标准新增的语法糖,也称为lambda表达式或匿名函数。
lambda函数的特点是:距离近、简洁、高效和功能强大。
示例:[](const int& no) -> void { cout << "亲爱的" << no << "号:我是一只傻傻鸟。\n"; };
语法:
示例:
include
include
include
using namespace std;
// 表白函数。
void zsshow(const int & no) {
cout << "亲爱的" << no << "号:我是一只傻傻鸟。\n";
}
// 表白仿函数。
class czs
{
public:
void operator()(const int & no) {
cout << "亲爱的" << no << "号:我是一只傻傻鸟。\n";
}
};
int main()
{
vector
// 第三个参数是普通函数。
for_each(vv.begin(), vv.end(), zsshow);
// 第三个参数是仿函数。
for_each(vv.begin(), vv.end(), czs());
// 第三个参数是lambda表达式。
for_each(vv.begin(), vv.end(),
[](const int& no) {
cout << "亲爱的" << no << "号:我是一只傻傻鸟。\n";
}
);
}
一、参数列表
参数列表是可选的,类似普通函数的参数列表,如果没有参数列表,()可以省略不写。
与普通函数的不同:
lambda函数不能有默认参数。
所有参数必须有参数名。
不支持可变参数。
二、返回类型
用后置的方法书写返回类型,类似于普通函数的返回类型,如果不写返回类型,编译器会根据函数体中的代码推断出来。
如果有返回类型,建议显式的指定,自动推断可能与预期不一致。
三、函数体
类似于普通函数的函数体。
四、捕获列表
通过捕获列表,lambda函数可以访问父作用域中的非静态局部变量(静态局部变量可以直接访问,不能访问全局变量)。
捕获列表书写在[]中,与函数参数的传递类似,捕获方式可以是值和引用。
以下列出了不同的捕获列表的方式。
1)值捕获
与传递参数类似,采用值捕获的前提是变量可以拷贝。
与传递参数不同,变量的值是在lambda函数创建时拷贝,而不是调用时拷贝。
例如:
size_t v1 = 42;
auto f = [ v1 ] { return v1; }; // 使用了值捕获,将v1拷贝到名为f的可调用对象。
v1 = 0;
auto j = f(); // j为42,f保存了我们创建它是v1的拷贝。
由于被捕获的值是在lambda函数创建时拷贝,因此在随后对其修改不会影响到lambda内部的值。
默认情况下,如果以传值方式捕获变量,则在lambda函数中不能修改变量的值。
2)引用捕获
和函数引用参数一样,引用变量的值在lambda函数体中改变时,将影响被引用的对象。
size_t v1 = 42;
auto f = [ &v1 ] { return v1; }; // 引用捕获,将v1拷贝到名为f的可调用对象。
v1 = 0;
auto j = f(); // j为0。
如果采用引用方式捕获变量,就必须保证被引用的对象在lambda执行的时候是存在的。
3)隐式捕获
除了显式列出我们希望使用的父作域的变量之外,还可以让编译器根据函数体中的代码来推断需要捕获哪些变量,这种方式称之为隐式捕获。
隐式捕获有两种方式,分别是[=]和[&]。[=]表示以值捕获的方式捕获外部变量,[&]表示以引用捕获的方式捕获外部变量。
int a = 123;
auto f = [ = ] { cout << a << endl; }; //值捕获
f(); // 输出:123
auto f1 = [ & ] { cout << a++ << endl; }; //引用捕获
f1(); //输出:123(采用了后++)
cout << a << endl; //输出 124
4)混合方式捕获
lambda函数还支持混合方式捕获,即同时使用显式捕获和隐式捕获。
混合捕获时,捕获列表中的第一个元素必须是 = 或 &,此符号指定了默认捕获的方式是值捕获或引用捕获。
需要注意的是:显式捕获的变量必须使用和默认捕获不同的方式捕获。例如:
int i = 10;
int j = 20;
auto f1 = [ =, &i] () { return j + i; }; // 正确,默认值捕获,显式是引用捕获
auto f2 = [ =, i] () { return i + j; }; // 编译出错,默认值捕获,显式值捕获,冲突了
auto f3 = [ &, &i] () { return i +j; }; // 编译出错,默认引用捕获,显式引用捕获,冲突了
5)修改值捕获变量的值
在lambda函数中,如果以传值方式捕获变量,则函数体中不能修改该变量,否则会引发编译错误。
在lambda函数中,如果希望修改值捕获变量的值,可以加mutable选项,但是,在lambda函数的外部,变量的值不会被修改。
int a = 123;
auto f = amutable { cout << ++a << endl; }; // 不会报错
cout << a << endl; // 输出:123
f(); // 输出:124
cout << a << endl; // 输出:123
6)异常说明
lambda可以抛出异常,用throw(…)指示异常的类型,用noexcept指示不抛出任何异常。
五、lambda函数的本质
当我们编写了一个lambda函数之后,编译器将它翻译成一个类,该类中有一个重载了()的函数。
1)采用值捕获
采用值捕获时,lambda函数生成的类用捕获变量的值初始化自己的成员变量。
例如:
int a =10;
int b = 20;
auto addfun = [=] (const int c ) -> int { return a+c; };
int c = addfun(b);
cout << c << endl;
等同于:
class Myclass
{
int m_a; // 该成员变量对应通过值捕获的变量。
public:
Myclass( int a ) : m_a(a){}; // 该形参对应捕获的变量。
// 重载了()运算符的函数,返回类型、形参和函数体都与lambda函数一致。
int operator()(const int c) const
{
return a + c;
}
};
默认情况下,由lambda函数生成的类是const成员函数,所以变量的值不能修改。如果加上mutable,相当于去掉const。这样上面的限制就能讲通了。
2)采用引用捕获
如果lambda函数采用引用捕获的方式,编译器直接引用就行了。
唯一需要注意的是,lambda函数执行时,程序必须保证引用的对象有效。
225、右值引用
一、左值、右值
在C++中,所有的值不是左值,就是右值。左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束后就不再存在的临时对象。有名字的对象都是左值,右值没有名字。
还有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值。
C++11扩展了右值的概念,将右值分为了纯右值和将亡值。
纯右值:a)非引用返回的临时变量;b)运算表达式产生的结果;c)字面常量(C风格字符串除外,它是地址)。
将亡值:与右值引用相关的表达式,例如:将要被移动的对象、T&&函数返回的值、std::move()的返回值、转换成T&&的类型的转换函数的返回值。
不懂纯右值和将亡值的区别其实没关系,统一看作右值即可,不影响使用。
示例:
class AA {
int m_a;
};
AA getTemp()
{
return AA();
}
int ii = 3; // ii是左值,3是右值。
int jj = ii+8; // jj是左值,ii+8是右值。
AA aa = getTemp(); // aa是左值 ,getTemp()的返回值是右值(临时变量)。
二、左值引用、右值引用
C++98中的引用很常见,就是给变量取个别名,在C++11中,因为增加了右值引用(rvalue reference)的概念,所以C++98中的引用都称为了左值引用(lvalue reference)。
右值引用就是给右值取个名字。
语法:数据类型&& 变量名=右值;
示例:
include
using namespace std;
class AA {
public:
int m_a=9;
};
AA getTemp()
{
return AA();
}
int main()
{
int&& a = 3; // 3是右值。
int b = 8; // b是左值。
int&& c = b + 5; // b+5是右值。
AA&& aa = getTemp(); // getTemp()的返回值是右值(临时变量)。
cout << "a=" << a << endl;
cout << "c=" << c << endl;
cout << "aa.m_a=" << aa.m_a << endl;
}
getTemp()的返回值本来在表达式语句结束后其生命也就该终结了(因为是临时变量),而通过右值引用重获了新生,其生命周期将与右值引用类型变量aa的生命周期一样,只要aa还活着,该右值临时变量将会一直存活下去。
引入右值引用的主要目的是实现移动语义。
左值引用只能绑定(关联、指向)左值,右值引用只能绑定右值,如果绑定的不对,编译就会失败。
但是,常量左值引用却是个奇葩,它可以算是一个万能的引用类型,它可以绑定非常量左值、常量左值、右值,而且在绑定右值的时候,常量左值引用还可以像右值引用一样将右值的生命期延长,缺点是,只能读不能改。
int a = 1;
const int& ra = a; // a是非常量左值。
const int b = 1;
const int& rb = b; // b是常量左值。
const int& rc = 1; // 1是右值。
总结一下,其中T是一个具体类型:
1)左值引用, 使用 T&, 只能绑定左值。
2)右值引用, 使用 T&&, 只能绑定右值。
3)已命名的右值引用是左值。
4)常量左值,使用 const T&, 既可以绑定左值又可以绑定右值。
226、移动语义
如果一个对象中有堆区资源,需要编写拷贝构造函数和赋值函数,实现深拷贝。
深拷贝把对象中的堆区资源复制了一份,如果源对象(被拷贝的对象)是临时对象,拷贝完就没什么用了,这样会造成没有意义的资源申请和释放操作。如果能够直接使用源对象拥有的资源,可以节省资源申请和释放的时间。C++11新增加的移动语义就能够做到这一点。
实现移动语义要增加两个函数:移动构造函数和移动赋值函数。
移动构造函数的语法:
类名(类名&& 源对象){......}
移动赋值函数的语法:
类名& operator=(类名&& 源对象){……}
注意:
1)对于一个左值,会调用拷贝构造函数,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++11为了解决这个问题,提供了std::move()方法来将左值转义为右值,从而方便使用移动语义。它其实就是告诉编译器,虽然我是一个左值,但不要对我用拷贝构造函数,用移动构造函数吧。左值对象被转移资源后,不会立刻析构,只有在离开自己的作用域的时候才会析构,如果继续使用左值中的资源,可能会发生意想不到的错误。
2)如果没有提供移动构造/赋值函数,只提供了拷贝构造/赋值函数,编译器找不到移动构造/赋值函数就去寻找拷贝构造/赋值函数。
3)C++11中的所有容器都实现了移动语义,避免对含有资源的对象发生无谓的拷贝。
4)移动语义对于拥有资源(如内存、文件句柄)的对象有效,如果是基本类型,使用移动语义没有意义。
示例:
include
using namespace std;
class AA
{
public:
int* m_data = nullptr; // 数据成员,指向堆区资源的指针。
AA() = default; // 启用默认构造函数。
void alloc() { // 给数据成员m_data分配内存。
m_data = new int; // 分配内存。
memset(m_data, 0, sizeof(int)); // 初始化已分配的内存。
}
AA(const AA& a) { // 拷贝构造函数。
cout << "调用了拷贝构造函数。\n"; // 显示自己被调用的日志。
if (m_data == nullptr) alloc(); // 如果没有分配内存,就分配。
memcpy(m_data, a.m_data, sizeof(int)); // 把数据从源对象中拷贝过来。
}
AA(AA&& a) { // 移动构造函数。
cout << "调用了移动构造函数。\n"; // 显示自己被调用的日志。
if (m_data != nullptr) delete m_data; // 如果已分配内存,先释放掉。
m_data = a.m_data; // 把资源从源对象中转移过来。
a.m_data = nullptr; // 把源对象中的指针置空。
}
AA& operator=(const AA& a) { // 赋值函数。
cout << "调用了赋值函数。\n"; // 显示自己被调用的日志。
if (this == &a) return *this; // 避免自我赋值。
if (m_data == nullptr) alloc(); // 如果没有分配内存,就分配。
memcpy(m_data, a.m_data, sizeof(int)); // 把数据从源对象中拷贝过来。
return *this;
}
AA& operator=(AA&& a) { // 移动赋值函数。
cout << "调用了移动赋值函数。\n"; // 显示自己被调用的日志。
if (this == &a) return *this; // 避免自我赋值。
if (m_data != nullptr) delete m_data; // 如果已分配内存,先释放掉。
m_data = a.m_data; // 把资源从源对象中转移过来。
a.m_data = nullptr; // 把源对象中的指针置空。
return *this;
}
~AA() { // 析构函数。
if (m_data != nullptr) {
delete m_data; m_data = nullptr;
}
}
};
int main()
{
AA a1; // 创建对象a1。
a1.alloc(); // 分配堆区资源。
*a1.m_data = 3; // 给堆区内存赋值。
cout << "a1.m_data=" << *a1.m_data << endl;
AA a2 = a1; // 将调用拷贝构造函数。
cout << "a2.m_data=" << *a2.m_data << endl;
AA a3;
a3 = a1; // 将调用赋值函数。
cout << "a3.m_data=" << *a3.m_data << endl;
auto f = [] { AA aa; aa.alloc(); *aa.m_data = 8; return aa; }; // 返回AA类对象的lambda函数。
AA a4 = f(); // lambda函数返回临时对象,是右值,将调用移动构造函数。
cout << "a4.m_data=" << *a4.m_data << endl;
AA a6;
a6 = f(); // lambda函数返回临时对象,是右值,将调用移动赋值函数。
cout << "a6.m_data=" << *a6.m_data << endl;
}
227、完美转发
在函数模板中,可以将参数“完美”的转发给其它函数。所谓完美,即不仅能准确的转发参数的值,还能保证被转发参数的左、右值属性不变。
C++11标准引入了右值引用和移动语义,所以,能否实现完美转发,决定了该参数在传递过程使用的是拷贝语义还是移动语义。
为了支持完美转发,C++11提供了以下方案:
1)如果模板中(包括类模板和函数模板)函数的参数书写成为T&& 参数名,那么,函数既可以接受左值引用,又可以接受右值引用。
2)提供了模板函数std::forward
示例:
include
using namespace std;
void func1(int& ii) { // 如果参数是左值,调用此函数。
cout << "参数是左值=" << ii << endl;
}
void func1(int&& ii) { // 如果参数是右值,调用此函数。
cout << "参数是右值=" << ii << endl;
}
// 1)如果模板中(包括类模板和函数模板)函数的参数书写成为T&& 参数名,
// 那么,函数既可以接受左值引用,又可以接受右值引用。
// 2)提供了模板函数std::forward
// 如果参数是一个右值,转发之后仍是右值引用;如果 参数是一个左值,转发之后仍是左值引用。
template
void func(TT&& ii)
{
func1(forward(ii));
}
int main()
{
int ii = 3;
func(ii); // 实参是左值。
func(8); // 实参是右值。
}
228、可变参数模板
可变参数模版是C++11新增的最强大的特性之一,它对参数进行了泛化,能支持任意个数、任意数据类型的参数。
示例:
include
include
using namespace std;
template
void show(T girl) // 向超女表白的函数,参数可能是超女编号,也可能是姓名,所以用T。
{
cout << "亲爱的" << girl << ",我是一只傻傻鸟。\n";
}
// 递归终止时调用的非模板函数,函数名要与展开参数包的递归函数模板相同。
void print()
{
cout << "递归终止。\n";
}
// 展开参数包的递归函数模板。
template <typename T, typename ...Args>
void print(T arg, Args... args)
{
//cout << "参数: " << arg << endl; // 显示本次展开的参数。
show(arg); // 把参数用于表白。
//cout << "还有" << sizeof...(args) << "个参数未展开。" << endl; // 显示未展开变参的个数。
print(args...); // 继续展开参数。
}
template <typename...Args>
void func(const string& str, Args...args) // 除了可变参数,还可以有其它常规参数。
{
cout << str << endl; // 表白之前,喊句口号。
print(args...); // 展开可变参数包。
cout << "表白完成。\n";
}
int main(void)
{
//print("金莲", 4, "西施");
//print("冰冰", 8, "西施", 3);
func("我是绝世帅歌。", "冰冰", 8, "西施", 3); // "我是绝世帅歌。"不是可变参数,其它的都是。
}
229、时间操作chrono库
C++11提供了chrono模版库,实现了一系列时间相关的操作(时间长度、系统时间和计时器)。
头文件:#include
命名空间:std::chrono
一、时间长度
duration模板类用于表示一段时间(时间长度、时钟周期),如:1小时、8分钟、5秒。
duration的定义如下:
template<class Rep, class Period = std::ratio<1, 1>>
class duration
{
……
};
为了方便使用,定义了一些常用的时间长度,比如:时、分、秒、毫秒、微秒、纳秒,它们都位于std::chrono命名空间下,定义如下:
using hours = duration<Rep, std::ratio<3600>> // 小时
using minutes = duration<Rep, std::ratio<60>> // 分钟
using seconds = duration
using milliseconds = duration<Rep, std::milli> // 毫秒
using microseconds = duration<Rep, std::micro> // 微秒
using nanoseconds = duration<Rep, std::nano> // 纳秒
注意:
duration模板类重载了各种算术运算符,用于操作duration对象。
duration模板类提供了count()方法,获取duration对象的值。
示例:
include
include // chrono库的头文件。
using namespace std;
int main()
{
chrono::hours t1(1); // 1小时
chrono::minutes t2(60); // 60分钟
chrono::seconds t3(60 * 60); // 60*60秒
chrono::milliseconds t4(60 * 60 * 1000); // 60601000毫秒
chrono::microseconds t5(60 * 60 * 1000 * 1000); // 警告:整数溢出。
chrono::nanoseconds t6(60 * 60 * 1000 * 1000*1000); // 警告:整数溢出。
if (t1 == t2) cout << "t1==t2\n";
if (t1 == t3) cout << "t1==t3\n";
if (t1 == t4) cout << "t1==t4\n";
// 获取时钟周期的值,返回的是int整数。
cout << "t1=" << t1.count() << endl;
cout << "t2=" << t2.count() << endl;
cout << "t3=" << t3.count() << endl;
cout << "t4=" << t4.count() << endl;
chrono::seconds t7(1); // 1秒
chrono::milliseconds t8(1000); // 1000毫秒
chrono::microseconds t9(1000 * 1000); // 1000*1000微秒
chrono::nanoseconds t10(1000 * 1000 * 1000); // 100010001000纳秒
if (t7 == t8) cout << "t7==t8\n";
if (t7 == t9) cout << "t7==t9\n";
if (t7 == t10) cout << "t7==t10\n";
// 获取时钟周期的值。
cout << "t7=" << t7.count() << endl;
cout << "t8=" << t8.count() << endl;
cout << "t9=" << t9.count() << endl;
cout << "t10=" << t10.count() << endl;
}
二、系统时间
system_clock类支持了对系统时钟的访问,提供了三个静态成员函数:
// 返回当前时间的时间点。
static std::chrono::time_pointstd::chrono::system_clock now() noexcept;
// 将时间点time_point类型转换为std::time_t 类型。
static std::time_t to_time_t( const time_point& t ) noexcept;
// 将std::time_t类型转换为时间点time_point类型。
static std::chrono::system_clock::time_point from_time_t( std::time_t t ) noexcept;
示例:
define _CRT_SECURE_NO_WARNINGS // localtime()需要这个宏。
include
include
include // put_time()函数需要包含的头文件。
include
using namespace std;
int main()
{
// 1)静态成员函数chrono::system_clock::now()用于获取系统时间。(C++时间)
auto now = chrono::system_clock::now();
// 2)静态成员函数chrono::system_clock::to_time_t()把系统时间转换为time_t。(UTC时间)
auto t_now = chrono::system_clock::to_time_t(now);
// t_now = t_now + 246060; // 把当前时间加1天。
// t_now = t_now + -16060; // 把当前时间减1小时。
// t_now = t_now + 120; // 把当前时间加120秒。
// 3)std::localtime()函数把time_t转换成本地时间。(北京时)
// localtime()不是线程安全的,VS用localtime_s()代替,Linux用localtime_r()代替。
auto tm_now = std::localtime(&t_now);
// 4)格式化输出tm结构体中的成员。
std::cout << std::put_time(tm_now, "%Y-%m-%d %H:%M:%S") << std::endl;
std::cout << std::put_time(tm_now, "%Y-%m-%d") << std::endl;
std::cout << std::put_time(tm_now, "%H:%M:%S") << std::endl;
std::cout << std::put_time(tm_now, "%Y%m%d%H%M%S") << std::endl;
stringstream ss; // 创建stringstream对象ss,需要包含
ss << std::put_time(tm_now, "%Y-%m-%d %H:%M:%S"); // 把时间输出到对象ss中。
string timestr = ss.str(); // 把ss转换成string的对象。
cout << timestr << endl;
}
三、计时器
steady_clock类相当于秒表,操作系统只要启动就会进行时间的累加,常用于耗时的统计(精确到纳秒)。
include
include
using namespace std;
int main()
{
// 静态成员函数chrono::steady_clock::now()获取开始的时间点。
auto start = chrono::steady_clock::now();
// 执行一些代码,让它消耗一些时间。
cout << "计时开始 ...... \n";
for (int ii = 0; ii < 1000000; ii++) {
// cout << "我是一只傻傻鸟。\n";
}
cout << "计时完成 ...... \n";
// 静态成员函数chrono::steady_clock::now()获取结束的时间点。
auto end = chrono::steady_clock::now();
// 计算消耗的时间,单位是纳秒。
auto dt = end - start;
cout << "耗时: " << dt.count() << "纳秒("<<(double)dt.count()/(100010001000)<<"秒)";
}
240、C++11线程
在C++11之前,C++没有对线程提供语言级别的支持,各种操作系统和编译器实现线程的方法不一样。
C++11增加了线程以及线程相关的类,统一编程风格、简单易用、跨平台。
一、创建线程
头文件:#include
线程类:std::thread
构造函数:
1)thread() noexcept;
默认构造函,构造一个线程对象,不执行任何任务(不会创建/启动子线程)。
2)template< class Function, class... Args >
explicit thread(Function&& fx, Args&&... args );
创建线程对象,在线程中执行任务函数fx中的代码,args是要传递给任务函数fx的参数。
任务函数fx可以是普通函数、类的非静态成员函数、类的静态成员函数、lambda函数、仿函数。
3)thread(const thread& ) = delete;
删除拷贝构造函数,不允许线程对象之间的拷贝。
4)thread(thread&& other ) noexcept;
移动构造函数,将线程other的资源所有权转移给新创建的线程对象。
赋值函数:
thread& operator= (thread&& other) noexcept;
thread& operator= (const other&) = delete;
线程中的资源不能被复制,如果other是右值,会进行资源所有权的转移,如果other是左值,禁止拷贝。
注意:
先创建的子线程不一定跑得最快(程序运行的速度有很大的偶然性)。
线程的任务函数返回后,子线程将终止。
如果主程序(主线程)退出(不论是正常退出还是意外终止),全部的子线程将强行被终止。
示例:
include
include // 线程类头文件。
include <windows.h> // Sleep()函数需要这个头文件。
using namespace std;
// 普通函数。
void func(int bh, const string& str) {
for (int ii = 1; ii <= 10; ii++)
{
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
Sleep(1000); // 休眠1秒。
}
}
// 仿函数。
class mythread1
{
public:
void operator()(int bh, const string& str) {
for (int ii = 1; ii <= 10; ii++)
{
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
Sleep(1000); // 休眠1秒。
}
}
};
// 类中有静态成员函数。
class mythread2
{
public:
static void func(int bh, const string& str) {
for (int ii = 1; ii <= 10; ii++)
{
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
Sleep(1000); // 休眠1秒。
}
}
};
// 类中有普通成员函数。
class mythread3
{
public:
void func(int bh, const string& str) {
for (int ii = 1; ii <= 10; ii++)
{
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
Sleep(1000); // 休眠1秒。
}
}
};
int main()
{
// 用普通函数创建线程。
//thread t1(func, 3, "我是一只傻傻鸟。");
//thread t2(func, 8, "我有一只小小鸟。");
// 用lambda函数创建线程。
auto f = [](int bh, const string& str) {
for (int ii = 1; ii <= 10; ii++)
{
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
Sleep(1000); // 休眠1秒。
}
};
//thread t3(f, 3, "我是一只傻傻鸟。");
// 用仿函数创建线程。
//thread t4(mythread1(), 3, "我是一只傻傻鸟。");
// 用类的静态成员函数创建线程。
//thread t5(mythread2::func, 3, "我是一只傻傻鸟。");
// 用类的普通成员函数创建线程。
mythread3 myth; // 必须先创建类的对象,必须保证对象的生命周期比子线程要长。
thread t6(&mythread3::func, &myth, 3, "我是一只傻傻鸟。"); // 第二个参数必须填对象的this指针,否则会拷贝对象。
cout << "任务开始。\n";
for (int ii = 0; ii < 10; ii++) {
cout << "执行任务中......\n";
Sleep(1000); // 假设执行任务需要时间。
}
cout << "任务完成。\n";
//t1.join(); // 回收线程t1的资源。
//t2.join(); // 回收线程t2的资源。
//t3.join(); // 回收线程t3的资源。
//t4.join(); // 回收线程t4的资源。
//t5.join(); // 回收线程t5的资源。
t6.join(); // 回收线程t6的资源。
}
二、线程资源的回收
虽然同一个进程的多个线程共享进程的栈空间,但是,每个子线程在这个栈中拥有自己私有的栈空间。所以,线程结束时需要回收资源。
回收子线程的资源有两种方法:
1)在主程序中,调用join()成员函数等待子线程退出,回收它的资源。如果子线程已退出,join()函数立即返回,否则会阻塞等待,直到子线程退出。
2)在主程序中,调用detach()成员函数分离子线程,子线程退出时,系统将自动回收资源。分离后的子线程不可join()。
用joinable()成员函数可以判断子线程的分离状态,函数返回布尔类型。
示例:
include
include // 线程类头文件。
include <windows.h> // Sleep()函数需要这个头文件。
using namespace std;
// 普通函数。
void func(int bh, const string& str) {
for (int ii = 1; ii <= 10; ii++)
{
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
Sleep(1000); // 休眠1秒。
}
}
int main()
{
// 用普通函数创建线程。
thread t1(func, 3, "我是一只傻傻鸟。");
thread t2(func, 8, "我有一只小小鸟。");
t1.detach(); t2.detach(); // 分离子线程。
//cout << "任务开始。\n";
//for (int ii = 0; ii < 12; ii++) {
// cout << "执行任务中......\n";
// Sleep(1000); // 假设执行任务需要时间。
//}
//cout << "任务完成。\n";
//t1.join(); // 回收线程t1的资源。
//t2.join(); // 回收线程t2的资源。
Sleep(12000);
}
三、this_thread的全局函数
C++11提供了命名空间this_thread来表示当前线程,该命名空间中有四个函数:get_id()、sleep_for()、sleep_until()、yield()。
1)get_id()
thread::id get_id() noexcept;
该函数用于获取线程ID,thread类也有同名的成员函数。
2)sleep_for() VS Sleep(1000) Linux sleep(1)
template <class Rep, class Period>
void sleep_for (const chrono::duration<Rep,Period>& rel_time);
该函数让线程休眠一段时间。
3)sleep_until() 2022-01-01 12:30:35
template <class Clock, class Duration>
void sleep_until (const chrono::time_point<Clock,Duration>& abs_time);
该函数让线程休眠至指定时间点。(可实现定时任务)
4)yield()
void yield() noexcept;
该函数让线程主动让出自己已经抢到的CPU时间片。
5)thread类其它的成员函数
void swap(std::thread& other); // 交换两个线程对象。
static unsigned hardware_concurrency() noexcept; // 返回硬件线程上下文的数量。
The interpretation of this value is system- andimplementation- specific, and may not be exact, but just an approximation.
Note that this does not need to match the actualnumber of processors or cores available in the system: A system can supportmultiple threads per processing unit, or restrict the access to its resourcesto the program.
If this value is not computable or well defined,the function returns 0.
示例:
include
include // 线程类头文件。
using namespace std;
// 普通函数。
void func(int bh, const string& str) {
cout << "子线程:" << this_thread::get_id() << endl;
for (int ii = 1; ii <= 3; ii++)
{
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒。
}
}
int main()
{
// 用普通函数创建线程。
thread t1(func, 3, "我是一只傻傻鸟。");
thread t2(func, 8, "我有一只小小鸟。");
cout << "主线程:" << this_thread::get_id() << endl;
cout << "线程t1:" << t1.get_id() << endl;
cout << "线程t2:" << t2.get_id() << endl;
t1.join(); // 回收线程t1的资源。
t2.join(); // 回收线程t2的资源。
}
四、call_once函数
在多线程环境中,某些函数只能被调用一次,例如:初始化某个对象,而这个对象只能被初始化一次。
在线程的任务函数中,可以用std::call_once()来保证某个函数只被调用一次。
头文件:#include
template< class callable, class... Args >
void call_once( std::once_flag& flag, Function&& fx, Args&&... args );
第一个参数是std::once_flag,用于标记函数fx是否已经被执行过。
第二个参数是需要执行的函数fx。
后面的可变参数是传递给函数fx的参数。
示例:
include
include // 线程类头文件。
include // std::once_flag和std::call_once()函数需要包含这个头文件。
using namespace std;
once_flag onceflag; // once_flag全局变量。本质是取值为0和1的锁。
// 在线程中,打算只调用一次的函数。
void once_func(const int bh, const string& str) {
cout << "once_func() bh= " << bh << ", str=" << str << endl;
}
// 普通函数。
void func(int bh, const string& str) {
call_once(onceflag,once_func,0, "各位观众,我要开始表白了。");
for (int ii = 1; ii <= 3; ii++)
{
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒。
}
}
int main()
{
// 用普通函数创建线程。
thread t1(func, 3, "我是一只傻傻鸟。");
thread t2(func, 8, "我有一只小小鸟。");
t1.join(); // 回收线程t1的资源。
t2.join(); // 回收线程t2的资源。
}
五、native_handle函数
C++11定义了线程标准,不同的平台和编译器在实现的时候,本质上都是对操作系统的线程库进行封装,会损失一部分功能。
为了弥补C++11线程库的不足,thread类提供了native_handle()成员函数,用于获得与操作系统相关的原生线程句柄,操作系统原生的线程库就可以用原生线程句柄操作线程。
示例:
include
include
include <pthread.h> // Linux的pthread线程库头文件。
using namespace std;
void func() // 线程任务函数。
{
for (int ii=1;ii<=10;ii++)
{
cout << "ii=" << ii << endl;
this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒。
}
}
int main()
{
thread tt(func); // 创建线程。
this_thread::sleep_for(chrono::seconds(5)); // 休眠5秒。
pthread_t thid= tt.native_handle(); // 获取Linux操作系统原生的线程句柄。
pthread_cancel(thid); // 取消线程。
tt.join(); // 等待线程退出。
}
六、线程安全
示例:
include
include // 线程类头文件。
using namespace std;
int aa = 0; // 定义全局变量。
// 普通函数,把全局变量aa加1000000次。
void func() {
for (int ii = 1; ii <= 1000000; ii++)
aa++;
}
int main()
{
// 用普通函数创建线程。
thread t1(func); // 创建线程t1,把全局变量aa加1000000次。
thread t2(func); // 创建线程t2,把全局变量aa加1000000次。
t1.join(); // 回收线程t1的资源。
t2.join(); // 回收线程t2的资源。
cout << "aa=" << aa << endl; // 显示全局变量aa的值。
}
242、互斥锁
C++11提供了四种互斥锁:
mutex:互斥锁。
timed_mutex:带超时机制的互斥锁。
recursive_mutex:递归互斥锁。
recursive_timed_mutex:带超时机制的递归互斥锁。
包含头文件:#include
一、mutex类
1)加锁lock()
互斥锁有锁定和未锁定两种状态。
如果互斥锁是未锁定状态,调用lock()成员函数的线程会得到互斥锁的所有权,并将其上锁。
如果互斥锁是锁定状态,调用lock()成员函数的线程就会阻塞等待,直到互斥锁变成未锁定状态。
2)解锁unlock()
只有持有锁的线程才能解锁。
3)尝试加锁try_lock()
如果互斥锁是未锁定状态,则加锁成功,函数返回true。
如果互斥锁是锁定状态,则加锁失败,函数立即返回false。(线程不会阻塞等待)
示例:
include
include // 线程类头文件。
include // 互斥锁类的头文件。
using namespace std;
mutex mtx; // 创建互斥锁,保护共享资源cout对象。
// 普通函数。
void func(int bh, const string& str) {
for (int ii = 1; ii <= 10; ii++)
{
mtx.lock(); // 申请加锁。
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
mtx.unlock(); // 解锁。
this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒。
}
}
int main()
{
// 用普通函数创建线程。
thread t1(func, 1, "我是一只傻傻鸟。");
thread t2(func, 2, "我是一只傻傻鸟。");
thread t3(func, 3, "我是一只傻傻鸟。");
thread t4(func, 4, "我是一只傻傻鸟。");
thread t5(func, 5, "我是一只傻傻鸟。");
t1.join(); // 回收线程t1的资源。
t2.join(); // 回收线程t2的资源。
t3.join(); // 回收线程t3的资源。
t4.join(); // 回收线程t4的资源。
t5.join(); // 回收线程t5的资源。
}
二、timed_mutex类
增加了两个成员函数:
bool try_lock_for(时间长度);
bool try_lock_until(时间点);
三、recursive_mutex类
递归互斥锁允许同一线程多次获得互斥锁,可以解决同一线程多次加锁造成的死锁问题。
示例:
include
include // 互斥锁类的头文件。
using namespace std;
class AA
{
recursive_mutex m_mutex;
public:
void func1() {
m_mutex.lock();
cout << "调用了func1()\n";
m_mutex.unlock();
}
void func2() {
m_mutex.lock();
cout << "调用了func2()\n";
func1();
m_mutex.unlock();
}
};
int main()
{
AA aa;
//aa.func1();
aa.func2();
}
四、lock_guard类
lock_guard是模板类,可以简化互斥锁的使用,也更安全。
lock_guard的定义如下:
template
class lock_guard
{
explicit lock_guard(Mutex& mtx);
}
lock_guard在构造函数中加锁,在析构函数中解锁。
lock_guard采用了RAII思想(在类构造函数中分配资源,在析构函数中释放资源,保证资源在离开作用域时自动释放)。
243、条件变量-生产消费者模型
条件变量是一种线程同步机制。当条件不满足时,相关线程被一直阻塞,直到某种条件出现,这些线程才会被唤醒。
C++11的条件变量提供了两个类:
condition_variable:只支持与普通mutex搭配,效率更高。
condition_variable_any:是一种通用的条件变量,可以与任意mutex搭配(包括用户自定义的锁类型)。
包含头文件:<condition_variable>
一、condition_variable类
主要成员函数:
1)condition_variable() 默认构造函数。
2)condition_variable(const condition_variable &)=delete 禁止拷贝。
3)condition_variable& condition_variable::operator=(const condition_variable &)=delete 禁止赋值。
4)notify_one() 通知一个等待的线程。
5)notify_all() 通知全部等待的线程。
6)wait(unique_lock
7)wait(unique_lock
8)wait_for(unique_lock
9)wait_for(unique_lock
10)wait_until(unique_lock
11)wait_until(unique_lock
二、unique_lock类
template
unique_lock和lock_guard都是管理锁的辅助类,都是RAII风格(在构造时获得锁,在析构时释放锁)。它们的区别在于:为了配合condition_variable,unique_lock还有lock()和unlock()成员函数。
示例1:
include
include
include // 线程类头文件。
include // 互斥锁类的头文件。
include // deque容器的头文件。
include // queue容器的头文件。
include <condition_variable> // 条件变量的头文件。
using namespace std;
class AA
{
mutex m_mutex; // 互斥锁。
condition_variable m_cond; // 条件变量。
queue<string, deque
public:
void incache(int num) // 生产数据,num指定数据的个数。
{
lock_guard
for (int ii=0 ; ii<num ; ii++)
{
static int bh = 1; // 超女编号。
string message = to_string(bh++) + "号超女"; // 拼接出一个数据。
m_q.push(message); // 把生产出来的数据入队。
}
m_cond.notify_one(); // 唤醒一个被当前条件变量阻塞的线程。
}
void outcache() // 消费者线程任务函数。
{
while (true)
{
string message;
{
// 把互斥锁转换成unique_lock
unique_lock
while (m_q.empty()) // 如果队列空,进入循环,否则直接处理数据。必须用循环,不能用if
m_cond.wait(lock); // 等待生产者的唤醒信号。
// 数据元素出队。
message = m_q.front(); m_q.pop();
}
// 处理出队的数据(把数据消费掉)。
this_thread::sleep_for(chrono::milliseconds(1)); // 假设处理数据需要1毫秒。
cout << "线程:" << this_thread::get_id() << "," << message << endl;
}
}
};
int main()
{
AA aa;
thread t1(&AA::outcache, &aa); // 创建消费者线程t1。
thread t2(&AA::outcache, &aa); // 创建消费者线程t2。
thread t3(&AA::outcache, &aa); // 创建消费者线程t3。
this_thread::sleep_for(chrono::seconds(2)); // 休眠2秒。
aa.incache(3); // 生产3个数据。
this_thread::sleep_for(chrono::seconds(3)); // 休眠3秒。
aa.incache(5); // 生产5个数据。
t1.join(); // 回收子线程的资源。
t2.join();
t3.join();
}
示例2:
include
include
include // 线程类头文件。
include // 互斥锁类的头文件。
include // deque容器的头文件。
include // queue容器的头文件。
include <condition_variable> // 条件变量的头文件。
using namespace std;
class AA
{
mutex m_mutex; // 互斥锁。
condition_variable m_cond; // 条件变量。
queue<string, deque
public:
void incache(int num) // 生产数据,num指定数据的个数。
{
lock_guard
for (int ii=0 ; ii<num ; ii++)
{
static int bh = 1; // 超女编号。
string message = to_string(bh++) + "号超女"; // 拼接出一个数据。
m_q.push(message); // 把生产出来的数据入队。
}
//m_cond.notify_one(); // 唤醒一个被当前条件变量阻塞的线程。
m_cond.notify_all(); // 唤醒全部被当前条件变量阻塞的线程。
}
void outcache() { // 消费者线程任务函数。
while (true) {
// 把互斥锁转换成unique_lock
unique_lock
// 条件变量虚假唤醒:消费者线程被唤醒后,缓存队列中没有数据。
//while (m_q.empty()) // 如果队列空,进入循环,否则直接处理数据。必须用循环,不能用if
// m_cond.wait(lock); // 1)把互斥锁解开;2)阻塞,等待被唤醒;3)给互斥锁加锁。
m_cond.wait(lock, [this] { return !m_q.empty(); });
// 数据元素出队。
string message = m_q.front(); m_q.pop();
cout << "线程:" << this_thread::get_id() << "," << message << endl;
lock.unlock(); // 手工解锁。
// 处理出队的数据(把数据消费掉)。
this_thread::sleep_for(chrono::milliseconds(1)); // 假设处理数据需要1毫秒。
}
}
};
int main()
{
AA aa;
thread t1(&AA::outcache, &aa); // 创建消费者线程t1。
thread t2(&AA::outcache, &aa); // 创建消费者线程t2。
thread t3(&AA::outcache, &aa); // 创建消费者线程t3。
this_thread::sleep_for(chrono::seconds(2)); // 休眠2秒。
aa.incache(2); // 生产2个数据。
this_thread::sleep_for(chrono::seconds(3)); // 休眠3秒。
aa.incache(5); // 生产5个数据。
t1.join(); // 回收子线程的资源。
t2.join();
t3.join();
}
244、原子类型atomic
C++11提供了atomic
原子操作由CPU指令提供支持,它的性能比锁和消息传递更高,并且,不需要程序员处理加锁和释放锁的问题,支持修改、读取、交换、比较并交换等操作。
头文件:#include
构造函数:
atomic() noexcept = default; // 默认构造函数。
atomic(T val) noexcept; // 转换函数。
atomic(const atomic&) = delete; // 禁用拷贝构造函数。
赋值函数:
atomic& operator=(const atomic&) = delete; // 禁用赋值函数。
常用函数:
void store(const T val) noexcept; // 把val的值存入原子变量。
T load() noexcept; // 读取原子变量的值。
T fetch_add(const T val) noexcept; // 把原子变量的值与val相加,返回原值。
T fetch_sub(const T val) noexcept; // 把原子变量的值减val,返回原值。
T exchange(const T val) noexcept; // 把val的值存入原子变量,返回原值。
T compare_exchange_strong(T &expect,const T val) noexcept; // 比较原子变量的值和预期值expect,如果当两个值相等,把val存储到原子变量中,函数返回true;如果当两个值不相等,用原子变量的值更新预期值,函数返回false。CAS指令。
bool is_lock_free(); // 查询某原子类型的操作是直接用CPU指令(返回true),还是编译器内部的锁(返回false)。
原子类型的别名:
注意:
atomic
atomic
原子整型可以用作计数器,布尔型可以用作开关。
CAS指令是实现无锁队列基础。
示例:
include
include // 原子类型的头文件。
using namespace std;
int main()
{
atomic
cout << "a=" << a.load() << endl; // 读取原子变量a的值。输出:a=3
a.store(8); // 把8存储到原子变量中。
cout << "a=" << a.load() << endl; // 读取原子变量a的值。 输出:a=8
int old; // 用于存放原值。
old = a.fetch_add(5); // 把原子变量a的值与5相加,返回原值。
cout << "old = " << old <<",a = " << a.load() << endl; // 输出:old=8,a=13
old = a.fetch_sub(2); // 把原子变量a的值减2,返回原值。
cout << "old = " << old << ",a = " << a.load() << endl; // 输出:old=13,a=11
atomic
int expect = 4; // 期待值
int val = 5; // 打算存入原子变量的值
// 比较原子变量的值和预期值expect,
// 如果当两个值相等,把val存储到原子变量中;
// 如果当两个值不相等,用原子变量的值更新预期值。
// 执行存储操作时返回true,否则返回false。
bool bret = ii.compare_exchange_strong(expect, val);
cout << "bret=" << bret << endl;
cout << "ii=" << ii << endl;
cout << "expect=" << expect << endl;
}
250、可调用对象
在C++中,可以像函数一样调用的有:普通函数、类的静态成员函数、仿函数、lambda函数、类的非静态成员函数、可被转换为函数的类的对象,统称可调用对象或函数对象。
可调用对象有类型,可以用指针存储它们的地址,可以被引用(类的成员函数除外)
一、普通函数
普通函数类型可以声明函数、定义函数指针和函数引用,但是,不能定义函数的实体。
示例:
include
using namespace std;
using Fun = void (int, const string&); // 普通函数类型的别名。
Fun show; // 声明普通函数。
int main()
{
show(1, "我是一只傻傻鸟。"); // 直接调用普通函数。
void(*fp1)(int, const string&) = show; // 声明函数指针,指向普通函数。
void(&fr1)(int, const string&) = show; // 声明函数引用,引用普通函数。
fp1(2, "我是一只傻傻鸟。"); // 用函数指针调用普通函数。
fr1(3, "我是一只傻傻鸟。"); // 用函数引用调用普通函数。
Fun* fp2 = show; // 声明函数指针,指向普通函数。
Fun& fr2 = show; // 声明函数引用,引用普通函数。
fp2(4, "我是一只傻傻鸟。"); // 用函数指针调用普通函数。
fr2(5, "我是一只傻傻鸟。"); // 用函数引用调用普通函数。
}
// 定义普通函数
void show(int bh, const string& message) {
cout << "亲爱的" << bh << "," << message << endl;
}
// 以下代码是错误的,不能用函数类型定义函数的实体。
//Func show1 {
// cout << "亲爱的" << bh << "," << message << endl;
//}
二、类的静态成员函数
类的静态成员函数和普通函数本质上是一样的,把普通函数放在类中而已。
示例:
include
using namespace std;
using Fun = void (int, const string&); // 普通函数类型的别名。
struct AA // 类中有静态成员函数。
{
static void show(int bh, const string& message) {
cout << "亲爱的" << bh << "," << message << endl;
}
};
int main()
{
AA::show(1, "我是一只傻傻鸟。"); // 直接调用静态成员函数。
void(*fp1)(int, const string&) = AA::show; // 用函数指针指向静态成员函数。
void(&fr1)(int, const string&) = AA::show; // 引用静态成员函数。
fp1(2, "我是一只傻傻鸟。"); // 用函数指针调用静态成员函数。
fr1(3, "我是一只傻傻鸟。"); // 用函数引用调用静态成员函数。
Fun* fp2 = AA::show; // 用函数指针指向静态成员函数。
Fun& fr2 = AA::show; // 引用静态成员函数。
fp2(4, "我是一只傻傻鸟。"); // 用函数指针调用静态成员函数。
fr2(5, "我是一只傻傻鸟。"); // 用函数引用调用静态成员函数。
}
三、仿函数
仿函数的本质是类,调用的代码像函数。
仿函数的类型就是类的类型。
示例:
include
using namespace std;
struct BB // 仿函数。
{
void operator()(int bh, const string& message) {
cout << "亲爱的" << bh << "," << message << endl;
}
};
int main()
{
BB bb;
bb(11, "我是一只傻傻鸟。"); // 用对象调用仿函数。
BB()(12, "我是一只傻傻鸟。"); // 用匿名对象调用仿函数。
BB& br = bb; // 引用函数
br(13, "我是一只傻傻鸟。"); // 用对象的引用调用仿函数。
}
四、lambda函数
lambda函数的本质是仿函数,仿函数的本质是类。
include
using namespace std;
int main()
{
// 创建lambda对象。
auto lb = [](int bh, const string& message) {
cout << "亲爱的" << bh << "," << message << endl;
};
auto& lr = lb; // 引用lambda对象。
lb(1, "我是一只傻傻鸟。"); // 用lambda对象调用仿函数。
lr(2, "我是一只傻傻鸟。"); // 用lambda对象的引用调用仿函数。
}
五、类的非静态成员函数
类的非静态成员函数有地址,但是,只能通过类的对象才能调用它,所以,C++对它做了特别处理。
类的非静态成员函数只有指针类型,没有引用类型,不能引用。
示例:
include
using namespace std;
struct CC // 类中有普通成员函数。
{
void show(int bh, const string& message) {
cout << "亲爱的" << bh << "," << message << endl;
}
};
int main()
{
CC cc;
cc.show(14, "我是一只傻傻鸟。");
void (CC:
标签:11,const,函数,int,C++,标准,线程,string,cout From: https://www.cnblogs.com/Gal0721/p/17724097.html