推薦使用 boost.property_tree,簡潔,高效,支持多種格式: ini , xml ...
并且使用統(tǒng)一的操作接口。
re: C++的一個疑惑 luckycat 2010-12-20 22:43
B(A& a){} 不但是一個構(gòu)造函數(shù),而且是一個自定義的類型轉(zhuǎn)換操作( A -> B),你的問題有出在這里,如果要去掉這種非有意的自定義類型轉(zhuǎn)換,使用 explicit B(A& a){}。
一個 非const引用,只能引用與其類型完全相同的對象,或者是其派生類的對象 ,所以 B &refB = objectB ; B &refB = objectB1 都是合法的,但是 B &refB = objectA 就不是合法的
,因?yàn)?A 與 B的類型不相同,且不是B的派生類,所以編譯時會報(bào)錯,于是 " B1( B& b ); A a; B1 b1(a)" 就不能通過,簡化一下就相當(dāng)于 " B &b = a".
一個 const引用 滿足 非const引用 的特性的同時,還有很重要的一點(diǎn),const 引用可以引用一個與其類型完全不相同的類型(因?yàn)榫幾g器會生成一個轉(zhuǎn)換后可引用的臨時對象),
前提是被引用的類型可以轉(zhuǎn)換為引用的類型(編譯器自定義的類型提升,或者是用戶自定義的類型轉(zhuǎn)換,如上面的 B(A& a)。 ),
舉個例子:
const int &iValue = 3.14; 就是OK的,這里使用編譯器內(nèi)部的類型轉(zhuǎn)換 double -> int.
const B &b = a; 也是OK的,因?yàn)槭褂?B( A &a) 可以將 A -> B ,于是 const B &b = a; 的背后,編譯器所做的就是:
const B tempB( a ); //調(diào)用 B( A &a)
const B &b = tempB;
BTW: 為什么在const引用情況下,編譯器會生成一個可被引用的臨時對象,原因很簡單,你是用一個 const引用 來操作這個臨時對象,所以,這個臨時對象的狀態(tài)是不會變的,
也就是安全的(當(dāng)然,如果你把const引用 const_cast 成一個非 const引用來操作這個編譯器生成的臨時對象,那么結(jié)果是未定義的).
re: Google Test測試框架 luckycat 2010-05-26 23:00
@ouyang
GoogleTest同樣可以用于測試Win32 GUI Application。
你的想法可能是MFC寫出來的應(yīng)用是沒有對應(yīng)的控制臺,所以GoogleTest無法將輸出結(jié)果顯示出來(實(shí)際上Win GUI應(yīng)用程序也可以同時具備Console Output,只不過這需要手工編碼實(shí)現(xiàn),默認(rèn)情況下是沒有的),當(dāng)然GoogleTest已經(jīng)考慮到這個問題了,GoogleTest支持將測試結(jié)果以XML文件格式輸出.參考下面的鏈接:
http://code.google.com/p/googletest/wiki/GoogleTestAdvancedGuide#Controlling_Test_Output
@小蘇
VC6我這里沒有,我上面的輸出是在VC2008下的測試結(jié)果,你換用VC2005/2008再試試.
我的建議是學(xué)習(xí)C++就不要用VC6了,可以用VC2005/2008.
如果你想用一個輕量級的環(huán)境學(xué)習(xí)C++,MinGW Studio,
wxDev-Cpp , CodeBlocks , CodeLite 都是不錯的選擇.
@小蘇
為了更好的理解我上面的分析,你可以打開VC2005/VC2008(我這里是VC2008)的單步調(diào)試,
在調(diào)試模式下的"自動窗口"(位于IDE下方).
觀察memset 前后"man -> strName -> _Bx -> _Ptr"的值的變化
_Ptr 實(shí)際上就是std::string內(nèi)部用于存儲字符串的堆內(nèi)存緩沖區(qū)的地址,也相當(dāng)于我上面提到的 m_pCharBuffer
@小蘇
在閱讀下面的分析之前,希望你對"C++對象的內(nèi)部布局"有一定的了解.
既然你也發(fā)現(xiàn)了內(nèi)存泄漏的情況,那么你再用下面的測試代碼運(yùn)行一下:
#include "Windows.h"
#include <string>
#include <cstring>
#include <cstdlib>
using namespace std;
typedef unsigned int UINT32;
typedef unsigned short UINT16;
typedef struct structMan
{
UINT32 sexType; //ENUM_SEXY_TYPE
UINT16 usAge;
string strName;
string strAddress;
bool operator < (const structMan &man) const
{
return usAge < man.usAge;
}
bool operator > (const structMan &man) const
{
return usAge > man.usAge;
}
}MAN;
int main( int argc , char *argv[] )
{
while( true )
{
MAN man;
fprintf( stdout , "before memset: char buffer address( heap address ) to store string = %p , size = %lu , capacity = %lu\n" , \
*reinterpret_cast< const int* >( man.strName.c_str() ) , \
man.strName.size() , man.strName.capacity() );
memset( &man , 0 , sizeof( MAN ) );
fprintf( stdout , "after memset: char buffer address( heap address ) to store string = %p , size = %lu , capacity = %lu\n\n\n" , \
*reinterpret_cast< const int* >( man.strName.c_str() ) , \
man.strName.size() , man.strName.capacity() );
man.strAddress = "abcdef";
man.strName = "abc";
Sleep( 1000 ); //這里sleep是為了讓大家有時間在任務(wù)管理器中看到內(nèi)存增長的過程,不至于一下子耗盡內(nèi)存.
}
return 0;
}
我選取我這里的一個循環(huán)中的輸出,如下:
before memset: char buffer address( heap address ) to store string = 00636200 ,
size = 0 , capacity = 15
after memset: char buffer address( heap address ) to store string = 00000000 ,
size = 0 , capacity = 0
下面把上述代碼進(jìn)行簡化便于分析:
while( true )
{
MAN man; //這里會使用man由編譯器自動生成缺省構(gòu)造函數(shù)來調(diào)用strName的缺省構(gòu)造函數(shù)對strName進(jìn)行構(gòu)造.
memset( &man , 0 , sizeof( MAN ) );
上面的memset操作會把 &man 這個地址開始的 sizeof( MAN )字節(jié)的內(nèi)存空間全部清零.
這也就意味著 man 對象內(nèi)部的每個成員子對象所占據(jù)的內(nèi)存都被清零.
man 對象內(nèi)部一個 std::string , 而std::string 內(nèi)部包含一個std::string用于實(shí)際存儲字符串的指向動態(tài)分配的堆內(nèi)存的指針,
我們假設(shè)這個指針的名稱為 m_pCharBuffer;
在std::string的析構(gòu)函數(shù)中釋放這個動態(tài)分配的堆內(nèi)存的時候需要使用這個m_pCharBuffer,也即是調(diào)用 delete[] m_pCharBuffer;
如果我寫出下在的代碼:
char *m_pCharBuffer = new char[ BUFFER_SIZE ]; 這一個操作即是strName的缺少構(gòu)造函數(shù)的操作,只不過 BUFFER_SIZE = 15 + 1(最后有一個'\0');
m_pCharBuffer = NULL; //這一個操作與上述的 memset 對 man.strName中用于指向動態(tài)內(nèi)存的指針?biāo)a(chǎn)生的作用相同:將指針?biāo)赶虻亩褍?nèi)存地址清零.
delete[] m_pCharBuffer; //此時 m_pCharBuffer 為NULL , 不過在C++中, delete NULL指針是安全的.不過因?yàn)?m_pCharBuffer 已經(jīng)不指向上述new出來的內(nèi)存
//所以這里進(jìn)行 delete[] m_pCharBuffer 時已經(jīng)不能進(jìn)行資源的釋放了,也即是發(fā)生了內(nèi)存泄漏.
man.strName = "abc";
上面的賦值操作中,實(shí)際上要調(diào)用: std::string::operator=( const char* );
首先,operator=會判斷當(dāng)前的strName的 capacity能否容納下"abc",由上面的memset之后我們可以看出此時存儲 capacity 值的變量因?yàn)閙emset為0,所以
man.strName.capacity() 輸出為0,這也就意味著這個"容積"不能容納下3個字節(jié)的"abc".
所以這時 operator= 要擴(kuò)大內(nèi)部用于存儲字符串的緩沖區(qū),擴(kuò)充的基本原理如下:(代碼進(jìn)行簡化處理)
std::string& operator=( const char *szString )
{
// check parameter
if( m_pCharBuffer != szString ) // 防止 self-assign
{
delete[] m_pCharBuffer;
m_pCharBuffer = new char[ NEW_SIZE ];
memcpy( m_pCharBuffer , szString , strlen( szString ) + 1 );
}
return *this;
}
上面的操作: *reinterpret_cast< const int* >( man.strName.c_str() ) 即是相當(dāng)于獲取這個 m_pCharBuffer 的地址.
這一點(diǎn)你一定要明白.
由上面的代碼以及運(yùn)行輸出可以知道,
注意: 在調(diào)用 strName = "abc"時,已經(jīng)進(jìn)行了memset操作,此時的 m_pCharBuffer 已經(jīng)因?yàn)樯厦娴?memset操作而被清零,即是 m_pCharBuffer = NULL,
因?yàn)閙emset操作不會調(diào)用析構(gòu)函數(shù) ,所以實(shí)際上在清零之前它所指向的動態(tài)內(nèi)存塊并沒有被釋放,
在 operator=中,delete[] m_pCharBuffer; 相當(dāng)于 delete[] NULL;
這就不能釋放 m_pCharBuffer 之前在缺省構(gòu)造時所指向的動態(tài)分配的 15 + 1 字節(jié)的內(nèi)存了,所以出現(xiàn)了內(nèi)存泄漏.
}
@小蘇
你運(yùn)行后仔細(xì)觀察這個編譯后運(yùn)行的exe在"windows任務(wù)管理器"中對應(yīng)的
"內(nèi)存使用"數(shù)值.
我用VC2005和VC2008都測試過,結(jié)果是"內(nèi)存不停增長".
還用哪位同學(xué)運(yùn)行過我上面的測試代碼,出來公布一下測試結(jié)果,謝謝!
@陳梓瀚(vczh)
你列舉的判斷標(biāo)準(zhǔn)都值得借鑒,不過你后續(xù)補(bǔ)充的對"例外情況"的處理方式不敢茍同:
因?yàn)闃?gòu)造可能沒有成功,那么我們需要調(diào)用IsAvailable之類的函數(shù),甚至于后續(xù)需要因?yàn)榕袛嘀皹?gòu)造函數(shù)的狀態(tài)來
對調(diào)用的每個成員函數(shù)進(jìn)行"try catch"或者還要從"每個成員函數(shù)的返回值中來判斷之前的構(gòu)造操作是否成功".
這種設(shè)計(jì)是可行的,但對類的使用者來說太復(fù)雜.
這種情況下我覺得使用"兩段構(gòu)造"可能更好一些,我們只需要判斷"兩段構(gòu)造"是否成功即可,如果構(gòu)造成功,在后續(xù)的成員函數(shù)調(diào)用過程中,
就再也不用為了確認(rèn)構(gòu)造函數(shù)的狀態(tài)來對每個被調(diào)用的成員函數(shù)進(jìn)行"try catch"或檢查返回值的操作,這樣的設(shè)計(jì)應(yīng)該更簡潔一些.
@小蘇
你后續(xù)修改的代碼,在我看來,即使在多個編譯器下都是OK的,但是就代碼風(fēng)格來說,還有改進(jìn)的地方.
在編碼過程中,我很少會對struct進(jìn)行memset操作,只是偶爾會對sockaddr進(jìn)行memset操作;更不會對class進(jìn)行memset操作.
在你上述的代碼中,你對MAN進(jìn)行memset操作,無非也就是想將各個成員的初值清零,如果基于這個出發(fā)點(diǎn),那設(shè)計(jì)一個構(gòu)造函數(shù)多好:
structMan::structMan( UINT32 enumSexType = SEXY_TYPE_MAN , \
UINT16 uiAge = 0 , \
const std::string &refStrName = "" , \
const std::string &refStrAddress = "" )
:sexType( enumSexType ) , usAge( uiAge ) , \
strName( refStrName ) , strAddress( refStrAddress )
{
// check parameters here。
}
只需要少量的代碼就會帶來大量的方便,而且你也就再也不用memset.
你也就不需要對struct的各個成員依次賦值了,直接傳參構(gòu)造就可以了,這樣代碼應(yīng)該會更優(yōu)雅一些.
另一方面,對 std::list 進(jìn)行sort操作從邏輯上是沒有問題的,但是設(shè)計(jì)風(fēng)格上是有問題的:
因?yàn)閟td::list中的每一個成員是基于鏈的形式連接在一起的,所以我們不能對其進(jìn)行隨機(jī)訪問,
如果我們要訪問std::list中的第N個成員,那么我們需要從鏈表頭開始向鏈表尾部依次迭代N次,
在這種情況下,如果一個鏈表過大,那么這里就有效率問題.
一般情況下,我們只對"類似于數(shù)組的可以隨機(jī)訪問"的std容器進(jìn)行排序.
呵呵,我就喜歡大家這種踴躍討論的氛圍,互相學(xué)習(xí):)
上面的代碼你在VC6下面測試通過了,因?yàn)閺奈业牡谝桓杏X來看,必定:coredump.
當(dāng)時我還真不太相信,所以我自己也測試了一下,結(jié)果如下:
Win32: VC2005 debug/release下均可運(yùn)行正常,不過因?yàn)閙emset非POD,出現(xiàn)內(nèi)存泄漏.
Win32: MinGW Studio 直接abort.(這是我預(yù)期的結(jié)果).
Linux: Slackware32/GCC 直接abort.(這也是我預(yù)期的結(jié)果).
為了證明上在win32/VC2005下上面的代碼出現(xiàn)內(nèi)存泄漏,大家可以用下面的代碼做測試:
(這里把小蘇同學(xué)的代碼取了一部分用于配合測試)
運(yùn)行下面的代碼,大家在任務(wù)管理器中觀察內(nèi)存增長情況:)
#include "Windows.h"
#include <string>
#include <cstring>
#include <cstdlib>
using namespace std;
typedef unsigned int UINT32;
typedef unsigned short UINT16;
typedef struct structMan
{
UINT32 sexType; //ENUM_SEXY_TYPE
UINT16 usAge;
string strName;
string strAddress;
bool operator < (const structMan &man) const
{
return usAge < man.usAge;
}
bool operator > (const structMan &man) const
{
return usAge > man.usAge;
}
}MAN;
int main( int argc , char *argv[] )
{
while( true )
{
MAN man;
memset( &man , 0 , sizeof( MAN ) );
man.strAddress = "abcdef";
man.strName = "abc";
Sleep( 10 ); //這里sleep是為了讓大家有時間在任務(wù)管理器中看到內(nèi)存增長的過程,不至于一下子耗盡內(nèi)存.
}
return 0;
}
@小蘇
sorry,沒有注意到最后一句話"注意: 以上代碼在VC6環(huán)境下編譯、測試通過".
我所指出的bug依然存在,不同的編譯器對"memset 非POD處理方式可能不一樣".
即使VC6測試通過,你可以換個編譯器試試.
看完代碼,給我的第一感覺:代碼存在嚴(yán)重的bug(不知道你自己測試過沒有).
簡單的說就是"不要對非POD類型進(jìn)行memset操作".
在C++中不要對class進(jìn)行memset操作;盡量不要對struct進(jìn)行memset操作.
@qiaojie
謝謝賜教!
"像std::invalid_arguement基本沒人會去用"這句話說得有點(diǎn)絕對了,用的人應(yīng)該還是有一些的,可能我們沒有接觸到.
另外,我們總是被"天朝"代表,想不到這次被 qiaojie 代表了:)
你說"保證參數(shù)的正確性是調(diào)用者的責(zé)任,而不是被調(diào)用的函數(shù)的責(zé)任",
這一點(diǎn)我也同意,不過我覺得作為函數(shù)的設(shè)計(jì)者,我們不應(yīng)當(dāng)對用戶所傳遞的參數(shù)有太多理想化的假設(shè),所以我們應(yīng)當(dāng)在函數(shù)中進(jìn)行參數(shù)合法性的檢查,
一方面,可以在函數(shù)的入口處盡早發(fā)現(xiàn)非法參數(shù)的問題,這樣就不至于后續(xù)會使用錯誤的參數(shù)在函數(shù)中進(jìn)行一些無意義的操作.
另一方面,在函數(shù)入口處檢查參數(shù)的合法性,可以增強(qiáng)函數(shù)的健壯性,進(jìn)一步增強(qiáng)系統(tǒng)的健壯性.
舉個例子,如果傳遞給函數(shù)的實(shí)參不應(yīng)該是NULL指針,用戶卻以NULL作為實(shí)參調(diào)用函數(shù),假設(shè)我們沒有進(jìn)行對應(yīng)參數(shù)合法性檢查,
那么后續(xù)基于這個NULL實(shí)參的操作可能會導(dǎo)致系統(tǒng)"coredump".
對于參數(shù)的合法性檢查,在debug版本和release版本下應(yīng)該都需要進(jìn)行,類似于"assert(0 < age && age < 200);"這種檢測參數(shù)的合法性的代碼只在debug版本下可以起作用,
在release版本下就不起用了,也就不能在release版本下作為參數(shù)合法性檢查的工具.
在debug版本下,如果assert斷言失敗,那么我們可以看到對應(yīng)的abort信息,然后程序異常退出.
實(shí)際上這樣做可能有的時候并不合適,因?yàn)樵谝恍┣闆r下,僅僅是參數(shù)非法,我們可以進(jìn)行相應(yīng)的處理而不需要系統(tǒng)因此而退出運(yùn)行.
"強(qiáng)調(diào)不要用異?;蛘咤e誤返回值,是因?yàn)槊つ康拇罅渴褂眠@類錯誤處理機(jī)制會導(dǎo)致整個項(xiàng)目變得混亂"
這句話如果僅僅是理論上來探討"如何讓系統(tǒng)設(shè)計(jì)的更優(yōu)雅",那么這無疑可以作為一個"系統(tǒng)設(shè)計(jì)準(zhǔn)則",
但是在實(shí)際的開發(fā)過程中,有的時候一個函數(shù)內(nèi)部出現(xiàn)"非正常情況"的可能性實(shí)在是太多了,我們必須要進(jìn)行相應(yīng)的處理.
如果我們既不使用"異常"也不使用"返回錯誤碼"的形式來告知調(diào)用者,
那么在反饋給調(diào)用者"函數(shù)內(nèi)部出現(xiàn)非正常情況"這一點(diǎn)上我們將"無能為力",但我們又必須在這一點(diǎn)有所作為.
在大多數(shù)情況下,"異常"和"錯誤碼"可能是我們僅有的兩個選擇方案,如何選擇其一作為最終的處理方案,
甚至如何在不使用"異常"和"錯誤碼"的前提下也達(dá)到相同的效果,這是一件很"糾結(jié)"的事情.
追求系統(tǒng)在架構(gòu)和代碼設(shè)計(jì)上的完美是開發(fā)者的一個方向,但是有時我們需要考慮"追求完美的代價(jià)",
在時間,人力以及成本的多重影響下,很多時候我們必須放棄對最優(yōu)方案的探索,而選擇一種"不那么完美但是可行,可以很好解決問題"的方案.
也許這個時候作為函數(shù)調(diào)用狀態(tài)反饋的"異常"和"錯誤碼"機(jī)制會在我們的思考和運(yùn)用范圍之內(nèi).
@qiaojie
也許你的異常哲學(xué)是正確的并值得大家學(xué)習(xí),還請你發(fā)文一篇讓大家有一個學(xué)習(xí)的機(jī)會,
如果從你的文章中我確實(shí)發(fā)現(xiàn)了自己的錯誤,也會從中有所改正,當(dāng)然,你也不需要"對牛彈琴"這個詞語.
我這篇文章中的"People"只是一個用于作為討論基礎(chǔ)的例子,根本的問題是對于"構(gòu)造函數(shù)的參數(shù)非法或是構(gòu)造失敗"時,我們應(yīng)當(dāng)如果告知調(diào)用者.
我并沒有說一定要把參數(shù)的合法性全部放在構(gòu)造函數(shù)中完成,但是在構(gòu)造函數(shù)中檢查參數(shù)的合法性是應(yīng)該的,
就像上面的同學(xué)說的"為了程序的健壯性,多余的操作也是必須的"。
在這個例子中,你可以往自己熟悉的GUI方向進(jìn)行特化,所以你可以使用"對話框"之類的工具來進(jìn)行傳入構(gòu)造函數(shù)之前的參數(shù)合法性檢驗(yàn)以及進(jìn)行相關(guān)的錯誤處理,
但是在那些"非GUI"的領(lǐng)域,在那些"我們不能確保傳入構(gòu)造函數(shù)的參數(shù)一定是合法的,不能保證構(gòu)造函數(shù)一定會構(gòu)造成功"的情況下,我們到底該如何處理,
我考慮到可以使用"基于異常"或"基于兩段構(gòu)造的形式".
C++提供的異常機(jī)制是一種工具,可以作為"函數(shù)內(nèi)部向函數(shù)的調(diào)用者傳遞函數(shù)內(nèi)部非正常運(yùn)行狀態(tài)"的一種方法.
就如同你說的"內(nèi)存耗盡,網(wǎng)絡(luò)錯誤,文件錯誤"這種情況下是異常,也許這種情況下我們應(yīng)當(dāng)使用"異常機(jī)制"(希望沒有理解錯).
但是如果一個函數(shù)內(nèi)部可能出現(xiàn)"內(nèi)存耗盡"也會出現(xiàn)"參數(shù)非法的問題"(再重申一遍,我們不能永遠(yuǎn)都保證傳入每一個函數(shù)的參數(shù)都是合法的).
"內(nèi)存耗盡"這種情況我們使用異常,但是"參數(shù)非法問題"我們使用什么呢,
按照你的看法,"參數(shù)非法"不屬于異常的范圍之內(nèi),我們不應(yīng)該使用"異常的形式",但我們還是要告知用戶"參數(shù)非法"的信息,
假定這里我們"無法使用類似于彈出對話框的形式來告知用戶參數(shù)非法",那么我可以想到的告知調(diào)用者這一信息的方式是"使用錯誤碼",
當(dāng)然,我們還可以選擇"errno"的形式.
這樣一來,我們就面臨一個問題"一個函數(shù)會以異常和錯誤碼兩種方式來告知調(diào)用者相關(guān)的非正常運(yùn)行信息",
接下來,調(diào)用者就要同時使用"try catch"和檢查函數(shù)的錯誤碼兩種方式來檢查函數(shù)的運(yùn)行狀態(tài),
我覺得如果真的這樣設(shè)計(jì)函數(shù)的話,這就是一種很糟糕的設(shè)計(jì),不知道你怎么認(rèn)為.
在告知調(diào)用者一個函數(shù)內(nèi)部的"非正常狀態(tài)"時,我只會擇優(yōu)使用"錯誤碼"或"異常這兩種形式"之一,不會同時使用.
基于這一點(diǎn),如果我選擇"以錯誤碼的形式"來反饋給調(diào)用者,那么在函數(shù)內(nèi)部"網(wǎng)絡(luò)錯誤"時我也會使用錯誤碼來告知調(diào)用者(按你的看法,這種情況應(yīng)該使用異常),
如果我選擇"基于異常"的形式,那對"參數(shù)非法"的信息我也會拋出"std::invalid_arguement".這是設(shè)計(jì)上的取舍產(chǎn)生的必然選擇.
說到這里,不知道你對于作為std異常類型之一的"std::invalid_arguement"這個詞語有什么感想,
我覺得你應(yīng)該向標(biāo)準(zhǔn)委員會指明"std::invalid_arguement"這個詞語,
"從使用異常的哲學(xué)上的角度上來看這個概念是錯誤的,因?yàn)閰?shù)非法根本就不是異常,我們又怎么能因?yàn)閰?shù)的非法而throw std::invalid_arguement,
這是在誤導(dǎo)廣大的std用戶,所以必須去掉".
@飯中淹
將"兩段構(gòu)造"與"Fatcory Pattern"結(jié)合起來確實(shí)是一種巧妙的設(shè)計(jì)!
內(nèi)部實(shí)現(xiàn)上還是"兩段構(gòu)造",但是對于 class 的用戶而言,class CPeople 展現(xiàn)的卻是一個單一的"構(gòu)造接口",
用戶一旦調(diào)用這個接口"構(gòu)造對象",那么"兩段構(gòu)造"自動完成,極大地減少了"兩段構(gòu)造"中因?yàn)橥浾{(diào)用"Initialize"所帶來的問題.
class CPeople 中的 Create 和 Release 所扮演的角色類似于"構(gòu)造函數(shù)和析構(gòu)函數(shù)",都是進(jìn)行資源的分配與回收操作.
單純從"資源管理"的角度來說,肯定是"構(gòu)造函數(shù)和析構(gòu)函數(shù)"相比如"Create 和 Release"更優(yōu)一些,
因?yàn)?quot;構(gòu)造函數(shù)和析構(gòu)函數(shù)"對于"非動態(tài)分配的對象以及非placement new方式生成的對象",
構(gòu)造和析構(gòu)都會由編譯器保證正確自動地調(diào)用,大大簡化了對資源的管理,或許這也是C++設(shè)計(jì)構(gòu)造和析構(gòu)的出發(fā)點(diǎn)之一.
在"兩段構(gòu)造" & "Fatcory Pattern"這種模式下,所有的CPeople對象將都由 Create 接口創(chuàng)建,這勢必需要我們管理大量的動態(tài)分配的對象,
在這種情況下,如果稍有不慎,我們將面臨"resource leak"的問題.這個時候如果我們能將動態(tài)分配的CPeople對象用一種更方便安全的方式來管理就更好了,
于是我想到了boost::shared_ptr,不知道大家想到了什么?
類似于下面這樣:
void FreeResource( CPeople *pPeople )
{
if( NULL != pPeople )
{
pPeople -> Release();
}
}
CPeople *pHebe = CPeople::Create( 2 );
if( NULL == pHebe )
{
// handle error
}
boost::shared_ptr< CPeople > pPeople( pHebe , FreeResource );
下面我們就可以使用 pPeople 這個智能指針"do whatever you want" :) ,而且使用起來直觀方便:
pPeople -> Sing();
也減少了對動態(tài)分配資源進(jìn)行管理的復(fù)雜度.
@qiaojie
呵呵,可不能一棒子打死啊.
至于說,類似于"用戶輸入非法數(shù)據(jù)"之類的問題到底是算作錯誤還是異常情況,這一點(diǎn)依賴每個人對同一個事物的認(rèn)知,
有的人認(rèn)為這是異常情況,有的人認(rèn)為這是錯誤,這個認(rèn)知層面上的問題我們先不討論,尊重每個人的看法.
實(shí)際上,即使存在上面的認(rèn)知差異也沒有關(guān)系,因?yàn)閱栴}的本質(zhì)是對"用戶輸入非法數(shù)據(jù)"這種異常也好,錯誤也好,
我們在代碼邏輯中應(yīng)該如何處理,你覺得應(yīng)該用類似于"對話框+ValidateUserInput"之類的方法來處理,
我覺得可以通過返回錯誤碼或拋出異常的形式來做處理. 本質(zhì)上都是在處理一種"非正常情況",只是我們的處理方式不同,
你說應(yīng)該用你的方法比較好,我覺得用我的方法處理也是可行的,到底用哪一種呢,
即使在這種情況下,我們還是很難選擇一個所有人都接受的處理方式. 這里就涉及到設(shè)計(jì)的權(quán)衡和取舍了,有很多種方法都可行,我們尊重每個人在特定的環(huán)境中所做出的選擇.
/*
"而是在用戶輸入完成,對話框結(jié)束,構(gòu)造People之前,加入一個ValidateUserInput()函數(shù)來校驗(yàn)用戶輸入,
如果age屬于非法值,彈出一個錯誤對話框向用戶說明錯誤的原因"
*/
你這里只是對一種特例的處理,實(shí)際上我們很難在所有的情況都保證傳入構(gòu)造函數(shù)的參數(shù)是合法的,要是我們真的找到了這樣一種方法,
那么"there is a silver bullet !" , 接下來,對于所有奮斗在開發(fā)一線的同學(xué)們而言,生活就要美好很多,應(yīng)該再也不會發(fā)生類似于"小貝"的悲劇了:)
在你處理的特例中,既然我們能夠保證傳入構(gòu)造函數(shù)的參數(shù)一定是合法的,那確實(shí)太好了,"使用異常"和"兩段構(gòu)造"都是多余的.
對于那種我們不能確保傳入構(gòu)造函數(shù)的參數(shù)是一定是合法的情況,我們該選擇哪種處理方式呢,這是這篇文章討論的根本問題.
如果因?yàn)闃?gòu)造函數(shù)的參數(shù)不合法,或者因?yàn)槠渌脑驑?gòu)造失敗,最基本的一點(diǎn),我們應(yīng)當(dāng)讓調(diào)用者知道這一情況,
至于調(diào)用者如何處理就不在我們關(guān)心的范圍之內(nèi)了,是"彈出對話框告知用戶重試","忽略這個錯誤",還是直接"abort",不同的場景下也有不用的選擇.
我們要做的就是在"構(gòu)造一個對象發(fā)生異常時"告知調(diào)用者"發(fā)生了非正常情況".
這篇文章的主題也就是討論"在構(gòu)造發(fā)生非正常情況時采取何種方式來告知調(diào)用者這一情況".
對于這個問題很難有一個"放之四海而皆準(zhǔn)"的處理方案,因?yàn)檫@涉及到不同的編程風(fēng)格,應(yīng)用場景和設(shè)計(jì)時的取舍.
不過我們還是可以踴躍地發(fā)表自己的看法,在討論和交流的過程中我們總能發(fā)現(xiàn)思維的閃光點(diǎn).互相學(xué)習(xí):)
@Corner Zhang
關(guān)于這一點(diǎn),我們的觀點(diǎn)還是很相近的,不同的編程風(fēng)格決定了不同的設(shè)計(jì)取舍以及代碼風(fēng)格,
沒有哪一種一直都是最優(yōu)的,選擇一種適合自己的并一直堅(jiān)持下去(當(dāng)然,適當(dāng)?shù)臅r候還是要變通一下).
實(shí)際上我并不愿意在代碼中大量使用"throw try catch",所以,基本上我不愿意去看java代碼,
就如你所說的,傳統(tǒng)的代碼風(fēng)格易于跟蹤調(diào)試;在我看來,傳統(tǒng)的"基于錯誤碼"的代碼比基于"使用異常"的代碼
要緊湊得多,因?yàn)槲覀兛梢栽阱e誤發(fā)生的地方立即處理錯誤,而不像"基于異常"的代碼中我們要向下跨越N行代碼
來進(jìn)行錯誤處理(這一點(diǎn)使得代碼的可讀性很差).
而且,如果try代碼塊中太大,那么在對應(yīng)的catch塊中盡管我們可以進(jìn)行相應(yīng)的異常處理,但是此時我們卻失去了
對發(fā)生錯誤的代碼上下文的必要了解.這一點(diǎn)使得我們降低了對代碼整體運(yùn)行流程的可預(yù)知性,
更重要的是也降低了錯誤處理的針對性,因?yàn)橥环N類型的異??赡苡蓆ry代碼塊中的多個地方throw.具體是哪一個throw的無從了解.
so,我的觀點(diǎn)是:讓構(gòu)造函數(shù)盡量簡單,減少誤用的可能性,并增加構(gòu)造函數(shù)的安全性(盡量減少構(gòu)造函數(shù)構(gòu)造失敗的可能性).
這樣我們也就能在一定程度上減少對異常機(jī)制的依賴.至于其它的可帶有返回值的成員函數(shù)都使用"返回錯誤碼"來取代"拋出異常".
我覺共享內(nèi)存的這種"非顯示刪除自保留性"是很有用的特性而不是問題,也許當(dāng)時設(shè)計(jì)共享內(nèi)存機(jī)制時這種特性是有意提供的.
設(shè)想一個簡單的例子:
我們使用一塊有訪問控制保護(hù)的內(nèi)存緩沖區(qū)來存儲進(jìn)程或線程之間共用的的數(shù)據(jù).
并且我們需要保證數(shù)據(jù)的安全性,即使server倒掉,這塊緩沖區(qū)里面的數(shù)據(jù)也不能丟失.
1. 對于多線程的情況,當(dāng)一個線程core掉之后(比如因?yàn)閟egment fault),線程所對應(yīng)的進(jìn)程將不能幸免.
在這種情況下,如果我們使用的是一般的"內(nèi)存緩沖區(qū)"而不是共享內(nèi)存,那么當(dāng)進(jìn)程退出后,
這塊緩沖區(qū)對應(yīng)的內(nèi)存空間將會被系統(tǒng)回收重新分配給其它進(jìn)程使用,緩沖區(qū)中對應(yīng)的數(shù)據(jù)也就丟失了.
但是如果我們換用共享內(nèi)存來作為線程間交換數(shù)據(jù)的緩沖區(qū),我們就能很好的解決這個問題.
2. 在多進(jìn)程的情況下,不但能滿足上述特性,而且共享內(nèi)存也是進(jìn)程間數(shù)據(jù)交換的一種高效方式.
至于共享內(nèi)存的手動刪除問題,我的做法是,在生成IPC對象時,將對應(yīng)的ftok生成的key值寫入到一個文件中,
生成類似于下面的shell腳本,這樣當(dāng)我們需要手動刪除IPC時也很方便:
ipcrm -M xxx
ipcrm -Q xxx
ipcrm -S xxx
相比于通過socket來作為進(jìn)程間通信的方式,共享內(nèi)存的最大不足在于"不能跨機(jī)器共用".
after all, there is no silver bullet :)
你的代碼中是通過判斷信號量的值為0來作為對共享內(nèi)存讀取操作的依據(jù),即是如下代碼:
wait_v(semid);
printf("Message geted is: %s \n",shm + 1);
但實(shí)際上這里有一個潛在的問題:
即如果 wait_v(semid); 成功后,在執(zhí)行接下來的printf("Message geted is: %s \n",shm + 1)之前,進(jìn)程被掛起.
那么此時 server 進(jìn)程可能會重新獲取這個信號量并對共享內(nèi)存中的數(shù)據(jù)進(jìn)行寫操作(當(dāng)然,你這里用server sleep的時間遠(yuǎn)大于client sleep的時間來解決這個問題)
當(dāng)掛起的進(jìn)程重新被調(diào)度投入運(yùn)行后,此時printf("Message geted is: %s \n",shm + 1)的數(shù)據(jù)實(shí)際上就不是wait_v(semid)成功后共享內(nèi)存中對應(yīng)的數(shù)據(jù).
我覺得對于這種典型的 "provider/consumer" 模型,一種更好的做法是
// for provider // for consumer
P( write_sem ); P( read_sem );
// write operation // read operation
V( read_sem ); V( write_sem );
當(dāng)然,這里我們需要使用 write_sem 和 read_sem 兩個信號量.
re: Linux下快速擴(kuò)展文件大小 luckycat 2010-03-02 21:28
@阿福:
一直都把truncate用作截?cái)辔募?,沒有發(fā)現(xiàn)truncate還可以用于擴(kuò)展文件大小,剛才看了一下 man 文檔:
int truncate(const char *path, off_t length);
If the file previously was larger than length, the extra data is discarded.
If the file was previously shorter than length, its size is increased, and the extended area appears as if it were zero-filled.
這樣一來,截?cái)嗪蛿U(kuò)展文件都可以用"truncate"來完成,這樣相對于上面的EnlargeFile就更簡潔了而且基于"truncate"的形式只需要一次系統(tǒng)調(diào)用即可實(shí)現(xiàn)相同的效果,效率上也更有優(yōu)勢.
看來這次真的是"reinvent the wheel"了:(
thank you for reminding me.
最后分享一個 linux 2.6 的 man pages 打包成的CHM文件,在上面的下載文件中.
re: Linux下快速擴(kuò)展文件大小 luckycat 2010-03-02 12:53
謝謝指教!
以前在Win32下做過一段時間,深感Microsoft的巨大努力給我們帶來的便利:) 海量的MSDN和豐富的Win32 API讓我們遇到問題有據(jù)可查,
同時也減少了大量"reinvent the wheel"的時間.
但是到了*nix下面,很多東西都不一樣了,*nix的哲學(xué)是"提供解決問題的機(jī)制而不是具體的實(shí)現(xiàn)",相反,Win32的哲學(xué)是"提供具體的實(shí)現(xiàn)但是不告訴你具體的機(jī)制"。
所以這篇文章的出發(fā)點(diǎn)就是"利用Linux提供的機(jī)制來解決一個實(shí)際的問題",形式上與Win32的"SetFilePointer & SetEndOfFile"組合不一樣,但是仔細(xì)分析一下,
它們是如此的相似:按照你的建議"先調(diào)用 SetFilePointer(設(shè)置文件邏輯指針位置) 然后調(diào)用 SetEndOfFile(設(shè)置文件物理末端位置)"即可快速擴(kuò)展文件大小。
在上面的代碼中將參數(shù)合法性判斷以及對應(yīng)的函數(shù)調(diào)用狀態(tài)判斷去掉,簡化一下就是下面這樣了:
bool EnlargeFile( int iFileHandle , off_t iNewSize )
{
1. lseek( iFileHandle , 0 , SEEK_CUR ); //保存文件指針的當(dāng)前位置以便于在擴(kuò)展文件大小后恢復(fù)到當(dāng)前位置
2. lseek( iFileHandle , iMoveOffset , SEEK_SET );
3. write( iFileHandle , " " , WRITE_BYTE_COUNT ); //寫入一個字節(jié)的數(shù)據(jù),完成對文件大小的更改
4. lseek( iFileHandle , iCurPos , SEEK_SET ); //恢復(fù)文件指針到之前保存的文件位置
return true;
}
其中的第1行和第4行是為了在擴(kuò)展文件的過程中保存和恢復(fù)文件指針位置,如果我們將這一點(diǎn)也簡化掉(實(shí)際上必須保留),如下:
bool EnlargeFile( int iFileHandle , off_t iNewSize )
{
// 設(shè)置文件指針(這里是邏輯指針)位置,相當(dāng)于Win32下調(diào)用 SetFilePointer
2. lseek( iFileHandle , iMoveOffset , SEEK_SET );
// 寫入一個字節(jié)的數(shù)據(jù),完成對文件大小的更改,即是設(shè)置了文件的物理末端指針位置,相當(dāng)于調(diào)用了 SetEndOfFile
3. write( iFileHandle , " " , WRITE_BYTE_COUNT );
return true;
}
這一次很清晰了,形式上不一樣,但是本質(zhì)上很相近了。