关于c++中的临时变量
為什么寫這樣一篇文章?
本人是c++的初學者, 剛接觸類這個概念沒多久, 但是遇到了許多問題困擾我, 其中有一個問題尤為致命, 我問了許多前輩, 他們許多都沒能如愿幫我徹底解決這個問題, 而寫這篇文章, 一是為了幫助自己再次梳理一遍近期的困惑, 二也是為了幫助后來者理解這方面的一系列問題.
當然, 由于是初學者, 我并不保證我下面的話不會有很多錯誤出現, 甚至有些在您看來有些滑稽之談. 請您諒解
首先: 什么是臨時變量?
如果你學過C語言, 那我這里提到的臨時變量對你來說應該是一個新的概念, 他不像你記憶中想象的那樣: 例如 你要交換a, b兩個整數時聲明的第三個變量temp就是臨時變量, 這個觀點是十分錯誤的! 在C++中, 臨時變量的產生是你不能直接看出來的. 至于怎么產生我們下面會聊到.
// 交換例子int a = 5, b = 10, temp; //temp并不是本文所指的臨時變量temp = a;a = b;b = temp;例子:
在c++用, 我們有了一個新的概念一一引用. 就像一個不能改變指向對象的指針一樣.
那如果我們現在有如下代碼:
int a = 5;int &b = a;這顯然是沒有問題的, 因為我們有一個int類型的變量, 我們用了int類型的引用去指向它.
可是如果我們玩點花樣, 不選擇使用一個同類型的引用呢?
比如我們這樣:
這個時候編譯器會報錯, 他告訴我不能用int&去指向一個double. 那如果這時候我把a強制類型轉換會如何?
像這樣:
很倒霉的是, 編譯器又報錯了, 他告訴我不是常引用的變量b不能作為左值. 這時候就很神奇, 這跟常引用又有什么關系呢? 不過我們還是遵循編譯器所期待的那樣,修改為如下的代碼:
double a = 10.8;const int &b = (int)a; //也可不寫(int), 它會自動轉換 const int &b = a;經歷過上述稍微有些繁瑣的操作后, 我們終于實現了我們的目的, 與此同時我們也拋出了一個問題, 就是為什么我們要在引用前加const呢?
經過幾番折騰, 我了解到, 我們在把一個double類型變量(強制)轉換為int類型時, 它會產生臨時變量. 我們相當于(int)a后, 得到了一個沒有名字的變量, 它的類型為int, 值為10, 我們把這個臨時變量(10)作為右值給了b變量. 并在執行完該語句后臨時變量會被釋放.
而臨時變量要賦給一個引用類型時, 我們必須要用const修飾, 原因我會在后文中給出.
我們現在來深入理解一下上述操作:
其實我們是可以接受在寫 const int &b = a; (這里省寫了類型轉換, 方便理解) 時, 我們并不是把a賦值給了b, 因為b得到的是10, 而a在進行類型強制轉換時, 是不會影響到a原本的值的, 他該是10.8就還是10.8 并不會變成10. 所以這個10一定不是從a那里得來的, 而是從別的地方一一臨時變量.
既然如此,我們可以得出一個結論: b引用的變量并不是a, 而是一個int類型的臨時變量.
那么如果我們通過修改a的值, 那b的值理論上就不應該會改變.
上述代碼就驗證了我們的說法, b的值沒有改變.
當然我們在平時寫代碼時雖然也不會照上面代碼那樣寫, 但當你把其中的double或者int換成自定義的用戶類型時, 在這一個過程中產生臨時變量的事情我們就不能再忽略了, 因為他會體現在調用構造函數上.
我上面的一系列操作也僅僅是為了讓你理解你在C語言中沒能理解到的臨時變量這一新概念, 而發生類型強制轉換則是產生臨時變量的方法之一.
臨時變量:
通過上述實例我引入了一個非常簡單的產生臨時變量的方法, 那么這里我們就來剖析一下臨時變量有什么樣的特性.
首先, 我們可以認為臨時變量都被const修飾:
但是我也在網上的某篇文上了解到, 有某本書上說過臨時變量是可以作為左值的, 但是絕大多數的文章也都告訴我臨時變量是一個const類型, 我選擇從眾, 在后文中提到的臨時變量也都認為被const所修飾.
既然他本身是一個const類型, 那么我們就可以理解前文為什么需要一個const int&類型的變量去接收這個臨時變量了. 我們可以這樣理解, 你在定義某個變量時, 如果你沒有說明他是一個const類型, 那么編譯器完全有理由去相信你是會修改這個變量的. 你修改一個臨時變量, 臨時變量本身被const修飾, 無法更改. 況且即使沒有const修飾, 這也將會是毫無意義的, 因為臨時變量他可能隨時就會消散, 而你去修改它, 編譯器耗費大量資源后, 又把它釋放了, 如果編譯器會說話, 你猜他會不會來句: 您逗我玩呢?
但是在這里我要說明一下: 我們不是說這個臨時變量是不能傳遞給一個變量(特指變量被沒有const修飾), 如下述代碼顯然是成立的, 畢竟我們之前也都是這么做的:
double a = 10.8;int b = (int)a;b = 20;我們是說臨時變量不能賦給非!常引用的變量. 但在這里我要特別強調: 我們需要用一個常引用作為左值的主要原因并不是因為臨時變量被const修飾過, 而是因為我們如果不用const來限制我們程序員自己, 我們可能會無意中修改了臨時變量, 這些操作都是毫無意義的.
據此,c++編譯器加入了臨時變量不能作為非const引用的這個語義限制,其意在于限制這個非!常規用法的潛在錯誤
臨時變量的產生:
其實看到這里, 你也應該會好奇了, 那么我們除了在類型轉換時會產生臨時變量, 那么還有嗎?
答案是肯定的, 大體上歸結為以下3點:
由于提到本人是個初學者, 我只遇到過上述三種情況, 并不一定全面, 而后兩種的產生途徑解釋我會在后文中說明.
注: 如果考慮到非編譯器自動產生臨時變量的方法, 其實還有一種用戶生成臨時變量的方法, 后文會提到.
正文(一級加粗):
呃, 你沒有看錯, 這里才算到正文, 前面都是鋪墊, 但是這里我們也要經過點鋪墊再到核心問題.
常引用與引用:
首先我想特殊說明一下常引用與引用在函數之間的傳遞, 其實這和常量與非常量在函數間傳遞是一樣的.
1. 當某個函數我們以 普通的引用 作為形參時: 不能接受常量作為實參傳遞
因為我們以普通引用作為形參, 在函數里編譯器有理由認為我們會對這個形參進行修改, 那么如果你傳遞的是一個常量, 把常量給了這個引用, 說白了不就是告訴編譯器您要修改一個常量嗎? 這當然是不成立的
2. 當某個函數我們以常引用作為形參時: 可以接受常量或變量作為實參傳遞
當以常引用作為形參的時候, 我們向編譯器說明我們不希望修改形參, 所以這里實參是可以接受變量傳遞的, 我不修改它而已. 而常量作為實參傳遞也當然可以.
特殊說明: const所修飾的變量并不一定完全無法修改, 只是為了限制程序員自身. 在此不做過多的贅述.
特別強調: 當以引用作為形參時我們也是傳遞的地址, 從而不會產生臨時變量
臨時變量的產生途徑2和3:
首先我們引入一段代碼, 并且我們來熟悉一下這段代碼, 便于我們后續理解.
#include <bits/stdc++.h> typedef long long ll; using namespace std; class node { //這個類名隨便起了, 我習慣了node private:int x, y; public:node() { cout << "默認構造函數" << endl; } //默認構造函數node(int a, int b) :x(a), y(b) { cout << "構造函數1" << endl; } //構造函數node(const node& a) { cout << "復制構造函數" << endl; *this = a; } //復制構造函數~node() { cout << "析構函數END" << endl; } //析構函數 }; /* 下面的可以先不用看了 */ node fact(node t) { return t; } int main(void) {node a;fact(a);cout << "END" << endl;return 0; }OK, 上述代碼熟悉完類體就可以了, 那么我們接下來可以來解釋臨時變量的產生途徑2和3:
首先, 我們在main函數里聲明了一個對象node a;, 然后我們把a這個對象作為參數傳遞給了fact()函數, 注意: 此處我們是傳遞的值.
隨后我們在fact函數中return了這個形參. (雖然在main函數中忽略了fact的返回值, 但是不影響代碼的正確性)
執行結果如下:
我們為了能更好的說明某些問題, 我們可以添加一些輸出地址的操作, 例如:
#include <bits/stdc++.h> typedef long long ll; using namespace std; class node { private:int x, y; public:node() { cout << "默認構造函數" << endl; } node(int a, int b) :x(a), y(b) { cout << "構造函數1" << endl; } node(const node& a) { cout << "復制構造函數" << endl; *this = a; cout << this << endl; } //增加了輸出當前被生成對象的地址~node() { cout << "析構函數END" << endl; } }; node fact(node t) { cout << &t << endl; return t; } //增加了輸出形參的地址 int main(void) {node a;cout << &a << endl; //增加了輸出對象a的地址fact(a);cout << "END" << endl;return 0; }經過操作后, 我們再次運行得到以下結果:
默認構造函數003AF7A4 //a的地址(我們的值不相同很正常) 只是以這個為例復制構造函數003AF69C //這是復制實參a后產生的臨時變量的地址003AF69C //這是形參t的地址復制構造函數003AF6C8 //這是返回值臨時變量的地址析構函數END析構函數ENDEND析構函數ENDOK, 到此我們就能理解后兩種說明產生臨時變量的方法了.
問題:
現在我們開始說一個很重要的問題, 其實除了上述三種途徑, 我們還可以自己通過構造函數來構造一個臨時變量, 對于我這個題而言, 我可以用 node(1, 2); 這條語句通過構造函數1來創造一個臨時變量.(這樣寫是成立的)
那么如果我這時想用這個臨時變量來初始化一個新的對象, 那我可以通過 node a = node(1, 2);
代碼如下:
注: 之后的類體內容也都以上述代碼為標準, 不再顯示在后文中.
運行結果如下:
這時候, 如果我把復制構造函數的參數中的const給刪除, 變為
node(node& a) { cout << "復制構造函數" << endl; *this = a; }此時main函數中的語句就會報錯, 由于編譯器不同, 大體有兩種錯誤提示:
這里特殊說明: 如果你改寫了我的代碼, 如下:
node a;a = node(1, 2);這樣是對的, 但是更改了問題的本質, 語句 node a = node(1, 2); 中表示聲明a的同時在給a初始化, 而 node a; a = node(1, 2); 則表示在給a賦值, 等號表示賦值符號了, 是有本質區別的.
或者我這樣給你說明, 構造函數, 他只能在初始化對象時調用, 而初始化, 指的是在定義某個變量時對他賦初值的操作. 第二種寫法先是聲明了a對象, 然后沒有給他進行賦值, 這時候里面的值會是隨機的. 而在==a = node(1, 2);==操作時, 此時便是賦值操作了, 因此他也不會去調用構造函數(對于a而言, 并不是指 node(1, 2) 這個臨時變量產生不會調用構造函數). 要實在還不明白可以直接將賦值號重載來理解.
總結下來就是 : 第一種相當于初始化, 而第二種相當于賦值.
OK, 那么到此我們就拋出一個大問號了, 為什么會有這種東西出現呢? 我又沒調用這個復制構造函數, 為啥我改了他我的代碼會錯?
這里我不多賣關子了, 根據剛才形參與臨時變量地址的問題, 我們同樣可以輸出這個臨時變量與我創建的這個a的地址, 在復制構造函數中加回const后, 在構造函數1里輸出臨時變量的地址, 在主函數里輸出a的地址, 我們會發現這兩個地址是相同的.
這時你是不是會稍微有點頭緒, 我們可以大膽猜測, 編譯器這時候偷了個懶, 他直接把臨時變量的地址給了a, 完成了初始化, 省略了再把臨時變量復制給a對象這步操作, 所以這也能解釋為什么我們運行時明明沒有調用復制構造函數, 但是修改它卻報了錯的原因一一我們原本就是需要用復制構造函數把臨時變量賦值給a的, 只是編譯器在執行的時候略去了這一步, 而在檢查時沒有略去.
所以在我們寫復制構造函數的時候, 請務必遵循規范, 加上const..
下面我們引入一段代碼, 來稍加進行分析:
node fact() { return node(1, 2); } int main(void) {node a = fact();return 0; }這段代碼我們讓fact()函數返回了一個臨時變量, 用他來給a進行初始化, 這時候由于本身我們返回值就是一個臨時變量 所以會省去一次復制構造函數的調用. 又根據我們剛才的猜想, 此時他會用臨時變量直接初始化a, 而不再進行復制構造函數的調用.
輸出結果如下:
構造函數1 析構函數END同樣我們可以通過輸出臨時變量與a的地址確定我的上述說法.
END
本文到此結束, 希望對您有所幫助
總結
以上是生活随笔為你收集整理的关于c++中的临时变量的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 海奥华预言--第七章 慕大陆和远东岛
- 下一篇: 简单人物画像_怎样把复杂的人物肖像画简单