一、為什么在使用String之前需要使用 using namespace std這樣一句?
Essential C++告訴我標準程序庫所提供的任何事物,都被封裝在命名空間std內。這樣子可以避免產(chǎn)生命名沖突。你看到這句話(huà)一定想要挖開(kāi)里面的東東,看看Microsoft是如何做的
很奇怪的我沒(méi)有看到我所想要找的
namespace std {
...
}
但是我注意到有 _STD_BEGIN 和 _STD_END 在幾乎每個(gè)標準庫頭文件中。如是我想Microsoft的有宏定義嗜好的程序員一定又在用#define這個(gè)法寶。我只需查找_STD_BEGIN的定義就好了。再一次感謝Microsoft在VC中提供的 Find in Files...功能,聽(tīng)到硬盤(pán)一陣狂響后我看到了在Yvals.h中定義的STD家族的幾行。(當然你也可以用GREP這個(gè)程序來(lái)查找,只是沒(méi)有VC這個(gè)方便吧了?。?br>#if defined(__cplusplus)
#define _STD std::
#define _STD_BEGIN namespace std {
#define _STD_END };
#define _STD_USING
#else
#define _STD ::
#define _STD_BEGIN
#define _STD_END
#endif /* __cplusplus */
很明顯,Microsoft已經(jīng)將標準庫封在命名空間std中了,所以下次我們要使用時(shí)一定不要忘記了加上
using namespace std;
呵呵
二、關(guān)于list::list的定義說(shuō)明幾點(diǎn)(Micrsoft VC 6.0版本)
對于list的使用大家一定不會(huì )陌生,可是一定會(huì )對如何實(shí)現這些是一些疑惑的,我也是如此。我一直想知道list是如何實(shí)現其定義的。好的,首先我們看到MSDN中給出list::list的實(shí)現
list::list
explicit list(const A& al = A());
explicit list(size_type n, const T& v = T(), const A& al = A());
list(const list& x);
list(const_iterator first, const_iterator last, const A& al = A());
說(shuō)明list有四種初使化方式,但我對于這四個(gè)定義有幾個(gè)問(wèn)題
1. explicit 是作什么用的?
explicit 的中文意義是"外在的" ,其反意詞是 implicit
那么它有什么特點(diǎn),讓我們來(lái)看一個(gè)例子(MSDN中有)
class X {
public:
explicit X(int); file://正確
explicit X(double) { file://正確
// ...
}
};
explicit X::X(int) {} file://不正確
...
說(shuō)明:explicit 僅能用于類(lèi)定義的內部
還有一個(gè)例子(定義為以上正確定義的類(lèi))
void f(X) {}
void g(int I) {
f(i); // 錯誤
}
void h() {
X x1(1); // 正確
}
如果沒(méi)有explicit定義,f(i)是可以通過(guò)的,因為編譯器可以實(shí)現一個(gè)隱式轉換
int->(implicit) X 用i來(lái)構造一個(gè)匿名X類(lèi),作為f()的參數。
而由于有了explicit定義,故而此一步無(wú)法實(shí)現。
MSDN的原文是: The function call f(i) fails because there is no available implicit conversion from int to X.
另外:需要注意的是 explict 不參定義多參數構造函數,否則引起其他構造函數不能隱式轉換。
2.我的第二疑問(wèn)是explicit list(size_type n, const T& v = T(), const A& al = A());
中 const T & v= T() 代表什么?
當我問(wèn)自已這問(wèn)題時(shí),我注意到我們聲明一個(gè)list變量時(shí)可以用
list<int> ilist1(10) file://聲明內含10個(gè)元素,元素值為默認值。
list<int> ilist2(10,5) file://10個(gè)元素,元素值均為5
兩種方式來(lái)定一個(gè)初使元素一定的list變量
注意到 T()
把 T 用int代替,得到
const int & v=int();
哈哈, 原來(lái)如此簡(jiǎn)單!
3.list(const list& x)的意義
這是一個(gè)典型的拷貝構造函數。
因為我們可以用
list <string> slist;
...
list<string> slist2(slist) file://將slist復制給slist2
至于為什么要用拷貝構造函數,你不會(huì )說(shuō)你不知道吧!
CString
CString是對于原來(lái)標準c中字符串類(lèi)型的一種的包裝。因為,通過(guò)很長(cháng)時(shí)間的編程,我們發(fā)現,很多程序的bug多和字符串有關(guān),典型的有:緩沖溢出、內存泄漏等。而且這些bug都是致命的,會(huì )造成系統的癱瘓。因此c++里就專(zhuān)門(mén)的做了一個(gè)類(lèi)用來(lái)維護字符串指針。標準c++里的字符串類(lèi)是string,在microsoft MFC類(lèi)庫中使用的是CString類(lèi)。通過(guò)字符串類(lèi),可以大大的避免c中的關(guān)于字符串指針的那些問(wèn)題。
這里我們簡(jiǎn)單的看看Microsoft MFC中的CString是如何實(shí)現的。當然,要看原理,直接把它的代碼拿過(guò)來(lái)分析是最好的。MFC里的關(guān)于CString的類(lèi)的實(shí)現大部分在strcore.cpp中。
CString就是對一個(gè)用來(lái)存放字符串的緩沖區和對施加于這個(gè)字符串的操作封裝。也就是說(shuō),CString里需要有一個(gè)用來(lái)存放字符串的緩沖區,并且有一個(gè)指針指向該緩沖區,該指針就是LPTSTR m_pchData。但是有些字符串操作會(huì )增建或減少字符串的長(cháng)度,因此為了減少頻繁的申請內存或者釋放內存,CString會(huì )先申請一個(gè)大的內存塊用來(lái)存放字符串。這樣,以后當字符串長(cháng)度增長(cháng)時(shí),如果增加的總長(cháng)度不超過(guò)預先申請的內存塊的長(cháng)度,就不用再申請內存。當增加后的字符串長(cháng)度超過(guò)預先申請的內存時(shí),CString先釋放原先的內存,然后再重新申請一個(gè)更大的內存塊。同樣的,當字符串長(cháng)度減少時(shí),也不釋放多出來(lái)的內存空間。而是等到積累到一定程度時(shí),才一次性將多余的內存釋放。
還有,當使用一個(gè)CString對象a來(lái)初始化另一個(gè)CString對象b時(shí),為了節省空間,新對象b并不分配空間,它所要做的只是將自己的指針指向對象a的那塊內存空間,只有當需要修改對象a或者b中的字符串時(shí),才會(huì )為新對象b申請內存空間,這叫做寫(xiě)入復制技術(shù)(CopyBeforeWrite)。
這樣,僅僅通過(guò)一個(gè)指針就不能完整的描述這塊內存的具體情況,需要更多的信息來(lái)描述。
首先,需要有一個(gè)變量來(lái)描述當前內存塊的總的大小。
其次,需要一個(gè)變量來(lái)描述當前內存塊已經(jīng)使用的情況。也就是當前字符串的長(cháng)度
另外,還需要一個(gè)變量來(lái)描述該內存塊被其他CString引用的情況。有一個(gè)對象引用該內存塊,就將該數值加一。
CString中專(zhuān)門(mén)定義了一個(gè)結構體來(lái)描述這些信息:
struct CStringData
{
long nRefs; // reference count
int nDataLength; // length of data (including terminator)
int nAllocLength; // length of allocation
// TCHAR data[nAllocLength]
TCHAR* data() // TCHAR* to managed data
{ return (TCHAR*)(this+1); }
};
實(shí)際使用時(shí),該結構體的所占用的內存塊大小是不固定的,在CString內部的內存塊頭部,放置的是該結構體。從該內存塊頭部開(kāi)始的sizeof(CStringData)個(gè)BYTE后才是真正的用于存放字符串的內存空間。這種結構的數據結構的申請方法是這樣實(shí)現的:
pData = (CStringData*) new BYTE[sizeof(CStringData) + (nLen+1)*sizeof(TCHAR)];
pData->nAllocLength = nLen;
其中nLen是用于說(shuō)明需要一次性申請的內存空間的大小的。
從代碼中可以很容易的看出,如果想申請一個(gè)256個(gè)TCHAR的內存塊用于存放字符串,實(shí)際申請的大小是: sizeof(CStringData)個(gè)BYTE + (nLen+1)個(gè)TCHAR
其中前面sizeof(CStringData)個(gè)BYTE是用來(lái)存放CStringData信息的。后面的nLen+1個(gè)TCHAR才是真正用來(lái)存放字符串的,多出來(lái)的一個(gè)用來(lái)存放’\0’。
CString中所有的operations的都是針對這個(gè)緩沖區的。比如LPTSTR CString::GetBuffer(int nMinBufLength),它的實(shí)現方法是:
首先通過(guò)CString::GetData()取得CStringData對象的指針。該指針是通過(guò)存放字符串的指針m_pchData先后偏移sizeof(CStringData),從而得到了CStringData的地址。
然后根據參數nMinBufLength給定的值重新實(shí)例化一個(gè)CStringData對象,使得新的對象里的字符串緩沖長(cháng)度能夠滿(mǎn)足nMinBufLength。
然后在重新設置一下新的CStringData中的一些描述值。
最后將新CStringData對象里的字符串緩沖直接返回給調用者。
這些過(guò)程用C++代碼描述就是:
if (GetData()->nRefs > 1 || nMinBufLength > GetData()->nAllocLength)
{
// we have to grow the buffer
CStringData* pOldData = GetData();
int nOldLen = GetData()->nDataLength; // AllocBuffer will tromp it
if (nMinBufLength < nOldLen)
nMinBufLength = nOldLen;
AllocBuffer(nMinBufLength);
memcpy(m_pchData, pOldData->data(), (nOldLen+1)*sizeof(TCHAR));
GetData()->nDataLength = nOldLen;
CString::Release(pOldData);
}
ASSERT(GetData()->nRefs <= 1);
// return a pointer to the character storage for this string
ASSERT(m_pchData != NULL);
return m_pchData;
很多時(shí)候,我們經(jīng)常的對大批量的字符串進(jìn)行互相拷貝修改等,CString 使用了CopyBeforeWrite技術(shù)。使用這種方法,當利用一個(gè)CString對象a實(shí)例化另一個(gè)對象b的時(shí)候,其實(shí)兩個(gè)對象的數值是完全相同的,但是如果簡(jiǎn)單的給兩個(gè)對象都申請內存的話(huà),對于只有幾個(gè)、幾十個(gè)字節的字符串還沒(méi)有什么,如果是一個(gè)幾K甚至幾M的數據量來(lái)說(shuō),是一個(gè)很大的浪費。
因此CString 在這個(gè)時(shí)候只是簡(jiǎn)單的將新對象b的字符串地址m_pchData直接指向另一個(gè)對象a的字符串地址m_pchData。所做的額外工作是將對象a的內存應用CStringData:: nRefs加一。
CString::CString(const CString& stringSrc)
{
m_pchData = stringSrc.m_pchData;
InterlockedIncrement(&GetData()->nRefs);
}
這樣當修改對象a或對象b的字符串內容時(shí),首先檢查CStringData:: nRefs的值,如果大于一(等于一,說(shuō)明只有自己一個(gè)應用該內存空間),說(shuō)明該對象引用了別的對象內存或者自己的內存被別人應用,該對象首先將該應用值減一,然后將該內存交給其他的對象管理,自己重新申請一塊內存,并將原來(lái)內存的內容拷貝過(guò)來(lái)。
其實(shí)現的簡(jiǎn)單代碼是:
void CString::CopyBeforeWrite()
{
if (GetData()->nRefs > 1)
{
CStringData* pData = GetData();
Release();
AllocBuffer(pData->nDataLength);
memcpy(m_pchData, pData->data(),
(pData- >nDataLength+1)*sizeof(TCHAR));
}
}
其中Release 就是用來(lái)判斷該內存的被引用情況的。
void CString::Release()
{
if (GetData() != _afxDataNil)
{
if (InterlockedDecrement(&GetData()->nRefs) <= 0)
FreeData(GetData());
}
}
當多個(gè)對象共享同一塊內存時(shí),這塊內存就屬于多個(gè)對象,而不在屬于原來(lái)的申請這塊內存的那個(gè)對象了。但是,每個(gè)對象在其生命結束時(shí),都首先將這塊內存的引用減一,然后再判斷這個(gè)引用值,如果小于等于零時(shí),就將其釋放,否則,將之交給另外的正在引用這塊內存的對象控制。
CString使用這種數據結構,對于大數據量的字符串操作,可以節省很多頻繁申請釋放內存的時(shí)間,有助于提升系統性能。
通過(guò)上面的分析,我們已經(jīng)對CString的內部機制已經(jīng)有了一個(gè)大致的了解了??偟恼f(shuō)來(lái)MFC中的CString是比較成功的。但是,由于數據結構比較復雜(使用CStringData),所以在使用的時(shí)候就出現了很多的問(wèn)題,最典型的一個(gè)就是用來(lái)描述內存塊屬性的屬性值和實(shí)際的值不一致。出現這個(gè)問(wèn)題的原因就是CString為了方便某些應用,提供了一些operations,這些operation可以直接返回內存塊中的字符串的地址值,用戶(hù)可以通過(guò)對這個(gè)地址值指向的地址進(jìn)行修改,但是,修改后又沒(méi)有調用相應的operations1使CStringData中的值來(lái)保持一致。比如,用戶(hù)可以首先通過(guò)operations得到字符串地址,然后將一些新的字符增加到這個(gè)字符串中,使得字符串的長(cháng)度增加,但是,由于是直接通過(guò)指針修改的,所以描述該字符串長(cháng)度的CStringData中的nDataLength卻還是原來(lái)的長(cháng)度,因此當通過(guò)GetLength獲取字符串長(cháng)度時(shí),返回的必然是不正確的。
存在這些問(wèn)題的operations下面一一介紹。
1. GetBuffer
很多錯誤用法中最典型的一個(gè)就是CString:: GetBuffer ()了.查了MSDN,里面對這個(gè)operation的描述是:
Returns a pointer to the internal character buffer for the CString object. The returned LPTSTR is not const and thus allows direct modification of CString contents。
這段很清楚的說(shuō)明,對于這個(gè)operation返回的字符串指針,我們可以直接修改其中的值:
CString str1("This is the string 1");――――――――――――――――1
int nOldLen = str1.GetLength();―――――――――――――――――2
char* pstr1 = str1.GetBuffer( nOldLen );――――――――――――――3
strcpy( pstr1, "modified" );――――――――――――――――――――4
int nNewLen = str1.GetLength();―――――――――――――――――5
通過(guò)設置斷點(diǎn),我們來(lái)運行并跟蹤這段代碼可以看出,當運行到三處時(shí),str1的值是”This is the string 1”,并且nOldLen的值是20。當運行到5處時(shí),發(fā)現,str1的值變成了”modified”。也就是說(shuō),對GetBuffer返回的字符串指針,我們將它做為參數傳遞給strcpy,試圖來(lái)修改這個(gè)字符串指針指向的地址,結果是修改成功,并且CString對象str1的值也響應的變成了” modified”。但是,我們接著(zhù)再調用str1.GetLength()時(shí)卻意外的發(fā)現其返回值仍然是20,但是實(shí)際上此時(shí)str1中的字符串已經(jīng)變成了” modified”,也就是說(shuō)這個(gè)時(shí)候返回的值應該是字符串” modified”的長(cháng)度8!而不是20?,F在CString工作已經(jīng)不正常了!這是怎么回事?
很顯然,str1工作不正常是在對通過(guò)GetBuffer返回的指針進(jìn)行一個(gè)字符串拷貝之后的。
再看MSDN上的關(guān)于這個(gè)operation的說(shuō)明,可以看到里面有這么一段話(huà):
If you use the pointer returned by GetBuffer to change the string contents, you must call ReleaseBuffer before using any other CString member functions.
原來(lái)在對GetBuffer返回的指針使用之后需要調用ReleaseBuffer,這樣才能使用其他CString的operations。上面的代碼中,我們在4-5處增建一行代碼:str2.ReleaseBuffer(),然后再觀(guān)察nNewLen,發(fā)現這個(gè)時(shí)候已經(jīng)是我們想要的值8了。
從CString的機理上也可以看出:GetBuffer返回的是CStringData對象里的字符串緩沖的首地址。根據這個(gè)地址,我們對這個(gè)地址里的值進(jìn)行的修改,改變的只是CStringData里的字符串緩沖中的值, CStringData中的其他用來(lái)描述字符串緩沖的屬性的值已經(jīng)不是正確的了。比如此時(shí)CStringData:: nDataLength很顯然還是原來(lái)的值20,但是現在實(shí)際上字符串的長(cháng)度已經(jīng)是8了。也就是說(shuō)我們還需要對CStringData中的其他值進(jìn)行修改。這也就是需要調用ReleaseBuffer()的原因了。
正如我們所預料的,ReleaseBuffer源代碼中顯示的正是我們所猜想的:
CopyBeforeWrite(); // just in case GetBuffer was not called
if (nNewLength == -1)
nNewLength = lstrlen(m_pchData); // zero terminated
ASSERT(nNewLength <= GetData()->nAllocLength);
GetData()->nDataLength = nNewLength;
m_pchData[nNewLength] = ‘\0‘;
其中CopyBeforeWrite是實(shí)現寫(xiě)拷貝技術(shù)的,這里不管它。
下面的代碼就是重新設置CStringData對象中描述字符串長(cháng)度的那個(gè)屬性值的。首先取得當前字符串的長(cháng)度,然后通過(guò)GetData()取得CStringData的對象指針,并修改里面的nDataLength成員值。
但是,現在的問(wèn)題是,我們雖然知道了錯誤的原因,知道了當修改了GetBuffer返回的指針所指向的值之后需要調用ReleaseBuffer才能使用CString的其他operations時(shí),我們就能避免不在犯這個(gè)錯誤了。答案是否定的。這就像雖然每一個(gè)懂一點(diǎn)編程知識的人都知道通過(guò)new申請的內存在使用完以后需要通過(guò)delete來(lái)釋放一樣,道理雖然很簡(jiǎn)單,但是,最后實(shí)際的結果還是有由于忘記調用delete而出現了內存泄漏。
實(shí)際工作中,常常是對GetBuffer返回的值進(jìn)行了修改,但是最后卻忘記調用ReleaseBuffer來(lái)釋放。而且,由于這個(gè)錯誤不象new和delete人人都知道的并重視的,因此也沒(méi)有一個(gè)檢查機制來(lái)專(zhuān)門(mén)檢查,所以最終程序中由于忘記調用ReleaseBuffer而引起的錯誤被帶到了發(fā)行版本中。
要避免這個(gè)錯誤,方法很多。但是最簡(jiǎn)單也是最有效的就是避免這種用法。很多時(shí)候,我們并不需要這種用法,我們完全可以通過(guò)其他的安全方法來(lái)實(shí)現。
比如上面的代碼,我們完全可以這樣寫(xiě):
CString str1("This is the string 1");
int nOldLen = str1.GetLength();
str1 = "modified";
int nNewLen = str1.GetLength();
但是有時(shí)候確實(shí)需要,比如:
我們需要將一個(gè)CString對象中的字符串進(jìn)行一些轉換,這個(gè)轉換是通過(guò)調用一個(gè)dll里的函數Translate來(lái)完成的,但是要命的是,不知道什么原因,這個(gè)函數的參數使用的是char*型的:
DWORD Translate( char* pSrc, char *pDest, int nSrcLen, int nDestLen );
這個(gè)時(shí)候我們可能就需要這個(gè)方法了:
CString strDest;
Int nDestLen = 100;
DWORD dwRet = Translate( _strSrc.GetBuffer( _strSrc.GetLength() ),
strDest.GetBuffer(nDestLen),
_strSrc.GetLength(), nDestlen );
_strSrc.ReleaseBuffer();
strDest.ReleaseBuffer();
if ( SUCCESSCALL(dwRet) )
{
}
if ( FAILEDCALL(dwRet) )
{
}
的確,這種情況是存在的,但是,我還是建議盡量避免這種用法,如果確實(shí)需要使用,請不要使用一個(gè)專(zhuān)門(mén)的指針來(lái)保存GetBuffer返回的值,因為這樣常常會(huì )讓我們忘記調用ReleaseBuffer。就像上面的代碼,我們可以在調用GetBuffer之后馬上就調用ReleaseBuffer來(lái)調整CString對象。
2. LPCTSTR
關(guān)于LPCTSTR的錯誤常常發(fā)生在初學(xué)者身上。
例如在調用函數
DWORD Translate( char* pSrc, char *pDest, int nSrcLen, int nDestLen );
時(shí),初學(xué)者常常使用的方法就是:
int nLen = _strSrc.GetLength();
DWORD dwRet = Translate( (char*)(LPCTSTR)_strSrc),
(char*)(LPCTSTR)_strSrc),
nLen,
nLen);
if ( SUCCESSCALL(dwRet) )
{
}
if ( FAILEDCALL(dwRet) )
{
}
他原本的初衷是將轉換后的字符串仍然放在_strSrc中,但是,當調用完Translate以后之后再使用_strSrc時(shí),卻發(fā)現_strSrc已經(jīng)工作不正常了。檢查代碼卻又找不到問(wèn)題到底出在哪里。
其實(shí)這個(gè)問(wèn)題和第一個(gè)問(wèn)題是一樣的。CString類(lèi)已經(jīng)將LPCTST重載了。在CString中LPCTST實(shí)際上已經(jīng)是一個(gè)operation了。對LPCTST的調用實(shí)際上和GetBuffer是類(lèi)似的,直接返回CStringData對象中的字符串緩沖的首地址。
其C++代碼實(shí)現是:
_AFX_INLINE CString::operator LPCTSTR() const
{ return m_pchData; }
因此在使用完以后同樣需要調用ReleaseBuffer()。
但是,這個(gè)誰(shuí)又能看出來(lái)呢?
其實(shí)這個(gè)問(wèn)題的本質(zhì)原因出在類(lèi)型轉換上。LPCTSTR返回的是一個(gè)const char*類(lèi)型,因此使用這個(gè)指針來(lái)調用Translate編譯是不能通過(guò)的。對于一個(gè)初學(xué)者,或者一個(gè)有很長(cháng)編程經(jīng)驗的人都會(huì )再通過(guò)強行類(lèi)型轉換將const char*轉換為char*。最終造成了CString工作不正常,并且這樣也很容易造成緩沖溢出。
第一章:關(guān)于對象(Object Lessons)
讀完這一章使我想到了一個(gè)很久以前看到的一個(gè)笑話(huà),編寫(xiě)一個(gè)HELLO WORLD的程序,隨著(zhù)水平和職務(wù)的不一樣,程序代碼也隨著(zhù)變化。當初看時(shí)完全當作笑話(huà)來(lái)看,現在看來(lái)寫(xiě)此笑話(huà)的人水平不一般。如果要使你的代碼能夠最大限度的適應不同的運行環(huán)境,和最大限度的復用,則在設計和編寫(xiě)的過(guò)程中需要考慮的問(wèn)題很多,因此代碼已變的不在具有C語(yǔ)言的簡(jiǎn)潔,高效。而犧牲了這些優(yōu)勢換來(lái)的是更好的封裝。當然如果你只是要打印Hello World則不必這樣做了。
以C++的思維方式解決問(wèn)題,對于對C語(yǔ)言已經(jīng)很熟悉的人來(lái)說(shuō)會(huì )很不能適應。需要一段時(shí)間來(lái)適應,不然會(huì )將代碼寫(xiě)的似是而非。而且不能邯鄲學(xué)步,必須從思想上徹底的C++(OO),如果只是依葫蘆畫(huà)瓢,那結果很可能是用C++的語(yǔ)法編寫(xiě)C式的程序。本人曾經(jīng)犯的典型的低級的錯誤之一,就是無(wú)意識的一個(gè)類(lèi)無(wú)限制的擴充,完全沒(méi)有考慮到類(lèi)的多層結構(基類(lèi)-派生類(lèi)),需要屬性或方法便在類(lèi)中增加,雖然也用到了多態(tài)、重載等一些OO的設計方式,但最后這個(gè)類(lèi)龐大無(wú)比,除了在當前系統中任勞任怨的工作外,一點(diǎn)復用的可能都沒(méi)有,如果另一個(gè)系統還需要一個(gè)類(lèi)似的東西,那只能重新設計實(shí)現一個(gè)新的類(lèi)。并且最致命的是在維護更新時(shí)帶來(lái)得麻煩,需要不斷全部編譯不說(shuō),而且代碼在用了大量注釋后,在過(guò)一段時(shí)間讀起來(lái)也是一件重腦力勞動(dòng)。及失去了C的簡(jiǎn)潔清晰和高效,也不完全具備C++的面向對象的特性。這根本不能叫C++程序。(我想有時(shí)間重寫(xiě)一下以前代碼也會(huì )有很多收獲,溫故而知新嗎)C和C++在編程思想上是相互矛盾的。這也就是說(shuō)如果你想學(xué)C++,完全可以不學(xué)C,只需要一本好書(shū)和一個(gè)不太笨的大腦再加上努力就可以了,如果你已有C的經(jīng)驗在一定的情況下反而會(huì )搗亂。
本章是對對象模型的一個(gè)大略瀏覽。既然我們選擇了C++而不是C作為開(kāi)發(fā)工具,那我們的編程思想也應該轉為C++的,而不能再延續C的Procedural方式。我們必須學(xué)會(huì )C++的思考方式。采用抽象數據類(lèi)型或用一個(gè)多層的class體系對數據以及數據處理函數進(jìn)行封裝,只有擺脫C程序的使用全局數據的慣性,才能充分發(fā)揮出C++對象模型的強大威力。
在C++中有兩種數據成員static和nonstatic,以及三種成員函數static、nonstatic和virtual。C++對象模型對內存空間和存取時(shí)間做了優(yōu)化,nonstatic的數據成員被置于類(lèi)對象之內,而static數據成員被置于類(lèi)對象之外。static和nonstatic成員函數被放在類(lèi)對象之外。而virtual函數是由類(lèi)對象的一個(gè)指向vtbl(虛函數表)的指針vptr來(lái)進(jìn)行支持。而vptr的設定和重置由類(lèi)的構造函數、析構函數以及copy assignment運算符自動(dòng)完成。
我們設計的每一個(gè)類(lèi)幾乎都要有一個(gè)或多個(gè)構造函數、析構函數和一個(gè)Assignment運算符。他們的作用是構造函數產(chǎn)生一個(gè)新的對象并確定它被初始化。析構函數銷(xiāo)毀一個(gè)對象并確定它已經(jīng)被適當的清理(避免出現內存泄露的問(wèn)題),Assignment運算符給對象一個(gè)新值。
這是第一章的第一部分,由于雷神最近幾天在做模式小組的主頁(yè),時(shí)間周轉不開(kāi)了。本想寫(xiě)完整個(gè)一章再發(fā),考慮一下還是先發(fā)一部分吧。原因有2。1、第一章的后半部可能又要拖上10天半個(gè)月的。2、筆記實(shí)在難寫(xiě),我不愿意將筆記做成將書(shū)上的重點(diǎn)再抄一邊,而是喜歡盡量將自己的理解描述出來(lái),誰(shuí)知第一章便如此的難以消化,已經(jīng)反復讀了3遍,還是有些夾生。所以本著(zhù)對大家和自己負責的態(tài)度,雷神準備再看它3遍在說(shuō)。突然發(fā)現自己的C++還差的很遠,好可怕呀。
| 深度探索C++對象模型(2) |
| 出自:雷神 2002年11月18日 13:06 |
深度探索C++對象模型(2)
筆記貼出后,有朋友便給我提出了一個(gè)很好的建議,原文如下: 史列因:我剛看了你寫(xiě)的“深度探索C++對象模型(1)”,感覺(jué)很不錯。不過(guò)我有一個(gè)建議:你說(shuō)“誰(shuí)知第一章便如此的難以消化,已經(jīng)反復讀了3遍,還是有些夾生”是很自然的。第一章是一個(gè)總覽,如果你能全看懂,后面的就沒(méi)什么看的必要了。第一章的內容后面都有詳細介紹,開(kāi)始只要有個(gè)大概印象就可以了。這本書(shū)中很多內容都是前后重復的。我建議你先不管看懂看不懂,只管向后看,之后再從頭看幾遍,那樣效果好得多。 我想史列因說(shuō)的應該是一種非常好的閱讀方式,類(lèi)似《深度探索C++對象模型》這樣的技術(shù)書(shū)籍,需要的是理解,和學(xué)習英文不同,不能靠死記硬背,如果出現理解不了的情況,那你不妨將書(shū)放下,打一盤(pán)紅警(俺驕傲的說(shuō),我是高手)?;蛘咛^(guò)去也是一個(gè)不錯的方法。好了,我們還是繼續研究C++的對象模型吧。
簡(jiǎn)單的對象模型 看書(shū)上的例子(注釋是表示solt的索引) Class Point { public: Point(float xval); //1 virtual ~Point(); //2
float x() const; //3 static int PointCount(); //4 protected: virtual ostream& print(ostream &os) const; //5 float _x; //6 static int _point_count; //7 } 每一個(gè)Object是一系列的Slots,每一個(gè)Slots指向一個(gè)members。
表格驅動(dòng)對象模型
當構造對象時(shí)便會(huì )有一個(gè)類(lèi)似指針數組的東西存放著(zhù)類(lèi)數據成員在內存中位置的指針,還有指向成員函數的指針。為了對一個(gè)類(lèi)產(chǎn)生的所有對象實(shí)體有一個(gè)標準的表達,所以對象模型采用了表格,把所有的數據成員放在數據成員表中,把所有的成員函數的地址放在了成員函數表中,而類(lèi)對象本身有指向這兩個(gè)表的指針。
為了便于理解,雷神來(lái)舉個(gè)不恰當的例子說(shuō)明一下,注意是不很恰當的例子
我們把寫(xiě)字樓看成一個(gè)類(lèi),寫(xiě)字樓中的人看成是類(lèi)的數據成員,而每一個(gè)租用寫(xiě)字樓的公司看成類(lèi)的成員函數。我們來(lái)看一個(gè)實(shí)體,我們叫它雷神大廈。雷神大廈的物業(yè)管理部門(mén)需要登記每個(gè)出入寫(xiě)字樓的人,以便發(fā)通行證,并且需要登記每個(gè)公司的房間號,并制作了一個(gè)牌子在大廳的墻上。實(shí)際上這便是類(lèi)的對象構造過(guò)程。你可以通過(guò)大廳墻上的公司列表找到任何一家在雷神大廈租房的公司,也可以通過(guò)物業(yè)提供的花名冊找到任何一個(gè)出入雷神大廈的人。
真是一個(gè)考驗大家想象力的例子。(如果你有更好例子的別忘了和雷神交流一下)。
C++的對象模型 C++對象模型是從簡(jiǎn)單對象模型派生得來(lái),并對內存空間和存取時(shí)間做了優(yōu)化。它引入了虛函數表(virtual table)的方案。每個(gè)類(lèi)產(chǎn)生一堆指向虛函數的指針,放在表格中。每個(gè)類(lèi)的對象被添加了一個(gè)指針(vptr),指向相關(guān)的虛函數表(virtual table)。而這個(gè)指針是由每一個(gè)類(lèi)的constructor、destructor和copy assignment運算符自動(dòng)完成。
我們還用上面的雷神大廈舉例,物業(yè)管理為了提高效率,對長(cháng)期穩定的公司和人員不再登記,指對不穩定或不能確定的公司進(jìn)行登記,以便于管理。 再次考驗大家的想象力。
得出結論,C++對象模型和雙表格對象模型相比,提高了空間和存儲時(shí)間的效率,卻失去了彈性。
試想一下,沒(méi)有整個(gè)雷神大廈人員和公司的名錄,如果他們發(fā)生變化,則需要物業(yè)管理部門(mén)做很多工作。重新確定長(cháng)期穩定的公司和人員是那些。對應應用程序則需要重新編譯。(這次更離譜,但為了保持連貫,大家請進(jìn)行理解性的思考,不要局限字面的意思)
這篇筆記是分成多次一點(diǎn)點(diǎn)寫(xiě)的,甚至每天抽出一個(gè)小時(shí)都不能保證(沒(méi)辦法最近實(shí)在忙),因此可能會(huì )有不連貫,如果你讀起來(lái)很不爽認為雷神的思維短路了,那屬于正常。不過(guò)雷神還是再上傳之前努力的將思路進(jìn)行了一下整理。希望能把這些支言片語(yǔ)串起來(lái)。
最后說(shuō)一句閱讀《深入C++對象模型》一書(shū)感覺(jué)沒(méi)有什么可以被成為重點(diǎn)的東西,感覺(jué)每一個(gè)字都不應該放過(guò),全是重點(diǎn)。經(jīng)過(guò)反復閱讀,雷神好象有些開(kāi)竅,繼續努力呀,我和大家都是。 |
| 深度探索C++對象模型(3) |
| 出自:雷神 2002年11月18日 13:08 |
介紹 多態(tài)是一種威力強大的設計機制,允許你繼承一個(gè)抽象的public接口之后,封裝相關(guān)的類(lèi)型,需要付出的代價(jià)就是額外的間接性--不論是在內存的獲得,或是在類(lèi)的決斷上,C++通過(guò)class的pointer和references來(lái)支持多態(tài),這種程序風(fēng)格就稱(chēng)為"面向對象". 正文 深度探索C++對象模型(3) 雷神: http://www.ai361.com 大家好,雷神關(guān)于《深度探索C++對象模型》筆記終于又和大家見(jiàn)面了,速度慢的真是可以。好了不浪費時(shí)間了,直接進(jìn)入主題。 這篇筆記主要解決了幾個(gè)常常被人問(wèn)到的問(wèn)題。 1、C++支持多重繼承嗎? 2、結構和類(lèi)的區別是什么? 3、如何設計一個(gè)面向對象的模型?
C++支持多重繼承(JAVA和C#不支持多重繼承),雖然我想我可能一輩子用不到它這一特性(C++是雷神的業(yè)余愛(ài)好),但至少我要知道它可以。典型的多重繼承是下面這個(gè): //iostream 從istream 和 ostream 兩個(gè)類(lèi)繼承。 class iostream:public istream,public ostream {......};
結構struct和類(lèi)class到底有沒(méi)有區別?VCHELP上前幾天還看到一個(gè)帖子在討論這個(gè)問(wèn)題。其實(shí)結構和類(lèi)真的沒(méi)什么區別,不過(guò)我們需要掌握的是什么時(shí)候用結構好,什么時(shí)候用類(lèi)好,當然這沒(méi)有嚴格的規定。通常我們混合使用它們,從書(shū)上的例子,我們可以看出為什么還需要保留結構,并且書(shū)上給出了一個(gè)方法: struct C_point{.......}; //這是一個(gè)結構 class Point { public: operator C_point(){return _c_point;} //.... private: C_point _c_point; //.... } 這種方法被成為組合(composition).它將一個(gè)對象模型的全部或部分用結構封裝起來(lái),這樣做的好處是你既可以在C++中應用這個(gè)對象模型,也可以在C中應用它。因為struct封裝了class的數據,使C++和C都能有合適的空間布局。
面向對象模型是有一些彼此相關(guān)的類(lèi)型,通過(guò)一個(gè)抽象的base class(用來(lái)提供接口),被封裝起來(lái)。真正的子類(lèi)都是通過(guò)它派生的。當然一個(gè)設計優(yōu)秀的對象模型還必須考慮很多的細節問(wèn)題,雷神根據自己的理解寫(xiě)出一個(gè)面向對象模型的代碼,大家可以看看,高手請給指出有沒(méi)有問(wèn)題。雷神先謝了。 思路:我想要實(shí)現一個(gè)人員管理管理的對象模型,雷神一直在思考一個(gè)人員管理的組件(當然最終它會(huì )用C#實(shí)現的一個(gè)業(yè)務(wù)邏輯對象,并通過(guò)數據庫控制對象和數據庫進(jìn)行交互,通過(guò)WEB FORM來(lái)顯示界面)。這里借用一下自己的已經(jīng)有的的想法,用C++先進(jìn)行一下實(shí)驗,由于只是為了體會(huì )面向對象的概念,我們采用面向對象的方法實(shí)現一個(gè)鏈表程序,而且沒(méi)有收集信息的接口。信息從mina()函數顯式給出。 這個(gè)對象模型應該可以實(shí)現對人員的一般性管理,要求具備以下功能: 創(chuàng )建一個(gè)人員信息鏈表 添加、刪除人員信息 顯示人員信息
//************************************************* //PersonnelManage.cpp //創(chuàng )建人:雷神 //日期:2002-8-30 //版本: //描述: //*************************************************
#include <iostream.h> #include <string.h> //基類(lèi),是此對象模型的最上層父類(lèi) class Personnel { friend class point_list; //用來(lái)實(shí)現輸出鏈表,以及插入或刪除人員的功能. protected: char serial_number[15];//編號 char name[10];//名稱(chēng) char password[15]//口令 Personnel *pointer; Personnel *next_link; public: Personnel(char *sn,char *nm,char *pwd) { strcpy(serial_number,sn); strcpy(name,sm); strcpy(password,pwd); next_link=0; } Personnel() { serial_number[0]=NULL; name[0]=NULL; password[0]=NULL; next_link=0; } void fill_serial_number(char *p_n) { strcpy(serial_number,p_n); } void fill_name(char *p_nm) { strcpy(name,p_nm); } void fill_password(char *p_pwd) { strcpy(password,p_pwd); } virtual void addnew(){} virtual void display() { cout<<"\n編號:"<<serial_number<<"\n"; cout<<"名字:"<<name<<"\n"; cout<<"口令:"<<password<<"\n" } }; //下面是派生的子類(lèi),為了簡(jiǎn)單些我在把子類(lèi)進(jìn)行了成員簡(jiǎn)化。 //思路:由父類(lèi)派生出成員子類(lèi),正式成員要求更詳細的個(gè)人資料,這里省略了大部份. //并且正式成員可以有一些系統的操作權限,這里省略了大部份。 //正式成員子類(lèi) class Member:public Personnel { friend class point_list; private: char member_email[50]; char member_gender[10]; double member_age; public: Member(char *sn,char *nm,char *pwd,char *em,char *gd,double ag):Personnel(sn,nm,pwd) { strcpy(member_email,em); strcpy(member_gender,gd); member_age=age; } Member():Personnel() { member_email[0]=NULL; member_gender=NULL; member_age=0.0; } void fill_email(char *p_em) { strcpy(member_email,p_em); } void fill_gender(char *p_gd) { strcpy(member_gender,p_gd); } void fill_age(double ages) { member_age=ages; }
void addnew() { pointer=this; } void display() { Personnel::display() cout<<"電子郵件:"<<member_email<<"\n"; cout<<"性別:"<<member_gender<<"\n"; cout<<"年齡"<<member_age<<"\n"; } };
//好了,我們還需要實(shí)現一個(gè)超級成員子類(lèi)和一個(gè)項目經(jīng)理的子類(lèi). //這是超級成員類(lèi) class Supermember:public Member { friend class point_list; private: int sm_documentcount;//提交的文檔數 int sm_codecount;//提交的代碼段數 public: Supermember(char *sn,char *nm,char *pwd,char *em,char *gd,double ag,int dc,int cc):Member(sn,nm,pwd,gd,ag) { sm_documnetcount=0; sm_codecount=0; } Spupermember():Member() { sm_documentcount=0; sm_codecount=0; } void fill_documentcount(int smdc) { sm_documentcount=smdc; } void fill_codecount(int smcc) { sm_codecount=smcc; }
void addnew() { pointer=this; } void display() { Member::display() cout<<"提交文章數:"<<sm_documentcount<<"\n"; cout<<"提交代碼段數"<<sm_codecount<<"\n"; } };
//實(shí)現友元類(lèi) class point_list { private: Personnel *location; public: point_list() { location=0; } void print(); void insert(Personnel *node); void delete(char *serial_number); } //顯示鏈表 void point_list::print() { Personnel *ps=location; while(ps!=0) { ps->display(); ps=ps->next_link; } } //插入鏈表 void point_list::insert(Personnel *node) { Personnel *current_node=location; Personnel *previous_node=0; while(current_node!=0 && (strcmp(current_node->name,node->name<0) { previous_node=current_node; current_node=current_node->next_link; } node->addnew() node->pointer->next_link=current_node; if(previous_node==0) location=node->pointer; else previous_node->next_link=node->pointer; }
//從鏈表中刪除 void point_list::delete(char *serial_number) { Personnel *current_node=location; Personnel *previous_node=0; while(current_node!=0 && strcmp(current_node->serial_number,serial_number)!=0) { previous_node=current_node; current_node=current_node->next_link; } if(current_node !=0 && previous_node==0) { location=current_node->next_link; } else if(current_node !=0 && previous_node!=0) { previous_node->next_link=current_node->next_link; } }
//這是主函數,我們顯式的增加3個(gè)Supermember信息,然后在通過(guò)編號刪除一個(gè) //我們沒(méi)有從成員再派生出管理成員,所以沒(méi)有辦法演示它,但我們可以看出要實(shí)現它并不難 //注意:此程序沒(méi)有經(jīng)過(guò)驗證,也許會(huì )有BUG. main() { point_list pl; Supermember sm1("000000000000001","雷神","123456","lsmodel@ai361.com","男",29.9,10,10); Supermember sm1("000000000000002","木一","234567","MY@ai361.com","男",26.5,20,5); Supermember sm1("000000000000003","落葉夏日","345678","LYXR@ai361.com","男",24.8,5,15); //如果我們還派生了管理人員,可能的方式如下: //Managemember mm1("000000000000004","ADMIN","888888","webmaster@ai361.com","男",30,5,15,......);
//下面是將上面的3個(gè)人員信息加到鏈表中 pl.insert(&sm1); pl.insert(&sm2); pl.insert(&sm3); //對應管理人員的 pl.insert(&mm1);
//下面是顯示他們 //下面是顯示人員列表 pl.print();
//下面是刪除一個(gè)人員信息 pl.delete("000000000000001"); //我們再顯示一次看看. cout<<"\n刪除后的列表:\n"; pl.print(); }
程序沒(méi)有上機驗證,在我的腦子里運行了一下,我想輸出結果應該是這樣的:
編號:000000000001 名稱(chēng):雷神 口令:123456 電子郵件:lsmodel@ai361.com 性別:男 年齡:29.9 提交文章數:10 提交代碼數:10
編號:000000000002 名稱(chēng):木一 口令:234567 電子郵件:MY@21CN.com 性別:男 年齡:26.5 提交文章數:20 提交代碼數:5
編號:000000000003 名稱(chēng):落葉夏日 口令:345678 電子郵件:LYXR@163.com 性別:男 年齡:24.8 提交文章數:5 提交代碼數:15
刪除后的列表:
編號:000000000002 名稱(chēng):木一 口令:234567 電子郵件:MY@21CN.com 性別:男 年齡:26.5 提交文章數:20 提交代碼數:5
編號:000000000003 名稱(chēng):落葉夏日 口令:345678 電子郵件:LYXR@163.com 性別:男 年齡:24.8 提交文章數:5 提交代碼數:15
*****************************************************************************************
通過(guò)上面的例子,我想我們能夠理解對象模型的給我們帶來(lái)的好處,我們用了大量的指針和引用,來(lái)完成多態(tài)的特性.和書(shū)上的資料庫的例子不同,我們多了一層,那是因為我考慮人員可能是匿名,也可能是注冊的,所以為了區別他們,用了兩層來(lái)完成接口,然后所有注冊的正式成員才都由Member類(lèi)派生出不同的權限的人員,例如超級成員和管理人員.
最后用書(shū)上的一段話(huà)總結一下吧.P34 總而言之,多態(tài)是一種威力強大的設計機制,允許你繼承一個(gè)抽象的public接口之后,封裝相關(guān)的類(lèi)型,需要付出的代價(jià)就是額外的間接性--不論是在內存的獲得,或是在類(lèi)的決斷上,C++通過(guò)class的pointer和references來(lái)支持多態(tài),這種程序風(fēng)格就稱(chēng)為"面向對象".
|
| 深度探索C++對象模型(4) |
| 出自:雷神 2002年11月19日 13:09 |
介紹 這本書(shū)真的是雷神所看過(guò)的書(shū)中,看的最慢的一本了。但這些深層的知識有必要了解的很清楚嗎,我們不知道編譯器如何合成缺省的構造函數不也能寫(xiě)程序嗎?雷神用侯大師的話(huà)來(lái)回答這個(gè)問(wèn)題:練從難處練,用從易處用。知其然而不知其所以然,不是一個(gè)嚴謹的學(xué)習態(tài)度。 正文 深度探索C++對象模型(4) 雷神 http://www.ai361.com 雷神跌跌撞撞的讀完了《深度探索C++對象模型》的第一章,雖然還是有些疑惑,但是已經(jīng)感到收獲很大。按照朋友的說(shuō)法,第一章是一個(gè)概括的介紹,具體的細節會(huì )在以后的章節闡述,如果沒(méi)有通讀本書(shū),第一章還是比較不容易理解的。雷神聽(tīng)過(guò)之后信心倍增,也不在有初看此書(shū)時(shí)的“世界末日”的感覺(jué)了(在第2篇雷神感到學(xué)了近一年的C++,居然水平如此之差),并且通過(guò)自己的努力,還是摸到了些門(mén)道,所以讓我們繼續快樂(lè )的出發(fā),踏上深度探索C++對象模型的旅程。記住我們在第一篇的小文《堅持不懈,直到成功》,這可是獲得成功的不二法門(mén)。 第二章主要講的的構造函數語(yǔ)意(Semantics),這是一個(gè)什么意思?我的英文和中文學(xué)的都不好,但我想是書(shū)上弄錯了(也許只是一個(gè)筆誤),也許應該翻譯成語(yǔ)義比較恰當。The study or science of meaning in anguage forms. 語(yǔ)義學(xué)以語(yǔ)言形式表示意思的研究或科學(xué)。我們要研究構造函數的,并且以語(yǔ)言的形式將它描述清楚。
看完題目我的第一個(gè)感覺(jué),構造函數我知道。構造函數是一個(gè)類(lèi)的成員函數,構造函數和析構函數是進(jìn)行對象數據的創(chuàng )建,初始化,清除工作的成員函數,可以重載構造函數,使一個(gè)類(lèi)不止具備一個(gè)構造函數,因有時(shí)需要以這些方法中的某一種分別創(chuàng )建不同的對象。不能重載析構函數。構造函數作為成員函數和類(lèi)有相同的名字。例:一個(gè)類(lèi)名為:aClass,構造函數就是aClass()。構造函數沒(méi)有返回值,而且不能定義其返回類(lèi)型,void也不行。析構函數同樣使用這一點(diǎn)。當編寫(xiě)重載函數時(shí),只有參數表不同,通過(guò)比較其參數個(gè)數或參數類(lèi)型可以區分兩個(gè)重載函數。但是我讀完第一小段后就知道這一章要告訴我們什么了。 這一章并不是要告訴我們什么是構造函數,它的作用是什么。而是要告訴我們的是構造函數是如何工作的。我的。在得知這點(diǎn)后我很興奮,因為我確實(shí)不知道構造函數是如何構造一個(gè)類(lèi)的對象的,并且一直想知道。我一直對面向對象神奇的功能很感興趣。為什么一個(gè)類(lèi)在被實(shí)例化時(shí),可以自動(dòng)的完成很多工作,使我們的主函數清晰,簡(jiǎn)單,穩健,高效。以前只看到了表面,沒(méi)有深入,這會(huì )我們有機會(huì )去皮剔肉深入骨髓了。
書(shū)上主要討論了幾種情況: 帶有缺省構造函數的成員對象。如果一個(gè)類(lèi)沒(méi)有任何的構造函數,但他有一個(gè)成員對象,這個(gè)對象的類(lèi)有一個(gè)缺省的構造函數,那么編譯器會(huì )在需要的時(shí)候為這個(gè)類(lèi)合成一個(gè)構造函數。 舉個(gè)例子: 我們有以下幾個(gè)類(lèi)。它們都有一個(gè)構造函數。 貓{public:貓(),......}; 狗{public:狗(),......}; 鳥(niǎo){public:鳥(niǎo)(),......}; 魚(yú){public:魚(yú)(),......}; 我們又有一個(gè)類(lèi)。寵物,我們將貓作為它的成員之一。并且沒(méi)有給它聲明構造函數。 寵物{ public: 貓 一只貓; 狗 一只狗; 鳥(niǎo) 一只鳥(niǎo); 魚(yú) 一只魚(yú); private: int ival; ...... } 則當需要的時(shí)候編譯器會(huì )為它合成一個(gè)構造函數,并且采用內聯(lián)方式。大概象下面的樣子。 inline 寵物::寵物() { 貓.貓::貓(); 狗.狗::狗(); 鳥(niǎo).鳥(niǎo)::鳥(niǎo)(); 魚(yú).魚(yú)::魚(yú)(); ival=0; } 為什么會(huì )這樣,我們來(lái)看看編譯器的行動(dòng)。編譯器開(kāi)始執行用戶(hù)的代碼,準備生成寵物對象之前,會(huì )首先調用必要的構造函數,來(lái)初始化類(lèi)的成員,以便為對象分配合適的內存空間。結果編譯器會(huì )合成上面的構造函數,如果程序員為寵物類(lèi)寫(xiě)了一個(gè)構造函數。 寵物::寵物(){ival=0;}那編譯器也會(huì )將這個(gè)構造函數擴張成上面的那樣。編譯器是怎樣實(shí)現的呢?原來(lái)當一個(gè)類(lèi)沒(méi)有任何用戶(hù)定義的構造函數,而是由編譯器自動(dòng)生成的話(huà),則這個(gè)被暗中生成的構造函數將會(huì )是一個(gè)沒(méi)有什么用處的構造函數。但是通過(guò)編譯器的工作能夠為我們合成一個(gè)nontrivial default constructor. 好象香港電影中演的,如果你惹上官司(你要設計一個(gè)類(lèi)),你又沒(méi)有錢(qián)去請高級的律師(沒(méi)有給出構造函數),那會(huì )給你分配一個(gè)律師(缺省的構造函數),當然這個(gè)律師的能力也許和那些大律師比起來(lái)有差距(trivial)。不過(guò)我們要知道他們也不是一點(diǎn)用都沒(méi)有。但是由于有律師行的督導,可以使這些律師能夠努力做到最好(nontrivial)。
同樣的道理,我們可以理解另外的幾種nontrivial default constructor的情況。 如果你的類(lèi)沒(méi)有任何的構造函數,并且它派生于一個(gè)有著(zhù)缺省構造函數的基類(lèi),那這個(gè)派生類(lèi)的缺省構造函數會(huì )被視為nontrivial,因此需要被合成出來(lái),他的合成步驟是調用上一層基類(lèi)的缺省構造函數,并根據它們的聲明次序為派生類(lèi)合成一個(gè)構造函數。
如果類(lèi)聲明或繼承了一個(gè)虛函數,或者類(lèi)派生于一個(gè)繼承串鏈,其中有一個(gè)或更多的虛擬基類(lèi)。由于缺少使用者聲明的構造函數,則編譯器會(huì )合成一個(gè)缺省的構造函數,以便正確的初始化每一個(gè)類(lèi)對象的vptr。
最后說(shuō)一點(diǎn),在合成的缺省構造函數中,只有基類(lèi)的子對象和類(lèi)的成員對象會(huì )被初始化,所有其他的非靜態(tài)數據成員都不會(huì )被初始化,因為這些操作是需要程序員來(lái)做的。編譯器沒(méi)有必要連這些工作都做了。
好了,這篇就寫(xiě)到這里吧。這本書(shū)真的是雷神所看過(guò)的書(shū)中,看的最慢的一本了。但這些深層的知識有必要了解的很清楚嗎,我們不知道編譯器如何合成缺省的構造函數不也能寫(xiě)程序嗎?雷神用侯大師的話(huà)來(lái)回答這個(gè)問(wèn)題:練從難處練,用從易處用。知其然而不知其所以然,不是一個(gè)嚴謹的學(xué)習態(tài)度。 |
| 深度探索C++對象模型(5) |
| 出自:雷神 2002年11月19日 13:10 |
介紹 我們這篇學(xué)習的內容是:當一個(gè)對象以另一個(gè)對象作為初始值時(shí),會(huì )發(fā)生什么事情. 正文 深度探索C++對象模型(5) 雷神
上一篇我們對合成確省的構造函數做了一個(gè)了解,這一篇我們繼續看看構造函數這個(gè)有趣的東西. Copy Constructor是什么?我們經(jīng)??吹酱a中有一些這樣的函數調用方式X(X&) (“X of X ref”). 這個(gè)函數用用戶(hù)自定義類(lèi)型作為參數,那它的參數的構造便是由Copy Constructor負責的. 可見(jiàn)這個(gè)玩意非常重要,實(shí)際上Copy Constructor是由編譯器自動(dòng)合成的,不需要你去作任何事情,但編譯器都做了些什么呢?我們的問(wèn)題出來(lái)了.
我們有三種情況需要用一個(gè)對象的內容作為另一個(gè)類(lèi)對象的初值.也就是需要編譯器來(lái)為我們自動(dòng)合成Copy Constructor.一種是我們在編程中肯定回用到的由類(lèi)生成對象例如以下形式: class ClassA{......} ClassA a; ClassA b=a; //一個(gè)Class對象以另一個(gè)對象做初值 另外的一種情況是以對象為參數在函數中傳遞看下面的偽碼: //例如我們有一個(gè)CUser類(lèi) CUser{ CUser(); ...... }; //我們還有一個(gè)CDatabase類(lèi),它有一個(gè)AddNew的方法 CDatabase{ ...... public: AddNew(CUser userone); ......} //我們用CUser類(lèi)產(chǎn)生了一個(gè)對象實(shí)例.userone,并將他作為AddNew函數的參數,以便 //AddNew函數能夠完成在數據庫中增加一條記錄,用來(lái)記錄一個(gè)用戶(hù)的信息 CDatabase db=new CDatabase(); db.AddNew(CUser userone) //在這里,你不用將你的用戶(hù)類(lèi)的成員全部展開(kāi). 還有一種當然是用做函數的return,例如你可以在CDatabase類(lèi)中添加一個(gè)函數用來(lái)讀取一個(gè)用戶(hù)的信息例如這樣CUser GetUserOne(int userID),通過(guò)一個(gè)用戶(hù)的唯一的編號可以獲得一個(gè)用戶(hù)的信息,并返回一個(gè)CUser類(lèi)的對象.
我們來(lái)看看Copy Constructor是如何工作的.首先Copy Constructor和Default Constructor一樣都是在需要的時(shí)候由編譯器產(chǎn)生出來(lái),一個(gè)類(lèi)如果沒(méi)有聲明一個(gè)Copy Constructor就會(huì )存在一個(gè)隱含的聲明(或定義).它也被分為trivial和nontrivial兩種.
我們來(lái)看書(shū)上的例子: Class Word { public: Word(const char*); ~Word(){delete [] str;} private: int cnt; Char *str; } 這個(gè)類(lèi)的聲明不需要合成出Default Copy Constructor.但當進(jìn)行如下應用時(shí): #include "Word.h" Word noun("lsmodel"); void foo() { Word verb=noun; } 結果將會(huì )出現災難性的后果.為什么?因為我們的邏輯對象verb和全局對象noun都指向了相同的字符串,在退出函數foo()之前verb會(huì )執行析構,則字符串被刪除,從此全局對象nonu指向了一堆無(wú)意義的東西.你可以聲明一個(gè)explicit copy constructor來(lái)解決這個(gè)問(wèn)題,當然還可以讓編譯器來(lái)自動(dòng)的給你合成一個(gè)Copy construct. 我們將上面的Word類(lèi)改寫(xiě)成下面的樣子: Class Word { public: Word(const String&);//注意這里和我們開(kāi)始的X(X&)形式一樣 ~Word(); //...... private: int cnt; String str; // 這個(gè)成員是String類(lèi)的對象,String是我們自定義的類(lèi)型 }; Class String { public: String(const char*); String(const String&);//這里聲明了一個(gè)Copy constructir ~String(); //...... } 這時(shí)在執行我們的代碼 #include "Word.h" Word noun("lsmodel"); void foo() { Word verb=noun; } 編譯器會(huì )為我們的Word類(lèi)合成一個(gè)Copy Constructor,用來(lái)調用它的str(member class String object)的Copy Constructor.象下面偽碼表示的這樣: inline Word::Word(const Word &wd) { str.String::String(wd.str); cnt=wd.cnt; } 當這個(gè)類(lèi)中有一個(gè)或多個(gè)虛函數時(shí),或者這個(gè)類(lèi)是派生于一個(gè)繼承串鏈,并且這個(gè)串中有一個(gè)或多個(gè)虛擬的基類(lèi)時(shí).這個(gè)類(lèi)在進(jìn)行拷貝時(shí)便不會(huì )展現逐次拷貝(bitwise copy).并且會(huì )通過(guò)合成的Copy Constructor來(lái)重新明確的設定vptr來(lái)指向虛函數表,而不是將右邊對象的vprt直接拷貝過(guò)來(lái).書(shū)上的ZooAnimal例子的圖可以很清晰的描述出這點(diǎn).
如果一個(gè)對象以另一個(gè)對象做初值,而后者有一個(gè)Virtual Base Class Subobject,那會(huì )怎樣呢?任何一個(gè)編譯器都會(huì )做到在派生類(lèi)對象中的virtual base class Subobject的位置在執行期就準備妥當,但bitwise copy可能會(huì )破壞這一位置,因此也需要由編譯器合成出一個(gè)copy constructor,來(lái)安插一些代碼來(lái)設定virtual base class pointer/offset,對每一個(gè)成員執行必要的memberwise初始化操作,以及執行內存相關(guān)的工作.
最后我們來(lái)總結一下上面說(shuō)的內容,確實(shí)有些亂.雷神越來(lái)越覺(jué)得自己的缺乏文字描述能力. 我們這篇學(xué)習的內容是:當一個(gè)對象以另一個(gè)對象作為初始值時(shí),會(huì )發(fā)生什么事情. 分成了兩種情況,一種是我們聲明了explicit copy constructor,這個(gè)不是這篇文章需要搞明白的(我想大家也都很明白了).我們想知道的是我們沒(méi)有為class聲明explicit copy constructor函數時(shí)編譯器都干了些什么.編譯器會(huì )為我們合成一個(gè)copy constructor.以便適應任何時(shí)候的對象被正確的初始化.并且我們了解了有以下四種情況class不在按位逐一進(jìn)行拷貝. 1.當你設計的類(lèi)聲明了一個(gè)explicit copy constructor函數時(shí). 2.當你設計的類(lèi)是由一個(gè)具有explicit copy constructor的基類(lèi)派生的時(shí). 3.當你設計的類(lèi)聲明了一個(gè)或多個(gè)虛函數時(shí). 4.當你設計的類(lèi)派生自一個(gè)繼承串鏈,這個(gè)繼承串鏈中有一個(gè)或多個(gè)virtual base classes時(shí).
|
| 深度探索C++對象模型(6) |
| 出自:雷神 2002年11月19日 13:11 |
介紹 在第三章一開(kāi)始,雷神就吃了一驚 正文 深度探索C++對象模型(6) 雷神
這是這個(gè)系列筆記的第7篇了,我們還在和構造函數打交道,以前寫(xiě)程序時(shí)怎么根本沒(méi)有考慮過(guò)構造函數的事情呢?原來(lái)編譯器為我們做了這么多的事情,我們都不知道.,要想完全搞明白,看來(lái)還需要一段時(shí)間.我們繼續向下走,進(jìn)入一個(gè)新的章節.每當雷神看完一章后,總是期盼下一章節,因為這意味又一個(gè)新的里程開(kāi)始了.對于這本書(shū)更是感覺(jué)強烈,因為全書(shū)總共才7章. 在第三章一開(kāi)始,雷神就吃了一驚..書(shū)上給出了一個(gè)例子: class X{}; class Y:public virtual class X{}; class Z:public virtual class X{}; class A:public Y,public Z{}; 下面的結果會(huì )因為機器,以及編譯有關(guān),不同的情況會(huì )產(chǎn)生不同的結果.(怎么會(huì )是這樣?) sizeof X; //結果為1 sizeof Y; //結果為8 sizeof Z; //結果為8 sizeof A; //結果為12 一個(gè)沒(méi)有任何成員的類(lèi),大小居然不是0. 為什么? 首先一個(gè)沒(méi)有明顯的含有成員的類(lèi),它的大小不是0,因為實(shí)際上它不是空的,它被編譯器安插了一個(gè)char,為的是使這個(gè)類(lèi)的兩個(gè)對象能夠在內存中被分配獨一無(wú)二的地址.至于兩個(gè)派生的類(lèi)Y和Z,因為語(yǔ)言本身造成的負擔,還有編譯器對于特殊情況進(jìn)行的優(yōu)化處理,再有Alignment的限制,因此結果變成了8.這個(gè)8是怎么組成的? 4個(gè)bytes用來(lái)存放指針,什么指針?指向virtual base class subobject的指針呀. 一個(gè)同class X一樣的char.它占了1 個(gè)bytes. 然后受到Alignment的限制,所以填補了3個(gè)bytes. 4+1+3=8 不過(guò)需要注意的是不同的編譯器Y和Z大小的結果也會(huì )不同.因為新的編譯器會(huì )將一個(gè)空的virtual base class看做是派生類(lèi)對象的開(kāi)頭部分,因此派生類(lèi)有了member,因此也就不必分配char的那一個(gè)bytes.也就用不到填補的3個(gè)bytes,因此有可能在某些編譯器中,class Y和class Z的大小為4. 最后看看A.根據我們對class Y的分析可以得出以下算式: 4+4+1+3=12; 不是我們想象的16,而是12.如果換成我們上面說(shuō)的新的編譯器來(lái)編譯,結果很有可能是8. 雷神1、4、8……的說(shuō)了一堆,也不知大家明白與否,但是這第三章,讀起來(lái)確實(shí)比前兩章順多了。我們繼續 我們來(lái)看Data Member 的Binding,現在我們對數據成員的綁定只需要記住一個(gè)防御性風(fēng)格:始終把嵌套類(lèi)型的聲明放在class的開(kāi)始部分,這樣做可以確保非直覺(jué)綁定的正確性??聪旅娴囊粋€(gè)例子:
typedef int length; //zai class point3d { public: //length被決議成global typedef 也就是int //_val被決議成Point3d::_val void mumble(length val){_val=val;} length mumble(){return _val;} //…… private: //length必須在這個(gè)class對它的第一個(gè)參考操作之前被看見(jiàn) //這樣聲明將使先前的參考操作不合法 typedef float length; length _val; //…… }; 怎么成了抄書(shū)了,雷神也不知不覺(jué),可能是在這章的理解上比較容易些吧,不用去想個(gè)看的見(jiàn)摸的著(zhù)的東西比劃。好象小朋友學(xué)算術(shù),一位數的計算不用掰手指頭,可是兩位數或者三位數的計算,手指頭加上腳指頭還是不夠。學(xué)習就是這么回事。理解力和抽象能力很重要?;貋?lái)繼續學(xué)習。 通過(guò)這一章我還知道了。數據成員的布局。數據成員的存取。并且對Static data members有了進(jìn)一步的了解,在class的生命周期中,靜態(tài)成員被看作是全局變量,每一個(gè)member的存取不會(huì )導致任何空間或效率上的額外負擔。不論是從一個(gè)復雜的繼承關(guān)系中繼承還是直接聲明的,Static data member都只會(huì )有一個(gè)實(shí)體。并且有著(zhù)非常直接的存取路徑。另外如果兩個(gè)類(lèi)都聲明了一個(gè)相同名字的靜態(tài)成員變量,那么編譯器會(huì )通過(guò)一種算法,為我們解決名字沖突的問(wèn)題。而非靜態(tài)的成員變量的存去實(shí)際上是通過(guò)implicit class object(this指針)來(lái)完成的。例如 Point3d Point3d::translate(const Point3d &pt) { x+=pt.x; y+=pt.y; z+=pt.z; } 被編譯器經(jīng)過(guò)內部轉換成為了下面這個(gè)樣子: Point3d Point3d::translate(Point3d *const this,const Point3d &pt) { this->x+=pt.x; this->y+=pt.y; this->z+=pt.z; } 如果要對一個(gè)非靜態(tài)的成員變量進(jìn)行存取,編譯器會(huì )把類(lèi)對象的起始地址加上數據成員的偏移量。例如: Point3d origin; origin._y=0.0; //地址&origin._y將等于 &origin+(&Point3d::_y-1); 目的是使編譯系統能夠區分出以下兩種情況: 一個(gè)指向數據成員的指針,用來(lái)指出類(lèi)的第一個(gè)成員。 一個(gè)指向數據成員的指針,沒(méi)有指出任何成員。 這是什么意思?什么是指向數據成員的指針。書(shū)上的例子: class Point3d { public: virtual ~Point3d(); //…… protected: static Point3d origin;//靜態(tài)的數據成員,位置在class object之外 float x,y,z;//每個(gè)float是4bytes } &Point3d::z; //這個(gè)值是什么? 我們在這篇文章開(kāi)始的時(shí)候已經(jīng)知道了還有一個(gè)vptr,不過(guò)vptr的位置也許在對象的開(kāi)始,也許在對象的結尾部。所以上面的操作的值應該是8或者12(如果vptr在前面的話(huà))。但實(shí)際上取會(huì )的值被加上了1。原因是必須要區別一個(gè)不指向任何成員的指針,和一個(gè)指向第一個(gè)成員的指針。又有點(diǎn)不好理解了,舉個(gè)例子: 想象你和你的另外兩個(gè)朋友合住一個(gè)三室一廳的房子,你住在第一間。如果你給一個(gè)你們三個(gè)人共同的朋友的地址你可以給房號就行了。不用給出你們的任意一個(gè)人的那間房子號(不指向任何成員)。但如果你給你的一個(gè)私人朋友地址,你會(huì )給出房間號和你的那個(gè)房間號。為了使這個(gè)地址有區別,你必須有一個(gè)廳來(lái)作為偏移量(offset)。不知道大家明白這個(gè)例子嗎,也許這個(gè)例子會(huì )影響你的正確思維。那就太糟糕了。不過(guò)我還是喜歡這樣想問(wèn)題,也許不太準確,但可以幫助我,因為想象一個(gè)內存空間比想象一個(gè)三居室要難好幾點(diǎn)兒。 |
| 深度探索C++對象模型(7) |
| 出自:雷神 2002年11月19日 13:12 |
介紹 在單一繼承的體系中,虛函數機制是一種很有效率的機制。我們判斷一個(gè)類(lèi)是否支持多態(tài),只需要看它有沒(méi)有虛函數便可以了。 正文 深度探索C++對象模型(7) 雷神
關(guān)于《深度探索C++對象模型》停頓了半個(gè)月,今天繼續啃這個(gè)骨頭,我的學(xué)習進(jìn)入了第四章,函數的語(yǔ)意學(xué)。先做個(gè)復習C++支持三種成員函數:靜態(tài)、虛、和非靜態(tài)。每一種函數的調用方式都不同,當然他們的作用也會(huì )有區別,一般來(lái)說(shuō)我們只要掌握根據我們的需要正確的使用這三種類(lèi)型的成員函數便可以了,至于內部是如何運做的我們可以不知。但是《深度探索C++對象模型》正是讓我們對這些不知道的東西進(jìn)行深度探索的一本書(shū)。通過(guò)前面的學(xué)習,我想我知道了一些以前不知道的東西,但是感覺(jué)并沒(méi)有提高多少,也許是我對此書(shū)的學(xué)習還停留在一個(gè)比較膚淺的層次上吧。我想我應該會(huì )抽時(shí)間再看幾遍。有些跑題了,因為雷神想說(shuō)明一下,這些筆記只是雷神看書(shū)是的一些想法的記錄,如果你再看僅供參考,因為我本人好象也只探索了不是很深的程度。
我們的在設計和使用類(lèi)時(shí)最常用的便是非靜態(tài)成員函數,使用成員函數是為了封裝和隱藏我們的數據,我想這是成員函數和外部函數的最明顯的區別。但是他們的效率是否有不同呢?我們不會(huì )想為了保護我們的數據而使用成員函數,最后確導致效率降低的結果。讓我們看看非靜態(tài)成員函數在實(shí)際的執行時(shí)被編譯器搞成了什么樣子。
float magnitude3d(const Point3d *_this){…} //這是一個(gè)外部函數,它有參數。表示它間接的取得坐標(Point3d)成員。 float Point3d::mangnitude3d() const {…} //這是一個(gè)成員函數,它直接取得坐標(Point3d)的成員。 表面上看,似乎成員函數的效率高很多,但實(shí)際上他們的效率真的想我們想象的那樣嗎?非也。實(shí)際上一個(gè)成員函數被內部轉化成了外部函數。 1、 一個(gè)this指針被加入到成員函數的參數中,為的是能夠使類(lèi)的對象調用這個(gè)函數。 2、 將對所有非靜態(tài)數據成員的存取操作改為由this來(lái)存取。 3、 對函數的名稱(chēng)進(jìn)行重新的處理,使它成為程序中獨一無(wú)二的。 這時(shí)后,經(jīng)過(guò)以上的轉換,成員函數已經(jīng)成為了非成員函數。 float Point3d::mangnitude3d() const {…}//成員函數將被變成下面的樣子 //偽碼 mangnitude3d__7Point3dFv(register Point3d * const this) { return sqrt(this->_x * this->x+ this->_y * this->y+ this->_z * this->z); }
調用此函數的操作也被轉換 obj. mangnitude3d() 被轉換成: mangnitude3d__7Point3dFv(*obj); 怎么樣看出來(lái)了吧,和我們開(kāi)始聲明的非成員函數沒(méi)有區別了。因此得出結論:兩個(gè)鐵球同時(shí)落地。
一般來(lái)說(shuō),一個(gè)成員的名稱(chēng)前面會(huì )被加上類(lèi)的名稱(chēng),形成唯一的命名。實(shí)際上在對成員名稱(chēng)做處理時(shí),除了加上了類(lèi)名,還會(huì )將參數的鏈表一并加上,這樣才能保證結果是獨一無(wú)二的。
我們在來(lái)看看靜態(tài)成員函數。我們有這樣的概念,成員函數的調用必須是用類(lèi)的對象,象這樣obj.fun();或者這樣ptr->fun().但實(shí)際上,只有一個(gè)或多個(gè)靜態(tài)數據成員被成員函數存取時(shí)才需要類(lèi)的對象。類(lèi)的對象提供一個(gè)指針this,用來(lái)將用到的非靜態(tài)數據成員綁定到類(lèi)對象對應的成員上。如果沒(méi)有用到任何一個(gè)成員數據,就不需要用到this指針,也就沒(méi)有必要通過(guò)類(lèi)的對象來(lái)調用一個(gè)成員函數。而且我們還知道靜態(tài)數據成員是在類(lèi)之外的,可以被視做全局變量的,只不過(guò)它只在一個(gè)類(lèi)的生命范圍內可見(jiàn)。(參考前面的筆記)。而且一般來(lái)說(shuō)我們會(huì )將靜態(tài)的數據成員聲明為一個(gè)非Public。這樣我們便必須提供一個(gè)或多個(gè)成員函數用來(lái)存取這個(gè)成員。雖然我們可以不依靠類(lèi)的對象存取靜態(tài)數據成員,但是這個(gè)可以用來(lái)存取靜態(tài)成員的函數確實(shí)必須綁定在類(lèi)的對象上的。為了更加好的解決這個(gè)問(wèn)題,cfront2.0引入了靜態(tài)成員函數的概念。
靜態(tài)成員函數是沒(méi)有this指針的。因為它不需要通過(guò)類(lèi)的對象來(lái)調用。而且它不能直接存取類(lèi)中的非靜態(tài)成員。并且不能夠被聲明為virtual,const,volatile.如果取得一個(gè)靜態(tài)成員函數的地址,那么我們獲得的是這個(gè)函數在內存中的位置。(非靜態(tài)成員函數的地址我們獲得的是一個(gè)指向這個(gè)類(lèi)成員函數的指針,函數指針)??梢钥吹接捎陟o態(tài)成員函數沒(méi)有this指針,和非成員函數非常的相似。
有了前面幾章的基礎,好象這些描述理解起來(lái)也不很費勁,而且我們的思路可以跟著(zhù)書(shū)上所說(shuō)的一路傾瀉下來(lái),這便是讀書(shū)的樂(lè )趣所在了,如果一本書(shū)讀起來(lái)都想讀第一章時(shí)那樣費勁,我想我讀不下去的可能性會(huì )很高。
繼續我們的學(xué)習,下面書(shū)上開(kāi)始將虛函數了。我們知道虛函數是C++的一個(gè)很重要的特性,面向對象的多態(tài)便是由虛函數實(shí)現的。多態(tài)的概念是一個(gè)用一個(gè)public base class的指針(或者引用),尋址出一個(gè)派生類(lèi)對象。虛函數實(shí)現的模型是這樣。每一個(gè)類(lèi)都有一個(gè)虛函數表,它包含類(lèi)中有作用的虛函數的地址,當類(lèi)產(chǎn)生對象時(shí)會(huì )有一個(gè)指針,指向虛函數表。為了支持虛函數的機制,便有了“執行期多態(tài)”的形式。
下面這樣。 我們可以定義一個(gè)基類(lèi)的指針。 Point *ptr; 然后在執行期使他尋址出我們需要的對象??梢允?br>ptr =new Point2d; 還可以是 ptr=new Pont3d; ptr這個(gè)指針負責使程序在任何地方都可以采用一組由基類(lèi)派生的類(lèi)型。這種多態(tài)形式是消極的,因為它必須在編譯時(shí)期完成。與之對應的是一種多態(tài)的積極形式,即在執行期完成用指針或引用查找我們的一個(gè)派生類(lèi)的對象。 象下面這樣: ptr->z(); 要想達到我們目的,這個(gè)函數z()應該是虛函數,并且還應該知道ptr所指的對象的真實(shí)類(lèi)型,以便我們選擇z()的實(shí)體。以及z()實(shí)體的位置,以便我們能夠調用它。這些工作編譯器都會(huì )為我們做好,編譯器是如何做的呢? 我們已知每一個(gè)類(lèi)會(huì )有一個(gè)虛函數表,這個(gè)表中含有對應類(lèi)的對象的所有虛函數實(shí)體的地址,并且可能會(huì )改寫(xiě)一個(gè)基類(lèi)的虛函數實(shí)體。如果沒(méi)有改寫(xiě)基類(lèi)存在的虛函數實(shí)體,則會(huì )繼承基類(lèi)的函數實(shí)體,這還沒(méi)完,還會(huì )有一個(gè)pure_virtual_called()的函數實(shí)體。每一個(gè)虛函數不論是繼承的還是改寫(xiě)的,都會(huì )被指派一個(gè)固定的索引值,這個(gè)索引在整個(gè)繼承體系中保持與特定的虛函數關(guān)聯(lián)。 說(shuō)明:當沒(méi)有改寫(xiě)基類(lèi)的虛函數時(shí),該函數的實(shí)體地址是被拷貝到派生類(lèi)的虛函數表中的。
這樣我們便實(shí)現了執行期的積極多態(tài)。這種形式的特點(diǎn)是,我們從頭到尾都不知道ptr指針指向了那一個(gè)對象類(lèi)型,基類(lèi)?派生類(lèi)1?派生類(lèi)2?我們不知道,也不需要知道。我們只需要知道ptr指向的虛函數表。而且我們也不知道z()函數的實(shí)體會(huì )被調用,我們只知道z()函數的函數地址被放在虛函數表中的位置。
總結:在單一繼承的體系中,虛函數機制是一種很有效率的機制。我們判斷一個(gè)類(lèi)是否支持多態(tài),只需要看它有沒(méi)有虛函數便可以了。
|
| 深度探索C++對象模型(8) |
| 出自:雷神 2002年11月19日 13:12 |
介紹 但是構造函數和析構函數和new和delete不同,他們并非必須成對的出現。決定是否為一個(gè)類(lèi)寫(xiě)構造函數或者析構函數,是取決于這個(gè)類(lèi)對象的生命在哪里結束(或開(kāi)始)。需要什么操作才能保證對象的完整。 正文 深度探索C++對象模型(8) 雷神
書(shū)的第四章后半部分詳細的講解內聯(lián)函數,由于比較容易理解,雷神做一個(gè)簡(jiǎn)單總結便過(guò)去吧。 內聯(lián)函數和其他的函數相比是一種效率很高的函數,未優(yōu)化的情況下效率可以提高25%,優(yōu)化以后簡(jiǎn)直是數量級的變化,書(shū)上的給出的數據是0.08比4.43。簡(jiǎn)直沒(méi)法比了。內聯(lián)函數對于封裝提供了一種必要的支持,可以有效的存去類(lèi)中的非共有數據成員,同時(shí)可以替代#define(前置處理宏)。但是它也有缺點(diǎn),程序會(huì )隨著(zhù)調用內聯(lián)函數次數的增多,而產(chǎn)生大量的擴展碼。 在內聯(lián)函數的擴展時(shí)每一個(gè)形式參數被對應的實(shí)參取代,因此會(huì )有副作用。通常需要引入臨時(shí)對象解決多次對實(shí)際參數求值的操作產(chǎn)生的副作用。
第五章的開(kāi)始給出了一個(gè)不恰當的抽象類(lèi)的聲明: class Abstract_base { public: virtual ~Abstract_base()=0;//純虛析構函數 virtual void interface() const=0; //純虛函數 virtual const char* mumble() const{return _mumble;} protected: char *_mumble; }; 這是一個(gè)不能產(chǎn)生實(shí)體的抽象類(lèi),因為它有純虛函數。為什么說(shuō)它存在不合適的地方呢?以下逐一進(jìn)行說(shuō)明。 1、 它沒(méi)有一個(gè)明確的構造函數,因為沒(méi)有構造函數來(lái)初始化數據成員則它的派生類(lèi)無(wú)法決定數據成員的初值。類(lèi)的成員數據應該在構造函數或成員函數中被指定初值,否則將破壞封裝性質(zhì)。 2、 每一個(gè)派生類(lèi)的析構函數會(huì )被編譯器進(jìn)行擴展以靜態(tài)調用方式調用其上層基類(lèi)的析構,哪怕是純虛函數。但是編譯器并不能在鏈接時(shí)找到純虛的析構函數,然后合成一個(gè)必要的函數實(shí)體,因此最好不要把虛的析構函數聲明成純虛的。 3、 除非必要,不要把所有的成員函數都聲明為虛函數。這不是一個(gè)好設計觀(guān)念。 4、 除非必要,不要使用const聲明函數,因為很多派生的實(shí)體需要修改數據成員。
有了以上的觀(guān)點(diǎn)上面的抽象類(lèi)應該改為下面這種樣子: class Abstract_base { public: virtual ~Absteact_base(); //不在是純虛 virtual void interface()=0; //不在是const const char * mumble() const{return _mumble;} //不在是虛函數 protected: Abstract_base(char *pc=0); //增加了唯一參數的構造 Char *_mumble; };
下一個(gè)問(wèn)題,對象的構造。構造一個(gè)對象出來(lái)很簡(jiǎn)單,這是我們在編程時(shí)經(jīng)常要做的事情。我理解書(shū)上的意思是為我們分析了各種不同的類(lèi),例如一個(gè)沒(méi)有Copy constructor,Copy operator的類(lèi),或者有私有變量但是沒(méi)有定義虛函數的類(lèi)等等,當他們構造對象時(shí)也有多種情況,global,local,還有在new時(shí),編譯器都做了什么,內存的分配情況如何。搞清楚它們也很有意思。另外這好象是前面幾章學(xué)到的東西的一個(gè)進(jìn)一步的研究。我們找出最復雜的虛擬繼承來(lái)進(jìn)行一下研究。當一個(gè)類(lèi)對象被構造時(shí),實(shí)際上這個(gè)類(lèi)的構造函數被調用,不論是我們自己寫(xiě)的,還是由編譯器為我們合成的。并且編譯器會(huì )背著(zhù)我們做很多的擴充工作,將記錄在成員初始化列表中的數據成員的初始化工作放進(jìn)構造函數,如果一個(gè)數據成員沒(méi)有在成員初始化列表中出現,則會(huì )調用默認的構造函數,這個(gè)類(lèi)的所有基類(lèi)的構造都會(huì )被調用,以基類(lèi)的聲明順序。所有的虛擬基類(lèi)的構造也會(huì )被調用。還要為virtual table pointers設定初始值,指向適當的virtual tables。好家伙,編譯器還真累。好象說(shuō)的不是很清楚,抄一段書(shū)上的代碼。
已知一個(gè)類(lèi)的層次結構和派生關(guān)系如下圖:
 見(jiàn)書(shū)上P211。 這是程序員給出的PVertex的構造函數: PVertex::PVertex(float x,float y,float z):_next(0),Vertex3d(x,y,z),Point(x,y) { if(spyOn) cerr<<”within PVertex::PVertex()”<<”size:”<<size()<<endl; }
它可能被擴展成為: //C++偽碼 // PVertex構造函數的擴展結果 PVertex * PVertex::PVertex(PVertex * this,bool most_derived,float x,float y,float z) { //條件式的調用虛基類(lèi)的構造函數 if(_most_derived!=false) this->Point::Point(x,y); //無(wú)條件的調用上層基類(lèi)的構造函數 this->Vertex3d::Vertex3d(x,y,z); //將相關(guān)的vptr初始化 this->_vptr_PVertex=_vtbl_PVertex; this->_vptr_Point_PVertex=_vtbl_Point_PVertex;
//原來(lái)構造函數中的代碼 if(spyOn) cerr<<”within PVertex::PVertex()”<<”size:” //經(jīng)虛擬機制調用 <<(*this->_vptr_PVertex[3].faddr )(this)<<endl; //返回被構造的對象 return this; } 通過(guò)上面的代碼我們可以比較清晰的了解在有多重繼承+虛擬繼承的時(shí)候構造一個(gè)對象時(shí),編譯會(huì )將構造函數擴充成一個(gè)什么樣子。以及擴充的順序。知道了這個(gè)相對于無(wú)繼承,或者不是虛擬繼承時(shí)對象的構造應該也可以理解了。與構造對象相對應的是析構。但是構造函數和析構函數和new和delete不同,他們并非必須成對的出現。決定是否為一個(gè)類(lèi)寫(xiě)構造函數或者析構函數,是取決于這個(gè)類(lèi)對象的生命在哪里結束(或開(kāi)始)。需要什么操作才能保證對象的完整。象構造函數一樣析構函數的最佳實(shí)現策略是維護兩份destructor實(shí)體。一個(gè)complete object實(shí)體,總是設定好vptrs,并調用虛擬基類(lèi)的析構函數。一個(gè)base class subobject實(shí)體。除非在析構函數中調用一個(gè)虛函數,否則絕不會(huì )調用虛擬基類(lèi)的析構函數,并設定vptrs。 一個(gè)對象生命結束于析構函數開(kāi)始執行的時(shí)候。它的擴展形式和構造函數的擴展順序相反。
|
| 深度探索C++對象模型(9) |
| 出自:雷神 2002年11月19日 13:13 |
介紹 當編譯一個(gè)C++程序時(shí),計算機的內存被分成了4個(gè)區域,一個(gè)包括程序的代碼,一個(gè)包括所有的全局變量,一個(gè)是堆棧,還有一個(gè)是堆(heap),我們稱(chēng)堆是自由的內存區域,我們可以通過(guò)new和delete把對象放在這個(gè)區域。你可以在任何地方分配和釋放自由存儲區。但是要注意因為分配在堆中的對象沒(méi)有作用域的限制,因此一旦new了它,必須delete它,否則程序將崩潰,這便是內存泄漏。(C#已經(jīng)通過(guò)內存托管解決了這一令人頭疼的問(wèn)題)。C++通過(guò)new來(lái)分配內存,new的參數是一個(gè)表達式,該表達式返回需要分配的內存字節數,這是我以前掌握的關(guān)于new的知識,下面看看通過(guò)這本書(shū),使我們能夠更進(jìn)一步的了解到些什么。 讀者評分 15 評分次數 5 正文 深度探索C++對象模型(9) 雷神
這一章主要是說(shuō)Runtime Semantics執行期語(yǔ)義學(xué)。
這是我們平時(shí)寫(xiě)的程序片段: Matrix identity; //一個(gè)全局對象 Main() { Matrix m1=identity; …… return 0; } 很常見(jiàn)的一個(gè)代碼片段,雷神從來(lái)沒(méi)有考慮過(guò)identity如何被構造,或者如何被銷(xiāo)毀。因為它肯定在Matrix m1=identity之前就被構造出來(lái)了,并且在main函數結束前被銷(xiāo)毀了。我們不用考慮這些問(wèn)題,好象C++就應該這樣。但這本書(shū)是研究C++底層機制的。既然我們在看這本書(shū),說(shuō)明我們希望了解C++的編譯器又做了那些大量的工作,使得我們可以這樣使用對象。
在C++程序中所有的全局對象都被放在data segment中,如果明確賦值,則對象以該值為初值,否則所配置到內存內容為0。也就是說(shuō),如果我們有以下定義 Int v1=1024; Int v2; 則v1和v2都被配置于data segment,v1值為1024,v2值為0。(雷神在VC6環(huán)境用MFC編程時(shí)中發(fā)現如果int v2;v2的值不為0,而是-8,不知為什么?編譯器造成的?)。
如果有一個(gè)全局對象,并且這個(gè)對象有構造函數和析構函數的話(huà),它需要靜態(tài)的初始化操作和內存釋放工作,C++是一種跨平臺的編程語(yǔ)言,因此它的編譯器需要一種可以移植的靜態(tài)初始化和內存釋放的方法。下面便是它的策略。 1、 為每一個(gè)需要靜態(tài)初始化的檔案產(chǎn)生一個(gè)_sit()函數,內帶構造函數或內聯(lián)的擴展。 2、 為每一個(gè)需要靜態(tài)的內存釋放操作的文件中,產(chǎn)生一個(gè)_std()函數,內帶析構函數或內聯(lián)的擴展。 3、 提供一個(gè)_main()函數,用來(lái)調用所有的_sti()函數,還有一個(gè)exit()函數調用所有的_std()函數。 侯先生說(shuō): Sit可以理解成static initialization的縮寫(xiě)。 Std可以理解成static deallocation的縮寫(xiě)。 那么main函數會(huì )被編譯器變成這樣: Matrix identity; //一個(gè)全局對象 Main() { _main();//對所有的全局對象做static initialization動(dòng)作。 Matrix m1=identity; …… exit();//對所有的全局對象做static deallocation動(dòng)作。 } 其中_main()會(huì )有一個(gè)對identity對象的靜態(tài)初始化的_sti函數,象下面偽碼這樣: // matrix_c是文件名編碼_identity表示靜態(tài)對象,這樣能夠保證向執行文件提供唯一的識別符號 _sti__matrix_c_identity() { identity.Matrix:: Matrix(); //這就是靜態(tài)初始化 } 相應的在exit()函數也會(huì )有一個(gè)_std_matrix_c_identity(),來(lái)進(jìn)行static deallocation動(dòng)作。 但是被靜態(tài)初始化的對象有一些缺點(diǎn),在使用異常時(shí),對象不能被放置在try區段內。還有對象的相依順序引出的復雜度,因此不建議使用需要靜態(tài)初始化的全局對象。
局部靜態(tài)對象在C++底層機制是如何構造和在內存中銷(xiāo)毀的呢? 1、 導入一個(gè)臨時(shí)對象用來(lái)保護局部靜態(tài)對象的初始化操作。 2、 第一次處理時(shí),臨時(shí)對象為false,于是構造函數被調用,然后臨時(shí)對象被改為true. 3、 臨時(shí)對象的true或者false便成為了判斷對象是否被構造的標準。 4、 根據判斷的結果決定對象的析構函數是否執行。
如果一個(gè)類(lèi)定義了構造函數或者析構函數,則當你定義了一個(gè)對象數組時(shí),編譯器會(huì )通過(guò)運行庫將你的定義進(jìn)行加工,例如: point knots[10]; //我們的定義 vec_new(&knots,sizeof(point),10,&point::point,0); //編譯器調用vec_new()操作。
下面給出vec_new()原型,不同的編譯器會(huì )有差別。 void * vec_new( void *array, //數組的起始地址 size_t elem_size, //每個(gè)對象的大小 int elem_count, //數組元素個(gè)數 void(*constructor)(void*), void(*destructor)(void* ,char) ) 對于明顯獲得初值的元素,vec_new()不再有必要,例如: point knots[10]={ Point(), //knots[0] Point(1.0,1.0,0.5), //knots[1] -1.0 //knots[2] }; 會(huì )被編譯器轉換成: //C++偽碼 Point::Point(&knots[0]); Point::Point(&knots[1],1.0,1.0,0.5); Point::Point(&knots[2],-1.0,0.0,0.0); vec_new(&knots,sizeof(point),10,&point::point,0); //剩下的元素,編譯器調用vec_new()操作。 怎么樣,很神奇吧。
當編譯一個(gè)C++程序時(shí),計算機的內存被分成了4個(gè)區域,一個(gè)包括程序的代碼,一個(gè)包括所有的全局變量,一個(gè)是堆棧,還有一個(gè)是堆(heap),我們稱(chēng)堆是自由的內存區域,我們可以通過(guò)new和delete把對象放在這個(gè)區域。你可以在任何地方分配和釋放自由存儲區。但是要注意因為分配在堆中的對象沒(méi)有作用域的限制,因此一旦new了它,必須delete它,否則程序將崩潰,這便是內存泄漏。(C#已經(jīng)通過(guò)內存托管解決了這一令人頭疼的問(wèn)題)。C++通過(guò)new來(lái)分配內存,new的參數是一個(gè)表達式,該表達式返回需要分配的內存字節數,這是我以前掌握的關(guān)于new的知識,下面看看通過(guò)這本書(shū),使我們能夠更進(jìn)一步的了解到些什么。 Point3d *origin=new Point3d; //我們new 了一個(gè)Point3d對象 編譯器開(kāi)始工作,上面的一行代碼被轉換成為下面的偽碼: Point3d * origin; If(origin=_new(sizeof(Point3d))) { try{ origin=Point3d::Point3d(origin); } catch(…){ _delete(origin); throw; } } 而delete origin; 會(huì )被轉換成(雷神將書(shū)上的代碼改為exception handling情況): if(origin!=0){ try{ Point3d::~Point3d(origin); _delete(origin); catch(…){ _delete(origin); //不知對否? throw; } } 一般來(lái)說(shuō)對于new的操作都直截了當,但語(yǔ)言要求每一次對new的調用都必須傳回一個(gè)唯一的指針,解決這個(gè)問(wèn)題的辦法是,傳回一個(gè)指針指向一個(gè)默認為size=1的內存區塊,實(shí)際上是以標準的C的malloc()來(lái)完成。同樣delete也是由標準C的free()來(lái)完成。原來(lái)如此。
最后這篇筆記再說(shuō)說(shuō)臨時(shí)對象的問(wèn)題。 T operator+(const T&,const T&); //如果我們有一個(gè)函數 T a,b,c; //以及三個(gè)對象: c=a+b; //可能會(huì )導致臨時(shí)對象產(chǎn)生。用來(lái)放置a+b的返回值。然后再由 T的copy constructor把臨時(shí)對象當作c的初值。也有可能直接由拷貝構造將a+b的值放到c中,這時(shí)便不需要臨時(shí)對象。另外還有一種可能通過(guò)操作符的重載定義,經(jīng)named return value優(yōu)化也可以獲得c對象。這三種方法結果一樣,區別在于初始化的成本。對臨時(shí)對象書(shū)上有很好的總結: 在某些環(huán)境下,有processor產(chǎn)生的臨時(shí)對象是有必要的,也是比較方便的,這樣的臨時(shí)對象由編譯器決定。 臨時(shí)對象的銷(xiāo)毀應該是對完整表達式求值過(guò)程的最后一個(gè)步驟。 因為臨時(shí)對象是根據執行期語(yǔ)義有條件的產(chǎn)生,因此它的生命規則就顯得很復雜。C++標準要求凡含有表達式執行結果的臨時(shí)對象,應該保留到對象的初始化操作完成為止。當然這樣也會(huì )有例外,當一個(gè)臨時(shí)對象被一個(gè)引用綁定時(shí),對象將殘留,直到被初始化的引用的生命結束,或者超出臨時(shí)對象的作用域。
|