参考上位:
https://blog.csdn.net/linwh8/article/details/51569807?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162640082216780255270978%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=162640082216780255270978&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-5-51569807.pc_search_result_control_group&utm_term=c%2B%2B11&spm=1018.2226.3001.4187
https://blog.csdn.net/hyman_yx/article/details/52044632
https://blog.csdn.net/caoshangpa/article/details/79129258
https://blog.csdn.net/zhanglu_1024/article/details/85049480
https://blog.csdn.net/aixintianshideshouhu/article/details/94548940
https://blog.csdn.net/jiange_zh/article/details/79356417
https://www.cnblogs.com/feng-sc/p/5710724.html#title33
文章目录
- 1、auto
- 2、左值、右值详解
- 3、decltype 类型推导
- 4、for
- 5、nullptr
- 6、列表初始化
- 7、构造函数
- 8、overrride 和final
- 9、默认函数控制
- 10、lambda表达式
- 11、std::function、std::bind封装可执行对象
- 12、线程库
- 13、正则表达式
- 14、新增容器
- 15、模板增强
1、auto
在C++11的版本中,删除了auto原本的功能,并进行了重新定义了。即C++11中的auto具有类型推导的功能。
在讲解auto之前,我们先来了解什么是静态类型,什么是动态类型。
静态类型,动态类型
通俗的来讲,所谓的静态类型就是在使用变量前需要先定义变量的数据类型,而动态类型无需定义。
严格的来讲,静态类型是在编译时进行类型检查,而动态类型是在运行时进行类型检查。
如今c++11中重新定义了auto的功能,这便使得静态类型也能够实现类似于动态类型的类型推导功能,十分有趣~
double func();
auto a = 1; // int, 尽管1时const int类型,但是auto会自动去const
auto b = func(); // double
auto c; // wrong, auto需要初始化的值进行类型推导,有点类似于引用
注意: 其实auto就相当于一个类型声明时的占位符,而不是一种“类型”的声明,在编译时期编译器会将auto替代成变量的实际类型
auto的优势
I. 拥有初始化表达式的复杂类型变量声明时的简化代码。
也就是说,auto能够节省代码量,使代码更加简洁, 增强可读性。
std::vector<std::string> array;
std::vector<std::string>::iterator it = array.begin();
// auto
auto it = array.begin();
auto在STL中应用非常广泛,如果在代码中需要多次使用迭代器,用auto便大大减少了代码量,使得代码更加简洁,增强了可读性。
II.免除程序员在一些类型声明时的麻烦,或者避免一些在类型声明时的错误。
class PI {
public:
double operator *(float v) {
return (double)val*v;
}
const float val = 3.1415927f;
};
int main(void) {
float radius = 5.23f;
PI pi;
auto circumference = 2*( pi*radius);
return 0;
}
设计PI类的作者将PI的*运算符进行了重载,使两个float类型的数相乘返回double类型。这样做的原因便是避免数据上溢以及精度降低。假如用户将circumference定义为float类,就白白浪费了PI类作者的一番好意,用auto便不会出现这样的问题。
但是auto并不能解决所有的精度问题,如:
unsigned int a = 4294967295; // unsigned int 能够存储的最大数据
unsigned int b = 1;
auto c = a+b;
cout << c << endl; // 输出c为0
a+b显然超出了unsigned int 能够存储的数据范围,但是auto不能自动将其匹配为能存储这一数据而不造成溢出的类型如unsigned long类型。所以在精度问题上自己还是要多留一点心,分析数据是否会溢出。
III.“自适应”性能,在一定程度上支持泛型编程。
-
如上面提到PI类,假如原作者要修改重载*返回的数据类型,即将double换成其他类型如long double,则它可以直接修改而无需修改main函数中的值。
-
再如这种“适应性”还能体现在模板的定义中:
template <typename T1, typename T2>
double func(const T1& a, const T2& b) {
auto c = a + b;
return c;
}
// 其实直接return a+b;也是可以的,这里只是举个例子,同时点出auto不能用于声明函数形参这一易错点
但是有一点要注意:不能将auto用于声明函数形参,所以不能用auto替代T1,T2。
IIII 函数返回占位符
在这种情况下,auto主要与decltype关键字配合使用,作为返回值类型后置时的占位符。此时,关键字不表示自动类型检测,仅仅是表示后置返回值的语法的一部分。
template<class T, class U>
auto add(T t, U u) -> decltype(t + u)
{
return t + u;
}
auto在这里的作用为返回值占位,它只是为函数返回值占了一个位置,真正的返回值是后面的decltype(t + u)。为何要将返回值后置呢?如果没有后置,则函数声明时为:
template<class T, class U>
decltype((*(T*)0) + (*(U*)0)) add(T t, U u)
{
return t + u;
}
此时虽然能实现相同的功能,但是代码编写要丑陋得多。
注意事项
auto 变量必须在定义时初始化,这类似于const关键字
局限性
- auto不能作为函数参数,否则无法通过编译;
- auto不能作为模板参数(实例化时), 否则无法通过编译。
- auto 不能用于声明数组,否则无法通过编译;
- auto不能推导非静态成员变量的类型,因为auto是在编译时期进行推导;
void func(auto x = 1) {} // (1)wrong
struct Node {
auto value = 10; // (2)wrong
};
int main(void) {
char x[3];
auto y = x;
auto z[3] = x; // (3)wrong
vector<auto> v = {1}; // (4)wrong
}
auto 使用细则
int x;
int* y = &x;
double foo();
int& bar();
auto* a = &x; // a:int*
auto& b = x; // b:int&
auto c = y; // c:int*
auto* d = y; // d:int*
auto* e = &foo(); // wrong, 指针不能指向临时变量
auto &f = foo(); // wrong, 左值引用不能存储右值
auto g = bar(); // int
auto &h = bar(); // int&
其实,对于指针而言, auto* a = &x
等价于 auto a = &x
。
但是对于引用而言,上面的情况就不遵循了,如果是引用, 要在auto后加&。
double foo();
float* bar();
const auto a = foo(); // a:const double
const auto &b = foo(); // b:const double&
volatile auto* c = bar(); // c:volatile float*
auto d = a; // d:double
auto &e = a; // e:const double
auto f = c; // f:float*
volatile auto& g = c; // g:volatile float*&
auto 会自动删除const(常量性),volatile(易失性)。
对于引用和指针,即auto*, auto&仍会保持const与volatile。
auto x = 1, y = 2; // (1) correct
const auto* m = &x, n = 1; // (2)correct
auto i = 1, j = 3.14f; // (3) wrong
auto o = 1, &p = 0, *q = &p; // (4)correct
auto有规定,当定义多个变量时,所有变量的类型都要一致,且为第一个变量的类型,否则编译出错。
对于(1): x, y都是int类型,符合auto定义多变量的机制, 编译通过;
对于(2):我们发现,m、n的类型不同,那为什么不报错?变量类型一致是指auto一致。m为const int*, 则auto匹配的是int,而n恰好为int类型,所以编译通过;
对于(3): i的类型是int, j的类型是float,类型不相同,编译出错;
对于(4): o的类型是int, p前有&,其实就是auto&, 即p为int&,而q前有,相当于auto,即q为int*,不难发现o, p, q三者auto匹配都为int,所以符合auto定义多变量的机制,编译通过。
int a = 10;
int &b = a;
auto c = b;//c的类型为int而非int&(去除引用)
auto &d = b;//此时c的类型才为int&
c = 100;//a =10;
d = 100;//a =100;
如果初始化表达式是引用,则去除引用语义
int a3[3] = { 1, 2, 3 };
auto b3 = a3;
cout << typeid(b3).name() << endl;//输出int*
初始化表达式为数组时,auto关键字推导类型为指针
int a7[3] = { 1, 2, 3 };
auto & b7 = a7;
cout << typeid(b7).name() << endl;//输出为int[3]
若表达式为数组且auto带上&,则推导类型为数组类型
class Test
{
public:
auto TestWork(int a ,int b);
};
在引用头文件调用TestWork函数时,编译无法通过。但如果把实现写在头文件中,可以编译通过,因为编译器可以根据函数实现的返回值确定auto的真实类型。如果读者用过inline类成员函数,这个应该很容易明白,此特性与inline类成员函数类似。
class Test
{
public:
auto TestWork(int a, int b)
{
return a + b;
}
};
2、左值、右值详解
在C++11中所有的值必属于左值、右值两者之一,右值又可以细分为纯右值、将亡值。在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。
举个例子,int a = b+c, a 就是左值,其有变量名为a,通过&a可以获取该变量的地址;表达式b+c、函数int func()的返回值是右值,在其被赋值给某一变量前,我们不能通过变量名找到它,&(b+c)这样的操作则不会通过编译。
右值、将亡值
在理解C++11的右值前,先看看C++98中右值的概念:C++98中右值是纯右值,纯右值指的是临时变量值、不跟对象关联的字面量值。
临时变量指的是非引用返回的函数返回值、表达式等,例如函数int func()的返回值,表达式a+b;不跟对象关联的字面量值,例如true,2,”C”等。
C++11对C++98中的右值进行了扩充。在C++11中右值又分为纯右值(prvalue,Pure Rvalue)和将亡值(xvalue,eXpiring Value)。其中纯右值的概念等同于我们在C++98标准中右值的概念,指的是临时变量和不跟对象关联的字面量值;将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过“盗取”的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。
将亡值即为右值引用,且左值与将亡值合称为泛左值。
左值引用、右值引用
左值引用就是对一个左值进行引用的类型。
右值引用就是对一个右值进行引用的类型,事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。
右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。
左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。
左值引用通常也不能绑定到右值,但常量左值引用是个“万能”的引用类型。它可以接受非常量左值、常量左值引用、右值对其进行初始化。
int &a = 2; # 左值引用绑定到右值,编译失败
int b = 2; # 非常量左值
const int &c = b; # 常量左值引用绑定到非常量左值,编译通过
const int d = 2; # 常量左值
const int &e = c; # 常量左值引用绑定到常量左值,编译通过
const int &b =2; # 常量左值引用绑定到右值,编程通过
右值引用
class String
{
public:
String(char* str = '")
{
if(str == nullptr)
_str = "";
_str = new char[strlen(str)+1];
strcpy(_str,str);
}
String(const String& s) : _str(new char[strlen(s._str)+1])
{
strcpy(_str,s._str);
}
~String()
{
if(_str)
delete[] _str;
}
private:
char* _str;
};
String GetString(char* pStr)
{
String strTemp(pStr);
return strTemp;
}
int main()
{
String s1("hello");
String s2(GetString("world"));
return 0;
}
在上面的代码中,GetString函数返回的临时对象,给s2拷贝成功之后,立马销毁了(临时对象的空间被释放);而s2拷贝构造的时,又需要分配空间,一个刚释放,一个又申请,有点多此一举,那能否把GetString返回的临时对象的空间直接交给s2呢?这样s2也不需要重新开辟空间了。
移动语义:将一个对象资源移动到另一个对象中的方式,在C++中要实现移动语义,必须使用右值引用.
格式:
类型&& 应用变量名字 = 实体;
使用场景:
1、与移动语义相结合,减少必须要的资源的开辟,提高运行效率
String&& GetString(char* pStr)
{
String strTemp(pStr);
return strTemp;
}
int main()
{
String s1("hello");
String s2(GetString("world"));
return 0;
}
2、给一个匿名对象取别名,延长匿名对象的生命周期
String GetString(char* pStr) {
return String(pStr);
}
int main()
{
String&& s = GetString("hello");
return 0;
}
注意:
- 右值引用在定义时必须初始化
- 右值引用不能引用左值
int a = 10;
int&& a1; // 未初始化,编译失败
int&& a2 = a; // 编译失败,a是一个左值
// 左值是可以改变的值
std::move()
C++ 11在标准库的头文件< utility >中提供了一个模板函数std::move。
std::move仅仅是简单地将左值转换为右值,它本身并没有转移任何东西。它仅仅是让对象可以转移。
总之,std::move(some_lvalue)将左值转换为右值(可以理解为一种类型转换),使接下来的转移成为可能。
注意:其更多用在生命周期即将结束的对象上。
1、在C++11中,关于默认构造函数有三个版本
Object()//无参构造函数
Object(const T&)//拷贝构造函数
Object(T&&)//移动构造函数
2、如果将移动构造函数声明为常右值引用或者返回右值的函数声明为常量,都会导致移动语义无法实现。个人理解: 即右值引用不能是const类型
String(const String&&);
const String GetString();
3、C++11默认成员函数,默认情况下,编译器会隐士生成一个移动构造函数,是按照位拷贝来进行。因此在涉及到资源管理时,最好自己定义移动构造函数。
class String
{
public:
String(char* str = "")
{
if(str == nullptr)
str = "";
_str = new char[strlen(str)+1];
strcpy(_str,str);
}
// 拷贝构造
// String s(左值对象)
String(const String& s) : _str(new char[strlen(s._str) + 1])
{
strcpy(_str,s_str);
}
// 移动构造
// String s(将亡值对象)
String(String&& s) : _str(nullptr)
{
swap(_str,s._str);
}
// 赋值
String& operator=(const String& s)
{
if(this != &s)
{
char* tmp = new char[strlen(s._str)+1];
stcpy(tmp,s._str);
delete[] _str;
_str = tmp;
}
return *this;
}
// 移动赋值
String& operator=(String&& s)
{
swap(_str,s._str);
return *this;
}
~String()
{
if(_str)
delete[] _str;
}
// s1 += s2 体现左值引用,传参和传值的位置减少拷贝
String& operator+=(const String& s)
{
// this->Append(s.c_str());
return *thisl
}
// s1 + s2
String operator+(const String& s)
{
String tmp(*this);
// tmp.Append(s.c_str());
return tmp;
}
const char* c_str()
{
return _str;
}
private:
char* _str;
};
int main()
{
String s1("hello"); // 实例化s1时会调用移动构造
String s2("world");
String ret
ret = s1 + s2 // +返回的是临时对象,这里会调用移动构造和移动赋值,减少拷贝
vector<String> v;
String str("world");
v.push_back(str); // 这里调用拷贝构造函数
v.push_back(move(str)); // 这里调用移动构造,减少一次拷贝
return 0;
}
总结:
右值引用: int&& bb = 10; 在传值返回和将亡值传参时,通过调用移动构造和移动赋值,减少拷贝,提高效率。
右值引用可以应用move后的左值
4、完美转发是指在函数模板中,完全按照模板的参数的类型,将参数传递给调用函数模板的函数
所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,就转发左值;如果是右值,就转发右值。(这样是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理,比如参数为左值时实施拷贝语义、参数为右值时实施移动语义)
在C++11中,通过forward函数来实现完美转发。
void Fun(int &x){cout << "lvalue ref" << endl;}
void Fun(int &&x){cout << "rvalue ref" << endl;}
void Fun(const int &x){cout << "const lvalue ref" << endl;}
void Fun(const int &&x){cout << "const rvalue ref" << endl;}
template<typename T>
void PerfectForward(T &&t){Fun(std::forward<T>(t));}
int main()
{
PerfectForward(10); // rvalue ref
int a;
PerfectForward(a); // lvalue ref
PerfectForward(std::move(a)); // rvalue ref
const int b = 8;
PerfectForward(b); // const lvalue ref
PerfectForward(std::move(b)); // const rvalue ref
return 0;
}
3、decltype 类型推导
类型推导是随着模板和泛型编程的广泛使用而引入的。在非泛型编程中,类型是明确的,而在模板与泛型编程中,类型是不明确的,它取决于传入的参数类型。
decltype与我前面讲到的auto还是有一些共同点的,如二者都是通过推导获得的类型来定义另外一个变量,再如二者都是在编译时进行类型推导。不过他们类型推导的方式有所不同,auto是通过初始化表达式推导出类型,而decltype是通过普通表达式的返回值推导出类型。
decltype实际上有点像auto的反函数,auto可以让你声明一个变量,而decltype则可以从一个变量或表达式中得到类型,例如:
int x = 3;
decltype(x) y = x;
不过在讲解decltype之前,我们先来了解一下typeid。
typeid
对于C语言,是完全不支持动态类型的;
对于C++,与C不同的是,C++98标准中已经有部分支持动态类型了,便是运行时类型识别(RTTI)。
RTTI机制:为每个类型产生一个type_info类型数据,程序员可以在程序中使用typeid随时查询一个变量的类型,typeid就会返回变量相应的type_info数据,type_info的name成员可以返回类型的名字。在C++11中,增加了hash_code这个成员函数,返回该类型唯一的哈希值,以供程序员对变量类型随时进行比较。
也许你会有这样一个想法:我直接对type_info.name进行字符串比较不就可以了么,为什么还要给每个类型一个哈希值?我认为,字符串比较的开销也是比较大的,如果用每个类型来对于一个哈希值,通过比较哈希值确定类型是否相同的方法,会比使用字符串比较的效率要高得多。
下面一段代码是对typeid()与type_info.name(), type_info.hash_code的应用:
# include <iostream>
# include <typeinfo>
using namespace std;
class white {};
class Black {};
int main(void) {
white a;
Black b;
// white 与 black 前的数字会因编译器的不同而不同,如g++打印的是5
cout << typeid(a).name() << endl; // 5 white
cout << typeid(b).name() << endl; // 5 Black
white c;
bool a_b_sametype = (typeid(a).hash_code() == typeid(b).hash_code());
bool a_c_sametype = (typeid(a).hash_code() == typeid(c).hash_code());
cout << "Same type?" << endl;
cout << "A and B" << (int)a_b_sametype << endl; // 0
cout << "A and C" << (int)a_c_sametype << endl; // 1
return 0;
}
然而,RTTI无法满足程序员的需求:因为RTTI在运行时才确定出类型,而更多的需求是在编译时确定类型。并且,通常的程序是要使用推导出来的这种类型而不是单纯地识别它。
decltype的历史
decltype
是GCC实现的第一个C++ 11新特性。它实际上起源于一个相当古老的GNU扩展关键字__typeof__
。这个非标准关键字也能够在C语言中使用,GNU Compiler Collection的专业用户可能对它更熟悉一些。2008年,GCC 4.3.x就实现了这个特性,同时去除了____typeof__
__的一些缺点。现在,decltype
和__decltype
两个关键字在GCC中都适用;前者只能用在C++ 11模式下,后者可以同时应用于C++ 11和 C++ 98模式。__typeof__
则已经停止使用。
下面来看看decltype
的基本使用。简单来说,decltype
关键字用于查询表达式的类型。不过,这只是其基本用法。当这个简单的表述同C++ 11的其它特性结合起来之后,一些意想不到的有趣用法就此产生。decltype
的语法是
decltype ( expression )
这里的括号是必不可少的。根据前面的说法,decltype
的的作用是“查询表达式的类型”,因此,上面语句的效果是,返回 expression 表达式的类型。注意,decltype
的仅仅“查询”表达式的类型,并不会对表达式进行“求值”。
const int&& foo();
int i;
struct A { double x; };
const A* a = new A();
decltype(foo()) x1; // const int&& (1)
decltype(i) x2; // int (2)
decltype(a->x) x3; // double (3)
decltype((a->x)) x4; // double& (4)
int i;
float f;
double d;
typedef decltype(i + f) type1; // float
typedef decltype(f + d) type2; // double
typedef decltype(f < d) type3; // bool
传统的__typeof__
有一个颇为诟病的地方,在于不能很好地处理引用类型。而decltype
则没有这个问题,decltype
能够很好地处理类型转换这里问题。或许你会对上面代码中的 (4) 心生疑问。为什么decltype((a->x))会是double&?这是由decltype
的推导规则决定的。
decltype推导四规则
在了解这些规则之前,我们先来了解**标记符表达式(id-expression)**的概念。
标记符表达式
标记符表达式(id-expression):所有除去关键字和字面量等编译器需要使用的标记以外的程序员自定义的标记(token)都可以是标记符(identifier), 而单个标记符对应的表达式就是标记符表达式。
如int arr[4]
, int i
, arr与i就是标记符表达式。对于前者,去除关键字int与字面量[4]后剩下的arr便是标记符表达式。
decltype推导的四规则
(1)如果e是一个没有带括号的标记符表达式或者类成员访问表达式,那么decltype(e)就是e所命名的实体的类型。此外,如果e是一个被重载的函数,可能会导致编译错误;
(2)否则,假设e的类型是T,如果e是一个将亡值(xvalue), 那么decltype(e)为T&&;
(3)否则,假设e的类型是T,如果e是一个左值,则decltype(e)为T&;
(4)否则,假设e的类型是个T, 则decltype(e)为T。个人理解: 右值
下面通过代码分别对四规则进行举例:
int arr[5] = {0};
int *ptr = arr;
struct S {
double d;
} s;
void overload(int);
void overload(char);
int&& RvalRef();
const bool Func(int);
// 规则1
decltype(arr) var1; // int [5]
decltype(ptr) var2; // int*
decltype(s.d) var4; // double
decltype(Overloaded) var5; // wrong
// 规则2
decltype(RvalRef()) val6 = 1; // int&&
// 规则3
decltype(true? i : i) var7 = i; // int&
decltype((i)) var8 = i; // int&
decltype(++i) var9 = i; // int&
decltype(arr[3]) var10 = i; // int&
decltype(*ptr) var11 = i; //int&
decltype("lval") var2 = "lval"; // (const char*)&
// 规则4
decltype(1) var13; // int
decltype(i++) var14; // int
decltype(Func(1)) var15; // const bool
上面的代码中,需要重点注意的是:
(1)++i 与 i++: ++i返回的是左值引用,i++返回的是右值。
(2)字符串的字面常量为左值,其它字符串字面量为右值。
(3)对于 decltype((i)) val8 = i 与 decltype((i)) val8 = 1;前者能通过编译,后者不可以,提示的错误信息如下:
aaa.cpp: In function ‘int main()’:
aaa.cpp:6:26: error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
decltype((i)) val8 = 1;
原因是:i是标记符表达式,而(i)不是标记符表达式,所以它遵循的应当是规则3,decltype推导的类型为int&,即左值引用。i是左值,将i赋给val8是可以的,但是讲1赋给val8却是不可以的。1是右值,编译器不允许将一个右值赋给一个左值引用,所以编译不通过,这点要注意!
补充一下,在C++11的标准库中提供了is_lvalue_reference<>与is_rvalue_reference<>, 用于判断方括号内的内容的类型是否为左值引用或右值引用。如果是,则返回1,如若不是,则返回0。所以可以利用他们来检验decltype推导出的类型是否为左值引用和右值引用。
const和volatile限制符的继承
与auto不同,decltype能够“带走”表达式的cv限制符。不过,如果对象的定义中有cv限制符时,其成员不会继承const或volatile限制符。
举例说明:
# include <iostream>
# include <type_traits>
using namespace std;
const int ic = 0;
volatile int iv;
struct S {
int i;
};
const S a = {0};
volatile S b;
volatile S* p = &b;
int main(void) {
cout << is_const<decltype(ic)>::value << endl; // 1
cout << is_volatile<decltype(iv)>::value << endl; // 1
cout << is_const<decltype(a)>::value << endl; // 1
cout << is_volatile<decltype(b)>::value << endl; // 1
cout << is_const<decltype(a.i)>::value << endl; // 0
cout << is_volatile<decltype(p->i)>::value << endl; // 0
return 0;
}
冗余的符号
使用decltype从表达式推导出类型后进行类型定义时,可能会出现一些冗余的符号:const和volatile限制符,符号引用&。如果推导出的类型已经有了这些属性,冗余的符号将会被忽略,这种规则叫做折叠规则。
typedef T& TR; // T&的位置是对于TR的类型定义
TR v; // TR的位置是声明变量v的类型
TR的类型定义 | 声明变量v的类型 | v的实际类型 |
---|---|---|
T& | TR | T& |
T& | TR& | T& |
T& | TR&& | T& |
T&& | TR | T&& |
T&& | TR& | T& |
T&& | TR&& | T&& |
规律:
当TR为T&时,无论定义类型v时有多少个&,最终v的类型都是T&;
当TR为T&&时,则v最终的类型与定义类型v时&的数量有关:
(1)如果&的总数量为奇数,则v的最终类型为T&;
(2)如果&的总数量为偶数,则v的最终类型为T&&。
上面主要是对引用符号&
的冗余处理,那么对于指针符号* decltype
该如何处理呢?
# include <iostream>
# include <type_traits>
using namespace std;
int i = 1;
int &j = i;
int *p = &i;
const int k = 1;
int main(void) {
decltype(i)& val1 = i;
decltype(i)& val2 = i;
cout << is_lvalue_reference<decltype(val1)>::value << endl; // 1
cout << is_rvalue_reference<decltype(val2)>::value << endl; // 0
cout << is_lvalue_reference<decltype(val2)>::value << endl; // 1
decltype(p)* val3 = &i; // 编译失败,val3为int**类型,等号右侧为int*类型
decltype(p)* val4 = &p; // int**
auto* val5 = p; // int*
v3 = &i;
const decltype(k) var4 = 1; // 冗余的const
return 0;
}
由上面的代码可知,auto对于*
的冗余,编译器采取忽略的措施,而decltype对于*
的冗余编译器采取不忽略的措施。
追踪返回类型
在C++98中,如果一个函数模板的返回类型依赖于实际的入口参数类型,那么该返回类型在模板实例化之前可能都无法确定,这样的话我们在定义该函数模板时就会遇到麻烦。
我们很快就会想到可以用decltype来定义:
// 注:本段代码不能通过编译,只是帮助读者了解追踪返回类型的由来
template<typename T1, typename T2>
decltype(t1+t2) Sum(T1& t1, T2& t2) {
return t1+t2;
}
不过这样有个问题。因为编译器是从左到右读取的,此时decltype内的t1, t2都未声明,而按照 C/C++ 编译器的规则,变量在使用前必须声明。
因此, 为了解决这一问题,C++11引进了新语法——追踪返回类型,来声明和定义这样的函数:
template<typename T1, typename T2>
auto Sum(T1& t1, T2& t2)->decltype(t1+t2) {
return t1+t2;
}
auto占位符与->return_type是构成追踪返回类型函数的两个基本元素。
应用
使模板更加泛化
如上面的Sum函数
简化函数的定义,提高代码的可读性。
# include <iostream>
# include <type_traits>
using namespace std;
int (*(*pf())()) () { return nullptr; }
// auto(*)()->int(*) () 返回int(*)()类型的函数p
// auto pf1()->auto(*)()->int(*)() // 返回函数p的函数
auto pf1()->auto(*)()->int(*)() { return nullptr; }
int main(void) {
cout << is_same<decltype(pf), decltype(pf1)>::value << endl; // 1
return 0;
}
解析:
先介绍一下函数指针与返回函数指针的函数的语法:
// function ptr
return_type(*func_pointer)(parameter_list)
// A function return func_pointer
return_type(*function(func_parameter_list))(parameter_list) {}
对于int (*(*pf())()) () { return nullptr; }
:
(1)该函数的返回类型为int(*)(), 是一个指向返回类型为int,参数列表为空的函数的指针;
(2)该函数的参数列表为空;
(3)该函数的名称为*pf();
(4)说明pf()返回的也是一个函数指针,且这个函数指针指向该函数。
这种函数的定义方式使得代码的可读性大大降低,C++11中的追踪返回类型能大大改善这种情况:
auto pf1()->auto(*)()->int(*)() { return nullptr; }
即pf1这个函数先返回auto()()->int()()的函数指针, 而这个函数指针auto()()指向的函数的返回类型为int()()的函数指针。如此一来,果真大大提高了代码的可读性。
广泛应用于转发函数
先了解一下转发函数的概念。
何为完美转发?是指在函数模板中,完全依照模板的参数类型,将参数传递给函数模板中调用另外一个函数。
有完美转发那么肯定也有不完美转发。如果在参数传递的过程中产生了额外的临时对象拷贝,那么其转发也就算不上完美转发。为了避免起不完美,我们要借助于引用以防止其进行临时对象的拷贝。
如上个人理解: 完美转发,即为了避免临时对象的拷贝。
举例:
# include <iostream>
using namespace std;
double foo(int a) {
return (double)a + 0.1;
}
int foo(double b) {
return (int)b;
}
template<class T>
auto Forward(T t)->decltype(foo(t)) {
return foo(t);
}
int main(void) {
cout << Forward(2) << endl; // 2.1
cout << Forward(0.5) << endl; // 0
return 0;
}
应用于函数指针及函数引用
// 函数指针
int (*fp)();
<=>
auto (*fp)()->int;
// 函数引用
int (&fr)();
<=>
auto (&fr)()->int;
4、for
范围循环
# include <iostream>
using namespace std;
int main(void) {
int a[5] = {1, 2, 3, 4, 5};
for (int& e: arr) e *= 2;
for (int& e: arr) cout << e << "\t";
// or(1)
for (int e: arr) cout << e << "\t";
// or(2)
for (auto e:arr) cout << e << "\t";
return 0;
}
注意:auto不会自动推导出引用类型,如需引用要加上&
auto& :修改
auto:不修改, 拷贝对象
使用条件:
(1)for循环迭代的范围是可确定的:对于类,需要有begin()与end()函数;对于数组,需要确定第一个元素到最后一个元素的范围;
(2)迭代器要重载++;
(3)迭代器要重载*
, 即*iterator;
(4)迭代器要重载== 和!=。
对于标准库中的容器,如string, array, vector, deque, list, queue, map, set,等使用基于范围的for循环没有问题,因为标准库总是保持其容器定义了相关操作。
注意:如果数组大小不能确定的话,是不能使用基于范围的for 循环的。
// 无法通过编译
# include <iostream>
using namespace std;
int func(int a[]) {
for (auto e: a) cout << e << "\t";
}
int main(void) {
int arr[] = {1, 2, 3, 4, 5};
func(arr);
return 0;
}
for_each
# include <iostream>
# include <algorithm>
using namespace std;
int action1(int &e) { e*=2; }
int action2(int &e) { cout << e << "\t"; }
int main(void) {
int arr[5] = {1, 2, 3, 4, 5};
for_each(arr, arr+sizeof(arr)/sizeof(a[0]), action1);
for_each(arr, arr+sizeof(arr)/sizeof(a[0]), action2);
return 0;
}
需要告诉循环体其界限范围,即arr到arr+sizeof(arr)/sizeof(a[0]),才能按元素执行操作。
5、nullptr
典型的初始化指针通常有两种:0与NULL, 意在表明指针指向一个空的位置。
int *p = 0;
int *q = NULL;
NULL其实是宏定义,在传统C头文件(stddef.h)中的定义如下
// stddef.h
# undef NULL
# if define(_cplusplus)
# define NULL 0
# else
# define NULL ((void*)0)
# endif
从上面的定义中我们可以看到,NULL既可被替换成整型0,也可以被替换成指针(void*)0。这样就可能会引发一些问题,如二义性:
# include <iostream>
using namespace std;
void f(int* ptr) {}
void f(int num) {}
int main(void) {
f(0);
f((int*)0);
f(NULL); // 编译不通过
return 0;
}
NULL既可以被替换成整型,也可以被替换成指针,因此在函数调用时就会出现问题。因此,在早期版本的C++中,为了解决这种问题,只能进行显示类型转换。
所以在C++11中,为了完善这一问题,引入了nullptr的指针空值类型的常量。为什么不重用NULL?原因是重用NULL会使已有很多C++程序不能通过C++11编译器的编译。为保证最大的兼容性且避免冲突,引入新的关键字是最好的选择。
而且,出于兼容性的考虑,C++11中并没有消除NULL的二义性。
那么,nullptr有没有数据类型呢?头文件对其类型的定义如下:
// <cstddef>typedef
decltype(nullptr) nullptr_t;
即nullptr_t为nullptr的类型, 称为指针空值类型。指针空值类型的使用有以下几个规则:
- 所有定义为nullptr_t类型的数据都是等价的,行为也是完全一致的。 也就是说,nullptr_t的对象都是等价,都是表示指针的空值,即满足“==”。
- nullptr_t类型的数据可以隐式转换成任意一个指针类型。
- nullptr_t类型数据不能转换成非指针类型,即使用reinterpret_cast()的方式也不可以实现转化;
- nullptr_t类型的对象不适用于算术运算的表达式;
- nullptr_t类型数据可以用于关系运算表达式,但仅能与nullptr_t类型数据或者是指针类型数据进行比较,当且仅当关系运算符为-=, <=, >=, 等时返回true。
# include <iostream>
# include <typeinfo>
using namespace std;
int main(void) {
// nullptr 隐式转换成char*
char* cp = nullptr;
// 不可转换成整型,而任何类型也不可能转化成nullptr_t
int n1 = nullptr; // 编译不通过
int n2 = reinterpret_cast<int>(nullptr); // 编译不通过
// nullptr 与 nullptr_t 类型变量可以作比较
nullptr_t nptr;
if (nptr == nullptr)
cout << "nullptr_t nptr == nullptr" << endl;
else
cout << "nullptr_t nptr != nullptr" << endl;
if (nptr < nullptr)
cout << "nullptr_t nptr < nullptr" << endl;
else
cout << "nullpte_t nptr !< nullptr" << endl;
// 不能转化成整型或bool类型,以下代码不能通过编译
if (0 == nullptr);
if (nullptr);
// 不可以进行算术运算,以下代码不能通过编译
// nullptr += 1
// nullptr * 5
// 以下操作均可以正常进行
// sizeof(nullptr) == sizeof(void*)
sizeof(nullptr);
typeid(nullptr);
throw(nullptr);
return 0;
}
输出:
nullptr_t nptr == nullptr
nullptr_t nptr !< nullptr
terminate called after throwing an instance of "decltype(nullptr)" Aborted
nullptr_t 看起来像个指针类型,用起来更像。但是在把nullptr_t应用于模板的时候,我们会发现模板只能把它作为一个普通的类型进行推导,并不会将其视为T*指针。
# include <iostream>
using namespace std;
template<typename T>
void g(T* t) {}
template<typename T>
void h(T t) {}
int main(void) {
// nullptr 并不会被编译器“智能”地推导成某种基本类型的指针或者void*指针。
// 为了让编译器推导出来,应当进行显示类型转换
g(nullptr); // 编译失败,nullptr的类型是nullptr_t,而不是指针
g((float*)nullptr); // T* 为 float*类型
h(0); // T 为 整型
h(nullptr); // T 为 nullptr_t类型
h((float*)nullptr); // T 为 float*类型
return 0;
}
补充:nullptr_t的对象的地址都可以被用户使用, nullptr是右值,取其地址没有意义,编译器也是不允许的。如果一定要取其地址,也不是没有办法。可以定义一个nullptr的右值引用,然后对该引用进行取地址。
const nullptr_t&& p = nullptr;
6、列表初始化
C++98中,标准允许使用花括号{}对数组元素进行统一的列表初始值设定。
int array1[] = {1,2,3,4,5};
int array2[] = {0};
对对于一些自定义类型,却不行.
vector<int> v{1,2,3,4,5};
在C++98中这样无法通过编译,因此需要定义vector之后,在使用循环进行初始赋值。
C++11扩大了用初始化列表的使用范围,让其适用于所有的内置类型和自定义类型,而且使用时,=可以不写
// 内置类型
int x1 = {10};
int x2{10}
// 数组
int arr1[5] {1,2,3,4,5}
int arr2[]{1,2,3,4,5};
// 标准容器
vector<int> v{1,2,3}
map<int,int> m{{1,1},{2,2}}
// 自定义类型
class Point
{
int x;
int y;
}
Power p{1,2};
对象的列表初始化
给类(模板类)添加一个带有initializer_list类型参数的构造函数即可支持对象的列表初始化.
#include <initializer_list>
class Magic {
public:
Magic(std::initializer_list<int> list) {}
};
Magic magic = {1,2,3,4,5};
std::vector<int> v = {1, 2, 3, 4};
7、构造函数
委派构造函数
C++11 引入了委托构造的概念,这使得构造函数可以在同一个类中一个构造函数调用另一个构造函数,从而达到简化代码的目的:
class Info
{
public;
Info()
:_type(0)
,_name('s')
{}
Info(int type)
:_type(type)
,_name('a')
{}
Info(char a)
:_type(0)
,_name(a)
{}
pirvate;
int _type;
char _name;
};
class Info
{
// 目标构造函数
public:
Info()
:_type(0)
,_a('a')
{}
// 委派构造函数
Info(int type)
:Info()
{
_type = type;
}
private;
int _type = 0;
char _a = 'a';
};
继承构造
在继承体系中,如果派生类想要使用基类的构造函数,需要在构造函数中显式声明。
假若基类拥有为数众多的不同版本的构造函数,这样,在派生类中得写很多对应的“透传”构造函数。如下:
struct A
{
A(int i) {}
A(double d,int i){}
A(float f,int i,const char* c){}
//...等等系列的构造函数版本
};
struct B:A
{
B(int i):A(i){}
B(double d,int i):A(d,i){}
B(folat f,int i,const char* c):A(f,i,e){}
//......等等好多个和基类构造函数对应的构造函数
};
struct A
{
A(int i) {}
A(double d,int i){}
A(float f,int i,const char* c){}
//...等等系列的构造函数版本
};
struct B:A
{
using A::A;
//关于基类各构造函数的继承一句话搞定
//......
};
如果一个继承构造函数不被相关的代码使用,编译器不会为之产生真正的函数代码,这样比透传基类各种构造函数更加节省目标代码空间。
8、overrride 和final
C++11提供override和final来修饰虚函数
//1、final修饰基类的虚函数不能被派生类重写
#include<iostream>
using namespace std;
class Car
{
public:
virtual void Drive() final{}
};
class Benz : public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
//2、override修饰派生类虚函数强制完成重写,如果没有重写会报错
class Car
{
public:
virtual void Drive() {}
};
class Benz : public Car
{
public:
virtual void Drive() override
{
cout << "Benz-舒适" << endl;
}
};
基类和派生类中,成员函数名、形参类型、常量属性 (constness) 和 引用限定符 (reference qualifier) 必须完全相同
如此多的限制条件,导致了虚函数重写如上述代码,极容易因为一个不小心而出错
C++11 中的 override 关键字,可以显式的在派生类中声明,哪些成员函数需要被重写,如果没被重写,则编译器会报错
9、默认函数控制
在C++11中,可以在默认函数定义或声明时加上=default,来让编译器生成该函数的默认版本。
class A
{
public:
A(int a)
:_a(a)
{}
A() = default; // 显式缺省构造函数
A& operator=(const A& a); // 在类中声明,在类外定义时,让编译器生成默认赋值运算符重载
private:
int _a;
};
A& A::operator=(const A& a) = default;
要想限制一些默认函数的生成,在C++98中,可以把该函数设为私有,不定义,这样,如果有人调用就会报错。在C++11中,可以给该函数声明加上=delete就可以。
class A
{
A(int a)
:_a(a)
{}
A(constA&) = delete; // 禁止编译器生成默认的拷贝构造函数
private:
int _a;
};
10、lambda表达式
Lambda 表达式,实际上就是提供了一个类似匿名函数的特性,而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。
Lambda 表达式的基本语法如下:
[ caputrue ] ( params ) opt -> ret { body; };
- capture是捕获列表;
- params是参数表;(选填)
- opt是函数选项;可以填mutable,exception,attribute(选填)
- mutable说明lambda表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获的对象的non-const方法。
- exception说明lambda表达式是否抛出异常以及何种异常。
- attribute用来声明属性。
- -> 用于追踪返回值类型。没有返回值时可以省略。返回值类型明确的情况下,也可以省略
- ret是返回值类型(拖尾返回类型)。(选填, 返回值类型明确的情况下,也可以省略) ;
- body是函数体。
捕获列表:lambda表达式的捕获列表精细控制了lambda表达式能够访问的外部变量,以及如何访问这些变量。
-
[]不捕获任何变量。
-
[&]捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。
-
[=]捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)。注意值捕获的前提是变量可以拷贝,且被捕获的变量在 lambda 表达式被创建时拷贝,而非调用时才拷贝。如果希望lambda表达式在调用时能即时访问外部变量,我们应当使用引用方式捕获。
-
按值捕获的变量值均复制一份存储在lambda表达式变量中,修改他们也并不会真正影响到外部,但我们却仍然无法修改它们。如果希望去修改按值捕获的外部变量,需要显示指明lambda表达式为mutable。被mutable修饰的lambda表达式就算没有参数也要写明参数列表。
原因:lambda表达式可以说是就地定义仿函数闭包的“语法糖”。它的捕获列表捕获住的任何外部变量,最终会变为闭包类型的成员变量。按照C++标准,lambda表达式的operator()默认是const的,一个const成员函数是无法修改成员变量的值的。而mutable的作用,就在于取消operator()的const。
int a = 0; auto f1 = [=] { return a++; }; //error auto f2 = [=] () mutable { return a++; }; //OK
-
-
[=,&foo]按值捕获外部作用域中所有变量,并按引用捕获foo变量。
-
[bar]按值捕获bar变量,同时不捕获其他变量。
-
[this]捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样的访问权限。如果已经使用了&或者=,就默认添加此选项。捕获this的目的是可以在lamda中使用当前类的成员函数和成员变量。
lambda表达式的大致原理:
每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类重载了()运算符),我们称为闭包类型(closure type),并且编译器是通过lambda_+uuid来唯一辨识一个lambda表达式的。
那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,是一个右值。
所以,我们上面的lambda表达式的结果就是一个个闭包。对于复制传值捕捉方式,类中会相应添加对应类型的非静态数据成员。在运行时,会用复制的值初始化这些成员变量,从而生成闭包。对于引用捕获方式,无论是否标记mutable,都可以在lambda表达式中修改捕获的值。
lambda表达式是不能重新被赋值的:
auto a = [] { cout << "A" << endl; };
auto b = [] { cout << "B" << endl; };
a = b; // 非法,lambda无法赋值
auto c = a; // 合法,生成一个副本
闭包类型禁用了赋值操作符,但是没有禁用复制构造函数,所以你仍然可以用一个lambda表达式去初始化另外一个lambda表达式而产生副本。
最好不要使用[=]和[&]捕获所有变量
std::function<int(int)> add_x(int x)
{
return [&](int a) { return x + a; };
}
参数x仅是一个临时变量,函数add_x调用后就被销毁了,但是返回的lambda表达式却引用了该变量,当调用这个表达式时,引用的是一个垃圾值,会产生没有意义的结果。上面这种情况,使用默认传值方式可以避免悬挂引用问题。
class Filter
{
public:
Filter(int divisorVal):
divisor{divisorVal}
{}
std::function<bool(int)> getFilter()
{
return [=](int value) {return value % divisor == 0; };
}
private:
int divisor;
};
这个类中有一个成员方法,可以返回一个lambda表达式,这个表达式使用了类的数据成员divisor。你可能认为这个lambda表达式也捕捉了divisor的一份副本,但是实际上并没有。因为数据成员divisor对lambda表达式并不可见。
即使更改为:
std::function<bool(int)> getFilter()
{
return [this](int value) {return value % this->divisor == 0; };
}
捕获的是指针,其实相当于以引用的方式捕获了当前类对象,这很危险,因为你仍然有可能在类对象析构后使用这个lambda表达式
应用
1、
lambda表达式可以赋值给对应类型的函数指针。但是使用函数指针并不是那么方便。
所以STL定义在< functional >头文件提供了一个多态的函数对象封装std::function,其类似于函数指针。它可以绑定任何类函数对象,只要参数与返回类型相同。
如下面的返回一个bool且接收两个int的函数包装器:
std::function<bool(int, int)> wrapper = [](int x, int y) { return x < y; };
2、
lambda表达式一个更重要的应用是其可以用于函数的参数,通过这种方式可以实现回调函数。
int value = 3;
vector<int> v {1, 3, 5, 2, 6, 10};
int count = std::count_if(v.beigin(), v.end(), [value](int x) { return x > value; });
3、
lambda表达式与函数指针、仿函数
typedef bool (*GTF) (int, int);
bool greater_func1(int l, int r)
{
return l > r;
}
struct greater_func2
{
bool operator()(int l, int r)
{
return l > r;
}
};
int main()
{
// 函数指针
GTF f1 = greater_func1; // typedef 定义
// bool (*f1) (int, int) = greater_func1; // 不typedef ,直接原生写法,可读性差
cout<< f1(1,2)<<endl;
// 仿函数
greater_func2 f2;
cout<< f2(1,2)<<endl;
// lamba表达式
auto f3 = [] (int l, int r) ->bool{return l > r;};
cout<< f3(1,2)<<endl;
return 0;
}
函数指针,仿函数,lambda用法上是一样的,但函数指针类型定义很难理解,仿函数需要实现运算符的重载,必须先定义一个类,而且一个类只能实现一个()operator的重载。而lambda可以定义好直接使用。
11、std::function、std::bind封装可执行对象
std::bind和std::function是从boost中移植进来的C++新标准,这两个语法使得封装可执行对象变得简单而易用。
此外,std::bind和std::function也可以结合lamda表达式一起使用,使得可执行对象的写法更加“花俏”。
我们下面通过实例一步步了解std::function和std::bind的用法:
class Test
{
public:
void Add()
{
}
};
//main.cpp 示例代码1.0 http://www.cnblogs.com/feng-sc/p/5710724.html
#include <functional>
#include <iostream>
#include "Test.h"
int add(int a,int b)
{
return a + b;
}
int main()
{
Test test;
test.Add();
return 0;
}
我们现在来考虑一下这个问题,假如我们的需求是让Test里面的Add由外部实现,如main.cpp里面的add函数,有什么方法呢?
没错,我们可以用函数指针。
class Test
{
public:
typedef int(*FunType)(int, int);
void Add(FunType fun,int a,int b)
{
int sum = fun(a, b);
std::cout << "sum:" << sum << std::endl;
}
};
....
....
Test test;
test.Add(add, 1, 2);
....
我们把问题升级,假如add实现是在另外一个类内部,如下代码:
class TestAdd
{
public:
int Add(int a,int b)
{
return a + b;
}
};
int main()
{
Test test;
//test.Add(add, 1, 2);
return 0;
}
假如add方法在TestAdd类内部,那你的Test类没辙了,因为Test里的Test函数只接受函数指针。
这个时候std::function和std::bind就帮上忙了。
class Test
{
public:
void Add(std::function<int(int, int)> fun, int a, int b)
{
int sum = fun(a, b);
std::cout << "sum:" << sum << std::endl;
}
};
int add(int a,int b)
{
std::cout << "add" << std::endl;
return a + b;
}
class TestAdd
{
public:
int Add(int a,int b)
{
std::cout << "TestAdd::Add" << std::endl;
return a + b;
}
};
int main()
{
Test test;
test.Add(add, 1, 2);
TestAdd testAdd;
test.Add(std::bind(&TestAdd::Add, testAdd, std::placeholders::_1, std::placeholders::_2), 1, 2);
return 0;
}
1、Test类中std::function<int(int,int)>表示std::function封装的可执行对象返回值和两个参数均为int类型。
2、
std::bind第一个参数为对象函数指针,表示函数相对于类的首地址的偏移量;
testAdd为对象指针;
std::placeholders::_1和std::placeholders::_2为参数占位符,表示std::bind封装的可执行对象可以接受两个参数。
12、线程库
std::thread
在C++11以前,C++的多线程编程均需依赖系统或第三方接口实现,一定程度上影响了代码的移植性。
C++11中,引入了boost库中的多线程部分内容,形成C++标准,形成标准后的boost多线程编程部分接口基本没有变化,这样方便了boost接口开发者的移植。
std::thread为C++11的线程类,C++11的std::thread解决了boost::thread中构成参数限制的问题
#include <thread>
void threadfun1()
{
std::cout << "threadfun1 - 1\r\n" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "threadfun1 - 2" << std::endl;
}
void threadfun2(int iParam, std::string sParam)
{
std::cout << "threadfun2 - 1" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(5));
std::cout << "threadfun2 - 2" << std::endl;
}
int main()
{
std::thread t1(threadfun1);
std::thread t2(threadfun2, 10, "abc");
t1.join();
std::cout << "join" << std::endl;
t2.detach();
std::cout << "detach" << std::endl;
}
std::thread()创建一个新的线程可以接受任意的可调用对象类型,包括lambda表达式,函数,函数对象,函数指针
// 使用lambda表达式作为线程函数创建线程
int main()
{
int n1 = 1;
int n2 = 2;
std::thread t([&](int addNum){n1 += addNum; n2 += addNum; }, 3);
t.join();
std::cout << n1 << " " << n2 << std:: endl;
system("pause");
return 0;
}
std::atomic
什么是原子数据类型?
从功能上看,简单地说,原子数据类型不会发生数据竞争,能直接用在多线程中而不必我们用户对其进行添加互斥资源锁的类型。从实现上,大家可以理解为这些原子类型内部自己加了锁。
我们下面通过一个测试例子说明原子类型std::atomic_int的特点。
int sum = 0;
std::mutex m;
void fun(size_t num)
{
for (size_t i = 0; i < num; i++)
{
m.lock();
sum++;
m.unlock();
}
}
int main()
{
std::cout << "before,sum=" << sum << std::endl;
std::thread t1(fun, 100000000);
std::thread t2(fun, 100000000);
t1.join();
t2.join();
std::cout << "After,sum=" << sum << std::endl;
system("pause");
return 0;
}
虽然加锁结果了这个问题:但是它有一个缺陷:只要有一个线程在对sum++的时候,其它线程就会阻塞,会影响程序运行的效率,而且锁如果控制不好,会导致死锁的问题。
因此在C++11中引入了原子操作。对应于内置的数据类型,原子数据类型都有一份对应的类型。
要使用以上的原子操作,需要添加头文件
#include<thread>
#include<mutex>
#include<atomic>
std::atomic_int sum{ 0 };
void fun(size_t num)
{
for (size_t i = 0; i < num; i++)
{
sum ++; // 原子的
}
}
int main()
{
std::cout << "before,sum=" << sum << std::endl;
std::thread t1(fun, 10000000);
std::thread t2(fun, 10000000);
t1.join();
t2.join();
std::cout << "After,sum=" << sum << std::endl;
system("pause");
return 0;
}
std::condition_variable
C++11中的std::condition_variable就像Linux下使用pthread_cond_wait和pthread_cond_signal一样,可以让线程休眠,直到被唤醒,再从新执行。
线程等待在多线程编程中使用非常频繁,经常需要等待一些异步执行的条件的返回结果。
// condition_variable example
#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];
for (int i = 0; i<10; ++i)
threads[i] = std::thread(print_id, i);
std::cout << "10 threads ready to race...\n";
go();
for (auto& th : threads) th.join();
return 0;
}
调用cv.wait(lck)的时候,线程将进入休眠,在调用go函数之前,10个线程都处于休眠状态,当cv.notify_all()运行后,线程休眠将结束,继续往下运行,最终输出如下结果。
13、正则表达式
正则表达式描述了一种字符串匹配的模式。一般使用正则表达式主要是实现下面三个需求:
- 检查一个串是否包含某种形式的子串;
- 将匹配的子串替换;
- 从某个串中取出符合条件的子串。
1、
C++11 提供的正则表达式库操作 std::string 对象,对模式 std::regex (本质是 std::basic_regex)进行初始化,通过 std::regex_match 进行匹配,从而产生 std::smatch (本质是 std::match_results 对象)。
#include <iostream>
#include <string>
#include <regex>
int main() {
std::string fnames[] = {"foo.txt", "bar.txt", "test", "a0.txt", "AAA.txt"};
// 在 C++ 中 `\` 会被作为字符串内的转义符,为使 `\.` 作为正则表达式传递进去生效,需要对 `\` 进行二次转义,从而有 `\\.`
std::regex txt_regex("[a-z]+\\.txt");
for (const auto &fname: fnames)
std::cout << fname << ": " << std::regex_match(fname, txt_regex) << std::endl;
}
[a-z]+.txt: [a-z] 表示匹配一个小写字母, + 可以使前面的表达式匹配多次,因此 [a-z]+ 能够匹配一个及以上小写字母组成的字符串。在正则表达式中一个 . 表示匹配任意字符,而 . 转义后则表示匹配字符 . ,最后的 txt 表示严格匹配 txt 这三个字母。因此这个正则表达式的所要匹配的内容就是文件名为纯小写字母的文本文件。
输出结果:
foo.txt: 1
bar.txt: 1
test: 0
a0.txt: 0
AAA.txt: 0
2、
另一种常用的形式就是依次传入 std::string/std::smatch/std::regex 三个参数,其中 std::smatch 的本质其实是 std::match_results,在标准库中, std::smatch 被定义为了 std::match_results,也就是一个子串迭代器类型的 match_results。使用 std::smatch 可以方便的对匹配的结果进行获取,例如:
std::regex base_regex("([a-z]+)\\.txt");
std::smatch base_match;
for(const auto &fname: fnames) {
if (std::regex_match(fname, base_match, base_regex)) {
// sub_match 的第一个元素匹配整个字符串
// sub_match 的第二个元素匹配了第一个括号表达式
if (base_match.size() == 2) {
std::string base = base_match[1].str();
std::cout << "sub-match[0]: " << base_match[0].str() << std::endl;
std::cout << fname << " sub-match[1]: " << base << std::endl;
}
}
}
输出结果:
sub-match[0]: foo.txt
foo.txt sub-match[1]: foo
sub-match[0]: bar.txt
bar.txt sub-match[1]: bar
14、新增容器
std::array
std::array 保存在栈内存中,相比堆内存中的 std::vector,我们能够灵活的访问这里面的元素,从而获得更高的性能。
std::array 会在编译时创建一个固定大小的数组,std::array 不能够被隐式的转换成指针,使用 std::array只需指定其类型和大小即可:
#include <array>
std::array<int, 4> arr= {1,2,3,4};
int len = 4;
std::array<int, len> arr = {1,2,3,4}; // 非法, 数组大小参数必须是常量表达式
当我们开始用上了 std::array 时,难免会遇到要将其兼容 C 风格的接口,这里有三种做法:
#include <array>
void foo(int *p, int len) {
return;
}
std::array<int,4> arr = {1,2,3,4};
// C 风格接口传参
// foo(arr, arr.size()); // 非法, 无法隐式转换
foo(&arr[0], arr.size());
foo(arr.data(), arr.size());
// 使用 `std::sort`
std::sort(arr.begin(), arr.end());
std::forward_list
std::forward_list 是一个列表容器,使用方法和 std::list 基本类似。
和 std::list 的双向链表的实现不同,std::forward_list 使用单向链表进行实现,当不需要双向迭代时,具有比 std::list 更高的空间利用率。
#include <forward_list>
int main()
{
std::forward_list<int> numbers = {1,2,3,4,5,4,4};
std::cout << "numbers:" << std::endl;
for (auto number : numbers)
{
std::cout << number << std::endl;
}
numbers.remove(4);
std::cout << "numbers after remove:" << std::endl;
for (auto number : numbers)
{
std::cout << number << std::endl;
}
return 0;
}
运行结果:
无序容器
C++11 引入了两组无序容器:
std::unordered_map/std::unordered_multimap 和 std::unordered_set/std::unordered_multiset。
无序容器中的元素是不进行排序的,内部通过 Hash 表实现,插入和搜索元素的平均复杂度为 O(constant)。
元组 std::tuple
元组的使用有三个核心的函数:
std::make_tuple: 构造元组
std::get: 获得元组某个位置的值
std::tie: 元组拆包
#include <tuple>
#include <iostream>
auto get_student(int id)
{
// 返回类型被推断为 std::tuple<double, char, std::string>
if (id == 0)
return std::make_tuple(3.8, 'A', "张三");
if (id == 1)
return std::make_tuple(2.9, 'C', "李四");
if (id == 2)
return std::make_tuple(1.7, 'D', "王五");
return std::make_tuple(0.0, 'D', "null");
// 如果只写 0 会出现推断错误, 编译失败
}
int main()
{
auto student = get_student(0);
std::cout << "ID: 0, "
<< "GPA: " << std::get<0>(student) << ", "
<< "成绩: " << std::get<1>(student) << ", "
<< "姓名: " << std::get<2>(student) << '\n';
double gpa;
char grade;
std::string name;
// 元组进行拆包
std::tie(gpa, grade, name) = get_student(1);
std::cout << "ID: 1, "
<< "GPA: " << gpa << ", "
<< "成绩: " << grade << ", "
<< "姓名: " << name << '\n';
}
合并两个元组,可以通过 std::tuple_cat 来实现。
auto new_tuple = std::tuple_cat(get_student(1), std::move(t));
15、模板增强
尖括号 “>”
在传统 C++ 的编译器中,>>一律被当做右移运算符来进行处理。但实际上我们很容易就写出了嵌套模板的代码:
std::vector<std::vector<int>> wow;
类型别名模板
在传统 C++中,typedef 可以为类型定义一个新的名称,但是却没有办法为模板定义一个新的名称。因为,模板不是类型。例如:
template< typename T, typename U, int value>
class SuckType {
public:
T a;
U b;
SuckType():a(value),b(value){}
};
template< typename U>
typedef SuckType<std::vector<int>, U, 1> NewType; // 不合法
C++11 使用 using 引入了下面这种形式的写法,并且同时支持对传统 typedef 相同的功效:
template <typename T>
using NewType = SuckType<int, T, 1>; // 合法
默认模板参数
我们可能定义了一个加法函数:
template<typename T, typename U>
auto add(T x, U y) -> decltype(x+y) {
return x+y
}
但在使用时发现,要使用 add,就必须每次都指定其模板参数的类型。
在 C++11 中提供了一种便利,可以指定模板的默认参数:
template<typename T = int, typename U = int>
auto add(T x, U y) -> decltype(x+y) {
return x+y;
}
外部模板
传统 C++ 中,模板只有在使用时才会被编译器实例化。在每个编译单元(文件)的代码中遇到了被完整定义的模板,都会实例化。这就产生了重复实例化而导致的编译时间的增加。并且,我们没有办法通知编译器不要触发模板实例化。
C++11 引入了外部模板,使得能够显式的告诉编译器何时进行模板的实例化:
template class std::vector<bool>; // 强行实例化
extern template class std::vector<double>; // 不在该编译文件中实例化模板