回顧
const int c
的c是可以被取地址的,盡管是常量。所以以是否為常量來判斷是否為右值是錯誤的。
左值與右值正確的區分方法是是否能夠被取地址。(能被取地址也就代表著是一個持久狀態,即有持久的存儲空間的值)
常見的左值有我們定義的變量、對象,或者解引用表達式和傳引用返回。(比如`string s(“2077”);然后s[0]就是一個傳引用返回值,也就是一個左值)
常見的右值有常量、(表達式求值過程中創建的)臨時對象、匿名對象。
左值引用和右值引用可以交叉引用但是有條件:
-
const左值引用可以引用右值
-
右值引用可以引用move(左值)
(move是庫里面的一個函數模版,本質內部是進行強制類型轉換,涉及一些引用折疊的知識)
類型是我們對內存里一塊空間的定義,而C++是可以對類型進行轉換的,也就是對內存空間不同的解釋。
類型決定了語法意義上我們怎么對這塊空間的數據進行處理。
拿鏈表指針和迭代器舉個例子:
這里ptr和it同樣都是4個字節存放地址,但是卻解釋為不同的類型,意義就不同了、
所以我們知道,內存存的是數據,但只是存儲數據本身沒什么意義,而將數據解釋為不同的類型,意義就千變萬化了。
3.延長生命周期
這一般指的是臨時對象、匿名對象。因為它們一般生命周期只在當前行,而延長后可以與整個域生命周期一樣長。但將其從一個棧幀延長到另一個棧幀,是做不到的。
比如這個str的生命周期不可能延長到main棧幀里去
因為延長生命周期沒有改變它的存儲位置
在這里,main函數里調用了函數Func1,調用結束后Func1的棧幀是要回收的,下次調用Func2還占用的是這塊空間,所以怎么可能將Func1中的str單獨延長生命周期呢?
所以要搞清楚延長生命周期的對象是指的誰。
延長生命周期沒有改變存儲位置。
再看一例:
這里創建一個匿名對象,我們可以看到它在下一句代碼之前就析構了,說明匿名對象的生命周期只在這一行。
那么現在使用const左值引用對其生命周期進行延長:
可以看到延長之后它的生命周期就跟著引用走了
注意:
右值引用延長生命周期效果同樣如此。
4.編譯器對拷貝的優化之所以復雜,有兩方面的因素:一方面要支持c++委員會制定的語法新規則,另一方面要為了c++的高效適當進行優化。
左值引用和右值引用最終目的都是減少拷貝提高效率(左值引用還有其他使用場景比如輸出型參數,修改參數或返回值)
(補充)輸出型參數:
在C++中,輸出型參數通常通過指針或引用來實現,因為函數參數默認是按值傳遞的,直接傳遞普通變量無法修改外部變量的值。以下是C++中輸出型參數的實現方式:
1. 使用指針作為輸出型參數
通過傳遞指針,函數可以修改指針所指向的內存地址中的值。
#include <iostream>
using namespace std;// 函數定義,使用指針作為輸出型參數
void calculate(int a, int b, int* sum, int* product) {*sum = a + b; // 修改sum指向的值*product = a * b; // 修改product指向的值
}int main() {int x = 5, y = 10;int sumResult, productResult;// 傳遞變量的地址calculate(x, y, &sumResult, &productResult);cout << "Sum: " << sumResult << endl; // 輸出:Sum: 15cout << "Product: " << productResult << endl; // 輸出:Product: 50return 0;
}
說明:
int* sum 和 int* product 是指針參數,用于接收外部變量的地址。
在函數內部通過 *sum 和 *product 修改外部變量的值。
2. 使用引用作為輸出型參數
引用是C++中更安全和直觀的方式,可以直接操作外部變量。
#include <iostream>
using namespace std;// 函數定義,使用引用作為輸出型參數
void calculate(int a, int b, int& sum, int& product) {sum = a + b; // 直接修改sum引用的值product = a * b; // 直接修改product引用的值
}int main() {int x = 5, y = 10;int sumResult, productResult;// 傳遞變量的引用calculate(x, y, sumResult, productResult);cout << "Sum: " << sumResult << endl; // 輸出:Sum: 15cout << "Product: " << productResult << endl; // 輸出:Product: 50return 0;
}
說明:
int& sum 和 int& product 是引用參數,直接綁定到外部變量。
在函數內部可以直接操作 sum 和 product,無需解引用。
3.指針和引用的對比
4. 使用輸出型參數的場景!
-
需要從函數中返回多個值。
-
需要修改傳入的參數值。
-
避免返回大型對象(通過引用或指針傳遞,避免拷貝開銷)。
5. 示例:返回多個值
以下是一個返回多個值的示例,使用引用作為輸出型參數:
#include <iostream>
#include <tuple> // 如果需要返回多個值,也可以使用std::tuple
using namespace std;void getResults(int a, int b, int& sum, int& diff, int& product) {sum = a + b;diff = a - b;product = a * b;
}int main() {int x = 10, y = 4;int sum, diff, product;getResults(x, y, sum, diff, product);cout << "Sum: " << sum << endl; // 輸出:Sum: 14cout << "Difference: " << diff << endl; // 輸出:Difference: 6cout << "Product: " << product << endl; // 輸出:Product: 40return 0;
}
5.左值引用的不足:
部分函數返回場景,只能傳值返回,不能左值引用返回。
當前函數的局部對象,出了當前函數的作用域生命周期到了銷毀了不能左值引用返回,只能傳值返回。
class Solution
{
public:// 這?的傳值返回拷?代價就太?了 vector<vector<int>> generate(int numRows) {vector<vector<int>> vv(numRows);for(int i = 0; i < numRows; ++i){vv[i].resize(i+1, 1);}for(int i = 2; i < numRows; ++i){for(int j = 1; j < i; ++j){vv[i][j] = vv[i-1][j] + vv[i-1][j-1];}}return vv;}
};
int main()
{vector<vector<int>> ret = Solution().generate(100);return 0;
}
不優化的情況下編譯器還要拷貝兩次:
如果new的話,忘記釋放可能會導致內存泄漏。
一種較老的比較好的解決方式(輸出型參數):
class Solution
{
public:void generate(int numRows,vector<vector<int>>& vv) {for(int i = 0; i < numRows; ++i){vv[i].resize(i+1, 1);}for(int i = 2; i < numRows; ++i){for(int j = 1; j < i; ++j){vv[i][j] = vv[i-1][j] + vv[i-1][j-1];}}}
};int main()
{vector<vector<int>> ret;Solution().generate(100,ret);return 0;
}
但這樣寫,用的角度,多多少少很別扭。
因為c++委員會更新標準較晚,編譯器的設計者選擇先從編譯器的角度進行優化。
編譯器的第一輪優化:“跳過中間商”
(從左邊這樣到右邊這樣)
namespace bit
{ string addStrings(string num1, string num2){string str;int end1 = num1.size() - 1, end2 = num2.size() - 1;int next = 0;while (end1 >= 0 || end2 >= 0){int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;int ret = val1 + val2 + next;next = ret / 10;ret = ret % 10;str += ('0' + ret);}if (next == 1)str += '1';reverse(str.begin(), str.end());cout << "******************************" << endl;return str;}
}// 場景1
int main()
{bit::string ret = bit::addStrings("11111", "2222");cout << ret.c_str() << endl;return 0;
}
把優化全關掉的場景:
這里前兩組的構造+拷貝構造是"11111"與 "2222"的,最后的兩個拷貝構造一次是str去拷貝構造臨時對象,一次是臨時對象去拷貝構造ret。
VS2019下的優化,合二為一:
可以看到參數的構造與拷貝構造合二為一了,返回值的兩次拷貝構造也合二為一。
2代優化非常恐怖:
可以看到是從構造+拷貝構造+拷貝構造到1代的構造+拷貝構造,再到2代的構造
最后變成了,干脆不創建str了,直接創建ret,讓str變成ret的別名。
這種優化的思路很像上面說的輸出型參數:
vector<vector<int>> ret;
Solution().generate(100,ret);
(編譯器優化后,右值引用沒有意義了嗎?)
答:優化是有限度的,能解決一些問題,但是一些問題也解決不了。
比如在這個場景中:
現在不是構造+兩次拷貝構造而是構造+一次拷貝構造+一次拷貝賦值的場景
徹底不優化是這樣的:
一代優化:
可以看到,傳參是合二為一優化了,但是拷貝構造+拷貝賦值沒有優化
2代優化:
2代優化是去掉了拷貝構造,本質是讓構造和拷貝構造合二為一成一次構造了。相當于一上來就構造了臨時對象,讓str是臨時對象的別名。
編譯器優化也是有限度的,優化終止于此了。
C++11出來后這個程序是如何解決的?
如果編譯器徹底不優化,是這樣的:
即使這樣這個效率也高,因為移動構造只是搶奪資源,不會拷貝,代價極低。
……
總結就是C++11之前很依賴編譯器的優化,有了移動拷貝和賦值之后,對編譯器的優化的依賴很小,只是錦上添花不再是雪中送炭。
一個問題:每個類在C++11后都要寫移動構造移動賦值嗎?
深拷貝的自定義類型(如string、vector、map…)寫才有價值。
而對于淺拷貝的類型來說,編譯器的優化小賺一筆,所以移動語義+編譯器優化是很無敵的存在。