c++ 左值 广义左值 右值 纯右值 将亡值

C/C++编程语言区分左值和右值,这源于历史原因、内存管理和语法规则。左值对应于可命名、可被赋值的变量,而右值通常用于临时表达式。这种区分有利于编译器优化,如移动语义,提高性能。在方法参数传递中,左值用于需要修改的对象,右值用于只读或资源转移。C++11引入的右值引用扩展了左值的概念,允许更灵活的编程和性能优化。
摘要由CSDN通过智能技术生成
  • 为什么C/C++等少数编程语言要区分左右值?
  1. 历史原因: C语言作为一门古老的编程语言,其设计初衷是为了在硬件资源有限的系统上进行高效的编程,因此其语法和语义设计相对较简单。左值和右值的概念最初是由C语言引入的,而C++则在此基础上进行了扩展。这种设计历史原因导致了C/C++等语言中需要区分左值和右值。

  2. 内存管理: C/C++是一种比较底层的编程语言,直接操作内存。左值通常对应于具名变量,它们在内存中有固定的存储位置,可以被多次引用。而右值通常对应于临时的表达式或者字面值,它们在内存中没有固定的存储位置,只是临时的值。区分左值和右值有助于编译器在内存管理方面做出合适的优化,例如对右值进行临时对象的优化,避免不必要的内存拷贝。

  3. 语法规则: C/C++等语言中有许多操作只能对左值或只能对右值进行,例如赋值语句中等号左边必须是左值,而函数调用中实参必须是右值。如果没有左值和右值的区分,这些语法规则就无法定义和解释。这种语义上的区别使得编程语言可以在语法层面对不同类型的表达式和变量进行限制和处理。

  4. 性能优化: 区分左值和右值有助于编译器进行性能优化。例如,C++11引入的移动语义(Move Semantics)就是利用了右值的特性,通过将临时对象的资源(如内存)直接转移给目标对象,避免了不必要的拷贝操作,从而提高了性能。

总的来说,C/C++等语言区分左值和右值是为了在内存管理、语义和性能优化等方面提供更多的灵活性和效率。然而,对于初学者来说,理解和使用左值和右值可能会有一定的难度,因此在编写代码时需要仔细考虑它们的语法和语义规则。

  • 对于方法来说,传递左值或右值作为参数各有不同的用途和行为:
    • 传递左值:将左值传递给方法,方法可以对其进行修改,并且对传递的左值的修改会在函数调用后保持。这对于需要在方法内部修改传递的对象,并且希望在函数调用后对原对象产生影响的情况非常有用。
    • 传递右值:将右值传递给方法,方法可以从其获取值或者资源,并在方法内部进行处理,例如移动语义(move semantics)的情况下,可以将右值的资源转移到方法内部,从而避免不必要的复制操作。传递右值还可以用于实现完美转发(perfect forwarding),将右值引用传递给下游函数。

在一般情况下,对于方法的入参:
如果方法需要修改传递的对象或者希望在函数调用后对原对象产生影响,则传递左值作为参数。
如果方法只需要获取传递的值或者资源,并且不需要修改传递的对象,或者希望实现移动语义或完美转发,则传递右值作为参数。这可以根据方法的具体需求和语义来进行选择。

需要注意的是,在 C++11 及之后的版本中,通过使用引用折叠规则(reference collapsing rules)和右值引用(rvalue reference),可以实现在方法中同时接收左值和右值的参数,从而在函数调用时不需要显式地区分左值和右值。这可以提供更加灵活和高效的参数传递方式。

表达式: 由运算符和运算对象构成的计算式。字面量、变量、函数返回值都是表达式。 表达式返回的结果,有两个属性:类型和值类别。

左值(lvalue)

  • 可以出现在赋值运算符左边的表达式。可以取地址的表达式

    • 左值(lvalue)是指一个可以标识内存位置并且可以被赋值的表达式 std::string str2; int i; 这些是左值因为它在内存中有一个名字和固定的存储位置
  • 可以通过 ‘&’ 取到左值的地址

  • 可修改的左值可用作 ‘=’ 的左操作数

  • 可用于初始化左值引用

  • 左值引用

    • lvalue reference 是一种引用类型,它指向一个已命名的内存位置(左值)
    • 比如:int& y;
//左值引用只可以绑定左值,而无法与右值绑定
int& i=42; //无法编译

//C++在初期阶段尚不具备右值引用的特性,而在现实中,代码却要向接受引用的函数传入临时变量,因而早期的C++标准破例特许了这种绑定方式。
int const& i=42;//ok  能将右值绑定到const左值引用上

// 绑定到const左值引用上示例:
//void print(std::string const& s);
print("hello"); //创建std::string类型的临时变量
void modifyLeftValue(int& x) {
    x = x + 1; // 修改传递的左值
}

int main(){
    int num = 10;
    modifyLeftValue(num); // 传递左值给方法
    std::cout << num << std::endl; // 输出 11,因为方法内部修改了传递的左值
    return 0;
}
//变量:包括普通变量、引用(包括左值引用和常量引用)以及指针。
int a = 10;        // 普通变量
int& b = a;       // 左值引用
const int& c = a; // 常量引用
int* ptr = &a;    // 指针

//数组元素:数组的元素可以通过索引访问,并且可以用作左值。
int arr[5] = {1, 2, 3, 4, 5};
arr[0] = 10; // 数组元素作为左值

//类成员:类的成员变量可以作为左值,包括普通成员变量和引用类型的成员变量。
class MyClass {
public:
    int x;
    int& y;
    MyClass(int a, int b) : x(a), y(b) {}
};

MyClass obj(1, 2);
obj.x = 10; // 类成员变量作为左值
obj.y = 20; // 类引用成员变量作为左值


//解引用指针:解引用指针可以得到一个左值。
int a = 10;
int* ptr = &a;
*ptr = 20; // 解引用指针作为左值


//由于 x 是一个静态局部变量,它会在程序的整个生命周期中存在,因此返回的引用在整个程序的生命周期内都是有效的,不会导致 悬空引用 的问题。
//如果 x 是一个局部变量,就会出现 悬空引用 问题
//函数返回的引用:如果一个函数返回一个引用类型的值,那么该返回值可以作为左值。
int& getReference() {
    static int x = 10;
    return x;
}
getReference() = 30; // 函数返回的引用作为左值


常见的左值表达式包括:

  • 变量,对象。例如, int i
  • 返回类型为左值引用的函数调用或重载运算符表达式,例如 str1 = str2、++it
  • 所有内建的赋值及复合赋值表达式,例如 a = b、a += b
  • 内建的前置自增前置自减 或 后置xx,例如 ++i、–i
  • 内建的间接寻址表达式,例如 *p
  • 字符串字面量,例如 “hello world”
    • 字符串字面量是指在代码中用双引号括起来的字符串常量,如:“hello world”。它们是固定的、只读的字符序列,存储在程序的内存空间中,并且不具有内存地址,因此不能作为左值使用。
    • 在 C++ 中,字符串字面量是右值,因为它们是只读的,不能直接获取其地址或更改其内容
  • 内建的下标表达式, 例如 a[n] 是左值 左值(lvalue)是指一个可以标识内存位置并且可以被赋值的表达式
  • 迭代器是左值,例如 vecotr::iterator iter
void test() {
    "hello world";//是右值
}
//常量(Constants):常量是无法改变的值,例如整数、浮点数等。尽管它们在语法上可以被视为左值,但它们不能作为左值引用的目标,因此不是广义左值。
const int a = 10;
a = 20; // 错误,a 是左值但不是广义左值
//数组(Arrays):数组在语法上可以被视为左值,但不能作为左值引用的目标,因此不是广义左值。
int arr[5];
arr = nullptr; // 错误(Array type 'int [5]' is not assignable),arr 是左值但不是广义左值

当声明一个数组int arr[5];时,编译器会在内存中为该数组分配5个整数大小的存储单元,并将arr指向这块内存。
由于数组的地址是固定的,所以不能用arr = nullptr;这样的赋值语句将其指向nullptr。

如果你想将数组置为空,可以通过循环遍历数组并将每个元素设置为0或者其他特定的值来实现

广义左值(Generalized lvalue)

  • 指可以出现在赋值运算符左边的表达式,包括左值(lvalue)和一些具有类似于左值性质的表达式。C++11 标准引入了广义左值的概念,扩展了左值的范围,使得一些原本不能作为左值的表达式也可以用于赋值操作。
  • 广义左值并不是所有可以出现在赋值运算符左边的表达式都可以作为左值使用,因为它们仍然受到语言规则和类型系统的限制。广义左值的引入主要是为了支持更灵活的赋值和修改操作,提供更方便的编程方式。
//临时对象(Temporary objects):临时对象是由表达式生成的临时对象,例如函数返回的临时对象。虽然临时对象在语法上可以被视为左值,但它们不能作为左值引用的目标,因此不是真正的左值。
int foo() { return 42; }
foo() = 10; // 错误,foo() 是广义左值但不是左值


//表达式的结果(Result of an expression):一些表达式的结果可以被视为广义左值,例如赋值操作符(=)的返回值。尽管它们在语法上可以被视为左值,但它们不能作为左值引用的目标,因此不是真正的左值。
int a = 10;
int b = 20;
(a + b) = 30; // 错误,(a + b) 是广义左值但不是左值

右值(rvalue)

  • C++11标准采纳了右值引用这一新特性,它只与右值绑定,而不绑定左值
int&& i=42;
int j=42;
int&& k=j;// 编译失败
  • 指临时的、不可取地址的表达式;可以通过使用双 ampersand (&&) 作为引用类型来实现。

  • 右值引用允许将临时对象(右值)绑定到引用上,并在方法中修改它们。C++ 的语言规范,对于右值引用绑定的临时对象,其生命周期通常在方法退出后结束,因此在方法退出后,对这个临时对象的修改可能会导致未定义行为。

  • 右值引用的设计初衷是用于优化性能,例如通过避免不必要的对象复制

  • 要避免对右值引用绑定的临时对象进行修改后导致未定义行为,可以使用 std::move 函数将右值引用转换为左值引用,从而使得在方法中修改的是原始对象而不是临时对象。

  • 谈到右值,就引出移动语义,右值往往是临时变量,故可以自由改变。假设我们预先知晓函数参数是右值,就能让其充当临时数据,或“窃用”它的内容而依然保持程序正确运行,那么我们只需移动右值参数而不必复制本体。按这种方式,如果数据结构体积巨大,而且需要动态分配内存,则能省去更多的内存操作,创造出许多优化的机会。

void processRightValue(std::string&& str) {
    // Processing: Hello , str : 0x308c32280
    std::cout << "Processing: " << str <<" , str : "<<&str<< std::endl; // 使用传递的右值
}

int main(){
    processRightValue("Hello"); // 传递右值给方法
    // 注意:传递的右值字符串在方法调用后会被销毁,因为它是临时的,不可取地址的表达式
    return 0;
}


```cpp
void test() {
    "hello world";//是右值
}

//

void processRightValue(std::string&& str) {
    std::cout << "Processing: " << str <<" , str : "<<&str<< std::endl;
    str = "xxx";
}

int main(){
    std::string val = "hello";
    //std::move(val) 和 std::forward<std::string>(val) 传递进去的值被修改的调用后 都能保留
    processRightValue(std::move(val));
    std::cout<<"--val : "<<val<<std::endl;//xxx
    return 0;
}

纯右值(Pure Right-hand-side Value)

  • 是一种表达式类型,通常可以作为右值引用(Rvalue Reference)的绑定目标。纯右值是指在表达式求值后不再被使用的临时对象,它可以被移动语义(Move Semantics)优化,从而避免不必要的拷贝操作,提高代码效率。
//临时对象:例如通过调用函数返回值、执行类型转换、进行算术运算等产生的临时对象都可以是纯右值。
int x = 1 + 2; // 表达式 1 + 2 是纯右值

//字面量:例如整型、浮点型、字符型等字面量常量都可以是纯右值。
字面量:例如整型、浮点型、字符型等字面量常量都可以是纯右值。

//强制类型转换产生的临时对象:例如使用 static_cast、dynamic_cast、const_cast、reinterpret_cast 等强制类型转换操作时,产生的临时对象可以是纯右值。
int z = static_cast<int>(3.14); // 强制类型转换产生的临时对象是纯右值

//在 C++11 标准引入右值引用之后,纯右值的概念扩展了,包括了具有右值引用类型的表达式也可以被视为纯右值。
int a = 1;
int&& rvalue_ref = std::move(a); // 右值引用是纯右值

纯右值和右值在实际使用中可能有一些细微的区别和限制,具体取决于编译器和语言标准的实现。

  • 纯右值和右值之间存在以下几个区别:
    • 定义:纯右值是指在表达式求值后不再被使用的临时对象,可以作为右值引用(Rvalue Reference)的绑定目标。而右值是指在表达式求值后,其值可以被移动(Move)但不可以被复制(Copy)的表达式,包括纯右值和具有右值引用类型的表达式。
    • 生命周期:纯右值的生命周期较短,通常仅在表达式求值期间有效,并且无法被持久化。而右值则可能具有更长的生命周期,例如通过右值引用延长其生命周期。
    • 可用性:纯右值通常在表达式求值后不再可用,不能再进行其他操作,因为它们是临时对象。而右值可能具有更长的可用性,例如通过右值引用继续使用它们。
    • 移动语义:纯右值在进行赋值或传递时,可以通过移动语义(Move Semantics)进行高效的资源转移,避免不必要的拷贝操作。而右值也可以通过移动语义进行资源转移,但具有更广泛的适用性,包括纯右值和具有右值引用类型的表达式。
    • 使用场景:纯右值主要用于临时对象的创建和传递,例如通过函数返回值、执行类型转换、进行算术运算等产生的临时对象。而右值则可以更广泛地用于各种场景,包括作为函数参数、返回值、成员变量、局部变量等。右值引用还可以用于实现移动语义、完美转发等高效的 C++ 特性。

将亡值(Expiring Value)

  • 亡值实际上是 C++ 11 引入了右值引用的概念而引入的,在C++ 11 之前,右值可以等价于纯右值。 亡值与右值引用息息相关。

    • 右值是临时值或无法取地址的表达式。亡值是右值的一种特殊形式,它表示即将被销毁的对象。
  • 将亡值指即将被销毁的临时对象或右值引用。C++11引入了将亡值的概念,它是一种介于左值和纯右值之间的临时对象。
    *

  • 需要注意的是,将亡值在转移资源所有权的同时,会让原始对象变为无效状态。因此,在使用将亡值时需要小心,确保不会在对象变为无效状态后再次访问它。同时,也需要了解不同编译器对将亡值的优化行为可能有所不同,因此在编写跨平台或可移植性较强的代码时需要谨慎使用将亡值。

  • 亡值的引入主要是为了优化资源管理和提高性能,通过移动语义来减少不必要的拷贝操作。常见的情况是当右值引用被绑定到一个将要销毁的临时对象时,该对象就成为亡值。

将亡值主要用于在C++11中引入的移动语义,允许将资源(如内存或文件句柄)从一个对象转移到另一个对象,而不进行昂贵的资源拷贝操作。将亡值通常出现在以下情况:

int main(){
    std::vector<int> source = {1, 2, 3};
    // 使用std::move将左值source转换为将亡值
    // std::move(source)返回一个将亡值,将source转移到右值引用xvalue_ref,在此过程中,source的资源所有权被移动。
    std::vector<int>&& rightVector = std::move(source);
    // source 现在为空,其资源已经被转移给 xvalue_ref
    std::cout<<"hello"<<std::endl;
}
  1. 将亡值作为函数返回值:当函数返回一个临时对象时,C++编译器可以将其优化为将亡值,从而避免不必要的拷贝操作,提高性能。
// 返回右值引用的字符串
std::string&& createString() {
    //虽然str是一个局部变量,但通过std::move,它被转换为一个将亡值并返回,其资源的所有权被移动到result
    std::string str = "Hello, World!";
    return std::move(str); // 使用std::move将局部变量str转换为将亡值
}

int main() {
    std::string&& result = createString();

    // 输出:Hello, World!
    std::cout << result << std::endl;

    return 0;
}
  1. 将亡值作为函数参数:当将一个临时对象传递给函数时,C++编译器可以将其优化为将亡值,从而避免不必要的拷贝操作,提高性能。
void process_string(std::string&& str) {
    // 处理str,这里str是将亡值
}

std::string str = "Hello";
process_string(std::move(str)); // 使用std::move将局部变量str转换为将亡值
  1. std::forward用于完美转发:
// 模板函数使用std::forward完美转发将亡值
template<typename T>
void processValue(T&& value) {
    std::cout << "Process value: " << &value << std::endl;
}

int main(){
    TestInfo test;
    std::cout<<"test: " <<&test<<std::endl;
    processValue(test); // 调用接受左值引用的函数  地址和原对象相同
    processValue(std::move(test)); // 调用接受右值引用的函数       地址和原对象相同
    processValue(std::forward<TestInfo>(test)); // 完美转发,调用适当的函数     地址和原对象相同

    std::cout<<"hello"<<std::endl;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值