C++11
1. C++11简介
C++11标准是 ISO/IEC 14882:2011 - Information technology – Programming languages – C++ 的简称 。C++11标准由国际标准化组织(ISO)和国际电工委员会(IEC)旗下的C++标准委员会(ISO/IEC JTC1/SC22/WG21)于2011年8月12日公布 ,并于2011年9月出版。2012年2月28日的国际标准草案(N3376)是最接近于C++11标准的草案(仅编辑上的修正)。此次标准为C++98发布后13年来第一次重大修正。——参考百度百科
在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
小故事(就是个各种鸽的故事。。):
1998年是C++标准委员会成立的第一年,本来计划以后每5年视实际需要更新一次标准,C++国际标准委员会在研究C++ 03的下一个版本的时候,一开始计划是2007年发布,所以最初这个标准叫C++ 07。但是到06年的时候,官方觉得2007年肯定完不成C++ 07,而且官方觉得2008年可能也完不成。最后干脆叫C++ 0x,x的意思是不知道到底能在07还是08还是09年完成。结果2010年的时候也没完成,最后在2011年终于完成了C++标准。所以最终定名为C++11。
2. 统一的列表初始化
注意!这里是列表初始化,而不是初始化列表,注意二者的区别。
2.1{}初始化
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定,也就是从C语言中继承下来的东西。比如:
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()
{
// 后面两种的初始化方式都不是很推荐,好好的直接赋值就OK,干嘛整这么奇奇怪怪的东西
// 但是为了防止有的人这样写,所以还是得认识了解
int x1 = 1;
int x2 = { 2 };
int x3{ 3 };
// 还是第一种舒服
int array[] = { 1, 2, 3, 4, 5 };
int array1[]{ 1, 2, 3, 4, 5 };
int array2[5]{ 0 };
Point p = { 1, 2 };
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(2024, 7, 14); // old style
// 这里实际上就是隐式类型转换,构造+拷贝构造->优化为直接构造
Date d2{ 2024, 7, 14 };
Date d3 = { 2024, 7, 14 };
// 比如
string s1 = "xxxx"; // 这里就是发生了隐式类型转换
// new一个自定义类型数组
Date* pd1 = new Date[3]{ d1, d2, d3 }; // C++11之前的老式写法
Date* pd1 = new Date[3]{ { 2024,7,13 }, { 2024,7,14 }, { 2024,7,15 } }; // 新式写法
Date* pd1 = new Date[3]{ { 2024,7,13 }, d2, { 2024,7,15 } }; // 混搭
return 0;
}
2.2 std::initializer_list
std::initializer_list
的介绍文档:
http://www.cplusplus.com/reference/initializer_list/initializer_list/
std::initializer_list
是什么类型:
int main()
{
// the type of il is an initializer_list
auto il = { 10, 20, 30 };
cout << typeid(il).name() << endl;
return 0;
}
std::initializer_list
使用场景:
std::initializer_list
一般是作为构造函数的参数,C++11对STL中的不少容器就增加std::initializer_list
作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=
的参数,这样就可以用大括号赋值。
int main()
{
vector<int> v = { 1,2,3,4 };
list<int> lt = { 1,2 };
// 这里{"sort", "排序"}会先初始化构造一个pair对象
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
// 使用大括号对容器赋值
v = { 10, 20, 30 };
return 0;
}
对v进行赋值后,也就变成了这样:
来看看initializer_list
的成员函数:
可以看到initializer_list
支持迭代器,那么它就可以这么玩了:
int main()
{
// the type of il is an initializer_list
initializer_list<int> il = { 10, 20, 30 };
cout << typeid(il).name() << endl;
initializer_list<int>::iterator it = il.begin();
while (it != il.end())
{
cout << *it << " ";
++it;
}
cout << endl;
// 亦支持范围for
for (auto e : il)
{
cout << e << " ";
}
cout << endl;
return 0;
}
也可以在自己模拟实现的vector中加入initializer_list
构造
vector(initializer_list<T> il)
{
reserve(il.size());
for (auto &e : il)
{
push_back(e);
}
}
3. 声明
C++11提供了多种简化声明的方式,尤其是在使用模板时。
3.1 auto
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
int main()
{
int i = 10;
auto p = &i;
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin();
return 0;
}
auto
可以自动推断类型,但是平时用的比较多的场景还是在范围for中,或者当一个类型名太长时,我们就可以用auto进行推断。
3.2 decltype
首先认识一下typeid
,他也可以推导类型,typeid().name
拿到的是类型的字符串,仅限于此,不能用他再去定义类型。
int main()
{
int i = 1;
double d = 5.2;
// 类型以字符串的形式获取到
cout << typeid(i).name() << endl;
cout << typeid(d).name() << endl;
return 0;
}
关键字decltype
将变量的类型声明为表达式指定的类型。来看看具体使用场景:
int main()
{
int i = 1;
double d = 5.2;
auto ret = i * d;
decltype(ret) r;
// 如果此时需要ret的类型实例化一个模板对象auto就不行了
vector<decltype(ret)> v;
v.push_back(1);
v.push_back(1.1);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
return 0;
}
decltype
可以推导对象的类型,这个类型是可以用来模板实参,或者再定义对象的。
3.3 nullptr
由于C++中NULL
被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整型常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr
,用于表示空指针。
NULL
的定义
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
nullptr
的定义
#if defined(nullptr)
#define nullptr EMIT WARNING C4005
#error The C++ Standard Library forbids macroizing the keyword "nullptr". \
Enable warning C4005 to find the forbidden define.
#endif // nullptr
4. 右值引用和移动语义
4.1 左值引用和右值引用
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址和可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。所以左值最大特点就是可以取地址。
int main()
{
// 以下的*p、b、c都是左值
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()
{
int i = 1;
int j = 2;
// 以下几个都是常见的右值
10;
i + j;
fmin(i, j);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = i + j;
double&& rr3 = fmin(i, j);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
i + j = 1;
fmin(i, j) = 1;
return 0;
}
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1
去引用,是不是感觉很神奇,这个了解一下,实际中右值引用的使用场景并不在于此,这个特性也不重要。
int main()
{
int i = 1, j = 2;
int&& rr1 = 10;
const int&& rr2 = i + j;
rr1 = 20;
rr2 = 5; // 报错
return 0;
}
4.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;
}
右值引用总结:
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以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;
}
4.3 右值引用的使用场景和意义
左值引用的短板:
当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如:我们自己实现的
string
类中的string to_string(int value)
函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。
(这里我用的vs2022,被优化为一次都没有了,真的万恶,建议大家在学习的时候用稍微低版本一点的编译器,稳定且方便观察现象)。
to_string的返回值是一个右值,用这个右值构造s1,如果没有移动构造,调用就会匹配调用拷贝构造,因为const左值引用是可以引用右值的,这里就是一个深拷贝。
但是这种情况,依旧是两次拷贝构造
// 拷贝构造
string(const string& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
// 调用构造函数
string tmp(s._str);
swap(tmp);
cout << "const string& s--深拷贝" << endl;
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
swap(s);
cout << "string&& s--移动构造" << endl;
}
int mian()
{
minnow::string s1; // 这里是我自己的命名空间
s1 = minnow::to_string(-1234);
}
to_string
的返回值是一个右值,用这个右值构造s1
,如果既有拷贝构造又有移动构造调用就会匹配调用移动构造,因为编译器会选择最匹配的参数调用。那么这里就是一个移动语义。
右值还分为:
- 纯右值,内置类型右值。
- 将亡值,自定义的右值。
右值引用和移动语义解决上述问题:
在我们自己实现的
string
类中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。
不仅仅有移动构造,还有移动赋值
在
string
类中增加移动赋值函数,再去调用to_string(-1234)
,不过这次是将to_string(-1234)
返回的右值对象赋值给ret1对象,这时调用的是移动构造。
移动语义是什么
定义:移动语义允许资源的所有权从一个对象转移到另一个对象,这意味着不需要进行资源的拷贝,从而提高性能。移动语义通常通过移动构造函数和移动赋值运算符实现。
如何提高性能呢?
- 减少拷贝:通过移动语义,可以将对象的资源直接转移给另一个对象,而不是拷贝一份新的资源。这在处理大型数据或资源密集型对象时尤其有用,因为避免了不必要的拷贝操作,从而节省了时间和内存。
- 优化临时对象的使用:当编译器遇到临时对象时,它可以使用右值引用和移动语义来优化代码,从而减少创建和销毁临时对象的开销。
4.4 右值引用引用左值及其一些更深入的使用场景分析
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move
函数将左值转化为右值。C++11中,std::move()
函数位于头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。并且move不改变左值的本身属性,只是函数返回了一个临时的右值属性。
int main()
{
minnow::string s1("hello world");
// 这里s1是左值,调用的是拷贝构造
minnow::string s2(s1);
// s1调用move,属性返回右值,调用移动构造
minnow::string s3(std::move(s1));
return 0;
}
这里我们把s1 move处理以后,会被当成右值,然后调用移动构造。但是这里要注意,一般是不要这样用的,因为我们会发现s1的资源被转移给了s3,s1被置空了。
4.5 完美转发
模板中的&&
,也就是万能引用
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<class T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a = 0;
PerfectForward(a); // 左值
const int b = 0;
PerfectForward(std::move(b)); // const 右值
PerfectForward(b); // const 左值
return 0;
}
结果全是左值引用,十分有九分的不对劲呐!
- 模板中的
&&
不代表右值引用,而是万能引用,其既能接收左值又能接收右值。也被称之为引用折叠- 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值。说简单点就是无论是左值还是右值传参传给Fun函数之后全部变成了左值。
- 我们希望能够在传递过程中保持它的左值或者右值的属性,就需要用我们下面学习的完美转发。
在 C++11 中,完美转发(Perfect Forwarding)是一种技术,用于在函数模板中有效地传递参数,以保持参数的左值/右值属性和类型信息。
完美转发通常结合 std::forward
函数模板来实现。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 = 0;
PerfectForward(a); // 左值
const int b = 0;
PerfectForward(std::move(b)); // const 右值
PerfectForward(b); // const 左值
return 0;
}
这里注意一点:如果不是模板是不可以这么玩的,因为不是模板就不是万能引用,就是单纯的右值引用。
5. 新的类的功能
5.1 默认成员函数
原来C++类中,有6个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
C++11 新增了两个:移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
- 如果你没有自己实现移动构造函数,且都没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个(也就是这仨都没有实现),那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
private:
minnow::string _name; // 这里用的我自己写的类才会有明显的效果
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = move(s1);
return 0;
}
这里我们的string实现了移动构造,于是乎就调用了移动构造。
屏蔽掉string的移动构造试试~
这里我们的string没有实现拷贝构造,于是调用了拷贝构造。
5.2 默认函数的关键字
- 强制生成默认函数的关键字default:
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default
关键字显示指定移动构造生成,也就是强制编译器默认生成。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p)
:_name(p._name)
,_age(p._age)
{}
Person(Person&& p) = default;
private:
minnow::string _name; // 这里用的我自己写的类才会有明显的效果
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = move(s1);
return 0;
}
- 禁止生成默认函数的关键字delete:
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(Person&& p) = delete; // 删除默认生成的移动构造
private:
minnow::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = move(s1);
return 0;
}
直接出错