在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 with classes | 类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符重载等 |
C++1.0 | 添加虚函数概念,函数和运算符重载,引用、常量等 |
C++2.0 | 更加完善支持面向对象,新增保护成员、多重继承、对象的初始化、抽象类、静态成员以及const成员函数 |
C++3.0 | 进一步完善,引入模板,解决多重继承产生的二义性问题和相应构造和析构的处理 |
C++98 | C++标准第一个版本,绝大多数编译器都支持,得到了国际标准化组织(ISO)和美国标准化协会认可,以模板方式重写C++标准库,引入了STL(标准模板库) |
C++03 | C++标准第二个版本,语言特性无大改变,主要:修订错误、减少多异性 |
C++05 | C++标准委员会发布了一份计数报告(Technical Report,TR1),正式更名C++0x,即:计划在本世纪第一个10年的某个时间发布 |
C++11 | 增加了许多特性,使得C++更像一种新语言,比如:正则表达式、基于范围for循环、auto关键字、新容器、列表初始化、标准线程库等 |
C++14 | 对C++11的扩展,主要是修复C++11中漏洞以及改进,比如:泛型的lambda表达式,auto的返回值类型推导,二进制字面常量等 |
C++17 | 在C++11上做了一些小幅改进,增加了19个新特性,比如:static_assert()的文本信息可选,Fold表达式用于可变的模板,if和switch语句中的初始化器等 |
C++20 | 制定ing |
在C++98中,标准允许使用花括号{}对数组元素进行统一的列表初始值设定。比如:
int array1[] = {1,2,3,4,5};
int array2[5] = {0};
对于一些自定义的类型,却无法使用这样的初始化。比如:
vector<int> v{1,2,3,4,5};
就无法通过编译,导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常不方便。C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
int main()
{
// 内置类型变量
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<int> 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 p{ 1, 2 };
return 0;
}
以vector为例(list等其他容器也是类似),其中有个构造函数是使用initializer_list
构造的。initializer_list
是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器以及获取区间中元素个数的方法size(),它只能使用花括号进行赋值。
std::initializer_list<int> list;
size_t n = list.size(); // n == 0
list = { 1, 2, 3, 4, 5 };
n = list.size(); // n == 5
list = { 3, 1, 2, 4 };
n = list.size(); // n == 4
容器支持花括号列表初始化,本质上是增加了一个initializer_list
的构造函数,initializer_list
支持接收一个花括号的列表。
#include
template<class T>
class Vector {
public:
// ...
Vector(initializer_list<T> l): _capacity(l.size()), _size(0)
{
_array = new T[_capacity];
for(auto e : l)
_array[_size++] = e;
}
Vector<T>& operator=(initializer_list<T> 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;
};
作用:简化类型写法
缺点:可读性变差
auto是编译时,根据初始化表达式类型进行推导的.因此,auto对运行时的类型推导是无能为力的.
使用auto可以在不知道需要实际类型怎么给,或者类型写起来特别复杂的情况下进行变量定义:
auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就无能为力。
因此decltype是根据表达式的实际类型推演出定义变量时所用的类型。
int main()
{
int a = 10;
int b = 20;
// 用decltype推演a+b的实际类型,作为定义c的类型
decltype(a+b) c;
cout<<typeid(c).name()<<endl;
return 0;
}
void* GetMemory(size_t size) {
return malloc(size);
}
int main()
{
// 如果没有带参数,推导函数的类型
cout << typeid(decltype(GetMemory)).name() << endl;
// 如果带参数列表,推导的是函数返回值的类型,注意:此处只是推演,不会执行函数
cout << typeid(decltype(GetMemory(0))).name() <<endl;
return 0;
}
auto和decltype不支持作为函数的参数:
C++ 98支持的RTTI(运行时类型识别)
typeid只能查看类型不能用其结果定义类型,因为一个函数栈桢的建立需要计算其变量的大小,如果是运行时推导,那么就无法计算大小
运行时类型识别的缺陷是降低程序运行的效率
override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
新增加容器—静态定长数组array、单链表forward_list以及unordered系列。
注意当容器存的对象比较大,或者这个对象要做深拷贝时,比如string,最好给&和const来减少拷贝提高效率,容器支持范围for的原理:范围for会被编译器替换成迭代器,也就意味着支持迭代器就支持范围for。
C++中的空类,会默认生成一些成员函数,但是这些函数如果程序员自己编写了,就不会默认生成。然而有时候又需要默认生成,这就容易造成混乱,因此C++11,提供两个关键字,让程序员自己决定是否需要编译器生成。
在默认函数定义或者声明时加上=default,可以显示的指示编译器生成该函数的默认版本,用=default修饰的函数称为显示缺省函数
在C++98之中,将函数设置成private并且不给定义,其它人就无法调用。
在C++11之中,只需要在函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,常用于防止拷贝。
C++98中提出了引用的概念,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通过指针来实现的,因此使用引用,可以提高程序的可读性。
为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用。
左值:使用空间
右值:使用内容
一般认为:
#include
using namespace std;
int main()
{
//左值引用
int a = 0;
int& b = a;
//右值引用
int x = 1, y = 2;
int&& c = 10;
int&& d = x + y;
}
总结:
C++11对右值进行了严格的区分(除了右值就是左值):
纯右值(基本类型的常量,或临时对象): 比如 a+b, 100,强制类型转换产生的临时对象。
将亡值(自定义类型的临时对象): 比如函数按值返回一个对象。
这些纯右值和将亡值都会在栈帧中分配空间来存放临时变量,而右值引用就是他们的名字,这样就让临时变量和普通变量一样,有自己的名字和内存空间,此时就可以通过这个右值引用使用它的空间。
C++11提出了移动语义概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效的缓解拷贝构造对象时资源浪费的情况。
class String
{
public:
String(const char* str = "")
:_str(new char[strlen(str) + 1])
{
strcpy(_str, str);
cout << "String(const char *str="")" << endl;
}
//正常构造
String(const String& str)
:_str(new char[strlen(str._str) + 1])
{
//深拷贝,代价比较大
strcpy(_str, str._str);
cout << "正常构造,深拷贝,代价比较大" << endl;
}
//移动构造
String(String&& str)//是右值
:_str(str._str)
{
str._str = nullptr;//直接进行资源转移,空间的交换,代价小,效率高
cout << "移动构造,空间的交换,代价小,效率高" << endl;
}
//移动赋值
String& operator=(String&& str)
{
cout << " 移动赋值,代价小,效率高" << endl;
if (_str != str._str)
{
_str = str._str;
str._str = nullptr;
}
return *this;
}
//正常赋值
String& operator=(const String& str)
{
if (_str != str._str)
{
cout << "正常赋值,深拷贝,效率低" << endl;
_str = new char[strlen(str._str) + 1];
strcpy(_str, str._str);
}
return *this;
}
~String()
{
if (_str)
{
delete[]_str;
cout << "~String()" << endl;
}
}
private:
char* _str;
};
String Getstring(const char* str)
{
String ret(str);
//该函数返回一个临时对象,是右值
return ret;
}
void test()
{
String s1("左值");
cout << endl;
String s2(s1);
cout << endl;
String s3(Getstring("右值-将亡值"));
cout << endl;
String s4(move(s1));
cout << endl;
String s5("左值");
s5 = Getstring("右值-将亡值");
cout << endl;
}
int main()
{
test();
return 0;
}
移动构造和移动赋值是把将亡值(右值)的空间直接给要赋值的对象,因为将亡值出了作用域就析构了,所以与其让其白白析构,还不如将其空间利用起来,给要赋值的对象,这样就避免了深拷贝带来的效率降低。
注意:
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
int main()
{
String s1("hello world");
String s2(move(s1));
String s3(s2);
return 0;
}
class Person
{
public:
Person(const String &name)
:_name(name)//调用String的拷贝构造函数
{}
Person(const Person& pl)
:_name(pl._name)
{
cout << "Person(const Person& pl)" << endl;
}
private:
String _name;
};
对上述代码进行优化,增加右值引用:
class Person
{
public:
Person(const String &name)
:_name(name)//调用String的拷贝构造函数
{}
Person(const Person& pl)
:_name(pl._name)
{
cout << "Person(const Person& pl)" << endl;
}
Person(const Person&& pl)
:_name(pl._name)//pl中的string是左值,所以还是调用的深拷贝
{
cout << "Person(const Person&& pl)" << endl;
}
private:
String _name;
};
继续优化,使用move函数:
class Person
{
public:
Person(const String &name)
:_name(name)//调用String的拷贝构造函数
{}
Person(const Person& pl)
:_name(pl._name)
{
cout << "Person(const Person& pl)" << endl;
}
Person(Person&& pl)
:_name(move(pl._name))//pl既然是一个将亡值,那么它的资源也是一个将亡值
{
cout << "Person(Person&& pl)" << endl;
}
private:
String _name;
};
C++98中引用作用:因为引用是一个别名,需要用指针操作的地方,可以使用指针来代替,可以提高代码的可读性以及安全性。
C++11中右值引用主要有以下作用:
int main()
{
string s1("hello");
string s2(" world");
string s3 = s1 + s2; // s3是用s1和s2拼接完成之后的结果拷贝构造的新对象
stirng&& s4 = s1 + s2; // s4就是s1和s2拼接完成之后结果的别名
return 0; }
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
void Func(int x) {
// ......
}
template<typename T>
void PerfectForward(T t) {
Fun(t);
}
PerfectForward为转发的模板函数,Func为实际目标函数,但是上述转发还不算完美,完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销,就好像转发者不存在一样。
所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。
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;
}
如果不使用forward函数,则会把右值变成左值。
资料参考:
C++11常用语法-壹
第三节 列表初始化—std::initializer_list