C++中有兩個地方一直感覺很隱晦。第一個是引用;其次就是臨時對象了。這個可能和我沒有系統的知識學習有關,到現在都沒把C++Primer看一遍。寒假決定就幹這一件事,把C++Primer系統的梳理一遍,並且做詳細總結。
若有錯誤 請指正 萬分感想
開篇:
一個經典的示例:
#include<iostream>
using namespace std;
void Swap(int a, int b){
int temp; //這個地方的temp算是臨時對象嘛?
temp = a;
a = b;
b = temp;
}
int main(){
//...
}
首先解決下第一個坑:臨時對象和局部變量。
千萬不要把局部變量和臨時對象混淆了。只要你能在程序中看見的變量,都不會是臨時對象。
臨時對象都是const的,並且由編譯器感知的。
爲什麼要總結臨時變量?
1.說明:
對我而言,主要是它讓我不舒服了,引用,指針,臨時變量這幾個方面着實讓我難受,一方面可能是自己水平不夠,看的一些書中着實沒讓我看明白。
對程序而言,研究它卻能提升效率。顧名思義,臨時。最常見的例子就是在類裏面了,因爲臨時對象的產生會調用析構和構造函數,產生一定的開銷。
2.臨時對象的說明:
臨時對象是一個沒有命名的非堆對象。
關於堆的一些概念,大家可以翻看我轉載的一篇博文,那裏面粗淺的介紹了,更細緻的瞭解的話,可以看看彙編,計算機組成原理等知識,因爲我目前沒有接觸,所以我無法賣弄。只能簡單講一下自己已經的知道的東西。
關於臨時對象的產生情景,我在文中介紹三種情況,如果你感覺有別的,那麼可能是被我合併到某一種中。
三種情形:
1)類型轉換時產生:
示例:
//簡單而能說明問題的例子。字符統計函數。
#include <iostream>
using namespace std;
int CountChar(const string& p, char c){
int count = 0;
for (int i = 0; i <p.size(); i++){
if (p[i] == c)
count++;
else
continue;
}
return count;
}
int main(){
char str[200];
cout << "Enter the array of str :";
cin.getline(str,200);
cout << "Enter the char that you want to count :";
char c;
cin >> c;
cout<<"The number of char c is "<<CountChar(str, c);
system("pause");
return 0;
}
看一下上面程序中函數的調用過程:
int CountChar(const string& p, char c);//這個是我定義的函數原型。
<pre name="code" class="cpp">CountChar(str, c);//這個是函數的調用語句。我這個地方的str明明是一個數組名稱,明明和我的函數原型不一致。
那爲什麼可以調用成功,運行成功?這個地方就要涉及到今天要談及的話題了,類型轉換。
首先明確一下,不是啥都能進行轉換的,我們之所以可以進行轉換,是因爲string內部存在相應的構造函數。
下面繼續舉個例子看一下是怎麼進行轉換的。
#include<iostream>
using namespace std;
class Base{
private:
int x;
public:
Base(){
cout << "Using the default Cstor" << endl;
}
Base(int x_x) :x(x_x){
cout << "Using the defined Cstor" << endl;
}
~Base(){
cout << "Using the Destructor" << endl;
}
Base(const Base& temp){
x = temp.x;
cout << "Using the Copy Cstor" << endl;
}
Base operator+(const Base& temp){
return Base(x + temp.x);
}
};
int main(){
//Base b=100;
b = 100;
system("pause");
return 0;
}
在上面的代碼中,Base b=100; 爲何會通過編譯?原因就是調用上面我定義的構造函數。如果你註釋了那幾行那麼這種行爲就是非法的。編譯器會調用構造函數,並且以100爲參數,產生了一個臨時對象。然後調用拷貝構造函數進行初始化,注意這個地方的用詞是初始化,而不是賦值。其實像這樣的語句: int var1=10;double var2=12.1;
那麼這樣的語句:double var3=var1+var2 也是要產生臨時對象的。這個地方需要進行類型轉換。
2)按值傳遞:
示例:
#include <iostream>
using namespace std;
class Base{
private:
int x;
public:
Base(int x_x = 0) :x(x_x){
cout << "Using the Cstor" << endl;
}
~Base() { cout << "Using the Destructor" << endl; }
Base(const Base& temp){
cout << "Using the Copy Cstor" << endl;
x = temp.x;
}
};
/*Base Test(Base temp){
cout << "Do nothing " << endl;
return temp;
}*/
//void Test(Base temp){ ... }
int main(){
Base base_one(1); //調用構造函數。
// Base base_two=Test(base_one);//這個地方會調用拷貝構造函數
Test(base_one);
system("pause");
return 0;
這個地方調用了Test函數,傳遞進去的是什麼?是base_one 自身嘛?
在進行測試的時候我們發現竟然會調用拷貝構造函數!爲什麼呢?
這個地方就涉及到今天的第二個話題了。
按值傳遞,傳遞進去根本不是自身,而是調用副本構造函數產生的臨時對象。
如果你不想進行拷貝,那麼我推薦使用引用進行處理,關於引用可以參照前面的博文,引用筆記。
3) 按值返回:
示例:
//測試下返回值時的臨時對象問題。
#include <iostream>
using namespace std;
class Base{
private:
int var;
public:
Base(int v_var = 0) :var(v_var){
cout << "Using the Constructor" << endl;
}
~Base(){ cout << "Using the Destrucotr" << endl; }
Base(const Base& other){
this->var = other.var;
cout << "Using the Copy Constructor" << endl;
}
friend Base operator+(const Base& p1, const Base& p2);
Base operator=(const Base& p){
this->var = p.var;
cout << "Using the assignment operator" << endl;
return (*this);
}
};
Base operator+(const Base& p1, const Base& p2){
/*Base tmp;
tmp.var=p1.var + p2.var;
return tmp;*/
return Base(p1.var + p2.var);
}
int main(){
/*Base base_one(3);//一次構造函數,一次析構
Base base_two(4);//同上
Base base_three = base_two + base_one;//這個地方並沒有調用Copy Cstor。
Base base_four;*/
//Base b1=100;
Base b1;
b1 = 100;
//base_four = base_one + base_two;//這個地方可以對比下賦值和初始化的.
system("pause");
return 0;
}
來看一下這個函數:Base operator+(const Base& p1, const Base& p2){
/*Base tmp;
tmp.var=p1.var + p2.var;
return tmp;*/
return Base(p1.var + p2.var);
}
這個地方可以看見我註釋了一個返回值。感興趣可以測試下兩種情況。
測試過後我們可以發現,當函數返回的時候,竟然又調用了拷貝構造函數,爲什麼呢?
這個地方就涉及到了返回值的問題。返回的不是自身,而是拷貝。
優化也就是用引用或者指針了。
關於具體的優化方案就不總結了,因爲那個一般是編譯器做的,平臺不同不好測試。但是良好的代碼書寫習慣卻是能避免許多不必要的浪費。
示例:
Base operator+(const Base& p1, const Base& p2){
/*Base tmp;
tmp.var=p1.var + p2.var;
return tmp;*/
return Base(p1.var + p2.var);
}
這個地方我的返回語句是:return Base(p1.var + p2.var);
不要小看這個寫法,可以省去一筆不小的空間花銷。
其次常見的優化措施就是用好引用和指針。
最後一種就是初始化和賦值,推薦初始化不推薦賦值。
關於初始化和賦值的問題我舉個例子:
#include <iostream>
using namespace std;
class Base{
private:
int x;
public:
Base(int x_x = 0):x(x_x){
cout << "Using the Cstor" << endl;
}
};
int main(){
Base b1(10);//這個叫初始化。
Base b2;//這個叫聲明
b2 = b1; //這個叫賦值。
//可以對比下兩者,聲明的時候直接初始化同先聲明後初始化的差異,用類的構造函數就可以看見了。
/*int x = 1;//這個是初始化操作。
int y;
y = 1; //這個就是賦值。
//這個地方可能看不出明顯的效果,複雜點的例子可以看下上面類的例子*/
}
臨時對象生命週期相關:
一般情況中:
C++中臨時對象的摧毀是在完整表達式求值過程中的最後一個步驟。
完整表達式是指包含臨時表達式最外圍的那個。任何一個由子表達式生成的臨時對象都有等到完整表達式被求解完畢後才能被銷燬。這個還是很符合常理的。如果你在一個嵌套表達式中,求解的過程產生的臨時對象還未被使用就已經被銷燬,那麼運算估計是很難進行下去的,畢竟計算機本質上就是在處理數字。
兩個特殊情況:
1)當一個對象被用來初始化一個對象:
示例:
#include <iostream>
using namespace std;
class Base{
int x;
public:
Base(int x_x = 0) :x(x_x){}
Base(const Base& temp){
x = temp.x;
cout << "Using the Copy Cstor" << endl;
}
Base operator+(const Base& temp){
return Base(x + temp.x);
}
};
int main(){
Base b1(1);
Base b2(3);
Base b3 = b1 + b2;//這個地方b1+b2會調用+函數,那麼函數是要產生一個臨時對象的,然後調用拷貝構造函數進行初始化。
//這個地方還是強調一下賦值和初始化.如果你寫成這個樣子,那麼
Base b4;
b4 = b1 + b2; //效果絕對和上面的初始化是不一樣的。自行測試。
system("pause");
return 0;
}
上面的用b1+b2來初始化b3,這個過程中b1+b2表達式在求解過程中產生的臨時對象並沒有馬上被銷燬,而是一直等到拷貝給b3後才銷燬。如果銷燬了,那麼b3怎麼辦?所以不管語言層面還是編譯器角度都是要考慮基本的規則。
2)當一個臨時對象同引用綁到一起:
臨時對象將保留知道初始化的引用生命結束或者是臨時對象的生命週期結束。
#include <iostream>
using namespace std;
class Base{
int x;
public:
Base(int x_x = 0) :x(x_x){
cout << "Using the Cstor"<<endl;
}
Base(const Base& temp){
x = temp.x;
cout << "Using the Copy Cstor" << endl;
}
int Getx(){
return x;
}
Base operator+(const Base& temp){
return Base(x + temp.x);
}
};
int main(){
Base b1(1);
Base b2(3);
const Base& b3 = b1 + b2;//這個地方b1+b2會調用+函數,那麼函數是要產生一個臨時對象的,然後調用拷貝構造函數進行初始化。
cout << b3.Getx() << endl;
system("pause");
return 0;
}
上面我們爲b1+b2的臨時對象建立了引用。這個時候的臨時對象的生命週期是符合上面所說的。
如果你發現你進行調試的結果和我的不一樣,那麼可能是編譯器有優化,不代表你錯或者我錯。當然很可能犯錯,有錯誤清指出。
參考文獻:
CSDN 博文
博客園博文
<<More Effective C++>>