🎬慕斯主页:修仙—别有洞天
♈️今日夜电波:マイノリティ脈絡—ずっと真夜中でいいのに。
0:24━━━━━━️💟──────── 4:02
🔄 ◀️ ⏸ ▶️ ☰
💗关注👍点赞🙌收藏您的每一次鼓励都是对我莫大的支持😍
目录
左值与右值
什么是左值?什么是左值引用?
什么是右值?什么是右值引用?
总结
移动构造与移动赋值
引入
纯右值和将亡值
移动构造与移动赋值
移动构造(Move Construction)
移动赋值(Move Assignment)
move
对于移动构造与移动赋值的一些注意事项
万能引用与完美转发
左值与右值
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。 如下:
// 以下的p、b、c、*p都是左值
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;
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。 如下:
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1
总结
左值可以被取地址,右值不可被取地址!左值引用和右值引用都不能互相给对方取别名!但是,const左值引用可以!右值引用可以move(左值)取别名!
一个右值被右值引用后属性是左值!!!右值不能被修改但是右值引用后需要被修改!否则无法实现移动构造和移动赋值!
移动构造与移动赋值
引入
接下来看一个场景:如下两个函数都可以传入右值,在C++11前这样对于左值以及右值是很难区分的,在引入右值后,就可以根据场景来使用左值引用还是右值引用了!
void Test(const int& aa)
{
cout << "const int& aa :" << aa << endl;
//aa = 30; err
}
void Test(int&& aa)
{
cout << "int&& aa :" << aa << endl;
aa = 30;
cout << "int&& aa :" << aa << endl;
}
如果是仅仅为了区分左右值那是不是太过鸡肋了?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的! 如下是之前我们实现的一个string类:
namespace lt
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
}
我们在如下的场景中使用了多次的深拷贝会导致运行效率的降低:当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。如下:lt::string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(这是因为编译器优化,如果是一些旧一点的编译器可能是两次拷贝构造)。也就是至少要进行一次深拷贝,那么这样的代价也太大了!
lt::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
bit::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
int main()
{
lt::string ret1 = lt::to_string(1234);
return 0;
}
我们可以利用右值的特性进一步的提升效率,首先理解两个概念:
纯右值和将亡值
纯右值(Pure Rvalue):
- 定义:纯右值通常指的是那些不与存储位置直接关联的表达式,例如临时对象、字面量、返回非引用类型的函数调用等。纯右值可以出现在需要移动或复制操作的语境中。
- 特点:纯右值的一个重要特征是它们没有命名,因此无法被访问者直接引用。它们通常用于初始化或赋值给其他对象。
- 例子:当一个函数返回一个非引用类型的值时,这个返回值就是一个纯右值,直到它被使用之前。
将亡值(Expiring Value):
- 定义:将亡值是指那些即将不再使用的对象的表达式,通常是因为作用域即将结束或者对象即将被销毁。将亡值可以通过返回类型为右值引用的表达式来表示。
- 特点:将亡值的关键特性是它们所引用的对象的生命周期即将结束,这意味着可以进行资源的有效转移而不需要考虑后续使用。
- 例子:当一个对象的生命周期即将结束时,它的成员或数组元素可以被视为将亡值。
移动构造与移动赋值
我们可以根据将亡值的特性,使用右值引用识别出将亡值,将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了!
移动构造(Move Construction)
移动构造是一种特殊的构造函数,它接收一个右值引用作为参数,用于从临时对象(右值)中“窃取”资源,而不是复制资源。这样,临时对象的资源可以被新创建的对象直接使用,避免了不必要的资源分配和释放。
移动构造函数的形参不能是const,因为移动构造后原对象的状态需要被修改(例如,指针设为NULL),以表示资源已被转移。同时,移动构造函数通常还需要检查自我赋值的情况,以避免将对象自身作为输入进行移动赋值。
移动构造的主要应用场景包括:
- 在函数中返回临时对象时,可以通过移动构造函数避免不必要的拷贝操作。
- 在容器中插入临时对象时,可以通过移动构造函数实现高效插入和删除操作。
- 在进行资源管理时,通过移动构造函数可以从一个对象转移资源所有权,提高性能。
如下为上面提到的string的移动构造:
// 移动构造
string(string && s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
我们会发现,这里没有调用深拷贝的拷贝构造,而是调用了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了。
移动赋值(Move Assignment)
移动赋值是一种特殊的赋值运算符,它同样使用右值引用作为参数,用于将一个对象的资源转移到另一个已存在的对象中。移动赋值避免了深拷贝,使得资源可以直接从一个对象转移到另一个对象,提高了赋值操作的效率。
实现移动赋值时,通常需要进行以下步骤:
- 检查自我赋值,确保不是将对象赋值给自己。
- 使用std::move将资源从其他对象移动到当前对象。
- 将其他对象中该资源的状态置为适当的默认状态,例如将指针设为NULL。
- 返回当前对象的引用。
移动赋值的主要应用场景与移动构造类似,都是在于优化资源的转移和管理,特别是在处理临时对象或即将被销毁的对象时。
如下为上面提到的string的移动赋值:
// 移动赋值
string& operator=(string && s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
他也避免了不必要的资源复制,从而提高程序的性能。
move
std::move是一个函数模板,用于将左值转换为右值引用,从而触发移动语义。std::move的引入使得程序员可以显式地告诉编译器他们想要转移资源而不是复制它们。C++11后STL容器插入接口函数也增加了右值引用版本 ,如下是几个例子:
但是需要注意的是:move接受一个左值作为参数,并返回该左值的右值引用,它通过返回右值引用,std::move告诉编译器可以将该对象视为临时对象,从而触发移动构造函数或移动赋值操作符。std::move只是转换了对象的类型,并没有实际执行任何资源的转移。实际的资源转移发生在移动构造函数或移动赋值操作符被调用时。
对于移动构造与移动赋值的一些注意事项
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
使用default 强制生成默认函数的关键字。使用delete禁止生成默认函数的关键字。final用于限制类的继承和函数的重写。override用于显式地表明派生类的成员函数重写了基类中的同名虚函数。
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:
bit::string _name;
int _age;
};
//
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p) = delete;
private:
bit::string _name;
int _age;
};
//
class Base {
public:
virtual void foo() {}
};
class Derived : public Base {
public:
void foo() override {} // 显式地表明重写了基类中的虚函数
};
//
class Base {
public:
virtual void foo() final {} // 声明为final,禁止派生类重写该函数
};
class Derived : public Base {
public:
// 尝试重写基类的foo函数会导致编译错误
// void foo() {} // 编译错误
};
万能引用与完美转发
首先,我们来理解这两个概念:
- 万能引用:
- 定义:通过使用模板参数
T
与引用符号&&
结合形成的T&&
被称为万能引用。它能够根据传入参数的不同,既可以作为左值引用也可以作为右值引用。- 应用场景:万能引用主要用于函数模板中,使得函数可以统一处理左值和右值引用类型的参数。
- 完美转发:
- 定义:完美转发是指函数模板在传递参数时保持参数的原始类别(左值或右值)不变的能力。
- 实现机制:通过结合万能引用、引用折叠以及
std::static_cast
来实现。接下来,我们深入探讨这两个概念的重要性和实际应用:
重要性:
- 完美转发确保了函数模板在调用其他函数时,能够将参数的左值或右值属性传递给被调用的函数,从而支持移动语义和避免不必要的拷贝。
- 万能引用是实现完美转发的关键,因为它允许函数模板参数适应不同的引用类型。
实际应用:
- 在编写泛型代码、库或者框架时,万能引用和完美转发可以帮助开发者设计出更加通用和高效的接口。
- 例如,在实现泛型容器类或者智能指针时,完美转发可以确保元素在插入或移除时的资源管理是最优的。
如下:
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<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
由于我们并没有使用完美转发,那么虽然我们是在万能引用下传入的值,但是由于右值被右值引用后属性是左值,因此会得到如下的结果:
在使用了完美转发后:
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<typename T>
void PerfectForward(T&& t)
{
Fun(forward<T>(t));
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
感谢你耐心的看到这里ღ( ´・ᴗ・` )比心,如有哪里有错误请踢一脚作者o(╥﹏╥)o!
给个三连再走嘛~