Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!
我的博客:<但凡.
我的专栏:《编程之路》、《数据结构与算法之美》、《题海拾贝》、《C++修炼之路》
欢迎点赞,关注!
C++11(原称 C++0x)是 C++ 编程语言的重大更新版本,于 2011 年 8 月 正式发布。它是自 C++98 之后最重要的修订,引入了大量新特性,显著提升了代码的可读性、安全性和性能。那么这一期我们就来讲讲C++11中的新特性。但是我会略过一些我们经常使用的范围for,auto之类的,只介绍一些没有使用过的。
目录
2、C++11中的std::initializer_list
1、列表初始化
在C++11之后,列表初始化成了我们常用的初始化方式。在C++11之前,只有数组和简单的结构体、类可以使用初始化列表。而在C++11之后,我们stl中的所有容器都支持了初始化列表,在我们前面的学习中也都接触过了。不仅如此,自定义的类也支持了初始化列表,并且在初始化时可以省略掉=:
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date(const Date& d)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1 = { 2025,6,1 };
Date d2{ 2025,6,2 };
}
这里实际上是花括号中的内容调用构造函数生成临时对象,临时对象再调用拷贝构造拷贝给d1,在编译器优化之后优化成直接构造。
在实践中我们使用花括号初始化。
2、C++11中的std::initializer_list
对于stl中的容器,比如vector,我们要是想在初始化时直接在vector中插入n个元素,那岂不是还需要写一堆构造函数,让构造函数的参数和n对应着?这太麻烦了。C++11中,initializer_list可以很好的解决这个问题。initializer_list本质上底层是一个数组,他的内部有两个指针分别指向数组的开始和末尾,在初始化时,我们遍历initializer_list,插入initializer_list中的每一个值,就能完成容器的初始化。
#include<iostream>
#include<vector>
using namespace std;
int main()
{
initializer_list<int> li;
li = { 10, 20, 30 };
cout << sizeof(li) << endl;//8
cout << li.size() << endl;
int i = 0;
cout << li.begin() << endl;
cout << &i << endl;
vector<int> v1 = { 1,2,3,4,5,6,7 };//构造临时对象+拷贝构造编译器优化为直接构造
vector<int> v2({ 1,2,3,4,5 });//直接构造
vector<int> v3{ 1,2,3,4,5,6,7 };
}
li中存储了两个地址,所以说大小为8个字节。由于i的地址和li的起始地址非常接近,所以说li底层的数组和局部数组一样,都是存放在栈空间的。
需要注意的是,对于我们自定义类和stl中容器的花括号初始化其实是不一样的,自定义类走的是构造函数,而stl容器走的是遍历加插入。
3、左值和右值
3.1、右值引用和移动语义
这个是C++11中很重要的新增特性了。首先我们得区分一下左值和右值。
首先说左值,左值 表示一个 有名称的、持久的内存位置,可以取地址(&)。他可以出现在等号的左边,也可以出现在等号的右边。我们通常定义的变量都是左值。
#include<iostream>
#include<string>
using namespace std;
int main()
{
int i = 0;
int* p = &i;
char ch = 'a';
const int c = 5;
string s = "ss";
}
以上几个都是左值。对于const修饰的左值,我们可以取其地址,但是不能对他进行修改。
再说右值,右值 表示一个 临时的、匿名 的对象,通常 不能取地址。因为右值根本没有地址,不在内存中存储。右值通常都是在寄存器中写死的。
右值通常是字面量常量,临时对象等等。他们只能出现在等号的右边:
#include<iostream>
#include<string>
using namespace std;
int main()
{
double x = 1.1, y = 2.2;
// 以下⼏个10、x + y、fmin(x, y)、string("11111")都是常⻅的右值
10;
x + y;
fmin(x, y);
string("11111");//临时对象
}
其实右值在C++11之后又分出了好多类,这个我们待会再说。总之左值和右值的核区别就是左值和取地址,而右值不能取地址。
3.2、右值引用和左值引用
int a = 0;
int& aa = a;//左值引用
int&& bb = 10;//右值引用
其实左值引用和右值引用的区别是左值引用只有一个&,而右值引用有两个&。
左值引用不能引用右值,但是const 左值引用可以引用右值。右值引用只能引用右值。
我们可以使用move将左值转化成右值,这个本质上就是强制类型转换。
int a = 0;
int* p = &a;
int& aa = a;
int*& pp = p;
const int& b = 10;
int&& b = 10;
string&& s = string("sss");
string s = "左值";
string&& s1 = move(s);
一个右值引用其他右值的变量,他的属性其实是左值。
另外,右值可以延长生命周期。一个临时对象他的生命周期只有当前这一行,如果右值引用他,他的生命周期就会延长。
std::string s1 = "Test";
const std::string& r2 = s1 + s1; // OK:到 const 的左值引⽤延⻓⽣存期
// r2 += "Test"; // 错误:不能通过到 const 的引⽤修改
std::string&& r3 = s1 + s1; // OK:右值引⽤延⻓⽣存期
r3 += "Test"; // OK:能通过到⾮ const 的引⽤修改
std::cout << r3 << '\n';
const左值引用也可以延长生命周期,但是不能修改该右值。
3.3、右值引用和移动语义的使用场景
我们在之前的学习过程中,经常使用const 左值引用。因为这样可以减少传参时的拷贝,提高效率。左值引用已经解决了大部分场景的拷贝效率问题,但是有一点是不能解决的,就是传引用返回。因为我们传引用返回的是临时对象,出了函数作用域就销毁了,所以说这里无论是左值引用还是右值引用都是无法解决这个效率问题的。那么我们有什么办法解决呢?
3.3.1、移动构造和移动赋值
移动构造函数是⼀种构造函数,类似拷贝构造函数,移动构造函数要求第⼀个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
移动构造相比传统的拷贝构造更加高效,它本质上通过交换,直接“窃取”走右值对象的值。
只有对于string,vector这类需要开空间的容易移动构造才有意义,对于data类这种和拷贝构造没有区别。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<assert.h>
#include<cstring>
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
std::cout << "string(char* str)-构造" << std::endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string(const string& s)
:_str(nullptr)
{
std::cout << "string(const string& s) -- 拷⻉构造" << std::endl;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
// 移动构造
string(string&& s)
{
std::cout << "string(string&& s) -- 移动构造" << std::endl;
swap(s);
}
string& operator=(const string& s)
{
std::cout << "string& operator=(const string& s) -- 拷⻉赋值" << std::endl;
if (this != &s)
{
_str[0] = '\0';
_size = 0;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
return *this;
}
// 移动赋值
string& operator=(string&& s)
{
std::cout << "string& operator=(string&& s) -- 移动赋值" << std::endl;
swap(s);
return *this;
}
~string()
{
std::cout << "~string() -- 析构" << std::endl;
delete[] _str;
_str = nullptr;
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
if (_str)
{
strcpy(tmp, _str);
delete[] _str;
}
_str = tmp;
_capacity = n;
}
}
void push_back(const char& ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string& operator+=(const char& ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
size_t size() const
{
return _size;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
int main()
{
string s1("xxxxx");
// 拷⻉构造
string s2 = s1;
// 构造+移动构造,优化后直接构造
string s3 = string("yyyyy");
// 移动构造
string s4 = std::move(s1);
s2 = "SSSS";
std::cout << "******************************" << std::endl;
return 0;
}
输出结果:
如果没有移动构造和移动赋值:
所有本该是移动赋值或移动拷贝的都变成了拷贝赋值和拷贝构造。这无疑会降低效率。
现在我们对当前的string类升级一下,再多弄几个右值引用接口出来。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<assert.h>
#include<cstring>
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
string(const char* str="")
:_size(strlen(str))
, _capacity(_size)
{
std::cout << "string(char* str)-构造" << std::endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
string(char*&& str)
:_size(strlen(str))
, _capacity(_size)
{
std::cout << "string(char* str)-右值构造" << std::endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string(const string& s)
:_str(nullptr)
{
std::cout << "string(const string& s) -- 拷⻉构造" << std::endl;
reserve(s._capacity);
for (auto ch : s)
{
push_back(std::move(ch));
}
}
// 移动构造
string(string&& s)
{
std::cout << "string(string&& s) -- 移动构造" << std::endl;
swap(s);
}
string& operator=(const string& s)
{
std::cout << "string& operator=(const string& s) -- 拷⻉赋值" << std::endl;
if (this != &s)
{
_str[0] = '\0';
_size = 0;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
return *this;
}
// 移动赋值
string& operator=(string&& s)
{
std::cout << "string& operator=(string&& s) -- 移动赋值" << std::endl;
swap(s);
return *this;
}
~string()
{
std::cout << "~string() -- 析构" << std::endl;
delete[] _str;
_str = nullptr;
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
if (_str)
{
strcpy(tmp, _str);
delete[] _str;
}
_str = tmp;
_capacity = n;
}
}
void push_back(const char& ch)
{
std::cout << "普通插入" << std::endl;
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity *2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
void push_back(char&& ch)
{
std::cout << "右值插入" << std::endl;
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string& operator+=(const char& ch)
{
push_back(ch);
return *this;
}
string& operator+=(char&& ch)
{
push_back(std::move(ch));
return *this;
}
const char* c_str() const
{
return _str;
}
size_t size() const
{
return _size;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
int main()
{
string s1("xxxxx");
// 构造+移动构造,优化后直接构造
string s3 = string("yyyyy");
// 移动构造
string s4 = std::move(s1);
s3.push_back('s');
s3.push_back('s');
s3 += 's';
std::cout << "******************************" << std::endl;
return 0;
}
我们需要注意,所有的变量都是左值类型的。如果我们想让他调用右值引用函数,首先要保证他是右值。
3.3.2、右值引用和移动语义解决传值返回问题
现在解决我们最开始提出的问题:
在有移动构造的情况下,编译器会把返回值move成左值,然后调用移动构造出一个临时对象,临时对象在移动构造成ret。
当然以上情况是在没有编译器优化的情况下讨论的。在vs2019debug之前,其实编译器会直接优化掉一次构造。我们只需要一次拷贝构造或者移动构造就够了。在vs2019release之后,在编译器优化下甚至一次拷贝都不需要了,我们的ret直接被优化成了str的引用。效率得到了极大的提高。
3.3.3、右值引用和移动语义在传参中的提效
其实C++11以后容器的push和insert系列的接口否增加的右值引用版本
当实参是一个左值时,容器内部继续调⽤拷贝构造进行拷贝,将对象拷贝到容器空间中的对象
当实参是一个右值,容器内部则调用移动构造,右值对象的资源到容器空间的对象上
但其实stl容器在C++11之后都新增了emplace接口,我们待会再说。
3.4、类型划分
C++11之后又对右值进行了进一步划分。我们继续来看看做了哪些划分,以及这些划分的意义是什么。
(1)纯右值
纯右值没有标识符(无法取地址)。通常是计算过程中的临时结果或字面量,可以用于初始化对象或作为右值引用的绑定目标。
(2)将亡值
有标识符(通常是变量或表达式),但即将被移动(资源可被“窃取”)。通过 std::move
或返回右值引用的函数生成。可以绑定到右值引用(T&&
)。
值类别 | 说明 | 示例 |
---|---|---|
左值(lvalue) | 有标识符、可取地址的持久化对象 | 变量名(如 int x; 中的 x )、返回左值引用的函数调用(如 std::cout << 1 ) |
纯右值(prvalue) | 临时对象、字面量(除字符串外)、没有标识符的临时结果 | 42 , x + 1 , std::string("hello") |
将亡值(xvalue) | 即将被移动的右值,既有标识符又可以被“窃取”资源(通过 std::move 生成) |
std::move(x) , 返回右值引用的函数调用(如 std::make_unique<int>(42) ) |
其实C++11对左值也进一步划分了一下,但是只做了解就好了:
类别 | 组成 | 特点 |
---|---|---|
泛左值(glvalue) | 左值 + 将亡值 | 有标识符(可以取地址) |
右值(rvalue) | 纯右值 + 将亡值 | 可移动(资源可被“窃取”) |
这些划分都是为了我们接下来要说的完美转发来做准备的。
3.5、引用折叠
C++中不能直接引用折叠:int& && r = i;,但是我们可以用过传参的方式进行引用折叠:
template<class T>
void f1(T& x)
{}
template<class T>
void f2(T&& x)
{}
对于上面两个模板,我们可以通过他们实现引用折叠。先说一下引用折叠的规则,只要不是右值引用折叠右值引用,其他的折叠都会折叠成左值引用。
也就是说,对于f1来说,不论我们T是什么,他都是左值引用。
f1<int>(n);
//f1<int>(0); // 报错
// 折叠->实例化为void f1(int& x)
f1<int&>(n);
//f1<int&>(0); // 报错
// 折叠->实例化为void f1(int& x)
f1<int&&>(n);
//f1<int&&>(0); // 报错
// 折叠->实例化为void f1(const int& x)
f1<const int&>(n);
f1<const int&>(0);
// 折叠->实例化为void f1(const int& x)
f1<const int&&>(n);
f1<const int&&>(0);
而对于f2来说,我们可以做到如果传左值就让他左值引用,如果传右值就右值引用。f2这种函数模版叫万能引用。
template<class T>
void Function(T&& t)
{
int a = 0;
T x = a;
//x++;
cout << &a << endl;
cout << &x << endl << endl;
}
int main()
{
// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(10); // 右值
int a;
// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)
Function(a); // 左值
// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(std::move(a)); // 右值
const int b = 8;
// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&
t)
// 所以Function内部会编译报错,x不能++
Function(b); // const 左值
// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&
t)
// 所以Function内部会编译报错,x不能++
Function(std::move(b)); // const 右值
return 0;
}
3.6、 完美转发
在完美转发执之前,我们所有的变量都是左值属性的。就算是一个引用右值的变量也是左值属性。那么有没有什么办法能让右值引用一个对象后该变量仍然保持右值属性呢?我们可以借助完美转发来实现。
template <class _Ty>
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
{
// forward an lvalue as either an lvalue or an rvalue
return static_cast<_Ty&&>(_Arg);
}
完美转发其实也是一种函数模版。 主要还是通过引用折叠的方式来实现。
#include <utility>
#include <iostream>
void process(int& x) {
std::cout << "处理左值: " << x << std::endl;
}
void process(int&& x) {
std::cout << "处理右值: " << x << std::endl;
}
template <typename T>
void logAndProcess(T&& arg) {
// 记录日志等操作...
process(std::forward<T>(arg)); // 完美转发
}
int main() {
int x = 10;
logAndProcess(x); // 调用左值版本
logAndProcess(20); // 调用右值版本
logAndProcess(std::move(x)); // 调用右值版本
}
std::forward
是一个有条件转换:当模板参数 T 是左值引用类型时,返回左值引用。否则,返回右值引用
如果我们传入左值,T被推导成int&,接下来调用forward,保持arg的左值属性,返回左孩子引用。如果我们传入右值,T被推到为int(假设传的20),此时调用forward 返回右值引用(T不是左值引用,返回右值引用)。
好了,今天的内容就分享到这,我们下期再见!