C++11 新特性
# C++11 新特性
# 统一的初始化方法
在C++11中可以直接在变量名后加上初始化列表来对对象进行初始化。
在此之前,C++中的初始化方式很多,有初始化列表、拷贝初始化和直接初始化等,例如:
class A{
public:
int a; int b;
A(int _a, int_b):a(_a), b(_b){}
A(const A& obja){
a = obja.a; b = obja.b;
}
};
int main(){
int arr[3] = {1,2,3}; // 初始化列表
A a(1,2); // 直接初始化
A a2(a); // 拷贝初始化
return 0;
}
这些不同的初始化方法,都有各自的适用范围和作用。最关键的是,这些种类繁多的初始化方法,没有一种可以通用所有情况。为了统一初始化方式,并且让初始化行为具有确定的效果,C++11 中提出了初始化列表(List-initialization)的概念。
这种方式扩大了原先的初始化列表的初始化方式的适用范围,在C++11中任何类型的对象都可以采用这种初始化方式,例如:
int main(){
int arr[3]{1, 2, 3};
vector<int> iv{1, 2, 3};
map<int, string> mp{{1, "a"}, {2, "b"}};
string str{"Hello World"};
int* p = new int[20]{1,2,3}; // 动态数组使用初始化列表
A a{1,2};
A* a1 = new A{1,2}; // new一个临时对象,然后荣过拷贝构造函数初始化a1
}
// 初始化列表作为函数返回值
A func(int m, int n){
return {m,n};
}
# auto关键字
C++11 中 auto 关键字被用来做自动类型变量推导。也就是说,使用了 auto 关键字以后,编译器会在编译期间自动推导出变量的类型,不再需要手动指明变量的数据类型,值得注意的是:auto的自动类型推导是根据变量的右值推到出变量的类型的,所以使用auto关键字时需要对变量进行初始化。简单使用示例如下:
auto i = 100; // i 是 int
auto p = new A(); // p 是 A *
auto k = 34343LL; // k 是 long long
auto *p = &i, j=100; // 连续定义多个变量,但是auto在推导的时候不能有二义性,即i和j的类型应该保持一致
使用auto定义STL迭代器
auto 的一个典型应用场景是用来定义 STL 的迭代器,使用迭代器遍历容器时,需要编写复杂冗长的容器类型,而使用auto关键字可以大大简化这一场景:
void printMap(map<string,int,greater<string> > mp){
for( auto i = mp.begin(); i != mp.end(); ++i){
cout << i->first << "," << i->second ;
}
}
在上述例子中如果不使用auto关键字定义迭代器auto i = mp.begin();
,那么就需要写全该迭代器类型map<string,int,greater<string> >::iterator i = mp.begin();
。而是用auto就可以直接通过mp.begin()
的返回值类型来推导出迭代器i
的类型。
auto用于泛型编程
在泛型编程中,往往不清楚变量的具体类型,有些情况下需要不具体指明变量的类型到达更加灵活编程的目的,而auto关键字就为这种需求提供了可能,例如:
class A { };
A operator + ( int n,const A & a){ // 重载+运算符,用具计算 int+A 的情况
return a;
}
//模板函数 实现两对象相加 函数的返回值类型auto推到 而decltype关键字用于推到出表达式的类型
template <typename T1, typename T2>
auto add(T1 x, T2 y) -> decltype(x + y){
return x+y;
}
int main(){
auto ans1 = add(100,1.5); // ans1 double类型
auto ans2 = add(100,A()); // ans2 A类型
}
上述例子中 ans1
是 double 类型因为其值为101.5;而 ans2
是A类型,模板函数 add 的函数体内是x+y
,+
运算符又被重载过,通过计算返回值是A()
创建的A类型的临时对象。
# decltype关键字
decltype(declare type) 声明类型,和auto关键字一样,decltype 关键字也被用来自动类型推导。和 auto 关键字 根据=
右边的初始值 value 推导出变量的类型不同,decltype 关键字根据表达式推导出变量的类型,该表达式可以是任意复杂的形式,但是必须保证表达式的结果是有类型的,不可以是void。decltype 使用示例如下:
int main(){
int i;
double t;
struct A { double x; };
const A* a = new A();
decltype(a) x1; // x1 is A *
decltype(i) x2; // x2 is int
decltype(a->x) x3; // x3 is double
decltype((a->x)) x4 = t; // x4 is double&
}
decltype 自动推导类型主要按照一下三条规则:
- 如上例中的
x1, x2, x3
,如果decltype声明的表达式是普通变量、一般表达式或者是类成员访问表达式,其推导结果与表达式类型一致 - 如auto关键字中的模板函数示例中,如果decltype用于声明函数调用类型,则其推导结果与函数返回值的类型一致
- 如上例中的
x4
,如果decltype声明的表达式是一个左值,或者被()
括号括起,那么其推导结果为该表达式类型的引用
# 返回类型后置
返回类型后置语法是将 decltype 和 auto 结合起来完成返回值类型的推导。这种语法的提出是为了解决数返回值类型依赖于参数而导致难以确定返回值类型的问题,例如 auto 关键字中的add
模板函数示例:
template <typename T1, typename T2>
auto add(T1 x, T2 y) -> decltype(x + y){
return x+y;
}
auto func() -> decltype(exp)
就是返回类型后置语法,如果不采用这种语法,该函数的实现就会变得很复杂:
template <typename T1, typename T2>
decltype(x + y) add(T1 x, T2 y){ // 这种写法是明显错误的,因为decltype(x + y)中的x,y还未定义
return x+y;
}
template <typename T1, typename T2>
decltype(T1() + T2()) add(T1 x, T2 y){ // 这种写法的前提是T1,T2类均有无参构造函数
return x+y;
}
总的来说,使用返回类型后置语法在一些特殊场景中,能够更简洁明了的描述出函数返回值的类型推导。
# 右值引用与移动构造函数
# 左值和右值的基本概念
左值和右值:左值是指那些在表达式执行结束后依然存在的数据,也就是持久性的数据;右值是指那些在表达式执行结束后不再存在的数据,也就是临时性的数据。有一种很简单的方法来区分左值和右值,对表达式取地址,如果编译器不报错就为左值,否则为右值。简而言之:有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。例如:
int a = 5;
int b = a; // a,b均为左值
5 = a; // 错误,5是右值
# 右值引用
在之前的C++引用中通常指的是左值引用,即允许使用常量左值引用操作右值,但不支持为右值建立非常量左值引用,例如:
class A{};
int main(){
A a = A();
A& b = a; // 左值引用
A& c = A(); // 错误,A()是无名变量,是右值
A&& c = A(); // 右值引用
}
为此,C++11中提出了右值引用使用&&
表示,和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化。提出右值引用的主要目的是提高程序运行的效率,减少需要进行深拷贝的对象进行深拷贝的次数。
# 移动构造函数
C++通常使用拷贝构造函数初始化一个同类新对象,而当类中拥有指针类型的成员变量时,拷贝构造函数中需要以深拷贝(而非浅拷贝)的方式复制该指针成员,这将导致严重的深拷贝效率问题 (opens new window)
为此C++11中采用右值引用提出了移动构造函数,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。通俗的讲,移动构造函数就是将其他对象(通常是临时对象)拥有的内存资源移为已用。对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。
当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。所以,通常在类中自定义移动构造函数的同时,会再为其自定义一个适当的拷贝构造函数,由此当用户利用右值初始化类对象时,会调用移动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数。
移动构造函数的调用时机是:用同类的右值对象初始化新对象
# move函数
默认情况下,左值初始化同类对象只能通过拷贝构造函数完成,如果想调用移动构造函数,则必须使用右值进行初始化。为了能够使用左值初始化同类对象时也通过移动构造函数完成,C++11提出了move函数:它可以将左值强制转换成对应的右值,由此便可以使用移动构造函数。
move函数语法为move(a)
,其中 a 表示指定的左值对象,该函数会返回 a 对象的右值形式。
一个移动构造函数使用示例:
#include <iostream>
#include <string>
#include <cstring>
using namespace std;
class String
{
public:
char * str;
// 构造函数
String():str(new char[1]) { str[0] = 0;}
String(const char * s) {
str = new char[strlen(s)+1];
strcpy(str,s);
}
// 拷贝构造函数
String(const String & s) {
cout << "copy constructor called" << endl;
str = new char[strlen(s.str)+1];
strcpy(str,s.str);
}
// 重载赋值运算符拷贝构造函数
String & operator=(const String & s) {
cout << "copy operator= called" << endl;
if( str != s.str) {
delete [] str;
str = new char[strlen(s.str)+1];
strcpy(str,s.str);
}
return * this;
}
// 移动构造函数
String(String && s):str(s.str) { // String && s 右值引用
cout << "move constructor called"<<endl;
s.str = new char[1];
s.str[0] = 0;
}
// 重载赋值运算符移动构造函数
String & operator = (String &&s) {
cout << "move operator= called"<<endl;
if (str!= s.str) {
delete [] str;
str = s.str;
s.str = new char[1];
s.str[0] = 0;
}
return *this;
}
// 析构函数
~String() { delete [] str; }
};
template <typename T>
void moveSwap(T& a, T& b){
T tmp(move(a)); // 直接构造 std::move(a) 为右值,move constructor called
a = move(b); // 赋值号拷贝构造 move(b) 为右值,move operator= called
b = move(tmp); // 赋值号拷贝构造 move(tmp) 为右值,move operator= called
}
int main(){
String s;
s = String("ok"); // String("ok")是右值
String && r = String("this"); // r 为String("this")右值引用
cout << r.str << endl;
String s1 = "hello",s2 = "world";
moveSwap(s1,s2);
cout << s1.str << ',' << s2.str << endl;
return 0;
}
/* Output:
move operator= called
this
move constructor called
move operator= called
move operator= called
world,hello
*/
# 智能指针 shared_ptr
大部分面向对象的程序语言中都有垃圾回收机制,而 C++ 中一直缺乏这种友好的内存管理机制。这也带来了很多内存资源管理不当的问题,例如:
- 野指针-指向了内存资源已经被释放的空间并被继续使用;
- 重复释放内存-内存资源在已经被释放的情况下,被试图再次释放导致程序崩溃;
- 内存泄漏-没有及时释放不再使用的内存资源,致使程序运行过程中占用的内存资源不断累加,最终导致程序崩溃。
C++11中提出了 shared_ptr, unique_ptr, weak_ptr
三种智能指针用于实现堆内存的自动回收,这种智能指针和普通指针的用法是相似的,不同之处在于,智能指针可以在适当时机自动释放分配的内存,而这种机制将有效避免内存泄漏的问题。
C++ 智能指针底层是采用引用计数的方式实现的。简单的理解,智能指针在申请堆内存空间的同时,会为其配备一个整形值(初始值为 1),每当有新对象使用此堆内存时,该整形值 +1;反之,每当使用此堆内存的对象被释放时,该整形值减 1。当堆空间对应的整形值为 0 时,即表明不再有对象使用它,该堆空间就会被释放掉。
此处介绍相对较为常用的shared_ptr
指针,和其他智能指针一样,shared_ptr
也是以类模板的方式定义在<memory>
头文件中的,并位于 std 命名空间中。
通过shared_ptr
的构造函数,可以让shared_ptr
对象托管一个new运算符返回的指针,写法如下:
shared_ptr<T> ptr(new T); // T 表示指针指向的具体数据类型
声明智能指针ptr
后,该指针就可以像普通T*
类型的指针一样来使用,即可以使用*ptr
取new动态分配的那个对象,同时也不必操心使用完后需要主动delete
释放内存。
值得注意的是,多个shared_ptr
对象可以同时托管一个指针,系统会维护一个引用计数,如果有shared_ptr
对象不再托管该指针,则将引用计数减 1,通过这种方式达到不影响其他指向同一指针 shared_ptr
对象的目的。只有引用计数为 0 时,即没有任何shared_ptr
对象托管该指针时,delete该指针,其对应的堆内存才会被自动释放。
一个智能指针的使用示例:
#include <memory>
#include <iostream>
using namespace std;
struct A {
int n;
A(int v = 0):n(v){ }
~A() { cout << n << " destructor" << endl;
};
int main()
{
shared_ptr<A> sp1(new A(2)); //sp1托管A(2)
shared_ptr<A> sp2(sp1); //sp2与sp1共同托管A(2)
cout << sp1->n << "," << sp2->n << endl;
A* p = sp1.get(); //p 指向 A(2)
cout << p->n << endl;
shared_ptr<A> sp3;
sp3 = sp1; //sp3也托管 A(2)
cout << (*sp3).n << endl; // 使用.号运算符取对象成员
// 使用reset()函数重置shared_ptr托管的指针
sp1.reset(); //sp1放弃托管 A(2)
if( !sp1 )
cout << "sp1 is null" << endl;
A * q = new A(3);
sp1.reset(q); // sp1托管q
cout << sp1->n << endl;
// 使用shared_ptr的一种常见错误
shared_ptr<A> sp4;
sp4.reset(q); // 这中托管方式并不会增加对指针q的引用计数,在程序结束时由于sp1和sp4的共同指向导致多次尝试释放q所指向的内存空间
// 验证引用指针计数为0时,shared_ptr托管的指针指向的空间被自动释放
shared_ptr<A> sp5(sp1); // sp5也托管q A(3)
sp1.reset(); //sp1放弃托管 q
cout << "before end main" << endl;
sp5.reset(); //sp5放弃托管 q,A(3)的引用计数为0,被自动释放,调用析构函数
cout << "end main" << endl;
return 0; //程序结束,会delete掉 A(2) 调用析构函数
/* 验证引用指针计数部分的输出为
before end main
3 destructor
end main
2 destructor
*/
}
# 空指针 nullptr
野指针往往没有明确的指向,这将极有可能导致程序发生异常。而避免产生野指针最为有效的方法就是在定义指针的同时完成初始化操作,而对于那些指向尚未明确的指针,就需要将其初始化为空指针。
通常情况下都使用NULL
对指针进行初始化,而该关键字在C++中其实就是一个事先定义号的宏#define NULL 0
,可以看出NULL
被定义为一个字面常量0,在一些特殊情况下,这将带来一定的缺陷导致程序运行错误。
为了进一步完善对指针的初始化,C++11中提出了nullptr
空指针关键字,nullptr
是 nullptr_t
类型的右值常量,专用于初始化空类型指针。nullptr_t
被称为指针空值类型,而 nullptr
则是该类型的一个实例,nullptr 可以被隐式转换成任意的指针类型,但是它无法隐式转换为整型,即语句int i = nullptr;
将导致错误。
总的来说,使用 nullptr
初始化空指针可以让程序更加健壮,但是仍然兼容 NULL
关键字。
一个空指针 nullptr
的使用示例:
#include <memory>
#include <iostream>
using namespace std;
int main()
{
int* p1 = NULL;
int* p2 = nullptr;
shared_ptr<double> p3 = nullptr;
if( p1 == p2)
cout << "equal 1" <<endl;
if( p3 == nullptr)
cout << "equal 2" <<endl;
if( p3 == NULL)
cout << "equal 3" <<endl;
bool b = nullptr; // nullptr 可以被隐式转换为bool类型 b = false
return 0;
}
# 基于范围的 for 循环
为了更加便捷地遍历 STL 容器,C++11中为 for 循环添加了一种全新的语法格式-基于范围的for循环:
for (declaration : expression){
//循环体
}
- declaration:表示此处要定义一个变量,该变量的类型为要遍历序列中存储元素的类型。需要注意的是,C++ 11 标准中,declaration 参数处定义的变量类型可以用 auto 关键字表示,使用该关键字让编译器自行推导出变量的数据类型。
- expression:表示要遍历的序列,常见的有普通数组或者 STL 容器等,还可以是用
{}
大括号初始化的序列。
与传统的 for 循环语法规则相比较,可以看出基于范围的 for 循环语法没有明确限定遍历范围,而只会逐个遍历 序列中的每个元素,直到全部遍历完成结束。
在使用基于范围的 for 循环时,值得注意的是:
- 可以使用 auto关键字声明变量,自动推导数据类型
- 可以使用 引用形式的变量,在遍历序列的过程中修改器内部元素的值
一个基于范围的 for 循环使用示例如下:
#include <iostream>
#include <vector>
using namespace std;
struct A {
int n;
A(int i):n(i) {}
};
int main() {
// 遍历普通数组
int ary[] = {1,2,3,4,5};
for(int e : ary)
cout << e << ",";
cout << endl;
// 遍历并修改数组元素
for(int & e: ary)
e*= 10;
// 遍历STL容器
vector<A> st(ary,ary+5);
for( auto & it: st) // 使用auto关键字
it.n *= 10;
for( A it: st)
cout << it.n << ",";
return 0;
}
# 无序容器
和关联容器一样,无序容器也使用键值对(pair 类型)的方式存储数据,但是他们在底层实现上有着本质上的不同:关联容器是采用红黑树结构实现的,而无序容器则是采用哈希表的存储结构实现的。
基于哈希表实现的无序容器,相较于关联容器有如下两个主要特点:
- 无序容器内部存储的键值对是无序的,各键值对的存储位置取决于该键值对中的键
- 由于采用哈希表的存储结构,无序容器在通过指定键查找对应的值的效率能够达到常数级别;而在遍历容器的使用场景下,无序容器的执行效率明显差于关联容器
C++11标准的 STL 中,在已经提供关联容器set/multiset, map/multimap
的情况下,又采用哈希表的存储结构对应新增了无序容器unordered_set/unordered_multiset, unordered_map/unordered_multimap
,提高了元素插入与查找的效率。
总的来说,无序容器和关联容器的使用方法一致,区别在于无序容器的无序型,以及其在查找应用场景中提供的高效率查找。
一个使用无序容器 unordered_map
的示例如下:
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
int main()
{
unordered_map<string,int> turingWinner;
// 使用 insert() 成员函数插入pair对象
turingWinner.insert(make_pair("Dijkstra",1972));
turingWinner.insert(make_pair("Scott",1976));
turingWinner.insert(make_pair("Wilkes",1967));
turingWinner.insert(make_pair("Hamming",1968));
// 使用 重载[]运算符的成员函数插入元素
turingWinner["Ritchie"] = 1983;
string name;
cin >> name;
// 使用 find() 成员函数查找指定元素
unordered_map<string,int>::iterator p = turingWinner.find(name);
if( p != turingWinner.end())
cout << p->second;
else
cout << "Not Found" << endl;
return 0;
}
# Lambda 表达式
就向一些临时变量一样,也存在临时函数的情况。有些简单函数或函数对象在整个程序中可能只需要被调用或使用一次。这样一次性的函数,如果为其单独声明函数或者编写一个类,可能降低程序的可读性。而C++11中提供的 Lambda 表达式提供了避免这一问题的方法,使用 Lambda 表达式构建匿名函数。
Lambda 表达式的简单语法格式如下,他和普通函数的唯一区别在于其没有名称用[外部变量]
代替其名称,即普通函数是func_name (parm_list) -> return_type{fucntion_body;}
而 Lambda表达式声明的匿名函数没有函数名称[extra_parm] (parm_list) -> return_type{fucntion_body;}
[外部变量访问方式说明符](参数表) -> 返回值类型
{
语句组;
}
外部变量:
[]
方括号用于向编译器表明当前是一个 lambda 表达式,类似于声明一个 lambda 表达式的关键字。在方括号内部,可以注明当前 lambda 函数的函数体中可以使用哪些外部变量,而外部变量指的是和当前 lambda 表达式位于同一作用域内的所有局部变量。外部变量的使用受以值还是以引用方式传递影响,[外部变量]
的几种常用定义方式如下外部变量格式 说明 []
不使用任何外部变量 [=]
只有一个 = 等号,表示以传值的形式使用所有外部变量 [&]
只有一个 & 符号,表示以引用形式使用所有外部变量 [x, &y]
x 以传值形式使用,y 以引用形式使用 [=,&x,&y]
x,y 以引用形式使用,其余变量以传值形式使用 [&,x,y]
x,y 以传值的形式使用,其余变量以引用形式使用 参数表:和普通函数的定义一样,lambda 匿名函数也可以接收外部传递的多个参数。和普通函数不同的是,如果不需要传递参数,可以连同
()
小括号一起省略返回值类型:在编写 lambda 表达式时,可以省略返回值类型,没有指定返回值类型则编译器自动推断其返回值类型
语句组(函数体):和普通函数一样,lambda 匿名函数包含的内部代码都放置在函数体中。该函数体内除了可以使用指定传递进来的参数之外,还可以使用指定的外部变量以及全局范围内的所有全局变量。需要注意的是,外部变量会受到以值传递还是以引用传递方式引入的影响,而全局变量则不会。换句话说,在 lambda 表达式内可以使用任意一个全局变量,必要时还可以直接修改它们的值
一个 Lambda 表达式的使用示例如下:
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main()
{
int x = 100,y=200,z=300;
// 不使用任何外部变量
cout << [ ](double a,double b) { return a + b; }(1.2,2.5) << endl;
// y,z以传引用的方式使用,x以传值方式使用
auto ff = [=,&y,&z](int n) {
cout << x << endl;
y++; z++;
return n*n;
};
cout << ff(15) << endl;
cout << y << "," << z << endl;
// 结合STL算法使用 Lambda 表达式,构造匿名函数对象
int a[4] = { 4,2,11,33};
sort(a,a+4,[ ](int x,int y) ->bool {
return x%10 < y%10;
});
// 结合STL容器和算法使用 Lambda 表达式
vector<int> a { 1,2,3,4};
int total = 0;
for_each(a.begin(),a.end(),[&](int & x){
total += x; x*=2;
});
cout << total << endl;
for_each(a.begin(),a.end(),[](int x){
cout << x << " ";
});
return 0;
}