C++ 左值右值引用梳理(一)
C++ 左值右值引用梳理(一)
左值与右值的区别
在参考资料上看到这样一句话
https://www.internalpointers.com/post/understanding-meaning-lvalues-and-rvalues-c
In C++ an lvalue is something that points to a specific memory location. On the other hand, a rvalue is something that doesn’t point anywhere. In general, rvalues are temporary and short lived, while lvalues live a longer life since they exist as variables. It’s also fun to think of lvalues as containers and rvalues as things contained in the containers. Without a container, they would expire.
总的来说,左值可以获取地址,而右值不能。lvalue指的是可以放在赋值表达式左边的事物——在栈上或堆上分配的命名对象,或者其他对象成员——有明确的内存地址。rvalue指的是可以出现在赋值表达式右侧的对象——例如,文字常量和临时变量。
同样的,在《现代C++语言核心特性解析》中有这句话:
在C++中所谓的左值一般是指一个指向特定内存的具有名称的值(具名对象),它有一个相对稳定的内存地址,并且有一段较长的生命周期。而右值则是不指向稳定内存地址的匿名值(不具名对象),它的生命周期很短,通常是暂时性的。基于这一特征,我们可以用取地址符&来判断左值和右值,能取到内存地址的值为左值,否则为右值。
右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。左值可以在左边也可以在右边,右值只能在右边,举例说明
int x = 666; // ok
int* y = &x; // ok
666 = y; // error!
这里666是一个右值;一个字面常量,没有特定的内存地址。这个数字被分配给x,这是一个变量。一个变量有一个特定的内存位置,所以它是一个左值。C++声明赋值需要左值作为其左操作数。
第二句话,通过取地址运算符&将x的地址取出,&x是一个临时变量,是一个右值。
第三句话,666是一个右值,不能放在左边。
返回左值和右值的函数
对比这两个例子
int setValue()
{
return 6;
}
// ... somewhere in main() ...
setValue() = 3; // error!
第一个例子,setValue()返回一个右值(临时数字6),它不能是赋值的左操作数。
int global = 100;
int& setGlobal()
{
return global;
}
// ... somewhere in main() ...
setGlobal() = 400; // OK
第二个例子,setGlobal返回一个引用,不像上面的setValue()。
参考链接:
【C 语言】变量本质 ( 变量概念 | 变量本质 - 内存空间别名 | 变量存储位置 - 代码区 | 变量三要素 )
关于引用为什么可以做左值,可以参考:,其实你看底层,就是指针的简化,理解后就可以明白为什么可以做左值。
从外到内理解c++引用,一文清晰解读。
C++引用在本质上是什么,它和指针到底有什么区别?
左值引用和右值引用的区别
左值引用
顾名思义,左值引用就是给左值的引用,给左值取别名。右值引用就是对右值的引用,给右值取别名。而C++11中新增了的右值引用语法特性。
在赋值运算符左侧是左值引用符,右侧必须是左值;在赋值运算符左侧是右值引用符,右侧必须是右值。
看下面的例子:
int y = 10;
int& yref = y;
yref++; // y is now 11
int& yref = 10; // wrong
void fnc(int& x)
{
}
int main()
{
fnc(10); // Nope!
// This works instead:
// int x = 10;
// fnc(x);
}
但是下面的例子成功:
void fnc(const int& x)
{
}
int main()
{
fnc(10); // OK!
}
常量引用(const T&)可以绑定到右值(right-value),这是C++11引入的新特性之一。下面举例:
const int &r = 10;
实际上,编译器会创建一个临时对象,并让常量引用 r 绑定到这个临时对象上。
const int _temp = 10;
const int &r = _temp;
这个临时对象 _temp 是由编译器隐式创建的,并且它的生命周期至少持续到引用 r 的生命周期结束。为什么这个临时对象是const类型。回答是这样的:
1.字面量值:字面量值 10 本身就是一个不可修改的值。
2.常量引用:常量引用 const int &r 确保了引用的不可修改性,即不能通过引用 r 来修改所引用的对象。
临时对象具有常性,因此也需要常量引用来绑定。
右值引用
右值引用有什么优点呢:
1.绑定临时对象,正如上面提到
2.移动语义,当一个类有移动构造和复制构造时,会优先选择移动构造:
下面的链接很好的说明了这一点:
https://stackoverflow.com/questions/68184575/why-does-c-give-preference-to-rvalue-reference-over-const-reference-while-func
const T&& 有什么用呢,为什么还需要const的右值引用呢?是在你没有const T& 的时候
下面的例子回答了这一点:
https://www.sandordargo.com/blog/2021/08/18/const-rvalue-references
#include <iostream>
struct T {};
void f(T&) { std::cout << "lvalue ref\n"; } // #1
void f(const T&) { std::cout << "const lvalue ref\n"; } // #2
void f(T&&) { std::cout << "rvalue ref\n"; } // #3
void f(const T&&) { std::cout << "const rvalue ref\n"; } // #4
const T g() { return T{}; }
int main() {
f(g()); // #4, #2
}
可以看到2和4是一样的效果。
在文章中出现cv的字眼,意思是“const”和“volatile”限定符,它们可以用来修饰类型,以表明该类型的某些特性。
注意:如果返回const value,意味着你不能再使用移动语义。
#include <iostream>
class MyString {
public:
MyString(const char* str) {
m_length = strlen(str);
m_data = new char[m_length + 1];
strcpy(m_data, str);
}
// 拷贝构造函数
MyString(const MyString& other) {
m_length = other.m_length;
m_data = new char[m_length + 1];
strcpy(m_data, other.m_data);
std::cout << "Copy constructor called." << std::endl;
}
// 移动构造函数
MyString(MyString&& other) noexcept {
m_length = other.m_length;
m_data = other.m_data;
other.m_length = 0;
other.m_data = nullptr;
std::cout << "Move constructor called." << std::endl;
}
// 拷贝赋值运算符
MyString& operator=(const MyString& other) {
if (this != &other) {
delete[] m_data;
m_length = other.m_length;
m_data = new char[m_length + 1];
strcpy(m_data, other.m_data);
}
return *this;
}
// 移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] m_data;
m_length = other.m_length;
m_data = other.m_data;
other.m_length = 0;
other.m_data = nullptr;
}
return *this;
}
~MyString() {
delete[] m_data;
std::cout << "Destructor called." << std::endl;
}
// 其他成员函数...
private:
int m_length;
char* m_data;
};
void print(MyString s) {
std::cout << "String: " << s.m_data << std::endl;
}
int main() {
MyString s1("Hello");
// 使用临时对象调用移动构造函数
MyString s2 = MyString("World");
print(s2);
// 使用右值引用调用移动构造函数
MyString s3 = std::move(s1);
print(s3);
return 0;
}
3.完美转发,在后面会提到。
4.避免不必要的拷贝