C++11简介
在
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
增加的语法特性非常篇幅非常多,这里没办法一 一讲解,所以本节主要讲解实际中比较实用的语法。
C++11 - cppreference.com
统一的列表初始化
{}初始化
在
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 ;};int main (){int x1 = 1 ;int x2 { 2 };int array1 []{ 1 , 2 , 3 , 4 , 5 };int array2 [ 5 ]{ 0 };Point p { 1 , 2 };// C++11 中列表初始化也可以适用于 new 表达式中int* pa = new int [ 4 ]{ 0 };return 0 ;}
创建对象时也可以使用列表初始化方式调用构造函数初始化
class Date{public :Date ( int year , int month , int day ): _year ( year ), _month ( month ), _day ( day ){cout << "Date(int year, int month, int day)" << endl ;}private :int _year ;int _month ;int _day ;};int main (){Date d1 ( 2022 , 1 , 1 ); // old style// C++11 支持的列表初始化,这里会调用构造函数初始化Date d2 { 2022 , 1 , 2 };Date d3 = { 2022 , 1 , 3 };return 0 ;}
std::initializer_list
std::initializer_list的介绍文档:
initializer_list - C++参考 (cplusplus.com)
std::initializer_list
是什么类型:
int main (){// the type of il is an initializer_listauto il = { 10 , 20 , 30 };cout << typeid ( il ). name () << endl ;return 0 ;}
std::initializer_list
使用场景:
std::initializer_list
一般是作为构造函数的参数,
C++11
对
STL
中的不少容器就增加
std::initializer_list
作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为
operator=
的参数,这样就可以用大括号赋值。
即:容器想用不固定的{}数据个数初始化,initializer_list支持
声明
c++11
提供了多种简化声明的方式,尤其是在使用模板时。
auto
在
C++98
中
auto
是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局
部的变量默认就是自动存储类型,所以
auto
就没什么价值了。
C++11
中废弃
auto
原来的用法,将
其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初
始化值的类型。
int main (){int i = 10 ;auto p = & i ;auto pf = strcpy ;cout << typeid ( p ). name () << endl ;cout << typeid ( pf ). name () << endl ;map < string , string > dict = { { "sort" , " 排序 " }, { "insert" , " 插入 " } };//map<string, string>::iterator it = dict.begin();auto it = dict . begin ();return 0 ;}
decltype
关键字
decltype
将变量的类型声明为表达式指定的类型。
int main()
{
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有些地方增加代码读起来难度
return 0;
}
nullptr
由于
C++
中
NULL
被定义成字面量
0
,这样就可能回带来一些问题,因为
0
既能指针常量,又能表示
整形常量。所以出于清晰和安全的角度考虑,
C++11
中新增了
nullptr
,用于表示空指针。
#ifndef NULL#ifdef __cplusplus#define NULL 0#else#define NULL ((void *)0)#endif#endif
STL中一些变化
新容器
用橘色圈起来是
C++11
中的一些几个新容器,但是实际最有用的是
unordered_map
和
unordered_set
。这两个我们前面已经进行了非常详细的讲解,其他的大家了解一下即可。
容器中的一些新方法
如果我们再细细去看会发现基本每个容器中都增加了一些
C++11
的方法,但是其实很多都是用得
比较少的。
比如提供了
cbegin
和
cend
方法返回
const
迭代器等等,但是实际意义不大,因为
begin
和
end
也是
可以返回
const
迭代器的,这些都是属于锦上添花的操作。
实际上
C++11
更新后,容器中增加的新方法最后用的插入接口函数的右值引用版本。
但是这些接口到底意义在哪?网上都说他们能提高效率,他们是如何提高效率的?
请看下面的右值引用和移动语义章节的讲解。另外
emplace
还涉及模板的可变参数,也需要再继
续深入学习后面章节的知识。
右值引用和移动语义
左值引用和右值引用
传统的
C++
语法中就有引用的语法,而
C++11
中新增了的右值引用语法特性,所以从现在开始我们
之前学习的引用就叫做左值引用。
无论左值引用还是右值引用,都是给对象取别名
。
什么是左值?什么是左值引用?
左值是一个表示数据的表达式
(
如变量名或解引用的指针
)
,
我们可以获取它的地址
,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边
。定义时
const
修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
// 左值是一个表达式,可以取地址的
// 左值和右值,能否取地址
// 左值:可以取地址的
// 右值:不可以取地址的int main (){// 以下的 p 、 b 、 c 、 *p 都是左值int* p = new int ( 0 );int b = 1 ;const int c = 2 ;// 以下几个是对上面左值的左值引用int* & rp = p ;int & rb = b ;const int & rc = c ;int & pvalue = * p ;return 0 ;}
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值
(
这个不能是左值引
用返回
)
等等,
右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能
取地址
。右值引用就是对右值的引用,给右值取别名。
右值分为纯右值(内置类型)和将亡值(自定义类型)
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 ;}
左值引用与右值引用比较
左值引用总结:
1.
左值引用只能引用左值,不能引用右值。
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 ;}
右值引用总结:
1.
右值引用只能右值,不能引用左值。
2.
但是右值引用可以
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 ;}
右值引用使用场景和意义
前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么
C++11
还要提出右值引
用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!
意义:减少拷贝,提高效率。
左值引用返回值的问题没有彻底解决,如果返回值是函数中的局部对象,不能用引用返回。
且效率提升,针对的是自定义类型的深拷贝的类,因为深拷贝的类才有转移资源的移动系列函数
对于内置类型,和浅拷贝自定义类型,没有移动系列函数。
对于内置类型,和浅拷贝自定义类型,没有移动系列函数。
namespace xx
{
class string
{
public:
//...
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
}
左值引用的使用场景:
做参数和做返回值都可以提高效率。
void func1 ( bit::string s ){}void func2 ( const bit::string & s ){}int main (){bit::string s1 ( "hello world" );// func1 和 func2 的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值func1 ( s1 );func2 ( s1 );// string operator+=(char ch) 传值返回存在深拷贝// string& operator+=(char ch) 传左值引用没有拷贝提高了效率s1 += '!' ;return 0 ;}
左值引用的短板:
但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,
只能传值返回。例如:
bit::string to_string(int value)
函数中可以看到,这里只能使用传值返回,
传值返回会导致至少
1
次拷贝构造
(
如果是一些旧一点的编译器可能是两次拷贝构造
)
。
namespace xx
{
bit::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
bit::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}
int main()
{
// 在bit::string to_string(int value)函数中可以看到,这里
// 只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷
贝构造)。
bit::string ret1 = bit::to_string(1234);
bit::string ret2 = bit::to_string(-1234);
return 0;
}
右值引用和移动语义解决上述问题:
在
bit::string
中增加移动构造,
移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不
用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己
。
// 移动构造string ( string && s ): _str ( nullptr ), _size ( 0 ), _capacity ( 0 ){cout << "string(string&& s) -- 移动语义 " << endl ;swap ( s );}int main (){bit::string ret2 = bit::to_string (- 1234 );return 0 ;}
再运行上面
bit::to_string
的两个调用,我们会发现,这里没有调用深拷贝的拷贝构造,而是调用
了移动构造,原因:str被编译器自动识别成右值,即将亡值(出了作用域str就销毁)。移动构造中没有新开空间,拷贝数据,所以效率提高了。
不仅仅有移动构造,还有移动赋值:
在
bit::string
类中增加移动赋值函数,再去调用
bit::to_string(1234)
,不过这次是将
bit::to_string(1234)
返回的右值对象赋值给
ret1
对象,这时调用的是移动构造。
// 移动赋值string & operator = ( string && s ){cout << "string& operator=(string&& s) -- 移动语义 " << endl ;swap ( s );return * this ;}int main (){bit::string ret1 ;ret1 = bit::to_string ( 1234 );return 0 ;}// 运行结果:// string(string&& s) -- 移动语义// string& operator=(string&& s) -- 移动语义
这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象
接收,编译器就没办法优化了。
bit::to_string
函数中会先用
str
生成构造生成一个临时对象,但是
我们可以看到,编译器很聪明的在这里把
str
识别成了右值,调用了移动构造。然后在把这个临时
对象做为
bit::to_string
函数调用的返回值赋值给
ret1
,这里调用的移动赋值。
STL
中的容器都是增加了移动构造和移动赋值:
字符串::字符串 - C++ 参考 (cplusplus.com)
vector::vector - C++ 参考 (cplusplus.com)
右值引用引用左值及其一些更深入的使用场景分析
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能
真的需要用右值去引用左值实现移动语义。
当需要用右值引用引用一个左值时,可以通过
move
函数将左值转化为右值
。
C++11
中,
std::move()
函数
位于 头文件中,该函数名字具有迷惑性,
它
并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义
。
template < class _Ty >inline typename remove_reference < _Ty > :: type && move ( _Ty && _Arg ) _NOEXCEPT{// forward _Arg as movablereturn (( typename remove_reference < _Ty > :: type && ) _Arg );}
int main (){bit::string s1 ( "hello world" );// 这里 s1 是左值,调用的是拷贝构造bit::string s2 ( s1 );// 这里我们把 s1 move 处理以后 , 会被当成右值,调用移动构造// 但是这里要注意,一般是不要这样用的,因为我们会发现 s1 的// 资源被转移给了 s3 , s1 被置空了。bit::string s3 ( std::move ( s1 ));return 0 ;}
STL容器插入接口函数也增加了右值引用版本:
list::p ush_back - C++参考 (cplusplus.com)
vector::p ush_back - C++参考 (cplusplus.com)
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) -- 移动语义
完美转发
函数模板中的
&&
万能引用
(将T&&看成一个整体)
// 函数模版里面,这里可以叫万能引用
// 实参传左值,就推成左值引用
// 实参传右值,就推成右值引用
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 );}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 ;}
说明:右值引用属性是左值,因为它可以取地址
解决方法:
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 ( std::forward < T > ( 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 ;}
完美转发实际中的使用场景:
template<class T>
struct ListNode
{
ListNode* _next = nullptr;
ListNode* _prev = nullptr;
T _data;
};
template<class T>
class List
{
typedef ListNode<T> Node;
public:
List()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
void PushBack(T&& x)
{
//Insert(_head, x);
Insert(_head, std::forward<T>(x));
}
void PushFront(T&& x)
{
//Insert(_head->_next, x);
Insert(_head->_next, std::forward<T>(x));
}
void Insert(Node* pos, T&& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = std::forward<T>(x); // 关键位置
// prev newnode pos
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
void Insert(Node* pos, const T& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = x; // 关键位置
// prev newnode pos
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
private:
Node* _head;
};
int main()
{
List<bit::string> lt;
lt.PushBack("1111");
lt.PushFront("2222");
return 0;
}
确保参数层层传递的过程中保持右值属性!!!不然传一层就成左值了
lambda表达式
C++98
中的一个例子
在
C++98
中,如果想要对一个数据集合中的元素进行排序,可以使用
std::sort
方法。
#include <algorithm>#include <functional>int main (){int array [] = { 4 , 1 , 8 , 5 , 3 , 7 , 0 , 9 , 2 , 6 };// 默认按照小于比较,排出来结果是升序std::sort ( array , array + sizeof ( array ) / sizeof ( array [ 0 ]));// 如果需要降序,需要改变元素的比较规则std::sort ( array , array + sizeof ( array ) / sizeof ( array [ 0 ]), greater < int > ());return 0 ;}
如果待排序元素为自定义类型,需要用户定义排序时的比较规则:
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 ;}};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 ());}
随着
C++
语法的发展,
人们开始觉得上面的写法太复杂了,每次为了实现一个
algorithm
算法,
都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,
这些都给编程者带来了极大的不便
。因此,在
C++11
语法中出现了
Lambda
表达式。
lambda表达式例子
例1:
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
//方法一:
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;
}
上述代码就是使用
C++11
中的
lambda
表达式来解决,可以看出
lambda
表达式实际是一个匿名函
数。
例2:
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表达式语法
lambda
表达式书写格式:
[capture-list] (parameters) mutable -> return-type { statement
}
1. lambda
表达式各部分说明
[capture-list] :
捕捉列表
,该列表总是出现在
lambda
函数的开始位置,
编译器根据
[]
来
判断接下来的代码是否为
lambda
函数
,
捕捉列表能够捕捉上下文中的变量供
lambda
函数使用
。
(parameters)
:参数列表。与
普通函数的参数列表一致
,如果不需要参数传递,则可以连同()
一起省略。
mutable
:默认情况下,
lambda
函数总是一个
const
函数,
mutable
可以取消其常量性。使用该修饰符时,参数列表不可省略(
即使参数为空
)
。
->returntype
:返回值类型
。用
追踪返回类型形式声明函数的返回值类型
,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推
导
。
{statement}
:函数体
。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意:
在
lambda
函数定义中,
参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为
空
。因此
C++11
中
最简单的
lambda
函数为:
[]{}
;
该
lambda
函数不能做任何事情。