推薦使用 boost.property_tree,簡潔,高效,支持多種格式: ini , xml ...
并且使用統一的操作接口。
re: C++的一個疑惑 luckycat 2010-12-20 22:43
B(A& a){} 不但是一個構造函數,而且是一個自定義的類型轉換操作( A -> B),你的問題有出在這里,如果要去掉這種非有意的自定義類型轉換,使用 explicit B(A& a){}。
一個 非const引用,只能引用與其類型完全相同的對象,或者是其派生類的對象 ,所以 B &refB = objectB ; B &refB = objectB1 都是合法的,但是 B &refB = objectA 就不是合法的
,因為 A 與 B的類型不相同,且不是B的派生類,所以編譯時會報錯,于是 " B1( B& b ); A a; B1 b1(a)" 就不能通過,簡化一下就相當于 " B &b = a".
一個 const引用 滿足 非const引用 的特性的同時,還有很重要的一點,const 引用可以引用一個與其類型完全不相同的類型(因為編譯器會生成一個轉換后可引用的臨時對象),
前提是被引用的類型可以轉換為引用的類型(編譯器自定義的類型提升,或者是用戶自定義的類型轉換,如上面的 B(A& a)。 ),
舉個例子:
const int &iValue = 3.14; 就是OK的,這里使用編譯器內部的類型轉換 double -> int.
const B &b = a; 也是OK的,因為使用 B( A &a) 可以將 A -> B ,于是 const B &b = a; 的背后,編譯器所做的就是:
const B tempB( a ); //調用 B( A &a)
const B &b = tempB;
BTW: 為什么在const引用情況下,編譯器會生成一個可被引用的臨時對象,原因很簡單,你是用一個 const引用 來操作這個臨時對象,所以,這個臨時對象的狀態是不會變的,
也就是安全的(當然,如果你把const引用 const_cast 成一個非 const引用來操作這個編譯器生成的臨時對象,那么結果是未定義的).
re: Google Test測試框架 luckycat 2010-05-26 23:00
@ouyang
GoogleTest同樣可以用于測試Win32 GUI Application。
你的想法可能是MFC寫出來的應用是沒有對應的控制臺,所以GoogleTest無法將輸出結果顯示出來(實際上Win GUI應用程序也可以同時具備Console Output,只不過這需要手工編碼實現,默認情況下是沒有的),當然GoogleTest已經考慮到這個問題了,GoogleTest支持將測試結果以XML文件格式輸出.參考下面的鏈接:
http://code.google.com/p/googletest/wiki/GoogleTestAdvancedGuide#Controlling_Test_Output
@小蘇
VC6我這里沒有,我上面的輸出是在VC2008下的測試結果,你換用VC2005/2008再試試.
我的建議是學習C++就不要用VC6了,可以用VC2005/2008.
如果你想用一個輕量級的環境學習C++,MinGW Studio,
wxDev-Cpp , CodeBlocks , CodeLite 都是不錯的選擇.
@小蘇
為了更好的理解我上面的分析,你可以打開VC2005/VC2008(我這里是VC2008)的單步調試,
在調試模式下的"自動窗口"(位于IDE下方).
觀察memset 前后"man -> strName -> _Bx -> _Ptr"的值的變化
_Ptr 實際上就是std::string內部用于存儲字符串的堆內存緩沖區的地址,也相當于我上面提到的 m_pCharBuffer
@小蘇
在閱讀下面的分析之前,希望你對"C++對象的內部布局"有一定的了解.
既然你也發現了內存泄漏的情況,那么你再用下面的測試代碼運行一下:
#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是為了讓大家有時間在任務管理器中看到內存增長的過程,不至于一下子耗盡內存.
}
return 0;
}
我選取我這里的一個循環中的輸出,如下:
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
下面把上述代碼進行簡化便于分析:
while( true )
{
MAN man; //這里會使用man由編譯器自動生成缺省構造函數來調用strName的缺省構造函數對strName進行構造.
memset( &man , 0 , sizeof( MAN ) );
上面的memset操作會把 &man 這個地址開始的 sizeof( MAN )字節的內存空間全部清零.
這也就意味著 man 對象內部的每個成員子對象所占據的內存都被清零.
man 對象內部一個 std::string , 而std::string 內部包含一個std::string用于實際存儲字符串的指向動態分配的堆內存的指針,
我們假設這個指針的名稱為 m_pCharBuffer;
在std::string的析構函數中釋放這個動態分配的堆內存的時候需要使用這個m_pCharBuffer,也即是調用 delete[] m_pCharBuffer;
如果我寫出下在的代碼:
char *m_pCharBuffer = new char[ BUFFER_SIZE ]; 這一個操作即是strName的缺少構造函數的操作,只不過 BUFFER_SIZE = 15 + 1(最后有一個'\0');
m_pCharBuffer = NULL; //這一個操作與上述的 memset 對 man.strName中用于指向動態內存的指針所產生的作用相同:將指針所指向的堆內存地址清零.
delete[] m_pCharBuffer; //此時 m_pCharBuffer 為NULL , 不過在C++中, delete NULL指針是安全的.不過因為 m_pCharBuffer 已經不指向上述new出來的內存
//所以這里進行 delete[] m_pCharBuffer 時已經不能進行資源的釋放了,也即是發生了內存泄漏.
man.strName = "abc";
上面的賦值操作中,實際上要調用: std::string::operator=( const char* );
首先,operator=會判斷當前的strName的 capacity能否容納下"abc",由上面的memset之后我們可以看出此時存儲 capacity 值的變量因為memset為0,所以
man.strName.capacity() 輸出為0,這也就意味著這個"容積"不能容納下3個字節的"abc".
所以這時 operator= 要擴大內部用于存儲字符串的緩沖區,擴充的基本原理如下:(代碼進行簡化處理)
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() ) 即是相當于獲取這個 m_pCharBuffer 的地址.
這一點你一定要明白.
由上面的代碼以及運行輸出可以知道,
注意: 在調用 strName = "abc"時,已經進行了memset操作,此時的 m_pCharBuffer 已經因為上面的 memset操作而被清零,即是 m_pCharBuffer = NULL,
因為memset操作不會調用析構函數 ,所以實際上在清零之前它所指向的動態內存塊并沒有被釋放,
在 operator=中,delete[] m_pCharBuffer; 相當于 delete[] NULL;
這就不能釋放 m_pCharBuffer 之前在缺省構造時所指向的動態分配的 15 + 1 字節的內存了,所以出現了內存泄漏.
}
@小蘇
你運行后仔細觀察這個編譯后運行的exe在"windows任務管理器"中對應的
"內存使用"數值.
我用VC2005和VC2008都測試過,結果是"內存不停增長".
還用哪位同學運行過我上面的測試代碼,出來公布一下測試結果,謝謝!
re: 設計的兩難:選擇異常還是兩段構造 luckycat 2010-03-06 21:27
@陳梓瀚(vczh)
你列舉的判斷標準都值得借鑒,不過你后續補充的對"例外情況"的處理方式不敢茍同:
因為構造可能沒有成功,那么我們需要調用IsAvailable之類的函數,甚至于后續需要因為判斷之前構造函數的狀態來
對調用的每個成員函數進行"try catch"或者還要從"每個成員函數的返回值中來判斷之前的構造操作是否成功".
這種設計是可行的,但對類的使用者來說太復雜.
這種情況下我覺得使用"兩段構造"可能更好一些,我們只需要判斷"兩段構造"是否成功即可,如果構造成功,在后續的成員函數調用過程中,
就再也不用為了確認構造函數的狀態來對每個被調用的成員函數進行"try catch"或檢查返回值的操作,這樣的設計應該更簡潔一些.
@小蘇
你后續修改的代碼,在我看來,即使在多個編譯器下都是OK的,但是就代碼風格來說,還有改進的地方.
在編碼過程中,我很少會對struct進行memset操作,只是偶爾會對sockaddr進行memset操作;更不會對class進行memset操作.
在你上述的代碼中,你對MAN進行memset操作,無非也就是想將各個成員的初值清零,如果基于這個出發點,那設計一個構造函數多好:
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的各個成員依次賦值了,直接傳參構造就可以了,這樣代碼應該會更優雅一些.
另一方面,對 std::list 進行sort操作從邏輯上是沒有問題的,但是設計風格上是有問題的:
因為std::list中的每一個成員是基于鏈的形式連接在一起的,所以我們不能對其進行隨機訪問,
如果我們要訪問std::list中的第N個成員,那么我們需要從鏈表頭開始向鏈表尾部依次迭代N次,
在這種情況下,如果一個鏈表過大,那么這里就有效率問題.
一般情況下,我們只對"類似于數組的可以隨機訪問"的std容器進行排序.
呵呵,我就喜歡大家這種踴躍討論的氛圍,互相學習:)
上面的代碼你在VC6下面測試通過了,因為從我的第一感覺來看,必定:coredump.
當時我還真不太相信,所以我自己也測試了一下,結果如下:
Win32: VC2005 debug/release下均可運行正常,不過因為memset非POD,出現內存泄漏.
Win32: MinGW Studio 直接abort.(這是我預期的結果).
Linux: Slackware32/GCC 直接abort.(這也是我預期的結果).
為了證明上在win32/VC2005下上面的代碼出現內存泄漏,大家可以用下面的代碼做測試:
(這里把小蘇同學的代碼取了一部分用于配合測試)
運行下面的代碼,大家在任務管理器中觀察內存增長情況:)
#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是為了讓大家有時間在任務管理器中看到內存增長的過程,不至于一下子耗盡內存.
}
return 0;
}
@小蘇
sorry,沒有注意到最后一句話"注意: 以上代碼在VC6環境下編譯、測試通過".
我所指出的bug依然存在,不同的編譯器對"memset 非POD處理方式可能不一樣".
即使VC6測試通過,你可以換個編譯器試試.
看完代碼,給我的第一感覺:代碼存在嚴重的bug(不知道你自己測試過沒有).
簡單的說就是"不要對非POD類型進行memset操作".
在C++中不要對class進行memset操作;盡量不要對struct進行memset操作.
re: 設計的兩難:選擇異常還是兩段構造 luckycat 2010-03-05 22:40
@qiaojie
謝謝賜教!
"像std::invalid_arguement基本沒人會去用"這句話說得有點絕對了,用的人應該還是有一些的,可能我們沒有接觸到.
另外,我們總是被"天朝"代表,想不到這次被 qiaojie 代表了:)
你說"保證參數的正確性是調用者的責任,而不是被調用的函數的責任",
這一點我也同意,不過我覺得作為函數的設計者,我們不應當對用戶所傳遞的參數有太多理想化的假設,所以我們應當在函數中進行參數合法性的檢查,
一方面,可以在函數的入口處盡早發現非法參數的問題,這樣就不至于后續會使用錯誤的參數在函數中進行一些無意義的操作.
另一方面,在函數入口處檢查參數的合法性,可以增強函數的健壯性,進一步增強系統的健壯性.
舉個例子,如果傳遞給函數的實參不應該是NULL指針,用戶卻以NULL作為實參調用函數,假設我們沒有進行對應參數合法性檢查,
那么后續基于這個NULL實參的操作可能會導致系統"coredump".
對于參數的合法性檢查,在debug版本和release版本下應該都需要進行,類似于"assert(0 < age && age < 200);"這種檢測參數的合法性的代碼只在debug版本下可以起作用,
在release版本下就不起用了,也就不能在release版本下作為參數合法性檢查的工具.
在debug版本下,如果assert斷言失敗,那么我們可以看到對應的abort信息,然后程序異常退出.
實際上這樣做可能有的時候并不合適,因為在一些情況下,僅僅是參數非法,我們可以進行相應的處理而不需要系統因此而退出運行.
"強調不要用異常或者錯誤返回值,是因為盲目的大量使用這類錯誤處理機制會導致整個項目變得混亂"
這句話如果僅僅是理論上來探討"如何讓系統設計的更優雅",那么這無疑可以作為一個"系統設計準則",
但是在實際的開發過程中,有的時候一個函數內部出現"非正常情況"的可能性實在是太多了,我們必須要進行相應的處理.
如果我們既不使用"異常"也不使用"返回錯誤碼"的形式來告知調用者,
那么在反饋給調用者"函數內部出現非正常情況"這一點上我們將"無能為力",但我們又必須在這一點有所作為.
在大多數情況下,"異常"和"錯誤碼"可能是我們僅有的兩個選擇方案,如何選擇其一作為最終的處理方案,
甚至如何在不使用"異常"和"錯誤碼"的前提下也達到相同的效果,這是一件很"糾結"的事情.
追求系統在架構和代碼設計上的完美是開發者的一個方向,但是有時我們需要考慮"追求完美的代價",
在時間,人力以及成本的多重影響下,很多時候我們必須放棄對最優方案的探索,而選擇一種"不那么完美但是可行,可以很好解決問題"的方案.
也許這個時候作為函數調用狀態反饋的"異常"和"錯誤碼"機制會在我們的思考和運用范圍之內.
re: 設計的兩難:選擇異常還是兩段構造 luckycat 2010-03-05 19:33
@qiaojie
也許你的異常哲學是正確的并值得大家學習,還請你發文一篇讓大家有一個學習的機會,
如果從你的文章中我確實發現了自己的錯誤,也會從中有所改正,當然,你也不需要"對牛彈琴"這個詞語.
我這篇文章中的"People"只是一個用于作為討論基礎的例子,根本的問題是對于"構造函數的參數非法或是構造失敗"時,我們應當如果告知調用者.
我并沒有說一定要把參數的合法性全部放在構造函數中完成,但是在構造函數中檢查參數的合法性是應該的,
就像上面的同學說的"為了程序的健壯性,多余的操作也是必須的"。
在這個例子中,你可以往自己熟悉的GUI方向進行特化,所以你可以使用"對話框"之類的工具來進行傳入構造函數之前的參數合法性檢驗以及進行相關的錯誤處理,
但是在那些"非GUI"的領域,在那些"我們不能確保傳入構造函數的參數一定是合法的,不能保證構造函數一定會構造成功"的情況下,我們到底該如何處理,
我考慮到可以使用"基于異常"或"基于兩段構造的形式".
C++提供的異常機制是一種工具,可以作為"函數內部向函數的調用者傳遞函數內部非正常運行狀態"的一種方法.
就如同你說的"內存耗盡,網絡錯誤,文件錯誤"這種情況下是異常,也許這種情況下我們應當使用"異常機制"(希望沒有理解錯).
但是如果一個函數內部可能出現"內存耗盡"也會出現"參數非法的問題"(再重申一遍,我們不能永遠都保證傳入每一個函數的參數都是合法的).
"內存耗盡"這種情況我們使用異常,但是"參數非法問題"我們使用什么呢,
按照你的看法,"參數非法"不屬于異常的范圍之內,我們不應該使用"異常的形式",但我們還是要告知用戶"參數非法"的信息,
假定這里我們"無法使用類似于彈出對話框的形式來告知用戶參數非法",那么我可以想到的告知調用者這一信息的方式是"使用錯誤碼",
當然,我們還可以選擇"errno"的形式.
這樣一來,我們就面臨一個問題"一個函數會以異常和錯誤碼兩種方式來告知調用者相關的非正常運行信息",
接下來,調用者就要同時使用"try catch"和檢查函數的錯誤碼兩種方式來檢查函數的運行狀態,
我覺得如果真的這樣設計函數的話,這就是一種很糟糕的設計,不知道你怎么認為.
在告知調用者一個函數內部的"非正常狀態"時,我只會擇優使用"錯誤碼"或"異常這兩種形式"之一,不會同時使用.
基于這一點,如果我選擇"以錯誤碼的形式"來反饋給調用者,那么在函數內部"網絡錯誤"時我也會使用錯誤碼來告知調用者(按你的看法,這種情況應該使用異常),
如果我選擇"基于異常"的形式,那對"參數非法"的信息我也會拋出"std::invalid_arguement".這是設計上的取舍產生的必然選擇.
說到這里,不知道你對于作為std異常類型之一的"std::invalid_arguement"這個詞語有什么感想,
我覺得你應該向標準委員會指明"std::invalid_arguement"這個詞語,
"從使用異常的哲學上的角度上來看這個概念是錯誤的,因為參數非法根本就不是異常,我們又怎么能因為參數的非法而throw std::invalid_arguement,
這是在誤導廣大的std用戶,所以必須去掉".
re: 設計的兩難:選擇異常還是兩段構造 luckycat 2010-03-05 12:02
@飯中淹
將"兩段構造"與"Fatcory Pattern"結合起來確實是一種巧妙的設計!
內部實現上還是"兩段構造",但是對于 class 的用戶而言,class CPeople 展現的卻是一個單一的"構造接口",
用戶一旦調用這個接口"構造對象",那么"兩段構造"自動完成,極大地減少了"兩段構造"中因為忘記調用"Initialize"所帶來的問題.
class CPeople 中的 Create 和 Release 所扮演的角色類似于"構造函數和析構函數",都是進行資源的分配與回收操作.
單純從"資源管理"的角度來說,肯定是"構造函數和析構函數"相比如"Create 和 Release"更優一些,
因為"構造函數和析構函數"對于"非動態分配的對象以及非placement new方式生成的對象",
構造和析構都會由編譯器保證正確自動地調用,大大簡化了對資源的管理,或許這也是C++設計構造和析構的出發點之一.
在"兩段構造" & "Fatcory Pattern"這種模式下,所有的CPeople對象將都由 Create 接口創建,這勢必需要我們管理大量的動態分配的對象,
在這種情況下,如果稍有不慎,我們將面臨"resource leak"的問題.這個時候如果我們能將動態分配的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();
也減少了對動態分配資源進行管理的復雜度.
re: 設計的兩難:選擇異常還是兩段構造 luckycat 2010-03-04 23:25
@qiaojie
呵呵,可不能一棒子打死啊.
至于說,類似于"用戶輸入非法數據"之類的問題到底是算作錯誤還是異常情況,這一點依賴每個人對同一個事物的認知,
有的人認為這是異常情況,有的人認為這是錯誤,這個認知層面上的問題我們先不討論,尊重每個人的看法.
實際上,即使存在上面的認知差異也沒有關系,因為問題的本質是對"用戶輸入非法數據"這種異常也好,錯誤也好,
我們在代碼邏輯中應該如何處理,你覺得應該用類似于"對話框+ValidateUserInput"之類的方法來處理,
我覺得可以通過返回錯誤碼或拋出異常的形式來做處理. 本質上都是在處理一種"非正常情況",只是我們的處理方式不同,
你說應該用你的方法比較好,我覺得用我的方法處理也是可行的,到底用哪一種呢,
即使在這種情況下,我們還是很難選擇一個所有人都接受的處理方式. 這里就涉及到設計的權衡和取舍了,有很多種方法都可行,我們尊重每個人在特定的環境中所做出的選擇.
/*
"而是在用戶輸入完成,對話框結束,構造People之前,加入一個ValidateUserInput()函數來校驗用戶輸入,
如果age屬于非法值,彈出一個錯誤對話框向用戶說明錯誤的原因"
*/
你這里只是對一種特例的處理,實際上我們很難在所有的情況都保證傳入構造函數的參數是合法的,要是我們真的找到了這樣一種方法,
那么"there is a silver bullet !" , 接下來,對于所有奮斗在開發一線的同學們而言,生活就要美好很多,應該再也不會發生類似于"小貝"的悲劇了:)
在你處理的特例中,既然我們能夠保證傳入構造函數的參數一定是合法的,那確實太好了,"使用異常"和"兩段構造"都是多余的.
對于那種我們不能確保傳入構造函數的參數是一定是合法的情況,我們該選擇哪種處理方式呢,這是這篇文章討論的根本問題.
如果因為構造函數的參數不合法,或者因為其它的原因構造失敗,最基本的一點,我們應當讓調用者知道這一情況,
至于調用者如何處理就不在我們關心的范圍之內了,是"彈出對話框告知用戶重試","忽略這個錯誤",還是直接"abort",不同的場景下也有不用的選擇.
我們要做的就是在"構造一個對象發生異常時"告知調用者"發生了非正常情況".
這篇文章的主題也就是討論"在構造發生非正常情況時采取何種方式來告知調用者這一情況".
對于這個問題很難有一個"放之四海而皆準"的處理方案,因為這涉及到不同的編程風格,應用場景和設計時的取舍.
不過我們還是可以踴躍地發表自己的看法,在討論和交流的過程中我們總能發現思維的閃光點.互相學習:)
re: 設計的兩難:選擇異常還是兩段構造 luckycat 2010-03-04 22:32
@Corner Zhang
關于這一點,我們的觀點還是很相近的,不同的編程風格決定了不同的設計取舍以及代碼風格,
沒有哪一種一直都是最優的,選擇一種適合自己的并一直堅持下去(當然,適當的時候還是要變通一下).
實際上我并不愿意在代碼中大量使用"throw try catch",所以,基本上我不愿意去看java代碼,
就如你所說的,傳統的代碼風格易于跟蹤調試;在我看來,傳統的"基于錯誤碼"的代碼比基于"使用異常"的代碼
要緊湊得多,因為我們可以在錯誤發生的地方立即處理錯誤,而不像"基于異常"的代碼中我們要向下跨越N行代碼
來進行錯誤處理(這一點使得代碼的可讀性很差).
而且,如果try代碼塊中太大,那么在對應的catch塊中盡管我們可以進行相應的異常處理,但是此時我們卻失去了
對發生錯誤的代碼上下文的必要了解.這一點使得我們降低了對代碼整體運行流程的可預知性,
更重要的是也降低了錯誤處理的針對性,因為同一種類型的異常可能由try代碼塊中的多個地方throw.具體是哪一個throw的無從了解.
so,我的觀點是:讓構造函數盡量簡單,減少誤用的可能性,并增加構造函數的安全性(盡量減少構造函數構造失敗的可能性).
這樣我們也就能在一定程度上減少對異常機制的依賴.至于其它的可帶有返回值的成員函數都使用"返回錯誤碼"來取代"拋出異常".
我覺共享內存的這種"非顯示刪除自保留性"是很有用的特性而不是問題,也許當時設計共享內存機制時這種特性是有意提供的.
設想一個簡單的例子:
我們使用一塊有訪問控制保護的內存緩沖區來存儲進程或線程之間共用的的數據.
并且我們需要保證數據的安全性,即使server倒掉,這塊緩沖區里面的數據也不能丟失.
1. 對于多線程的情況,當一個線程core掉之后(比如因為segment fault),線程所對應的進程將不能幸免.
在這種情況下,如果我們使用的是一般的"內存緩沖區"而不是共享內存,那么當進程退出后,
這塊緩沖區對應的內存空間將會被系統回收重新分配給其它進程使用,緩沖區中對應的數據也就丟失了.
但是如果我們換用共享內存來作為線程間交換數據的緩沖區,我們就能很好的解決這個問題.
2. 在多進程的情況下,不但能滿足上述特性,而且共享內存也是進程間數據交換的一種高效方式.
至于共享內存的手動刪除問題,我的做法是,在生成IPC對象時,將對應的ftok生成的key值寫入到一個文件中,
生成類似于下面的shell腳本,這樣當我們需要手動刪除IPC時也很方便:
ipcrm -M xxx
ipcrm -Q xxx
ipcrm -S xxx
相比于通過socket來作為進程間通信的方式,共享內存的最大不足在于"不能跨機器共用".
after all, there is no silver bullet :)
你的代碼中是通過判斷信號量的值為0來作為對共享內存讀取操作的依據,即是如下代碼:
wait_v(semid);
printf("Message geted is: %s \n",shm + 1);
但實際上這里有一個潛在的問題:
即如果 wait_v(semid); 成功后,在執行接下來的printf("Message geted is: %s \n",shm + 1)之前,進程被掛起.
那么此時 server 進程可能會重新獲取這個信號量并對共享內存中的數據進行寫操作(當然,你這里用server sleep的時間遠大于client sleep的時間來解決這個問題)
當掛起的進程重新被調度投入運行后,此時printf("Message geted is: %s \n",shm + 1)的數據實際上就不是wait_v(semid)成功后共享內存中對應的數據.
我覺得對于這種典型的 "provider/consumer" 模型,一種更好的做法是
// for provider // for consumer
P( write_sem ); P( read_sem );
// write operation // read operation
V( read_sem ); V( write_sem );
當然,這里我們需要使用 write_sem 和 read_sem 兩個信號量.
re: Linux下快速擴展文件大小 luckycat 2010-03-02 21:28
@阿福:
一直都把truncate用作截斷文件,沒有發現truncate還可以用于擴展文件大小,剛才看了一下 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.
這樣一來,截斷和擴展文件都可以用"truncate"來完成,這樣相對于上面的EnlargeFile就更簡潔了而且基于"truncate"的形式只需要一次系統調用即可實現相同的效果,效率上也更有優勢.
看來這次真的是"reinvent the wheel"了:(
thank you for reminding me.
最后分享一個 linux 2.6 的 man pages 打包成的CHM文件,在上面的下載文件中.
re: Linux下快速擴展文件大小 luckycat 2010-03-02 12:53
謝謝指教!
以前在Win32下做過一段時間,深感Microsoft的巨大努力給我們帶來的便利:) 海量的MSDN和豐富的Win32 API讓我們遇到問題有據可查,
同時也減少了大量"reinvent the wheel"的時間.
但是到了*nix下面,很多東西都不一樣了,*nix的哲學是"提供解決問題的機制而不是具體的實現",相反,Win32的哲學是"提供具體的實現但是不告訴你具體的機制"。
所以這篇文章的出發點就是"利用Linux提供的機制來解決一個實際的問題",形式上與Win32的"SetFilePointer & SetEndOfFile"組合不一樣,但是仔細分析一下,
它們是如此的相似:按照你的建議"先調用 SetFilePointer(設置文件邏輯指針位置) 然后調用 SetEndOfFile(設置文件物理末端位置)"即可快速擴展文件大小。
在上面的代碼中將參數合法性判斷以及對應的函數調用狀態判斷去掉,簡化一下就是下面這樣了:
bool EnlargeFile( int iFileHandle , off_t iNewSize )
{
1. lseek( iFileHandle , 0 , SEEK_CUR ); //保存文件指針的當前位置以便于在擴展文件大小后恢復到當前位置
2. lseek( iFileHandle , iMoveOffset , SEEK_SET );
3. write( iFileHandle , " " , WRITE_BYTE_COUNT ); //寫入一個字節的數據,完成對文件大小的更改
4. lseek( iFileHandle , iCurPos , SEEK_SET ); //恢復文件指針到之前保存的文件位置
return true;
}
其中的第1行和第4行是為了在擴展文件的過程中保存和恢復文件指針位置,如果我們將這一點也簡化掉(實際上必須保留),如下:
bool EnlargeFile( int iFileHandle , off_t iNewSize )
{
// 設置文件指針(這里是邏輯指針)位置,相當于Win32下調用 SetFilePointer
2. lseek( iFileHandle , iMoveOffset , SEEK_SET );
// 寫入一個字節的數據,完成對文件大小的更改,即是設置了文件的物理末端指針位置,相當于調用了 SetEndOfFile
3. write( iFileHandle , " " , WRITE_BYTE_COUNT );
return true;
}
這一次很清晰了,形式上不一樣,但是本質上很相近了。