金融網(wǎng)站建設(shè)成功案例品牌關(guān)鍵詞優(yōu)化哪家便宜
【C++隨筆02】左值和右值
- 一、左值和右值
- 1、字面理解——左值、右值
- 2、字面理解的問(wèn)題
- 3、左值、右值
- 4、左值的特征
- 5、 右值的特征
- 6、x++和++x是左值還是右值
- 7、復(fù)合例子
- 8、通常字面量都是一個(gè)右值,除字符串字面量以外:
- 二、左值引用和右值引用
- 三、左值引用
- 1、常量左值引用
- 2、制構(gòu)造函數(shù)和復(fù)制賦值運(yùn)算符函數(shù)——左值引用
- 四、右值引用
- 1、移動(dòng)語(yǔ)義和完美轉(zhuǎn)發(fā)
- 2、移動(dòng)語(yǔ)義
- 3、完美轉(zhuǎn)發(fā)
- 3.1、簡(jiǎn)介、demo
- 3.2、問(wèn)題:c++中的完美轉(zhuǎn)發(fā)(std::forward)存在的意義?
- 簡(jiǎn)單總結(jié)
一、左值和右值
1、字面理解——左值、右值
最簡(jiǎn)單的字面理解,表達(dá)式等號(hào)左邊的值為左值,等號(hào)右邊的值為右值,比如
int x = 1;
int y = 3;
int z = x + y;
以上面的代碼為例,
- x是左值,1是右值;
- y是左值,3是右值;
- z是左值,x+y的結(jié)果是右值。(注意,是結(jié)果)
2、字面理解的問(wèn)題
在第一行代碼中我們判斷a是一個(gè)左值,它卻在第二行變成了右值,所以這就是問(wèn)題,要準(zhǔn)確地區(qū)分左值和右值還是應(yīng)該理解其內(nèi)在含義。
3、左值、右值
在C++中
- 左值一般是指一個(gè)指向特定內(nèi)存的具有名稱的值(具名對(duì)象),它有一個(gè)相對(duì)穩(wěn)定的內(nèi)存地址,并且有一段較長(zhǎng)的生命周期。
- 右值則是不指向穩(wěn)定內(nèi)存地址的匿名值(不具名對(duì)象)
簡(jiǎn)單來(lái)說(shuō),左值是一個(gè)有內(nèi)存地址的表達(dá)式,而右值是一個(gè)沒(méi)有內(nèi)存地址的表達(dá)式。
基于這一特征,我們可以用取地址符&來(lái)判斷左值和右值,能取到內(nèi)存地址的值為左值,否則為右值。還是以上面的代碼為例,因?yàn)?amp;a和&b都是符合語(yǔ)法規(guī)則的,所以a和b都是左值。
4、左值的特征
左值具有以下特征:
- 左值在內(nèi)存中有唯一的地址。
- 左值可以出現(xiàn)在賦值操作符的左邊。
- 左值可以被取地址操作符(&)獲取其內(nèi)存地址。
- 左值可以作為函數(shù)參數(shù)或返回值。
int x = 10; // x是一個(gè)左值
int* ptr = &x; // 獲取x的地址并賦給指針ptr
5、 右值的特征
右值具有以下特征:
- 右值沒(méi)有內(nèi)存地址。
- 右值不能作為賦值操作符的左邊。
- 右值不能被取地址操作符獲取其內(nèi)存地址。
int y = 20; // 20是一個(gè)右值
int z = x + y; // 表達(dá)式x + y的結(jié)果是一個(gè)右值
6、x++和++x是左值還是右值
int *p = &x++; // 編譯失敗
int *q = &++x; // 編譯成功
- x++是右值,因?yàn)樵诤笾?#43;+操作中編譯器首先會(huì)生成一份x值的臨時(shí)復(fù)制,然后才對(duì)x遞增,最后返回臨時(shí)復(fù)制內(nèi)容。
- ++x則不同,它是直接對(duì)x遞增后馬上返回其自身,所以++x是一個(gè)左值。
7、復(fù)合例子
int x = 1;
int get_val()
{return x;
}
void set_val(int val)
{x = val;
}
int main()
{x++;++x;int y = get_val();set_val(6);
}
- get_val函數(shù),該函數(shù)返回了一個(gè)全局變量x,雖然很明顯變量x是一個(gè)左值,但是它經(jīng)過(guò)函數(shù)返回以后變成了一個(gè)右值。
原因和x++類似,在函數(shù)返回的時(shí)候編譯器并不會(huì)返回x本身,而是返回x的臨時(shí)復(fù)制,所以int * p = &get_val();也會(huì)編譯失敗。
- set_val函數(shù),該函數(shù)接受一個(gè)參數(shù)并且將參數(shù)的值賦值到x中。在main函數(shù)中set_val(6);實(shí)參6是一個(gè)右值,但是進(jìn)入函數(shù)之后形參val卻變成了一個(gè)左值。
我們可以對(duì)val使用取地址符.
- 左值到右值的隱式轉(zhuǎn)換
左值可以被隱式地轉(zhuǎn)換為右值,例如在某些表達(dá)式中,需要右值而傳入一個(gè)左值參數(shù)時(shí),編譯器會(huì)自動(dòng)進(jìn)行轉(zhuǎn)換。
void printValue(int value)
{cout << value << endl;
}int x = 10;
printValue(x); // 編譯器將x隱式地轉(zhuǎn)換為右值傳遞給函數(shù)
8、通常字面量都是一個(gè)右值,除字符串字面量以外:
int x = 1;
set_val(6);
auto p = &“hello world”;
編譯器會(huì)將字符串字面量存儲(chǔ)到程序的數(shù)據(jù)段中,程序加載的時(shí)候也會(huì)為其開(kāi)辟內(nèi)存空間(rodata),所以我們可以使用取地址符&來(lái)獲取字符串字面量的內(nèi)存地址。
- 給大家寫一個(gè)有編譯錯(cuò)誤的例子,大家調(diào)調(diào)
#include <iostream>int add1(int& x)
{return x+1;
}int main() {int a = add1(1);std::cout << a << std::endl;return 0;
}
二、左值引用和右值引用
void processValue(int& value) {// 對(duì)左值進(jìn)行處理
}void processValue(int&& value) {// 對(duì)右值進(jìn)行處理
}
-
左值引用和右值引用在C++11中,引入了右值引用的概念。左值引用(L-value references)用于綁定到左值,右值引用(R-value references)用于綁定到右值。
-
左值引用的聲明使用單個(gè)&符號(hào):
int x = 10;
int& lvalueRef = x; // 左值引用綁定到左值x
- 右值引用的聲明使用兩個(gè)&&符號(hào):
int y = 20;
int&& rvalueRef = y + 30; // 右值引用綁定到右值表達(dá)式的結(jié)果
引用可以方便地對(duì)變量進(jìn)行修改或者將其傳遞給函數(shù)。
右值引用在移動(dòng)語(yǔ)義(Move Semantics)和完美轉(zhuǎn)發(fā)(Perfect Forwarding)中具有重要的作用,可以提高性能和代碼效率。
三、左值引用
1、常量左值引用
常量左值引用的特性顯得更加有趣,它除了能引用左值,還能夠引用右值,比如:
int &x1 = 7; // 編譯錯(cuò)誤
const int &x = 11; // 編譯成功
在上面的代碼中,第一行代碼會(huì)編譯報(bào)錯(cuò),因?yàn)閕nt&無(wú)法綁定一個(gè)int類型的右值,但是第二行代碼卻可以編譯成功。請(qǐng)注意,雖然在結(jié)果上const int &x = 11和const int x = 11是一樣的,但是從語(yǔ)法上來(lái)說(shuō),前者是被引用了,所以語(yǔ)句結(jié)束后11的生命周期被延長(zhǎng),而后者當(dāng)語(yǔ)句結(jié)束后右值11應(yīng)該被銷毀。雖然常量左值引用可以引用右值的這個(gè)特性在賦值表達(dá)式中看不出什么實(shí)用價(jià)值,但是在函數(shù)形參列表中卻有著巨大的作用。一個(gè)典型的例子就是復(fù)制構(gòu)造函數(shù)和復(fù)制賦值運(yùn)算符函數(shù),
2、制構(gòu)造函數(shù)和復(fù)制賦值運(yùn)算符函數(shù)——左值引用
當(dāng)我們使用左值引用時(shí),通常會(huì)涉及到復(fù)制構(gòu)造函數(shù)和復(fù)制賦值運(yùn)算符函數(shù)。復(fù)制構(gòu)造函數(shù)用于創(chuàng)建一個(gè)新對(duì)象,并將其初始化為已存在的對(duì)象的副本。復(fù)制賦值運(yùn)算符函數(shù)用于將一個(gè)已存在的對(duì)象的值復(fù)制給另一個(gè)已存在的對(duì)象。
以下是一個(gè)簡(jiǎn)單的示例程序,演示了左值引用、復(fù)制構(gòu)造函數(shù)和復(fù)制賦值運(yùn)算符函數(shù)的使用:
#include <iostream>class MyObject {
private:int data;public:MyObject(int d) : data(d) {std::cout << "Constructor called with value: " << d << std::endl;}MyObject(const MyObject& other) : data(other.data) {std::cout << "Copy constructor called. Copied value: " << data << std::endl;}MyObject& operator=(const MyObject& other) {if (this != &other) {data = other.data;std::cout << "Copy assignment operator called. Copied value: " << data << std::endl;}return *this;}void printData() const {std::cout << "Data: " << data << std::endl;}
};int main() {MyObject obj1(42); // 調(diào)用構(gòu)造函數(shù)MyObject obj2(obj1); // 調(diào)用復(fù)制構(gòu)造函數(shù)MyObject obj3 = obj1; // 調(diào)用復(fù)制構(gòu)造函數(shù)MyObject obj4(55); // 調(diào)用構(gòu)造函數(shù)obj4 = obj1; // 調(diào)用復(fù)制賦值運(yùn)算符函數(shù)obj1.printData(); // 輸出: Data: 42obj2.printData(); // 輸出: Data: 42obj3.printData(); // 輸出: Data: 42obj4.printData(); // 輸出: Data: 42return 0;
}
輸出
Constructor called with value: 42
Copy constructor called. Copied value: 42
Copy constructor called. Copied value: 42
Constructor called with value: 55
Copy assignment operator called. Copied value: 42
Data: 42
Data: 42
Data: 42
Data: 42
在上述示例中,我們首先定義了一個(gè)名為 MyObject 的類,該類具有一個(gè)帶有整數(shù)參數(shù)的構(gòu)造函數(shù)、一個(gè)復(fù)制構(gòu)造函數(shù)和一個(gè)復(fù)制賦值運(yùn)算符函數(shù)。然后我們創(chuàng)建了幾個(gè) MyObject 類型的對(duì)象,并通過(guò)不同方式進(jìn)行初始化和賦值。
在 main() 函數(shù)中,我們創(chuàng)建了 obj1,并使用拷貝構(gòu)造函數(shù)將其值分別復(fù)制給 obj2 和 obj3。接下來(lái),我們創(chuàng)建了 obj4,然后使用賦值運(yùn)算符將 obj1 的值復(fù)制給 obj4。
最后,我們調(diào)用各個(gè)對(duì)象的成員函數(shù) printData() 來(lái)打印它們的數(shù)據(jù)值。你可以看到,obj2、obj3 和 obj4 的數(shù)據(jù)值都與 obj1 相同,這表明復(fù)制構(gòu)造函數(shù)和復(fù)制賦值運(yùn)算符函數(shù)成功地將一個(gè)對(duì)象的值復(fù)制給了另一個(gè)對(duì)象。
四、右值引用
顧名思義,右值引用是一種引用右值且只能引用右值的方法。在語(yǔ)法方面右值引用可以對(duì)比左值引用,在左值引用聲明中,需要在類型后添加&,而右值引用則是在類型后添加&&,例如:
int i = 0;
int &j = i; // 左值引用
int &&k = 11; // 右值引用
在上面的代碼中,k是一個(gè)右值引用,如果試圖用k引用變量i,則會(huì)引起編譯錯(cuò)誤。右值引用的特點(diǎn)之一是可以延長(zhǎng)右值的生命周期。
1、移動(dòng)語(yǔ)義和完美轉(zhuǎn)發(fā)
右值引用在移動(dòng)語(yǔ)義和完美轉(zhuǎn)發(fā)中起著重要的作用。
-
移動(dòng)語(yǔ)義:移動(dòng)語(yǔ)義是指通過(guò)右值引用將資源(如動(dòng)態(tài)分配的內(nèi)存、文件句柄等)從一個(gè)對(duì)象轉(zhuǎn)移到另一個(gè)對(duì)象,而不是進(jìn)行深拷貝。這樣可以避免不必要的內(nèi)存分配和釋放,提高程序性能。
-
完美轉(zhuǎn)發(fā):完美轉(zhuǎn)發(fā)是指將一個(gè)函數(shù)中的參數(shù)以原樣傳遞給另一個(gè)函數(shù),包括參數(shù)的左值或右值屬性信息。通過(guò)使用右值引用和模板,可以實(shí)現(xiàn)完美轉(zhuǎn)發(fā),避免了不必要的拷貝。
2、移動(dòng)語(yǔ)義
當(dāng)涉及到移動(dòng)語(yǔ)義和完美轉(zhuǎn)發(fā)時(shí),我們需要先了解一些基本概念和問(wèn)題。在C++中,對(duì)象的拷貝構(gòu)造函數(shù)(Copy Constructor)和拷貝賦值運(yùn)算符(Copy Assignment Operator)會(huì)對(duì)資源進(jìn)行拷貝操作,這可能導(dǎo)致內(nèi)存分配和釋放的開(kāi)銷。在某些情況下,我們希望能夠高效地轉(zhuǎn)移資源的所有權(quán)而不是進(jìn)行深拷貝,這就引入了移動(dòng)語(yǔ)義和完美轉(zhuǎn)發(fā)。
移動(dòng)語(yǔ)義(Move Semantics)
移動(dòng)語(yǔ)義是指通過(guò)右值引用將資源(如動(dòng)態(tài)分配的內(nèi)存、文件句柄等)從一個(gè)對(duì)象轉(zhuǎn)移到另一個(gè)對(duì)象,而不是進(jìn)行深拷貝。移動(dòng)語(yǔ)義可以大大提高程序性能,因?yàn)樗苊饬瞬槐匾膬?nèi)存分配和釋放。
移動(dòng)語(yǔ)義的實(shí)現(xiàn)依賴于右值引用。右值引用(R-value Reference)通過(guò)雙個(gè)&&符號(hào)進(jìn)行聲明,并且可以綁定到右值。通過(guò)移動(dòng)構(gòu)造函數(shù)(Move Constructor)和移動(dòng)賦值運(yùn)算符(Move Assignment Operator),可以使用移動(dòng)語(yǔ)義來(lái)實(shí)現(xiàn)資源的高效轉(zhuǎn)移。
簡(jiǎn)單來(lái)說(shuō),移動(dòng)語(yǔ)義允許我們從一個(gè)臨時(shí)的右值對(duì)象或者一個(gè)將要被銷毀的對(duì)象中“竊取”資源,然后將其傳遞給新對(duì)象,而無(wú)需進(jìn)行資源的復(fù)制操作。
示例代碼:
#include <iostream>class MyObject {
private:int data;public:MyObject(int d = 0) : data(d) {std::cout << "Constructor called with value: " << d << std::endl;}MyObject(const MyObject& other) : data(other.data) {std::cout << "Copy constructor called. Copied value: " << data << std::endl;}MyObject& operator=(const MyObject& other) {if (this != &other) {data = other.data;std::cout << "Copy assignment operator called. Copied value: " << data << std::endl;}return *this;}MyObject(MyObject&& other) noexcept : data(other.data) {std::cout << "Move constructor called. Moved value: " << data << std::endl;other.data = 0; // 清空原對(duì)象的值}MyObject& operator=(MyObject&& other) noexcept {if (this != &other) {data = other.data;std::cout << "Move assignment operator called. Moved value: " << data << std::endl;other.data = 0; // 清空原對(duì)象的值}return *this;}void printData() const {std::cout << "Data: " << data << std::endl;}
};int main() {MyObject obj1(42); // 調(diào)用構(gòu)造函數(shù)MyObject obj2(obj1); // 調(diào)用復(fù)制構(gòu)造函數(shù)MyObject obj3 = obj1; // 調(diào)用復(fù)制構(gòu)造函數(shù)MyObject obj4(55); // 調(diào)用構(gòu)造函數(shù)obj4 = obj1; // 調(diào)用復(fù)制賦值運(yùn)算符函數(shù)MyObject obj5(std::move(obj1)); // 調(diào)用移動(dòng)構(gòu)造函數(shù)MyObject obj6 = std::move(obj2); // 調(diào)用移動(dòng)賦值構(gòu)造函數(shù)obj1.printData(); // 輸出: Data: 42obj2.printData(); // 輸出: Data: 0obj3.printData(); // 輸出: Data: 42obj4.printData(); // 輸出: Data: 42obj5.printData(); // 輸出: Data: 42obj6.printData(); // 輸出: Data: 42return 0;
}
Constructor called with value: 42
Copy constructor called. Copied value: 42
Copy constructor called. Copied value: 42
Constructor called with value: 55
Copy assignment operator called. Copied value: 42
Move constructor called. Moved value: 42
Move constructor called. Moved value: 42
Data: 0
Data: 0
Data: 42
Data: 42
Data: 42
Data: 42
- obj1和obj2為0,這證明了移動(dòng)構(gòu)造函數(shù)被調(diào)用,并且資源成功被轉(zhuǎn)移。
3、完美轉(zhuǎn)發(fā)
3.1、簡(jiǎn)介、demo
完美轉(zhuǎn)發(fā)(perfect forwarding)是C++11引入的概念,用于在函數(shù)模板中將參數(shù)按原樣傳遞給另一個(gè)函數(shù),同時(shí)保留其值類別(lvalue或rvalue)。它可以在保持精確性的同時(shí)避免不必要的復(fù)制或移動(dòng)操作,提高代碼的效率。
完美轉(zhuǎn)發(fā)通常與轉(zhuǎn)發(fā)引用類型參數(shù)(如模板中的萬(wàn)能引用)一起使用,以實(shí)現(xiàn)泛型編程中的參數(shù)傳遞。在函數(shù)模板中,我們可以使用 std::forward 函數(shù)來(lái)進(jìn)行完美轉(zhuǎn)發(fā)。
下面是一個(gè)使用完美轉(zhuǎn)發(fā)的示例:
#include <iostream>
#include <utility>// 定義一個(gè)接收傳入?yún)?shù)的函數(shù)
void process(int& i) {std::cout << "Lvalue: " << i << std::endl;i = 100; // 修改傳入的左值參數(shù)
}void process(int&& i) {std::cout << "Rvalue: " << i << std::endl;
}// 將參數(shù)轉(zhuǎn)發(fā)到 process 函數(shù)
template <typename T>
void wrapper(T&& arg) {process(std::forward<T>(arg));
}int main() {int a = 42;wrapper(a); // 傳遞左值wrapper(123); // 傳遞右值std::cout << "a: " << a << std::endl;int b = 42;process(b);process(123);std::cout << "b: " << a << std::endl;return 0;
}
輸出
Lvalue: 42
Rvalue: 123
a: 100
Lvalue: 42
Rvalue: 123
b: 100
在上述示例中,我們定義了兩個(gè)重載的 process 函數(shù),一個(gè)接受左值引用參數(shù),一個(gè)接受右值引用參數(shù)。然后,我們創(chuàng)建了一個(gè)模板函數(shù) wrapper,該函數(shù)接受一個(gè)通用引用類型的參數(shù) T&& arg。
在 wrapper 函數(shù)內(nèi)部,我們通過(guò)調(diào)用 std::forward 來(lái)進(jìn)行完美轉(zhuǎn)發(fā),將 arg 參數(shù)原封不動(dòng)地傳遞給 process 函數(shù)。std::forward 根據(jù)參數(shù)的值類別(lvalue還是rvalue)將參數(shù)作為對(duì)應(yīng)類型的引用進(jìn)行轉(zhuǎn)發(fā)。
在 main 函數(shù)中,我們先傳遞了一個(gè)左值 x 給 wrapper 函數(shù),然后傳遞了一個(gè)右值 123。程序會(huì)根據(jù)參數(shù)的值類別,選擇調(diào)用合適的 process 函數(shù),并輸出相應(yīng)的結(jié)果。
使用完美轉(zhuǎn)發(fā)可以避免不必要的拷貝和移動(dòng)操作,提高代碼的效率和性能。
3.2、問(wèn)題:c++中的完美轉(zhuǎn)發(fā)(std::forward)存在的意義?
-
我們從上面的demo中可以看到,既然不使用forward也可以達(dá)到類似的效果,那為何還要使用完美轉(zhuǎn)發(fā)(std::forward)呢?
-
說(shuō)這個(gè)問(wèn)題前,我們先把上面的一個(gè)demo改下
#include <iostream>class MyObject {
private:int data;public:MyObject(int d = 0) : data(d) {std::cout << "Constructor called with value: " << d << std::endl;}MyObject(const MyObject& other) : data(other.data) {std::cout << "Copy constructor called. Copied value: " << data << std::endl;}MyObject& operator=(const MyObject& other) {if (this != &other) {data = other.data;std::cout << "Copy assignment operator called. Copied value: " << data << std::endl;}return *this;}MyObject(MyObject&& other) noexcept : data(other.data) {std::cout << "Move constructor called. Moved value: " << data << std::endl;other.data = 0; // 清空原對(duì)象的值}MyObject& operator=(MyObject&& other) noexcept {if (this != &other) {data = other.data;std::cout << "Move assignment operator called. Moved value: " << data << std::endl;other.data = 0; // 清空原對(duì)象的值}return *this;}void printData() const {std::cout << "Data: " << data << std::endl;}
};template<typename T>
T* createObject(T&& t)
{return new MyObject(t);
}int main() {MyObject* newMyObject = createObject(std::move(MyObject(110)));return 0;
}
- 大家猜猜看,上面應(yīng)該輸出什么?應(yīng)該調(diào)用移動(dòng)構(gòu)造函數(shù)的,對(duì)吧,實(shí)際情況輸出如下,調(diào)用的卻是拷貝構(gòu)造函數(shù)。
Constructor called with value: 110
Copy constructor called. Copied value: 110
- 是不是很好奇,為啥調(diào)用的是拷貝構(gòu)造函數(shù),而非我們之前想象的移動(dòng)構(gòu)造函數(shù)呢?
我們把createObject做下簡(jiǎn)單的更改,增加std::forward,
template<typename T>
T* createObject(T&& t)
{return new MyObject(std::forward<T>(t));
}
- 結(jié)果輸出如下
Constructor called with value: 110
Move constructor called. Moved value: 110
這時(shí),才真正的如我們所想,真的調(diào)用了移動(dòng)構(gòu)造函數(shù)。
- 分析:經(jīng)歷了模版參數(shù)t這一次轉(zhuǎn)發(fā),t右值的屬性被改變?yōu)榱俗笾怠?/li>
簡(jiǎn)單總結(jié)
- 左值引用用于引用左值并允許修改,
- 右值引用用于引用右值并具有移動(dòng)語(yǔ)義和完美轉(zhuǎn)發(fā)的特性。