简介
在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于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++98中{}的初始化问题
- 在C++98中,标准允许使用花括号{}对数组元素进行统一的列表初始值设定。比如
int array1[] = {1,2,3,4,5}; int array2[5] = {0};
对于一些自定义的类型,却无法使用这样的初始化。比如:
vector v{1,2,3,4,5};
就无法通过编译,导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常不方便。C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
内置类型的列表初始化
注意:列表初始化可以在{}之前使用等号,其效果与不使用=没有什么区别。
// 内置类型变量 int x1 = { 10 }; int x2{ 10 }; int x3 = 1 + 2; int x4 = { 1 + 2 }; int x5{ 1 + 2 }; // 数组 int arr1[5]{ 1,2,3,4,5 }; int arr2[]{ 1,2,3,4,5 }; // 动态数组,在C++98中不支持 int* arr3 = new int[5]{ 1,2,3,4,5 }; // 标准容器 vector v{ 1,2,3,4,5 }; map<int, int> m{ {1,1}, {2,2,},{3,3},{4,4} }; return 0;
自定义类型的列表初始化
class Point { public: Point(int x = 0, int y = 0): _x(x), _y(y) {} private: int _x; int _y; }; int main() { Pointer p1={ 1, 2 };//隐式类型转换(相近类型) Pointer p{ 1, 2 }; return 0; }
- 多个对象的列表初始化
多个对象想要支持列表初始化,需给该类(模板类)添加一个带initializer_list类型参数的构造函数即可。注意:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器以及获取区间中元素个数的方法size()。
#include <initializer_list> template class Vector { public: // … Vector(initializer_list l): _capacity(l.size()), _size(0) { _array = new T[_capacity]; for(auto e : l) _array[_size++] = e; } Vector& operator=(initializer_list l) { delete[] _array; size_t i = 0; for (auto e : l) _array[i++] = e; return this; } // … private: T _array; size_t _capacity; size_t _size; };
变量类型推导
为什么需要类型推导
在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂,比如:
short a = 32670; short b = 32670; // c如果给成short,会造成数据丢失,如果能够让编译器根据a+b的结果推导c的实际类型,就不会存 在问题 short c = a + b;
int main() { std::map<std::string, std::string> m{ {“apple”, “苹果”}, {“banana”,“香蕉”} }; // 使用迭代器遍历容器, 迭代器类型太繁琐 //std::map<std::string, std::string>::iterator it = m.begin(); //可以使用auto推导降低代码长度 auto it = m.begin(); //auto推导是把双刃剑,如果不知道返回对象的类型,会降低代码的可读性 while (it != m.end()) { cout << it->first << " " << it->second << endl; ++it; } }
C++11中,可以使用auto来根据变量初始化表达式类型推导变量的实际类型,可以给程序的书写提供许多方便。将程序中c与it的类型换成auto,程序可以通过编译,而且更加简洁。
decltype类型推导
为什么需要decltype
auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就无能为力。
template<class T1, class T2> T1 Add(const T1& left, const T2& right) { return left + right; }
如果能用加完之后结果的实际类型作为函数的返回值类型就不会出错,但这需要程序运行完才能知道结果的 实际类型,即RTTI(Run-Time Type Identification 运行时类型识别)。 C++98中确实已经支持RTTI:
cout << typeid(it).name() << endl;
获取it的类型 
- dynamic_cast只能应用于含有虚函数的继承体系中
运行时类型识别的缺陷是降低程序运行的效率。
如果定义一个和it类型相同的对象:
auto it1=it decltype(it) copy; //两个都是定义一个和it类型相同的对象 auto是编译时,通过初始化对象推导 //decltype是编译时通过对象推导 void fun(auto it);//这里定义错误,auto不能做函数参数类型,不能做返回值类型 //auto需要编译时推导类型 语法层概念 //底层实现是编译成指令需要先建立占帧
默认成员函数控制
在C++中对于空类编译器会生成一些默认的成员函数,比如:构造函数、拷贝构造函数、运算符重载、析构函数和&和const&的重载、移动构造、移动拷贝构造等函数。如果在类中显式定义了,编译器将不会重新生成默认版本。有时候这样的规则可能被忘记,最常见的是声明了带参数的构造函数,必要时则需要定义不带参数的版本以实例化无参的对象。而且有时编译器会生成,有时又不生成,容易造成混乱,于是C++11让程序员可以控制是否需要编译器生成。
显式缺省函数
一般针对的是构造函数 在C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版本,用=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; int main() { A a1(10); A a2; a2 = a1; return 0; }
如果没有 A() = default;这个在定义a2变量就出报错,用=default修饰的函数称为显式缺省函数。
删除默认函数
C++98 防拷贝/防复制-》只声明不实先(防止生成默认的)+声名私有(防止在类外定义) 一般针对的是拷贝构造函数和赋值 如果能想要限制某些默认函数的生成,在C++98中,是该函数设置private,并且不给定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class A { public: // 显式缺省构造函数,由编译器删除 A() = delete; private: int _a; }; int main() { A a2; return 0; }
 编译器删除了无参构造,使得a2无法定义。
class A { public: A(int a): _a(a) {} // 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载 A(const A&) = delete; A& operator(const A&) = delete; private: int _a; }; int main() { A a1(10); // 编译失败,因为该类没有拷贝构造函数 //A a2(a1); // 编译失败,因为该类没有赋值运算符重载 A a3(20); a3 = a2; return 0; }
右值引用
右值引用概念
C++98中提出了引用的概念,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通过指针来实现的,因此使用引用,可以提高程序的可读性。
void Swap(int& left, int& right) { int temp = left; left = right; right = temp; } int main() { int a = 10; int b = 20; Swap(a, b); }
为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用。
int Add(int a, int b) { return a + b; } int main() { const int&& ra = 10; // 引用函数返回值,返回值是一个临时变量,为右值 int&& rRet = Add(10, 20); return 0; }
为了与C++98中的引用进行区分,C++11将该种方式称之为右值引用
左值与右值
左值与右值是C语言中的概念,但C标准并没有给出严格的区分方式,一般认为:可以放在=左边的,或者能够取地址的称为左值,只能放在=右边的,或者不能取地址的称为右值,但是也不一定完全正确。
int & 左值引用 int && 右值引用 左值:可以放到‘=’的值,一般可以修改 右值:一般不可以修改的一些值。比如:常量,临时变量、表达式返回值… 引用就是给对象取别名 左值引用一般理解成左值取名 右值引用一般理解成右值取名
int main() { int a = 0; int& a1 = a;//左值引用一般给左值取别名 const int&a2 = int(1);//const 左值引用也可以给右值取别名 //右值引用不可修改所以左值加上const 就变成不可修改的,就可以给右值取别名 int&& a1 = int(0);//右值引用一般给右值取别名 int&& a2 = move(a);//右值引用不能直接给左值取别名,但是加上move()可以给取别名。 //左值引用修改 而右值不可修改,所以右值引用必须使用move给左值取别名 return 0; }
int g_a = 10; // 函数的返回值结果为引用 int& GetG_A() { return g_a; } int main() { int a = 10; int b = 20; // a和b都是左值,b既可以在=的左侧,也可在右侧, // 说明:左值既可放在=的左侧,也可放在=的右侧 a = b; b = a; const int c = 30; // 编译失败,c为const常量,只读不允许被修改 //c = a; // 因为可以对c取地址,因此c严格来说不算是左值 cout << &c << endl; // 编译失败:因为b+1的结果是一个临时变量,没有具体名称,也不能取地址,因此为右值 //b + 1 = 20; GetG_A() = 100; return 0; }
因此关于左值与右值的区分不是很好区分,一般认为:
- 普通类型的变量,因为有名字,可以取地址,都认为是左值。
- const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是
const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间), C++11认为其是左值。 - 如果表达式的运行结果是一个临时变量或者对象,认为是右值。
- 如果表达式运行结果或单个变量是一个引用则认为是左值。
总结: - 不能简单地通过能否放在=左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质
判断,比如上述:c常量 - 能得到引用的表达式一定能够作为引用,否则就用常引用。
C++11对右值进行了严格的区分: C语言中的纯右值,比如:a+b, 100 将亡值。比如:表达式的中间结果、函数按照值的方式进行返回。
引用与右值引用比较
在C++98中的普通引用与const引用在引用实体上的区别:
int main() { // 普通类型引用只能引用左值,不能引用右值 int a = 10; int& ra1 = a; // ra为a的别名 //int& ra2 = 10; // 编译失败,因为10是右值 const int& ra3 = 10; const int& ra4 = a; return 0; }
注意: 普通引用只能引用左值,不能引用右值,const引用既可引用左值,也可引用右值。 C++11中右值引用:只能引用右值,一般情况不能直接引用左值。
int main() { // 10纯右值,本来只是一个符号,没有具体的空间, // 右值引用变量r1在定义过程中,编译器产生了一个临时变量,r1实际引用的是临时变量 int&& r1 = 10; r1 = 100; int a = 10; int&& r2 = a; // 编译失败:右值引用不能引用左值 return 0; }
问题:既然C++98中的const类型引用左值和右值都可以引用,那为什么C++11还要复杂的提出右值引用呢?
值的形式返回对象的缺陷
如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将 会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值,就会出错,比如:
class String { public: String(const char* str =" ") { if (nullptr == str) str = “”; _str = new char[strlen(str) + 1]; strcpy(_str, str); } String(const String& s) : _str(new char[strlen(s._str) + 1]) { cout << “String(const String& s)” << endl; strcpy(_str, s._str); } String& operator=(const String& s) { cout << “String& operator=(const String& s)” << endl; if (this != &s) { char* pTemp = new char[strlen(s._str) + 1]; strcpy(pTemp, s._str); delete[] _str; _str = pTemp; } return this; } String& operator+=(const String& s) { cout << “String& operator+=(const String& s)” << endl; return this; } String operator+(const String& s) { cout << “String operator+(const String& s)” << endl; char pTemp = new char[strlen(_str) + strlen(s._str) + 1]; strcpy(pTemp, _str); strcpy(pTemp + strlen(_str), s._str); String strRet(pTemp); return strRet; } ~String() { if (_str) delete[] _str; } private: char _str; }; int main() { String s1(“hello”); String s2(“world”); String s3 = s1 + s2; //编译器优化本来应该是两次拷贝构造,编译器可能优化成一次 /String s4; s4 = s1 += s2;/ String s5; s5 = s1 + s2; return 0; }
结论:传值返回,会多一次拷贝(这次拷贝有可能会被优化) 传1左值引用返回,会少一次拷贝构造 左值引用的作用: <1>函数引用传参 – 减少拷贝+输出型参数 <2>函数引用传返回值–减少拷贝 核心作用:提高效率+提高程序可读性(不需要用复杂指针)
左值引用决绝效率问题的盲区:有些函数不用传引用返回 
在operator+中:strRet在按照值返回时,必须创建一个临时对象,临时对象创建好之后,strRet就被销毁 了,最后使用返回的临时对象构造s3,s3构造好之后,临时对象就被销毁了。仔细观察会发现:strRet、临 时对象、s3每个对象创建后,都有自己独立的空间,而空间中存放内容也都相同,相当于创建了三个内容完 全相同的对象,对于空间是一种浪费,程序的效率也会降低,而且临时对象确实作用不是很大,那能否对该 种情况进行优化呢?
移动语义
C++11提出了移动语义概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解该问题
右值引用来解决上面的问题,如何解决? 
在C++11中如果需要实现移动语义,必须使用右值引用。上述String类增加移动构造:
//拷贝构造–深拷贝 String(const String& s) : _str(new char[strlen(s._str) + 1]) { cout << “String(const String& s)” << endl; strcpy(_str, s._str); } //移动拷贝–浅拷贝 //纯右值 和 将亡值 String(String&& s) :_str(nullptr) { swap(_str, s._str); }
因为strRet对象的生命周期在创建好临时对象后就结束了,即将亡值,C++11认为其为右值,在用strRet构造临时对象时,就会采用移动构造,即将strRet中资源转移到临时对象中。而时对象也是右值,因此在用临时对象构造s3时,也采用移动构造,将临时对象中资源转移到s3中,整个过程,只需要创建一块堆内存即可,既省了空间,又大大提高程序运行的效率。 注意:
- 移动构造函数的参数千万不能设置成const类型的右值引用,因为资源无法转移而导致移动语义失效。
- 在C++11中,编译器会为类默认生成一个移动构造,该移动构造为浅拷贝,因此当类中涉及到资源管理
时,用户必须显式定义自己的移动构造。 
class String { public: String(const char* str =" “) { if (nullptr == str) str = “”; _str = new char[strlen(str) + 1]; strcpy(_str, str); } //拷贝构造–深拷贝 String(const String& s) : _str(new char[strlen(s._str) + 1]) { cout << “String(const String& s)” << endl; strcpy(_str, s._str); } //移动拷贝–浅拷贝 //纯右值 和 将亡值 String(String&& s) :_str(nullptr) { swap(_str, s._str); } //移动复制–浅拷贝 //s2=string(”***"); String& operator=(const String&& s) { swap(_str,s._str); return this; } String& operator=(const String& s) { cout << “String& operator=(const String& s)” << endl; if (this != &s) { char pTemp = new char[strlen(s._str) + 1]; strcpy(pTemp, s._str); delete[] _str; _str = pTemp; } return this; } String& operator+=(const String& s) { cout << “String& operator+=(const String& s)” << endl; return this; } String operator+(const String& s) { cout << “String operator+(const String& s)” << endl; char pTemp = new char[strlen(_str) + strlen(s._str) + 1]; strcpy(pTemp, _str); strcpy(pTemp + strlen(_str), s._str); String strRet(pTemp); return strRet; } ~String() { if (_str) delete[] _str; } private: char _str; }; int main() { String s1(“hello”); String s2(“world”); String s3 = s1 + s2; String s4 = s1 + s2; //编译器优化本来应该是两次拷贝构造,编译器可能优化成一次 /String s4; s4 = s1 += s2;/ String s5; s5 = s1 + s2; return 0; } 
右值引用借用移动构造和移动赋值,区分出传值返回时,返回的是右值(将忘值) 移动构造和移动赋值,浅拷贝,转移将亡值的资源,减少了深拷贝,提高了效率
右值引用做参数
vector v; v.push_back(“lihua”);//void push_back(_Ty&& _Val) string str(“li”); v.push_back(str);// void push_back(const _Ty& _Val)  字符串”lihua“会形成string临时变量当作将忘值,通过移动构造插入到v里边,而字符串str是通过拷贝构造插入到v里面去。
右值引用引用左值
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
vector v; string str(“li1111111”); v.push_back(str); string str2(“li1111jdsahdjjdsa111”); v.push_back(move(str2)); return 0;
 字符串str是左值通过深拷贝插入到v当中,不会变成空字符串,而str2通过move()变成了右值引用,通过移动构造str2中的资源转移到v中,而str2变成无效字符串
template inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT { // forward _Arg as movable return ((typename remove_reference<_Ty>::type&&)_Arg); }
注意:
- 被转化的左值,其生命周期并没有随着左值的转化而改变,即std::move转化的左值变量lvalue不会被销
毁。 - STL中也有另一个move函数,就是将一个范围中的元素搬移到另一个位置。
class Person { public: Person(char* name, char* sex, int age) : _name(name) , _sex(sex) , _age(age) {} Person(const Person& p) : _name(p._name) , _sex(p._sex) , _age(p._age) {} #if 0 Person(Person&& p) : _name(p._name) , _sex(p._sex) , _age(p._age) {} #else Person(Person&& p) : _name(move(p._name)) , _sex(move(p._sex)) , _age(p._age) {} #endif private: String _name; String _sex; int _age; }; Person GetTempPerson() { Person p(“prety”, “male”, 18); return p; } int main() { Person p(GetTempPerson()); return 0; }
同一个接口函数,通过左值引用和右值引用区分出左值和右值,价值是什么? 当T是内置类型的时候没有什么区别 当T是自定义类型,还需要细分 a.当T是data的普通类,不涉及深拷贝没有什么区别 b.当T是string,vector list等需要深拷贝价值恒大,自定义类的右值又叫做将忘值,他的生命周期快到了,。那我们可以使用移动构造/移动赋值,转移走他的资源,就不需要做深拷贝,效率得到提升
C++11不仅仅增加了unorder_XXX的新容器 对已有的string vector list等等也增加移动构造/移动赋值,价值非常大,进一步提高了效率
|