C++用戶自定義轉(zhuǎn)換(User-Defined Conversion)
在計算機語言中,類型的存在讓我們可以更有針對性的進行數(shù)據(jù)和功能的處理,但是卻又存在了類型轉(zhuǎn)化的問題。C++如同其他計算機語言一樣,也同樣都 這些問題。不過它相對于C而言多了引用類型(Reference);相對與C#來講,又多了指針類型(Point)。這似乎讓它的類型轉(zhuǎn)化變得更加撲朔迷 離。
也許是C方面知識的延續(xù),我對C++的基礎(chǔ)類型之間的轉(zhuǎn)換還算比較清楚的,但是由于C#的Convert這個類庫是那么的好用,以至于我一直不太適應(yīng)C++的轉(zhuǎn)換方式。不過經(jīng)過導(dǎo)師的教授,發(fā)現(xiàn)C++的也可以構(gòu)建禇類似Convert的轉(zhuǎn)換方式。
在導(dǎo)師經(jīng)過一堂課關(guān)于C++類型轉(zhuǎn)換的課后,我又在網(wǎng)上查閱相關(guān)資料,以便更清晰詳細的總結(jié)C++在類型轉(zhuǎn)換的方式。
傳統(tǒng)轉(zhuǎn)換方式(Traditional Type-Casting)
C++作為C語言的超集,完全繼承了C語言所具有的類型轉(zhuǎn)換方法和能力,因此對于這部分在基礎(chǔ)數(shù)值類型上的轉(zhuǎn)換是比較容易理解的。但是因為C++是面向?qū)ο蟮恼Z言,有類的概念,因此讓又多一層需要理解的內(nèi)容。
隱式轉(zhuǎn)換 (Implicit Conversion)
隱式轉(zhuǎn)換不需要任何轉(zhuǎn)換運算符,編譯器會自動根據(jù)類型兼容性進行不同類型之間的轉(zhuǎn)換。一般情況下,在C/C++中這種轉(zhuǎn)換多出現(xiàn)在基本數(shù)值類型上,其基本原則就是所需內(nèi)存小的類型可以直接轉(zhuǎn)換成內(nèi)存大相同的或者。
內(nèi)存大小相同的類型之間也可以互相轉(zhuǎn)換,但是得到的結(jié)果可能不是預(yù)期的,因而可能會得到編譯器的警告。比如 unsigned int uintVariable = -1; 。
雖說:程序員只在意錯誤(Error),不關(guān)心警告(Warning),但是導(dǎo)師也是嚴令禁止在程序中出現(xiàn)的,所以對于這樣的隱式轉(zhuǎn)換也會盡量避免。
顯示轉(zhuǎn)換 (Explicit Conversion)
顯示轉(zhuǎn)換要表明所要轉(zhuǎn)換的目標對象是什么樣的類型,然后編譯器會做出轉(zhuǎn)換,它有兩種格式:
- C語言格式(C-like Cast)
- 函數(shù)式(Function-style Cast)
示例代碼
#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++的類而言,也可以在其實例對象上使用傳統(tǒng)的類型轉(zhuǎn)換,這是利用了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;
}
在這個例子中,我定義了三個類,這三個類之間沒有繼承和被繼承的關(guān)系,也沒有friend關(guān)系,其基本聯(lián)系就是:Ape可以進化成為Human,Human經(jīng)過不斷的訓(xùn)練就可以成為Programmer,不過Programmer也有可能退化變成Ape。
分析
從main函數(shù)中他們進行的轉(zhuǎn)換操作,可以看出這是一種隱式的轉(zhuǎn)換。不過三個類的對象之間能夠?qū)崿F(xiàn)轉(zhuǎn)換的真正原因卻并不相同。
首先,從Ape到Human的轉(zhuǎn)換方式
Human h = a;
其實是調(diào)用了Human的promotion構(gòu)造函數(shù)
Human(Ape& a);
這個函數(shù)接受了Ape作為構(gòu)造參數(shù),實例化了一個Human對象。
從Human到 Programmer,則是因為我在Human中定義了一個到Programmer的轉(zhuǎn)換運算符:
operator Programmer()
因此,在main函數(shù)中的兩個賦值語句:Programmer p = h;
p0 = h;
都是調(diào)用了這個轉(zhuǎn)換函數(shù)。
從Programmer退化到Ape是一件很不情愿的事情(就因為在中文里,我們是程序猿),在代碼中的實現(xiàn)方式,則是在Ape類中定義了一個接受Programmer引用作為參數(shù)的,Assignment運算符的重載形式。
Ape& operator =(Programmer& p)
于是下邊的語句a2 = p;
就得以在程序中運行了
進一步分析
已經(jīng)看到了Ape, Human,Programmer的之間的轉(zhuǎn)換都是使用了不同的C++特性,調(diào)用的是不同的方法。但是深究起來,這些方法還是各有個的不同。
以Human到Programmer為基準,這個轉(zhuǎn)換用的是用戶自定義轉(zhuǎn)換(user-defined cast),因此可以說這種方式才是真正的類型之間的轉(zhuǎn)換。
也因此我們在main中看到了兩種語法格式的轉(zhuǎn)換都是有效的:
- 定義并初始化
Programmer p = h;
- 賦值
p0 = h;
但是Ape到Human的轉(zhuǎn)換調(diào)用的構(gòu)造函數(shù),因此它只有在類實例化對象并初始化的時候才有效,也因此下邊的語句會得到編譯器的錯誤: Human h2;
h2 = a; // Error, no match assignment opeartor
因為Human從出生就知道自己是怎么來的,不應(yīng)該后來才發(fā)現(xiàn)自己不是媽生的(當(dāng)然,這些情況不是不可能的,比如“人猿泰山”)。
而Programmer到Ape是后天形成的,不然一出生就變成猴子了,那就只能用腳趾了Coding了。所以以下代碼也是編譯不過的:Ape a3 = p; // Error, no match constructor
在回過來講講Human到Programmer,我們還可以用更多不同的形式來寫,比如兩種形式的顯示轉(zhuǎn)換: Programmer p1 = Programmer(h);
Programmer p2 = (Programmer)h;
(是初始化還是賦值都無所謂)
但是真正編譯之后,其格式應(yīng)該是: Programmer p3 = h.operator Programmer();
對于Assignment運算符其實也是如此,真正調(diào)用的還是: a2.operator =(p);
后記
其實在實際編程中,可能受到了C#的影響(因為C#的初始化并不是得到一個全新的對象,而只是獲得引用),并不會經(jīng)常使用到用戶自定義轉(zhuǎn)換,也很少重載一個接受非自身對象引用的Assignment運算符。
真正要轉(zhuǎn)換的時候,多數(shù)還是通過構(gòu)造函數(shù)進行。或者是,實例化好對象,通過兩者的接口對數(shù)據(jù)進行賦值。畢竟以上講到各種方式中,也只能調(diào)用到接收到對象的外部接口,不能進行私有數(shù)據(jù)的操作。
關(guān)于數(shù)值類型的轉(zhuǎn)換和類對象的轉(zhuǎn)換,前面都已經(jīng)提到了,但似乎還遺漏了什么?
是的,C++還有引用類型(reference)和指針類型(pointer)。這兩者在不同類型之間的轉(zhuǎn)換還沒有說。
在C++中,指針類型似乎是被視為是沒有差異的,想想的確如此,因為它只是存放所指對象的地址,因此所需內(nèi)存空間、格式都是一致的,也因此在C++ 不同類型 之間的指針是可以隨意轉(zhuǎn)換的,一般需要用顯示轉(zhuǎn)換。但是這種轉(zhuǎn)換是沒有意義,因為地址所指的類型并非所需要的類型,通過該地址進行偏移找到的數(shù)據(jù)或方法就 不會是我們所需要的了,在運行的時候,程序就會發(fā)生異常。
對于引用類型,在沒有繼承關(guān)系的類型之間進行轉(zhuǎn)換似乎也并不合理,引用其實在實現(xiàn)上可以被視為指針,只是在語法格式上,它被當(dāng)做對象使用。如果進行引用類型的轉(zhuǎn)換,我們到底是想要一個新的對象呢,還是只要地址?讓人迷糊。
另外,指針和引用的應(yīng)該是用在已經(jīng)存在的對象或?qū)ο笞兞可?。因此如果是轉(zhuǎn)換中返回轉(zhuǎn)換運算符的方法之內(nèi)的一個局部變量(像Human類的 operator Programmer()方法中我注釋掉的那行代碼),那么在離開轉(zhuǎn)換運算符的方法之后,那些變量就會被回收,在指向那些地址也是沒有意義了;如果是在內(nèi) 部new一個心的對象,這個對象的管理就變得沒有約束性,我們不知道該在何時會去delete它;即使像我實現(xiàn)的那樣,在Human類中帶著一個 Programmer的對象Heart,但是這個設(shè)計似乎也并不是非常好的,因為不能保證每個人都有一顆作為程序員的心。
遺留問題
前面也提到了指針和引用在類型轉(zhuǎn)換上的問題,因此對于用戶自定義轉(zhuǎn)換符,在我的代碼中,我所使用的是基于對象的轉(zhuǎn)換:operator Programmer();
不是基于指針:operator Programmer*();
也不是基于引用operator Programmer&()
在我看來,這是合理的,也是合適的。
但是如果我在Programmer類中定義了一個copy構(gòu)造函數(shù),那么無論以上提到4種的從Human到Programmer的代碼格式都得到編譯錯誤。
這似乎可以理解:編譯器會從構(gòu)造函數(shù)中發(fā)現(xiàn)合適的入口,但是它失敗了,所以就錯誤了。
但是為何h.operator Programmer();
的格式也是錯誤就讓我十分的不解了。
再進一步,如果我把基于對象的轉(zhuǎn)換改成基于引用的轉(zhuǎn)換,編譯器就沒有報出錯誤了。但是我認為這個在邏輯上應(yīng)該是不對的。
C++真的一門復(fù)雜的語言(當(dāng)然跟JavaScript還是沒得比的),對于這個問題,如果讀者你知道這方面的原因,還請你能跟我分享一下你的理解和看法。