在計算機語言中,類型的存在讓我們可以更有針對性的進行數據和功能的處理,但是卻又存在了類型轉化的問題。C++如同其他計算機語言一樣,也同樣都這些問題。不過它相對於C而言多了引用類型(Reference);相對與C#來講,又多了指針類型(Point)。這似乎讓它的類型轉化變得更加撲朔迷離。
也許是C方面知識的延續,我對C++的基礎類型之間的轉換還算比較清楚的,但是由於C#的Convert這個類庫是那麼的好用,以至於我一直不太適應C++的轉換方式。不過經過導師的教授,發現C++的也可以構建禇類似Convert的轉換方式。
在導師經過一堂課關於C++類型轉換的課後,我又在網上查閱相關資料,以便更清晰詳細的總結C++在類型轉換的方式。
傳統轉換方式(Traditional Type-Casting)
C++作為C語言的超集,完全繼承了C語言所具有的類型轉換方法和能力,因此對於這部分在基礎數值類型上的轉換是比較容易理解的。但是因為C++是面向對象的語言,有類的概念,因此讓又多一層需要理解的內容。
隱式轉換(Implicit Conversion)
隱式轉換不需要任何轉換運算符,編譯器會自動根據類型兼容性進行不同類型之間的轉換。一般情況下,在C/C++中這種轉換多出現在基本數值類型上,其基本原則就是所需內存小的類型可以直接轉換成內存大相同的或者。
內存大小相同的類型之間也可以互相轉換,但是得到的結果可能不是預期的,因而可能會得到編譯器的警告。比如unsigned int uintVariable = -1; 。
雖說:程序員只在意錯誤(Error),不關心警告(Warning),但是導師也是嚴令禁止在程序中出現的,所以對於這樣的隱式轉換也會盡量避免。
顯示轉換(Explicit Conversion)
顯示轉換要表明所要轉換的目標對象是什麼樣的類型,然後編譯器會做出轉換,它有兩種格式:
• C語言格式(C-like Cast)
(new_type) expression
• 函數式(Function-style Cast)
new_type (expression)
示例代碼
#include <iostream>
using namespace std;
int main() {
int x0 = 100;
float num0 = x0;
float num = 98.76;
int x1 = (int) num;
int x2 = int(num);
cout << "num0 = " << num0 << endl;
cout << "x1 = " << x1 << endl;
cout << "x2 = " << x2 << endl;
cout << "x3 = " << x3 << endl;
}
對於C++的類而言,也可以在其實例對象上使用傳統的類型轉換,這是利用了C++的一些語言特性。
下邊就以例子來做解釋
代碼
#include<iostream>
#include<string>
using namespace std;
//macro definitions
#define IDER_DEBUG 1
#define FUNC_TRAC(info) {if(IDER_DEBUG)cout<<"----"<<info<<"----"<<endl;}
//class declaration
class Human;
class Ape;
class Programmer;
//class definition
class Programmer
{
public:
Programmer(string where = "genius")
{
FUNC_TRAC("Programmer Default Constructor");
from = where;
}
/*Programmer(Programmer& p)
{
FUNC_TRAC("Programmer Copy Constructor");
from = p.from;
}*/
void Speach(){cout<<"I am a Programmer, I am "<< from <<endl;}
private:
string from;
};
class Human
{
public:
Human(string where = "delivered by Parents"):heart("Human with Training")
{
FUNC_TRAC("Human Default Constructor");
from = where;
}
Human(Ape& a):heart("Human with Practice")
{
FUNC_TRAC("Hummer Ape-Promotion Constructor");
from = "Evolution from an Ape";
}
operator Programmer() //here is weird, it is really different whether we have "&" or not
{
FUNC_TRAC("Hummer Programmer-Cast Operator");
return heart;
//Programmer("Human with Practice"); // it is not good to return temporary variable
}
Human& operator =(Human& h)
{
FUNC_TRAC("Hummer Assignment Operator");
cout<<"Call assignment"<<endl;
return *this;
}
void Speach(){cout<<"I am a Human, I am "<< from <<endl;}
private:
string from;
Programmer heart; //Every one has a heart to be a programmer
};
class Ape
{
public:
Ape(string where = "from Nature")
{
FUNC_TRAC("Ape Default Constructor");
from = where;
}
Ape& operator =(Programmer& p)
{
FUNC_TRAC("Ape Programmer-Assignment Operator");
from="Degeneration from a Programmer";
return *this;
}
/*Ape& operator =(Ape& p)
{
FUNC_TRAC("Ape Assignment Operator");
cout<<"Ape assign"<<endl;
return *this;
}*/
void Speach(){cout<<"#(*%^, !@#$&)( "<< from <<endl;}
private:
string from;
};
//main function
int main(void) {
Ape a;
//a.Speach();
Human h = a; // using promtion constructor
//h.Speach();
Human h2;
h2 = a; // Error, no match assignment opeartor
Programmer p = h; // using Programmer-cast operaotor
//p.Speach();
Programmer p0;
p0 = h; // using Programmer-cast operaotor
Programmer p1 = h.operator Programmer();
Programmer p2 = Programmer(h);
Programmer p3 = (Programmer)h;
Ape a2;
a2 = p; //using assignment operator
//a2.Speach();
Ape a3 = p; // Error, no match constructor
return 0;
}
在這個例子中,我定義了三個類,這三個類之間沒有繼承和被繼承的關系,也沒有friend關系,其基本聯系就是:Ape可以進化成為Human,Human經過不斷的訓練就可以成為Programmer,不過Programmer也有可能退化變成Ape。
分析
從main函數中他們進行的轉換操作,可以看出這是一種隱式的轉換。不過三個類的對象之間能夠實現轉換的真正原因卻並不相同。
首先,從Ape到Human的轉換方式
Human h = a;
其實是調用了Human的promotion構造函數
Human(Ape& a);
這個函數接受了Ape作為構造參數,實例化了一個Human對象。
從Human到Programmer,則是因為我在Human中定義了一個到Programmer的轉換運算符:operator Programmer()
因此,在main函數中的兩個賦值語句:Programmer p = h; p0 = h;
都是調用了這個轉換函數。
從Programmer退化到Ape是一件很不情願的事情(就因為在中文裡,我們是程序猿),在代碼中的實現方式,則是在Ape類中定義了一個接受Programmer引用作為參數的,Assignment運算符的重載形式。Ape& operator =(Programmer& p)
於是下邊的語句a2 = p;
就得以在程序中運行了
進一步分析
已經看到了Ape, Human,Programmer的之間的轉換都是使用了不同的C++特性,調用的是不同的方法。但是深究起來,這些方法還是各有個的不同。
以Human到Programmer為基准,這個轉換用的是用戶自定義轉換(user-defined cast),因此可以說這種方式才是真正的類型之間的轉換。
也因此我們在main中看到了兩種語法格式的轉換都是有效的:
• 定義並初始化
Programmer p = h;
• 賦值
p0 = h;
但是Ape到Human的轉換調用的構造函數,因此它只有在類實例化對象並初始化的時候才有效,也因此下邊的語句會得到編譯器的錯誤:Human h2; h2 = a; // Error, no match assignment opeartor
因為Human從出生就知道自己是怎麼來的,不應該後來才發現自己不是媽生的(當然,這些情況不是不可能的,比如“人猿泰山”)。
而Programmer到Ape是後天形成的,不然一出生就變成猴子了,那就只能用腳趾了Coding了。所以以下代碼也是編譯不過的:Ape a3 = p; // Error, no match constructor
在回過來講講Human到Programmer,我們還可以用更多不同的形式來寫,比如兩種形式的顯示轉換:Programmer p1 = Programmer(h); Programmer p2 = (Programmer)h;
(是初始化還是賦值都無所謂)
但是真正編譯之後,其格式應該是:Programmer p3 = h.operator Programmer();
對於Assignment運算符其實也是如此,真正調用的還是:a2.operator =(p);
後記
其實在實際編程中,可能受到了C#的影響(因為C#的初始化並不是得到一個全新的對象,而只是獲得引用),並不會經常使用到用戶自定義轉換,也很少重載一個接受非自身對象引用的Assignment運算符。
真正要轉換的時候,多數還是通過構造函數進行。或者是,實例化好對象,通過兩者的接口對數據進行賦值。畢竟以上講到各種方式中,也只能調用到接收到對象的外部接口,不能進行私有數據的操作。
關於數值類型的轉換和類對象的轉換,前面都已經提到了,但似乎還遺漏了什麼?
是的,C++還有引用類型(reference)和指針類型(pointer)。這兩者在不同類型之間的轉換還沒有說。
在C++中,指針類型似乎是被視為是沒有差異的,想想的確如此,因為它只是存放所指對象的地址,因此所需內存空間、格式都是一致的,也因此在C++不同類型 之間的指針是可以隨意轉換的,一般需要用顯示轉換。但是這種轉換是沒有意義,因為地址所指的類型並非所需要的類型,通過該地址進行偏移找到的數據或方法就不會是我們所需要的了,在運行的時候,程序就會發生異常。
對於引用類型,在沒有繼承關系的類型之間進行轉換似乎也並不合理,引用其實在實現上可以被視為指針,只是在語法格式上,它被當做對象使用。如果進行引用類型的轉換,我們到底是想要一個新的對象呢,還是只要地址?讓人迷糊。
另外,指針和引用的應該是用在已經存在的對象或對象變量上。因此如果是轉換中返回轉換運算符的方法之內的一個局部變量(像Human類的operator Programmer()方法中我注釋掉的那行代碼),那麼在離開轉換運算符的方法之後,那些變量就會被回收,在指向那些地址也是沒有意義了;如果是在內部new一個心的對象,這個對象的管理就變得沒有約束性,我們不知道該在何時會去delete它;即使像我實現的那樣,在Human類中帶著一個Programmer的對象Heart,但是這個設計似乎也並不是非常好的,因為不能保證每個人都有一顆作為程序員的心。
遺留問題
前面也提到了指針和引用在類型轉換上的問題,因此對於用戶自定義轉換符,在我的代碼中,我所使用的是基於對象的轉換:operator Programmer();
不是基於指針:operator Programmer*();
也不是基於引用operator Programmer&()
在我看來,這是合理的,也是合適的。
但是如果我在Programmer類中定義了一個copy構造函數,那麼無論以上提到4種的從Human到Programmer的代碼格式都得到編譯錯誤。
這似乎可以理解:編譯器會從構造函數中發現合適的入口,但是它失敗了,所以就錯誤了。
但是為何h.operator Programmer();
的格式也是錯誤就讓我十分的不解了。
再進一步,如果我把基於對象的轉換改成基於引用的轉換,編譯器就沒有報出錯誤了。但是我認為這個在邏輯上應該是不對的。
C++真的一門復雜的語言(當然跟JavaScript還是沒得比的),對於這個問題,如果讀者你知道這方面的原因,還請你能跟我分享一下你的理解和看法。