【C++】C++11新增语法(右值引用、完美转发)
戴墨镜的恐龙 2024-08-08 09:35:12 阅读 67
文章目录
1.C++11新增常用语法1.1 统一的列表初始化1.2 initializer_list初始化1.3 声明相关1.4 继承与多态相关
2. 右值引用与移动语义2.1 左值引用与右值引用2.2 右值引用与移动语义的使用场景2.3 右值引用引用左值(move)
3. 完美转发4. 新的类功能4.1 新增两个默认成员函数4.2 其它与类成员相关
相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。下面我们介绍一些常用的语法:
1.C++11新增常用语法
1.1 统一的列表初始化
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。
C++11扩大了用花括号括起的列表(初始化列表)的使用范围(一切皆可用花括号初始化),使其可用于所有的内置类型和用户自定义的类型,<code>使用初始化列表时,可添加等号(=),也可不添加。
1.2 initializer_list初始化
C++11对STL中的不少容器就增加std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。
其实底层类似于这样
<code>list(initializer_list<T> lt)
{
empty_init();
for (const auto e : lt)
{
push_back(e);
}
}
1.3 声明相关
auto
C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
decltype
关键字decltype将变量的类型声明为表达式指定的类型。
1.4 继承与多态相关
final
final修饰虚函数,表示该虚函数不能再被重写。
final 修饰一个类,表示该类为最终类,无法被继承。
override
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
2. 右值引用与移动语义
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
2.1 左值引用与右值引用
左值与左值引用
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址。
左值引用就是给左值的引用,给左值取别名
右值与右值引用
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(有时不能是左值引用返回)、匿名对象等等 。
右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址。<code>右值引用就是对右值的引用,给右值取别名。
右值引用是C++11中引入的一种新的引用类型,用于引用那些即将被销毁的对象(即右值)。右值引用通过类型后加&&来声明,例如int&&。
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置(别名变成了左值),且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是r1引用后,可以对r1取地址,也可以修改r1。如果不想r1被修改,可以用const int&& r1。
左值引用与右值引用比较
左值引用总结:
左值引用只能引用左值,不能引用右值。但是const左值引用既可引用左值,也可引用右值。
右值引用总结:
右值引用只能引用右值,不能引用左值。但是右值引用可以引用move以后的左值
2.2 右值引用与移动语义的使用场景
C++中,右值引用和移动语义是紧密相关的概念,它们共同提供了一种优化资源管理的机制,特别是在处理临时对象、返回值和大型数据结构时。
右值引用
右值引用允许我们将一个即将被销毁的对象的资源转移到另一个对象上,而不是进行复制或赋值操作。
移动语义
移动语义允许我们通过转移(而非复制)资源的方式,从一个对象(源对象)到另一个对象(目标对象)高效地传递数据。这通常通过定义一个移动构造函数和一个移动赋值操作符来实现。
这些特殊成员函数接受一个右值引用作为参数,并利用这个右值引用的“即将被销毁”的特性,来窃取(或转移)源对象的资源,而不是复制它们。
关系
右值引用是实现移动语义的关键工具。<code>没有右值引用,我们就无法区分一个对象是作为右值(将被销毁的对象)还是左值(持续存在的对象)被引用的。因此,就无法有效地实现资源的转移,而只能进行复制。
为了验证效果,我们先简单手搓一个string。
namespace my
{
class string
{
public:
//构造
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "String(const char* str = "")--构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//拷贝构造
string(const string& str)
:_size(str._size)
, _capacity(_size)
{
cout << "string(const string& str)--拷贝构造,深拷贝" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str._str);
}
//赋值重载
string& operator=(const string& str)
{
cout << "string& operator=(string str)-- 赋值重载" << endl;
my::string tmp(str);
swap(tmp);
return *this;
}
void swap(string& str)
{
std::swap(_str, str._str);
std::swap(_size, str._size);
std::swap(_capacity, str._capacity);
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
对于左值引用而言,其作为参数和返回值时都减少了拷贝
左值引用的不足:当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。
右值引用和移动语义解决上述问题:
在my::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。
<code>//移动构造
string(string&& str)
:_str(nullptr)
,_size(0)
, _capacity(0)
{
cout << "string(const string&& str)--移动构造,交换数据" << endl;
this->swap(str); //直接交换资源
}
我们会发现,这里没有调用深拷贝的拷贝构造,而是调用了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了。
不仅仅有移动构造,还有移动赋值: 在my::string类中增加移动赋值函数,再去调用fun函数,不过这次是将func函数返回的右值对象赋值给ret对象,这时调用的是移动构造。
<code>//移动赋值重载
string& operator=(string&& str)
{
cout << "string& operator=(string&& str)-- 移动赋值重载" << endl;
swap(str);
return *this;
}
这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象接收,编译器就没办法优化了。func()中会先用ret构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把ret识别成了右值,调用了移动构造。然后在把这个临时对象做为func函数调用的返回值赋值给ret1,这里调用的移动赋值。
总的来说,使用右值引用作为参数实现的移动构造和移动赋值就是减少了拷贝。
2.3 右值引用引用左值(move)
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?- -不一定
因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。
当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。
C++11中,std::move()函数位于头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
STL容器插入接口函数也增加了右值引用版本:
3. 完美转发
模板中的&&不代表右值引用,而是万能引用(引用折叠),其既能接收左值又能接收右值。
<code>template<class T>
void PerfectForward(T&& t)//万能引用
{
Fun(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<class T>
void PerfectForward(T&& t)//万能引用
{
Fun(t);
}
给万能引用的函数模板传递相应的参数,它应该调用对应的函数,但是运行结果跟我们想象的不一样,为什么呢?
模板的万能引用只是提供了能够同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,传递过程中都退化成了左值(右值引用本身是左值),我们希望能够在传递过程中<code>保持它的左值或者右值的属性, 就需要用完美转发。
使用时模板后面的括号不能少,这样就达到了想要的效果
因此,在对右值引用进行传递时,为了避免其退化为左值,都需要使用完美转发
4. 新的类功能
4.1 新增两个默认成员函数
原来C++类中,有6个类的默认成员函数:
构造函数析构函数拷贝构造函数拷贝赋值重载取地址重载const 取地址重载
最重要的是前4个,后两个用处不大。
C++11 新增了两个:移动构造函数和移动赋值运算符重载
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。
默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,<code>自定义类型成员,则需要看这个成员是否实现移动构造。如果实现了就调用移动构造,没有实现就调用拷贝构造。 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。
默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值
。如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似) 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
为什么这两个默认成员函数的要求这么苛刻呢?析构函数 、拷贝构造、拷贝赋值重载中的任意一个都不能实现
如果你未实现析构、拷贝构造、赋值重载函数,那就意味着没有资源需要释放,不需要进行深拷贝,都是浅拷贝,对浅拷贝而言,移动构造没有意义。
那为什么编译器还要自动生成呢?
对于向下方person这样的类而言,它自己确实没有资源需要释放,但是它的成员有资源需要释放;编译器自动生成的移动构造就会去调用成员变量的移动构造、移动赋值,这样就减少了拷贝。
自动生成
未自动生成
4.2 其它与类成员相关
类成员变量初始化
C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化。
强制生成默认函数的关键字default
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
禁止生成默认函数的关键字delete
如果能想要限制某些默认函数的生成,在C++98中,是将该函数声明设置成private,并且不实现它,这样只要其他人想要调用就会报错。
在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
假如我们不想让一个类被拷贝,那就可以将其拷贝构造、赋值重载给限制掉,就可以使用delete。
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。