我的個人網站:
http://riun.xyz
問題是:在C++中定義一個類A,A a定義一個對象,然後可以通過 a = 5這樣的代碼進行賦值。搞不明白爲什麼,於是做了幾個實驗去窺探其中的原理。
一、實驗
【實驗1】
#include<iostream>
using namespace std;
class A
{
public:
int val;
int t;
A(int n = 0)
{
cout << "構造函數int n" << endl;
val = n;
t = 0;
cout << "構造函數調用完畢" << endl;
}
A(int n, int t)
{
cout << "構造函數int n, int t" << endl;
(*this).val = n;
(*this).t = t;
}
A& GetObj()
{
return *this;
}
};
int main(void)
{
A a;//調用構造函數
cout << a.val << endl << endl;
a .GetObj() = 5;//賦值 再次調用構造函數
cout << endl;
a = 3;//賦值 再次調用構造函數
cout << endl;
return 0;
}
上述代碼中,除了在創建一個對象,進行初始化操作時調用了構造函數,在對對象進行賦值時也調用了構造函數。至於爲什麼能直接給對象賦值,現在我們還未理解。但是我們發現每次賦值都調用了構造函數,是這樣嗎?我們再試試:
【實驗2.1】
int main(void)
{
A a;//調用構造函數
cout << a.val << endl << endl;
A b(1);//調用構造函數
return 0;
}
調用了兩次構造函數,因爲有兩次創建A對象。接下來我們再將b賦值給a:
【實驗2.2】
int main(void)
{
A a;//調用構造函數
cout << a.val << endl << endl;
A b(1);//調用構造函數
a = b;
return 0;
}
咦,居然沒有再次調用構造函數,但是在【實驗1】中每次賦值都調用了構造函數了呀,這是爲什麼呢。
回想一下,相同類型的對象之間賦值是不需要調用構造函數的,構造函數只有在創建對象的時候才調用。【實驗1】中將int類型的值賦給a對象時調用了構造函數,是否說明這裏賦值時創建了一個對象呢?
我猜想是的,接着我就給A類寫了一個析構函數:
【實驗3】
#include<iostream>
using namespace std;
class A
{
public:
int val;
int t;
A(int n = 0)
{
cout << "構造函數int n" << endl;
val = n;
t = 0;
cout << "構造函數調用完畢" << endl;
}
A(int n, int t)
{
cout << "構造函數int n, int t" << endl;
(*this).val = n;
(*this).t = t;
}
A& GetObj()
{
return *this;
}
//新增手寫的析構函數
~A()
{
cout << "析構函數調用" << endl;
}
};
int main(void)
{
A a;//調用構造函數
cout << a.val << endl << endl;
a .GetObj() = 5;//再次調用構造函數
cout << endl;
a = 3;//再次調用構造函數
cout << endl;
return 0;
}
我們可以看到,紅框中的是a對象的生命週期,籃框和黃框內就是在賦值過程中生成A類型的臨時對象,在賦值時生成,賦值結束後就被釋放了。它們的作用只是將值賦給a對象。
而C++爲了能在代碼層面完成可以直接將不同類型的值賦給對象這樣的操作,在賦值時臨時生成了一個對象,然後就可以像【實驗2.2】那裏一樣作爲同類型對象間的賦值。
所以,像 a = 5 這樣的代碼只是生成了一個臨時對象,用臨時對象給a賦值。而生成臨時對象需要調用對應的構造函數,如果你沒有符合的構造函數,就不會生成對象,就會報錯。我們將單參的構造函數屏蔽起來試試:
【實驗4】
#include<iostream>
using namespace std;
class A
{
public:
int val;
int t;
/*A(int n = 0)
{
cout << "構造函數int n" << endl;
val = n;
t = 0;
cout << "構造函數調用完畢" << endl;
}*/
A(int n, int t)
{
cout << "構造函數int n, int t" << endl;
(*this).val = n;
(*this).t = t;
}
A& GetObj()
{
return *this;
}
};
int main(void)
{
A a(0,0);//調用構造函數
cout << a.val << endl << endl;
a .GetObj() = 5;//再次調用構造函數
cout << endl;
a = 3;//再次調用構造函數
cout << endl;
return 0;
}
報錯原因是無法將int類型賦給對象,因爲沒有對應的運算符類型。
二、結論
結論:在C++中,可以將普通類型的變量和對象間進行賦值操作,賦值時會利用對應的構造函數生成臨時對象,然後進行同類型對象間的賦值。這相當於幫我們做了隱式轉換,將不同類型轉換爲相同類型,再進行賦值。
這給我們賦值提供了一種新的解決方案。
- 1、原有的解決方案是:重載賦值運算符,將賦值符號右邊的變量作爲重載函數的形參傳遞,然後在函數內部進行賦值。
A& operator=(int i)
{
this->val = i;
return *this;
}
這樣就可以進行 a = 5
這樣的運算了。
- 2、新的解決方案是:編寫對應的構造函數,在賦值時C++會自動生成臨時對象,然後進行同類型對象賦值。
三、擴展
既然知道了這些,那麼疑問又來了:如果同時出現構造函數和重載運算符,那要執行哪個呢?
【實驗5】
#include<iostream>
using namespace std;
class A
{
public:
int val;
int t;
A(int n = 0)
{
cout << "構造函數int n" << endl;
val = n;
t = 0;
cout << "構造函數調用完畢" << endl;
}
A(int n, int t)
{
cout << "構造函數int n, int t" << endl;
(*this).val = n;
(*this).t = t;
}
A& GetObj()
{
return *this;
}
//不需要手動重載,但是如果你顯示的重載了賦值運算符,那麼就會調用你手動寫的,就不會再使用默認的重載函數了
A& operator=(A anthor)
{
cout << "手寫重載運算符 A anthor" << endl;
this->val = 100;
return *this;
}
A& operator=(int i)
{
cout << "手寫重載運算符 int i" << endl;
this->val = 50;
return *this;
}
};
int main(void)
{
A a;
cout << a.val << endl << endl;
a.GetObj() = 5; //(1)
cout << a.val << endl << endl;
A a2(2);
a = a2; //(2)
cout << a.val << endl << endl;
A a3(1);
a.GetObj() = a3; //(3)
cout << a.val << endl << endl;
a = 5; //(4)
cout << a.val << endl << endl;
return 0;
}
可以看到,紅色框內是對a對象的初始化;藍色框內因爲兩邊都是A類型,所以調用的是A anthor的操作符重載(這裏輸出的構造函數是創建a2, a3對象時調用的,不要錯認爲是賦值時調用的哦);黃色框和最底下的青色框是一樣的情況,都是將5賦值給a對象,賦值時執行了int i 的操作符重載,而沒有將int類型的數字再轉化爲A類型的對象,由此可見操作符重載的優先級較高,有對應的操作符重載函數,賦值時就不會去生成臨時對象了。