我們在學習C++的時候,有很多人不知怎樣學習它、怎樣學好它,也不知應先從那里開始學起,關于C++的入門其實很簡單,你只要一步步按照你手中的那本C++教材來學就可以了,也許你把C++學的很爛,這時你千萬不要認為我好像很精通C++了。
我個人認為要想學習C++,最好直接學習它,不要先學習C語言,然后在學習C++,雖然C++是從C語言上發展過來的,但如果你對C語言了解的越
多,在你寫C++程序的時候,你很難擺脫C的風格,既使你是一位很有經驗的程序員,如果你對C很了解,在學習C++的時候,盡量使用C++的風格,我這樣
并不是說C不好,關鍵我們現在要了解的是C++而不是C。
現在讓我們深入學習C++吧!C++的難學,不僅在它那廣博的語法、語法背后的語義、語義背后的深層思維、深層思維背后的對像模型;C++的難
學,還在與它提供了四種不同的編程思維模型。當我們找來一本C++教材時,當我們翻開第一頁時,這時我們已進入了C++的世界,我們現在開始探索,開始在
追求新技術的旅程中!
想學好C++,熟練掌握它的語法是不可少的,當你掌握了C++的語法時,那么我要恭喜你,你已正正進入了C++的世界,要想學好C++,你只有努
力的學習,經常的思考多多的實踐,這時你會問了,我應該還要學習什么呢?
C++的語法我都已掌握了啊!我是不是可以學習Windows編程了呢?不要急,你是已掌握了C++的語法,但你能用它寫出高效率的程序嗎?你已對C++
所有運行機制都了解嗎?是的,單單了解C++語法是不夠的,接下來你的任務很多,要學習如何高效地使用C++語言。現在我就教你怎樣的學好它,怎樣的高效
使用它。
我們還是先從C++的語法開始說起吧!這里我只做一個簡單的概述,當我們學習C++的時候,你先要了解它的編程模式,其中包括面向對像編程、通用
編程和傳統的過程化編程。當你在學習一個C++語法時,如果你一時感到很難理解,不妨你先跳過這一段,繼續向后學習,當你看完你所學習C++的那本教材
時,你在回過頭來學習C++,你會發現其實它就是那么回事,有很多人在學習C++時,剛學習到了一半,突然感到好像以前學習的語法忘了許多,他們會把書又
翻回去,找回那忘掉的語法,如果你在學習C++時也有這樣的情況,你大可不必那么擔心,你現在的任務是繼續你的學習,不要去管那一時不記得的語法,如果你
現在去重新學習那一時忘掉的C++,恩,不錯,這看起來你好像對那語法已深深的牢記在心,當你的C++在學習到這里時,你能保證前面的語法不在遺忘嗎?這
時的你在學習新的C++語法時,但心會忘掉前面剛剛找回的C++,你說這時你能學好新的C++語法嗎?你會一邊學習新的,一邊重復舊的,這樣一來,那就糟
了,這時的你會很容易搞亂新舊C++語法,新的記不住,舊的又被新的語法搞亂了,這時的你不得不從頭再來(畢竟你是初學者)。
對于初學者來說,C++的廣博語法是件頭疼的事,學會了這個卻忘了那個,就像我上面提到的那樣,這時的你應該繼續的學習C++新知識,等看完你手
中的那本C++教材時,你在來學習忘掉的語法,這時你會感覺好像C++很簡單,沒有我們開始說的那么難學啊!你會覺得我開始說C++難學是用來嚇唬人的。
我說C++難學當然不是用來嚇唬人的,這時的你對C++語法已非常熟悉了,這時你千萬不要認為對C++已很精通,就像我開頭所說的那樣,雖然現在你已擺脫
了初學著的稱呼,但你也不能算是位精通人士啊!你只掌握了C++的大概,接下來的你就要深入學習拉!
本文的寫作目的并不在于提供C/C++程序員求職面試指導,而旨在從技術上分析面試題的內涵。文中的大多數面試題來自各大論壇,部分試題解答也參考了網友的意見。
許多面試題看似簡單,卻需要深厚的基本功才能給出完美的解答。企業要求面試者寫一個最簡單的strcpy函數都可看出面試者在技術上究竟達到了怎樣的程
度,我們能真正寫好一個strcpy函數嗎?我們都覺得自己能,可是我們寫出的strcpy很可能只能拿到10分中的2分。讀者可從本文看到strcpy
函數從2分到10分解答的例子,看看自己屬于什么樣的層次。此外,還有一些面試題考查面試者敏捷的思維能力。
分析這些面試題,本身包含很強的趣味性;而作為一名研發人員,通過對這些面試題的深入剖析則可進一步增強自身的內功。
2.找錯題
試題1:
void test1()
{
char string[10];
char* str1 = "0123456789";
strcpy( string, str1 );
}
試題2:
void test2()
{
char string[10], str1[10];
int i;
for(i=0; i<10; i++)
{
str1
= 'a';
}
strcpy( string, str1 );
}
試題3:
void test3(char* str1)
{
char string[10];
if( strlen( str1 ) <= 10 )
{
strcpy( string, str1 );
}
}
解答:
試題1字符串str1需要11個字節才能存放下(包括末尾的’\0’),而string只有10個字節的空間,strcpy會導致數組越界;
對試題2,如果面試者指出字符數組str1不能在數組內結束可以給3分;如果面試者指出strcpy(string,
str1)調用使得從str1內存起復制到string內存起所復制的字節數具有不確定性可以給7分,在此基礎上指出庫函數strcpy工作方式的給10
分;
對試題3,if(strlen(str1) <= 10)應改為if(strlen(str1) < 10),因為strlen的結果未統計’\0’所占用的1個字節。
剖析:
考查對基本功的掌握:
(1)字符串以’\0’結尾;
(2)對數組越界把握的敏感度;
(3)庫函數strcpy的工作方式,如果編寫一個標準strcpy函數的總分值為10,下面給出幾個不同得分的答案:
2分
void strcpy( char *strDest, char *strSrc )
{
while( (*strDest++ = * strSrc++) != ‘\0’ );
}
4分
void strcpy( char *strDest, const char *strSrc )
//將源字符串加const,表明其為輸入參數,加2分
{
while( (*strDest++ = * strSrc++) != ‘\0’ );
}
7分
void strcpy(char *strDest, const char *strSrc)
{
//對源地址和目的地址加非0斷言,加3分
assert( (strDest != NULL) && (strSrc != NULL) );
while( (*strDest++ = * strSrc++) != ‘\0’ );
}
10分
//為了實現鏈式操作,將目的地址返回,加3分!
char * strcpy( char *strDest, const char *strSrc )
{
assert( (strDest != NULL) && (strSrc != NULL) );
char *address = strDest;
while( (*strDest++ = * strSrc++) != ‘\0’ );
return address;
}
從2分到10分的幾個答案我們可以清楚的看到,小小的strcpy竟然暗藏著這么多玄機,真不是蓋的!需要多么扎實的基本功才能寫一個完美的strcpy啊!
(4)對strlen的掌握,它沒有包括字符串末尾的'\0'。
讀者看了不同分值的strcpy版本,應該也可以寫出一個10分的strlen函數了,完美的版本為: int strlen( const char *str ) //輸入參數const
{
assert( strt != NULL ); //斷言字符串地址非0
int len;
while( (*str++) != '\0' )
{
len++;
}
return len;
}
試題4:
void GetMemory( char *p )
{
p = (char *) malloc( 100 );
}
void Test( void )
{
char *str = NULL;
GetMemory( str );
strcpy( str, "hello world" );
printf( str );
}
試題5:
char *GetMemory( void )
{
char p[] = "hello world";
return p;
}
void Test( void )
{
char *str = NULL;
str = GetMemory();
printf( str );
}
試題6:
void GetMemory( char **p, int num )
{
*p = (char *) malloc( num );
}
void Test( void )
{
char *str = NULL;
GetMemory( &str, 100 );
strcpy( str, "hello" );
printf( str );
}
試題7:
void Test( void )
{
char *str = (char *) malloc( 100 );
strcpy( str, "hello" );
free( str );
... //省略的其它語句
}
解答:
試題4傳入中GetMemory( char *p )函數的形參為字符串指針,在函數內部修改形參并不能真正的改變傳入形參的值,執行完
char *str = NULL;
GetMemory( str );
后的str仍然為NULL;
試題5中
char p[] = "hello world";
return p;
的p[]數組為函數內的局部自動變量,在函數返回后,內存已經被釋放。這是許多程序員常犯的錯誤,其根源在于不理解變量的生存期。
試題6的GetMemory避免了試題4的問題,傳入GetMemory的參數為字符串指針的指針,但是在GetMemory中執行申請內存及賦值語句 tiffany
bracelets
*p = (char *) malloc( num );
后未判斷內存是否申請成功,應加上:
if ( *p == NULL )
{
...//進行申請內存失敗處理
}
試題7存在與試題6同樣的問題,在執行
char *str = (char *) malloc(100);
后未進行內存是否申請成功的判斷;另外,在free(str)后未置str為空,導致可能變成一個“野”指針,應加上:
str = NULL;
試題6的Test函數中也未對malloc的內存進行釋放。
剖析:
試題4~7考查面試者對內存操作的理解程度,基本功扎實的面試者一般都能正確的回答其中50~60的錯誤。但是要完全解答正確,卻也絕非易事。
對內存操作的考查主要集中在:
(1)指針的理解;
(2)變量的生存期及作用范圍;
(3)良好的動態內存申請和釋放習慣。
再看看下面的一段程序有什么錯誤:
swap( int* p1,int* p2 )
{
int *p;
*p = *p1;
*p1 = *p2;
*p2 = *p;
}
在swap函數中,p是一個“野”指針,有可能指向系統區,導致程序運行的崩潰。在VC++中DEBUG運行時提示錯誤“Access Violation”。該程序應該改為:
swap( int* p1,int* p2 )
{
int p;
p = *p1;
*p1 = *p2;
*p2 = p;
}
我們現在編寫一個程序越來越容易了。利用一些軟件開發工具,往往只要通過鼠標的拖拖點點,計算機就會自動幫你生成許多代碼。但在很多時候,計算機的這種能力被濫用了,我們往往只考慮把這個程序搭起來,而不去考慮程序的性能如何,程序是否足夠的健壯。而此節課的目的主要是介紹一些編碼的經驗,讓大家編寫的程序更加健壯和高性能。
在C++編程中應該盡量使用const和inline來代替#define,盡量做到能不用#define就不用。#define常見的用途有“定義常量”以及“定義宏”,但其中存在諸多的弊病。
第一,查錯不直觀,不利于調試。Define的定義是由預處理程序處理的,作的是完全的文本替換,不做任何的類型檢查。在編譯器處理階段,define定義的東西已經被完全替換了,這樣在debug的時候就看不到任何的相關信息,即跟蹤時不能step into宏。例如,把ASPECT_RATIO用define定義成1.653,編譯器就看不到ASPECT_RATIO這個名字了。如果編譯器報1.653錯,那么就無從知道此1.653來自于何處。在真正編碼的時候應該使用如下的語句來定義:
static const double ASPECT_RATIO = 1.653;
第二,沒有任何類型信息,不是type safe。因為它是文本級別的替換,這樣不利于程序的維護。
第三,define的使用很容易造成污染。比如,如果有兩個頭文件都定義了ASPECT_RATIO, 而一個CPP文件又同時包含了這兩個頭文件,那么就會造成沖突。更難查的是另外一種錯誤,比如有如下的代碼:
// in header file def.h
#define Apple 1
#define Orange 2
#define Pineapple 3
…
// in some cpp file that includes the def.h
enum Colors {White, Black, Purple, Orange};
在.h文件中Orange被定義成水果的一種,而在.cpp文件中Orange又成為了一種顏色,那么編譯器就會把此處的Orange替換成2,編譯可能仍然可以通過,程序也能夠運行,但是這就成了一個bug,表現出古怪的錯誤,且很難查錯。再比如定義了一個求a與b哪個數大的宏,#define max(a,b) ((a) > (b) ? (a) : (b))
int a = 5, b = 0;
max(++ a, b);
max(++ a, b + 10);
190-823 117-202 在上面的操作中,max(++ a, b); 語句中a被++了兩次,而max(++ a, b + 10); 語句中a只加了一次,這樣在程序處理中就很有可能成為一個bug,且此bug也非常的難找。在實際編碼時可以使用如下的語句來做:
template<class T>
inline const T&
max(const T& a, const T& b) { return a > b ? a : b; }
2、Prefer C++-style casts 在程序中經常會需要把一種類型轉換成另外一種類型,在C++中應該使用static_cast、const_cast、dynamic_cast、reinterpret_cast關鍵字來做類型轉換。因為這有以下好處,一是其本身就是一種注釋,在代碼中看到上面這些關鍵字就可馬上知道此處是進行類型轉換。二是C語言中類型轉換通常是很難進行搜索的,而通過關鍵字cast則可以很容易的找到程序中出現類型轉換的地方了。
3、Distinguish between prefix and postfix forms of increment and decrement operators 通常對于操作系統或編譯器自身支持的類型,prefix(前綴,如++i)與postfix(后綴,如i++)的效果是一樣的。因為現在的編譯器都很聰明,它會自動做優化,這兩者的匯編代碼是一樣的,性能不會有差別。但有時候也會有不同的,如一些重載了操作符的類型。下面是模擬prefix與postfix的操作過程,可以發現在postfix操作中會生成一個臨時變量,而這一臨時變量是會占用額外的時間和開銷的。
// prefix form: increment and fetch
UPInt& UPInt::operator++()
{
*this += 1; // increment
return *this; // fetch
}
// postfix form: fetch and increment
const UPInt UPInt::operator++(int)
{
UPInt oldValue = *this; // fetch
++(*this); // increment
return oldValue; // return what was fetched
}
一般情況下不需要區分是先++,還是后++,但是我們在編寫程序的時候最好能習慣性的將其寫成++i的形式,如在使用STL中的iterator時,prefix與postfix會有相當大的性能差異。請不要小看這些細節,實際在編寫程序的時候,若不注意具體細節,你會發現程序的性能會非常的低。但要注意,雖然在大多數情況下可以用prefix來代替postfix,但有一種情況例外,那就是有[]操作符時,比如gzArray [++index] 是不等于 gzArray[index++]的。
4、Minimizing Compile-time Dependencies
有些人在編寫程序時,往往喜歡將一個.h文件包含到另一個.h文件,而實踐證明在做大型軟件時這是一個非常不好的習慣,因這樣會造成很多依賴的問題,包含較多的.h文件,別人又使用了這個class,而在他的那個工程中可能并不存在這些.h文件,這樣很可能就編譯不能通過。而且這樣做,還可能造成很難去更新一個模塊的情況。因為一個.h文件被很多模塊包含的話,如果修改了此.h文件,在編譯系統的時候,編譯器會去尋找哪些模塊依賴于某個被修改過的.h文件,那么就導致了所有包含入此.h文件的模塊全都要進行重新編譯。在項目比較小的時候,大家可能還感覺不到差別,但是如果說是在大型的軟件系統里,你可能編譯一遍源碼需要七、八個小時。如果你這個.h文件被很多模塊包含的話,就算在.h文件中加了一行注釋,在編譯時編譯器檢查哪些文件被改動,那么所有包含入此.h文件的模塊都會被重新編譯,造成巨大的時間和精力負擔。對于此問題,解決的方法就是讓.h文件自包含,也就是說讓它包含盡量少的東西。所謂盡量少是指如刪掉任何一個它包含進來的.h文件,都將無法正常進行工作。其實在很多情況下,并不需要一個.h文件去包含另一個.h文件,完全可以通過class聲明來解決依賴關系的這種問題。再來看下面這個例子:1Z0-043
#include "a.h" // class A
#include "b.h" // class B
#include "c.h" // class C
#include "d.h" // class D
#include "e.h" // class E
class X : public A, private B
{
public:
E SomeFunctionCall(E someParameter);
private:
D m_dInstance;
};
當類X從類A和類B中派生時,需要知道X在內存中都有哪些data,通常在內存中前面是基類的data,后面緊跟的是此派生類自身定義的data,因此就必須知道類A與類B的內部細節,要不然編譯器就無法來安排內存了。但是在處理參數以及參數返回值的時候,實際上并不需要知道這些信息,在此處定義的SomeFunctionCall()只需知道E是個class就足夠了,并不需要知道類E中的data如長度等的具體細節。上面的代碼應該改寫成如下的形式,以減少依賴關系:
#include "a.h" // class A
#include "b.h" // class B
#include "c.h" // class C
#include "d.h" // class D
class E;
class X : public A, private B
{
public:
E SomeFunctionCall(E someParameter);
private:
D m_dInstance;
};
5、Never treat arrays polymorphically
不要把數組和多態一起使用,請看下面的例子。
class BST { ... };
class BalancedBST: public BST { ... };
void printBSTArray(ostream& s, const BST array[], int numElements)
{
for (int i = 0; i < numElements; ++i)
{
s << array[i];
// this assumes an operator<< is defined for BST
}
}
BalancedBST bBSTArray[10];
printBSTArray(cout, bBSTArray, 10);
數組在內存中是一個連續的內存空間,而在數組中應該如何來定位一個元素呢?過程是這樣的,編譯器可以知道每個數據類型的長度大小,如果數組的index是0,則會自動去取第一個元素;如果是指定了某個index,編譯器則會根據此index與該數據類型的長度自動去算出該元素的位置。
在printBSTArray()函數中,盡管傳入的參數是BalancedBST類型,但由于其本來定義的類型是BST,那么它依然會根據BST來計算類型的長度。而通常派生類實例所占的內存要比基類實例所占的內存大一些,因此該程序在編譯時會報錯。請記住,永遠不要把數組和C++的多態性放在一起使用。
6、Prevent exceptions from leaving destructors
析構函數中一定不要拋出異常。通常有兩種情況會導致析構函數的調用,一種是當該類的對象離開了它的域,或delete表達式中一個該類對象的指針,另一種是由于異常而引起析構函數的調用。
如果析構函數被調用是由于exception引起,而此時在析構函數中又拋出了異常,程序會立即被系統終止,甚至都來不及進行內存釋放。因此如果在析構函數中拋出異常的話,就很容易混淆引起異常的原因,且這樣的軟件也會讓用戶非常惱火。由于析構函數中很可能會調用其它的一些函數,所以在寫析構函數的時候一定要注意,對這些函數是否會拋出異常要非常清楚,如果會的話,就一定要小心了。比如下面這段代碼:
Session::~Session()
{
logDestruction(this);
}
比如logDestruction()函數可能會拋出異常,那么我們就應該采用下面這種代碼的形式:
Session::~Session()
{
try
{
logDestruction(this);
}
catch (...)
{
}
}
這樣程序出錯的時候不會被立即關掉,可以給用戶一些其它的選擇,至少先讓他把目前在做的工作保存下來。
7、Optimization:Remember the 80-20 rule
在軟件界有一個20-80法則,其實這是一個很有趣的現象,比如一個程序中20%的代碼使用了該程序所占資源的80%;一個程序中20%的代碼占用了總運行時間的80%;一個程序中20%的代碼使用了該程序所占內存的80%;在20%的代碼上面需要花費80%的維護力量,等等。這個規律還可以被繼續推廣下去,不過這個規律無法被證明,它是人們在實踐中觀察得出的結果。從這個規律出發,我們在做程序優化的時候,就有了針對性。比如想提高代碼的運行速度,根據這個規律可以知道其中20%的代碼占用了80%的運行時間,因此我們只要找到這20%的代碼,并進行相應的優化,那么我們程序的運行速度就可以有較大的提高。再如有一個函數,占用了程序80%的運行時間,如果把這個函數的執行速度提高10倍,那么對程序整體性能的提高,影響是非常巨大的。如果有一個函數運行時間只占總時間的1%,那就算把這個函數的運行速度提高1000倍,對程序整體性能的提高也是影響不大的。所以我們的基本思想就是找到占用運行時間最大的那個函數,然后去優化它,哪怕只是改進了一點點,程序的整體性能也可以被提高很多。
要想找出那20%的代碼,我們的方法就是使用Profiler,它實際上是一些公司所開發的工具,可以檢查程序中各個模塊所分配內存的使用情況,以及每個函數所運行的時間等。常見的Profiler有Intel公司開發的VTune,微軟公司開發的Visual Studio profiler,DevPartner from Compuware等
首先說指導思想。這是一個價值觀問題,我們在此提出三條標準:簡單,高性能,可移植。
我們在開篇就對簡單性目標作了敘述,這里再稍微展開討論一下。我們提出的簡單標準,首先是外部接口簡單,其次是內部結構簡單。我們知道,類庫是提供給上層應用程序使用的,也就是按照一定的接口規范,向上層提供一定的功能服務。接口設計得越簡單,對上層用戶來說就越方便,就越不容易產生Bug。我們可以注意到,流行的成功類庫都是擁有簡單接口的。為了使接口簡單,常常不得不把有關具體實現的復雜性封裝于類庫內部,也就是說,關于簡單性的設計原則,外部接口簡單優先于內部實現簡單。
高性能是C++語言優于其它OO語言的一個特性。C++的高性能應該首先歸于它運行模式,和大多數OO語言不同,C++程序編譯后直接產生本地平臺代碼(Native Code),理論上具備了可能的最大執行性能。另外的一個原因是主流的C++編譯器都被設計得非常精巧,具有優越的代碼優化能力。對于C++類庫設計者來說,保持C++的高性能是一個重要目標。程序的高性能可以從兩方面來評價,一是時間性能,以盡量短的時間來解決盡量多的業務;二是資源性能,以盡量少的資源消耗,包括CPU使用、內存占用、網絡流量、磁盤空間等等,來維持正常的程序功能。提高性能的主要手段是數據結構、算法和程序體現結構的優化設計000-861 117-102 。
再說可移植性。C++的編譯后輸出代碼是本地平臺代碼,因此C++本身不具有目標代碼可移植性,C++的可移植性只能是源代碼可移植性。源代碼的可移植性是指,同一軟件產品的全部或者部分源代碼可以在不同的編譯環境中進行編譯(不需要編譯的除外),并且其結果具有相同的品質特性(依優先順序包括功能性、可靠性、可用性、性能性、可維護性等)。編譯環境可以大致分為三個層次,最底層的是操作系統,也就是平臺(Platform),其次是對源代碼直接進行處理的編譯器,然后是其它在編譯過程中必需的中間件物品,如庫文件等。我們知道C++雖然在語言規范上獲得了統一(ISO/IEC),其編譯器卻是群雄割據的局面,具有代表性的有Borland C++系列(已經淡出市場),Microsoft的Visual Studio系列的C++編譯器和GNU陣營的壓軸產品gc中的g++。源代碼經編譯環境處理后產生的可執行代碼的執行平臺稱為目標平臺,不同的編譯器的目標平臺也不同,有的支持多平臺,如g++,有的是單一平臺,如Visual C++。對于類庫設計者來說,想要獲得完全的可移植性是非常困難的(除非是象STL這樣被納入語言規范的類庫,因為不支持STL就是不支持標準的C++。即便如此不同的編譯環境還是存在不同的STL實現版本,造成“一個類庫多個實現”的局面),我們只能有選擇地支持一部分環境。我們在開篇就已經說明,我們選擇g++和Visual C++編譯器,選擇Linux和Windows 32位目標平臺。
接下來我們來討論C++類庫設計的方法論。
首先,我們采用僅用頭文件的類庫設計方式(Header-only,STL的大多數實現版本都是采用Header-only的方式),也就是在頭文件(.h)中聲明和定義類,將其成員函數全部定義為內聯函數,而不使用源程序文件(.cpp)。
我們知道在C語言的開發環境中,所謂庫文件包含兩個部分,頭文件部分和二進制文件部分。根據二進制文件和用戶目標文件結合方式的不同,又可分為靜態鏈接文件和動態鏈接文件。這種庫的構成模式已成為事實上的C語言開發環境的標準,絕大多數平臺、絕大多數編譯器都使用這種模式 117-301 190-721 。
然而C++語言開發環境,這種庫構成模式遭遇到一個重大問題,就是符號命名問題。舉例來說,C++允許多個函數可以被重載(Overload),可以具有相同的名稱,而通過參數列表不同被予以區別。這樣就帶來一個問題,編譯完成的目標代碼中怎樣來區別這些在源代碼中具有相同名稱的函數?常見的做法是在編譯器輸出的函數的符號名稱中加入描述類型信息的字符串,這種方法通常被稱為名稱裝飾(Name decoration)或者名稱糟化(Name mangling,這個術語真不好翻譯,筆者的感覺是發明這個詞的人覺得編譯器把本來簡單干凈的符號給搞亂了)。比如說,g++3.4.4對于函數void func(int),其編譯輸出符號名稱為_Z1funci,對于函數void func(int, int),其輸出符號名稱為_Z1funcii,等等。但是,這種名稱裝飾規則沒有統一規范,也就是說不同的編譯器有各自不同的名稱裝飾規則,這樣就導致不同的C++編譯器只能識別自己的輸出文件,而沒有辦法處理其他編譯器的輸出文件。因此,如果將C++程序制作成二進制的庫文件,則其能夠支持的開發環境只能限于原始的開發環境,基本上不具有多種開發環境間的通用性。
一個解決辦法是將庫文件保持在源代碼形態(包括頭文件和源文件),而不編譯成二進制文件。比如STL的許多實現版本都是以頭文件形式存在。這樣雖然解決了名稱裝飾所帶來的不可移植問題,但同時又會帶來代碼編譯時間增長,源代碼完全公開等問題。在C++的名稱裝飾規則未被統一之前,看起來這個問題是很難兩全其美地解決了。
在本系列中,我們也仿照g++的STL實現方式,完全以頭文件形式來編寫類庫。為什么不把代碼放到源文件中去呢?主要原因是,頭文件只需要用戶使用包含指令(#include)就可以處理了,而源文件則需要配置到用戶工程的編譯目標列表中,和用戶的源程序形成共同編譯的形式,破壞了用戶工程的編譯目標的封閉性,比較麻煩而且不符合軟件開發的一般習慣。
其次我們來討論如何支持多平臺。我們已經說過在本系列中我們的線程庫支持Linux平臺的Posix線程和Windows 32位平臺的線程模式。我們可以參考C++的Pimpl“慣語”(Pimpl idiom,在Herb Sutter的《Exceptional C++》中有介紹),采用2層類構造方式。上次類亦即接口類,為用戶提供統一的類接口,在用戶看來具有唯一的類行為定義;下層類亦即實現類,將接口類的行為定義轉化為某個平臺的具體實現。
1.引言
C++語言的創建初衷是“a better
C”,但是這并不意味著C++中類似C語言的全局變量和函數所采用的編譯和連接方式與C語言完全相同。作為一種欲與C兼容的語言,C++保留了一部分過程式語言的特點(被世人稱為“不徹底地面向對象”),因而它可以定義不屬于任何類的全局變量和函數。但是,C++畢竟是一種面向對象的程序設計語言,為了支持函數的重載,C++對全局函數的處理方式與C有明顯的不同。
2.從標準頭文件說起
某企業曾經給出如下的一道面試題:
面試題
為什么標準頭文件都有類似以下的結構?
#ifndef __INCvxWorksh
#define __INCvxWorksh
#ifdef
__cplusplus
extern "C" {
#endif
/*...*/
#ifdef
__cplusplus
}
#endif
#endif /* __INCvxWorksh
*/
分析 顯然,頭文件中的編譯宏“#ifndef __INCvxWorksh、#define
__INCvxWorksh、#endif” 的作用是防止該頭文件被重復引用。
那么
#ifdef __cplusplus
extern "C" {
#endif
#ifdef
__cplusplus
}
#endif
的作用又是什么呢?我們將在下文一一道來。
3.深層揭密extern
"C"
extern "C"
包含雙重含義,從字面上即可得到:首先,被它修飾的目標是“extern”的;其次,被它修飾的目標是“C”的。讓我們來詳細解讀這兩重含義。
被extern
"C"限定的函數或變量是extern類型的;
extern是C/C++語言中表明函數和全局變量作用范圍(可見性)的關鍵字,該關鍵字告訴編譯器,其聲明的函數和變量可以在本模塊或其它模塊中使用。記住,下列語句:
extern int
a;
僅僅是一個變量的聲明,其并不是在定義變量a,并未為a分配內存空間。變量a在所有模塊中作為一種全局變量只能被定義一次,否則會出現連接錯誤。
通常,在模塊的頭文件中對本模塊提供給其它模塊引用的函數和全局變量以關鍵字extern聲明。例如,如果模塊B欲引用該模塊A中定義的全局變量和函數時只需包含模塊A的頭文件即可。這樣,模塊B中調用模塊A中的函數時,在編譯階段,模塊B雖然找不到該函數,但是并不會報錯;它會在連接階段中從模塊A編譯生成的目標代碼中找到此函數。
與extern對應的關鍵字是static,被它修飾的全局變量和函數只能在本模塊中使用。因此,一個函數或變量只可能被本模塊使用時,其不可能被extern
“C”修飾。
被extern "C"修飾的變量和函數是按照C語言方式編譯和連接的;
未加extern
“C”聲明時的編譯方式 首先看看C++中對類似C的函數是怎樣編譯的。
作為一種面向對象的語言,C++支持函數重載,而過程式語言C則不支持。函數被C++編譯后在符號庫中的名字與C語言的不同。例如,假設某個函數的原型為:
void foo( int x, int y
);
該函數被C編譯器編譯后在符號庫中的名字為_foo,而C++編譯器則會產生像_foo_int_int之類的名字(不同的編譯器可能生成的名字不同,但是都采用了相同的機制,生成的新名字稱為“mangled
name”)。
_foo_int_int這樣的名字包含了函數名、函數參數數量及類型信息,C++就是靠這種機制來實現函數重載的。例如,在C++中,函數void
foo( int x, int y )與void foo( int x, float y
)編譯生成的符號是不相同的,后者為_foo_int_float。
同樣地,C++中的變量除支持局部變量外,還支持類成員變量和全局變量。用戶所編寫程序的類成員變量可能與全局變量同名,我們以"."來區分。而本質上,編譯器在進行編譯時,與函數的處理相似,也為類中的變量取了一個獨一無二的名字,這個名字與用戶程序中同名的全局變量名字不同。
未加extern
"C"聲明時的連接方式 假設在C++中,模塊A的頭文件如下:
// 模塊A頭文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
int foo(
int x, int y );
#endif
在模塊B中引用該函數:
// 模塊B實現文件 moduleB.cpp
#include "moduleA.h"
foo(2,3);
實際上,在連接階段,連接器會從模塊A生成的目標文件moduleA.obj中尋找_foo_int_int這樣的符號!
加extern
"C"聲明后的編譯和連接方式
加extern "C"聲明后,模塊A的頭文件變為:
// 模塊A頭文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
extern "C"
int foo( int x, int y );
#endif
在模塊B的實現文件中仍然調用foo( 2,3
),其結果是:
(1)模塊A編譯生成foo的目標代碼時,沒有對其名字進行特殊處理,采用了C語言的方式;
(2)連接器在為模塊B的目標代碼尋找foo(2,3)調用時,尋找的是未經修改的符號名_foo。
如果在模塊A中函數聲明了foo為extern
"C"類型,而模塊B中包含的是extern int foo( int x, int y )
,則模塊B找不到模塊A中的函數;反之亦然。
所以,可以用一句話概括extern
“C”這個聲明的真實目的(任何語言中的任何語法特性的誕生都不是隨意而為的,來源于真實世界的需求驅動。我們在思考問題時,不能只停留在這個語言是怎么做的,還要問一問它為什么要這么做,動機是什么,這樣我們可以更深入地理解許多問題):
實現C++與C及其它語言的混合編程。
明白了C++中extern
"C"的設立動機,我們下面來具體分析extern "C"通常的使用技巧。
4.extern
"C"的慣用法
(1)在C++中引用C語言中的函數和變量,在包含C語言頭文件(假設為cExample.h)時,需進行下列處理:
extern "C"
{
#include "cExample.h"
}
而在C語言的頭文件中,對其外部函數只能指定為extern類型,C語言中不支持extern "C"聲明,在.c文件中包含了extern
"C"時會出現編譯語法錯誤。
筆者編寫的C++引用C函數例子工程中包含的三個文件的源代碼如下:
/* c語言頭文件:cExample.h */
#ifndef C_EXAMPLE_H
#define
C_EXAMPLE_H
extern int add(int x,int y);
#endif
/* c語言實現文件:cExample.c
*/
#include "cExample.h"
int add( int x, int y )
{
return x +
y;
}
// c++實現文件,調用add:cppFile.cpp
extern "C"
{
#include
"cExample.h"
}
int main(int argc, char* argv[])
{
add(2,3);
return 0;
}
如果C++調用一個C語言編寫的.DLL時,當包括.DLL的頭文件或聲明接口函數時,應加extern "C"
{ }。
(2)在C中引用C++語言中的函數和變量時,C++的頭文件需添加extern "C",但是在C語言中不能直接引用聲明了extern
"C"的該頭文件,應該僅將C文件中將C++中定義的extern
"C"函數聲明為extern類型。
筆者編寫的C引用C++函數例子工程中包含的三個文件的源代碼如下:70-210 1Y0-327
//C++頭文件 cppExample.h
#ifndef CPP_EXAMPLE_H
#define
CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif
//C++實現文件
cppExample.cpp
#include "cppExample.h"
int add( int x, int y
)
{
return x + y;
}
/* C實現文件 cFile.c
/* 這樣會編譯出錯:#include
"cExample.h" */
extern int add( int x, int y );
int main( int argc, char*
argv[] )
{
add( 2, 3 );
return 0;
}
如果深入理解了第3節中所闡述的extern
"C"在編譯和連接階段發揮的作用,就能真正理解本節所闡述的從C++引用C函數和C引用C++函數的慣用法。對第4節給出的示例代碼,需要特別留意各個細節。
1.把C++當成一門新的語言學習(和C沒啥關系!真的。);
2.看《Thinking In
C++》,不要看《C++變成死相》;
3.看《The C++ Programming Language》和《Inside The C++
Object Model》,不要因為他們很難而我們自己是初學者所以就不看;
4.不要被VC、BCB、BC、MC、TC等詞匯所迷惑——他們都是集成開發環境,而我們要學的是一門語言;
5.不要放過任何一個看上去很簡單的小編程問題——他們往往并不那么簡單,或者可以引伸出很多知識點;
6.會用Visual
C++,并不說明你會C++;
7.學class并不難,template、STL、generic
programming也不過如此——難的是長期堅持實踐和不遺余力的博覽群書;
8.如果不是天才的話,想學編程就不要想玩游戲——你以為你做到了,其實你的C++水平并沒有和你通關的能力一起變高——其實可以時刻記住:學C++是為了編游戲的;
9.看Visual C++的書,是學不了C++語言的;
10.浮躁的人容易說:XX語言不行了,應該學YY;——是你自己不行了吧!?
11.浮躁的人容易問:我到底該學什么;——別問,學就對了;
12.浮躁的人容易問:XX有錢途嗎;——建議你去搶銀行;
13.浮躁的人容易說:我要中文版!我英文不行!——不行?學呀!
14.浮躁的人容易問:XX和YY哪個好;——告訴你吧,都好——只要你學就行;
15.浮躁的人分兩種:a)只觀望而不學的人;b)只學而不堅持的人;
16.把時髦的技術掛在嘴邊,還不如把過時的技術記在心里;
17.C++不僅僅是支持面向對象的程序設計語言;
18.學習編程最好的方法之一就是閱讀源代碼;
19.在任何時刻都不要認為自己手中的書已經足夠了;
20.請閱讀《The Standard C++
Bible》(中文版:標準C++寶典),掌握C++標準;
21.看得懂的書,請仔細看;看不懂的書,請硬著頭皮看;
22.別指望看第一遍書就能記住和掌握什么——請看第二遍、第三遍;
23.請看《Effective C++》和《More
Effective C++》以及《Exceptional C++》;
24.不要停留在集成開發環境的搖籃上,要學會控制集成開發環境,還要學會用命令行方式處理程序;
25.和別人一起討論有意義的C++知識點,而不是爭吵XX行不行或者YY與ZZ哪個好;
26.請看《程序設計實踐》,并嚴格的按照其要求去做;
27.不要因為C和C++中有一些語法和關鍵字看上去相同,就認為它們的意義和作用完全一樣;
28.C++絕不是所謂的C的“擴充”——如果C++一開始就起名叫Z語言,你一定不會把C和Z語言聯系得那么緊密 117-102 117-301 ;
29.請不要認為學過XX語言再改學C++會有什么問題——你只不過又在學一門全新的語言而已;
30.讀完了《Inside The C++
Object Model》以后再來認定自己是不是已經學會了C++;
31.學習編程的秘訣是:編程,編程,再編程;
32.請留意下列書籍:《C++面向對象高效編程(C++ Effective Object-Oriented Software
Construction)》《面向對象軟件構造(Object-Oriented Software Construction)》《設計模式(Design
Patterns)》《The Art of Computer Programming》;
33.記住:面向對象技術不只是C++專有的;
34.請把書上的程序例子親手輸入到電腦上實踐,即使配套光盤中有源代碼;
35.把在書中看到的有意義的例子擴充;
36.請重視C++中的異常處理技術,并將其切實的運用到自己的程序中;
37.經常回顧自己以前寫過的程序,并嘗試重寫,把自己學到的新知識運用進去;
38.不要漏掉書中任何一個練習題——請全部做完并記錄下解題思路;
39.C++語言和C++的集成開發環境要同時學習和掌握;
40.既然決定了學C++,就請堅持學下去,因為學習程序設計語言的目的是掌握程序設計技術,而程序設計技術是跨語言的;
41.就讓C++語言的各種平臺和開發環境去激烈的競爭吧,我們要以學習C++語言本身為主;
42.當你寫C++程序寫到一半卻發現自己用的方法很拙劣時,請不要馬上停手;請盡快將余下的部分粗略的完成以保證這個設計的完整性,然后分析自己的錯誤并重新設計和編寫(參見43);
43.別心急,設計C++的class確實不容易;自己程序中的class和自己的class設計水平是在不斷的編程實踐中完善和發展的;
44.決不要因為程序“很小”就不遵循某些你不熟練的規則——好習慣是培養出來的,而不是一次記住的;
45.每學到一個C++難點的時候,嘗試著對別人講解這個知識點并讓他理解——你能講清楚才說明你真的理解了;
46.記錄下在和別人交流時發現的自己忽視或不理解的知識點;
47.請不斷的對自己寫的程序提出更高的要求,哪怕你的程序版本號會變成Version 100.XX;
48.保存好你寫過的所有的程序——那是你最好的積累之一;
49.請不要做浮躁的人;
50.請熱愛C++!
一.C++語言的基礎
說起入門慢,第一個原因莫過于語言基礎了.高中時期學校組織的微機興趣小組學習的是PASCAL語言(我也不知道為什么要講這個語言,如果說是為了應付比賽,當時也有C語言組呀),所以在大學轉向Windows編程的時候,我首先選擇了Delphi.大三的時候學校要求考國家二級,二級沒有 Delphi,于是又轉向了VB,原因是VB做起來與Delphi很像。后來發現VB的IDE做的比Delphi好用,而且BASIC語言寫起來簡單,于是便棄Delphi而去(明眼人恐怕又要罵我了,若不是太懶,怎么會喜歡VB的IDE呢?的確是這樣,后文會提到,懶不僅僅是學習VC的大敵,而且懶人是什么都學不好的)。長年與VB打交道,讓我對C/C++語言很不習慣——我不喜歡C++寫一個句語要打一個分號,我不喜歡大小寫字母要嚴格區分,我不喜歡比較的時候要寫兩個等號,我不喜歡……總之,對C++很沒好感,沒好感也就沒興趣學了(后面提到興趣是相當重要的)。當然如果你現在再問我應該學習什么語言,我會毫不猶豫地向你推薦C++,因為就常用語言而言,C++語言中包含的知識是相當全面的——從面向過程,到基于對象/面向對象,再到模板和范型,可以說是應有盡有,不夸張地說,別的語言在某種程度上而言是C++語言的子集或者說是在模仿C++、向C++靠攏。
在數次失敗中,給我很明顯的感覺就是,不學好C++語言就學習Visual
C++純粹是一種自虐。這次入門之前,我花了3個多月的時間系統地學習了C++語言,夠意思吧。然后我信心實足地敲響Visual
C++的家門,呵呵,這次她終于肯給面子了。舉個例子吧,在看Dll的調用時,用到“函數指針”,順理成章就看下去了,想一想如果沒有C++語言的基礎,基本是不可能的。所以說,沒學會中文之前,別看《紅樓夢》,那不是《看圖識字》。奉勸想從VB轉向VC學習的朋友,如果你指望能像學習VB一樣邊學習VC 邊學習C++語言,那你可就錯了。
順便提醒C++語言入門的朋友一點,應該關注ANSI/ISO
C++,也就是標準C++了,市面上C++的書良莠不齊,很多書是“舊書換新皮”,講的仍然是非標準C++,一定要選好。計算機書很貴,大家不妨找電子版的來看,網上有很多,甚至《C++
Primer》或者《C++沉思錄》這樣的好書也有熱心朋友放到了網上。不過,我最喜歡的是《C++編程金典》這本書,不愧是教育大師寫的書,用來學習很合適。至于編譯器的選擇,如果條件允許就安裝VS.NET2003吧,據說Visual
C++7.1的編譯器是目前對標準C++支持的最好的編譯器了。
二.VC學習資料的選擇
VC入門難有很多原因,其中不容忽視的一個就是優秀的VC學習資相對較少。C++語言較深,Visual
C++用起來復雜,再加上資料少——難上加難。資料少,并不意味著沒有,怎樣選擇或者說挖掘就是關鍵。暫把資料分為光盤、書籍(包括電子書)和文檔(包括網上的)三類。
在選擇資料方面,大家一定要擯棄中國人思想中的兩大劣根性:<1>不勞而獲<2>一夜暴富。
“不勞而獲”的思想會導致趨向于選擇“講課”類的資料,比如多媒體光盤。結果是光盤容量往往很少但又要求內容面面俱到(不然怎么賣出去呀),這就造成了知識的連貫性差而且講的又飛快,任你一遍一遍地聽,不見成效又打擊信心,最后只能放棄。期待早日有內容豐富,講解精彩的光盤面市。
“一夜暴富”的思想會讓你趨向于選擇“速成”類教材。那樣的教材大多是騙人的——能寫個彈出窗口Hello一下World,這就能算是會Visual
C++了?我們還是不要自欺欺人的好。至少也要能連數據庫、能使用Socket吧……而這些知識怎么可能“速成”呢?
我有很多Visual
C++的學習資料,但沒有一本我是抱著一啃到底的,因為沒有哪本書十全十美,我是交替著使用些資料,這樣做的好處在于:
<1>知識的連貫性好,跳躍性小,進階坡度較小,讀起來舒服。都說Visual
C++的學習坡度比較陡,那個陡坡是出現在由單純的C++語言學習轉向Windows編程的時候,C++語言本身的學習并沒有那么困難。
<2>有積累效應,這本書講的不精不透,另一本書會幫你補上,這本書你沒留心,下本書總該長個心眼。還有就是一些小例子程序,把MFC的類或者函數拆開來給你看,目的非常明確,效果也不錯。每天學一點,不圖快,圖扎實。呵呵,跟VC搞“面向對象”,當然要一天一點戀愛了。
<3>舉一返三,動手實踐。如果多本書中都把它列為重點,那就一定要熟記在心而且上機操作,書上的例子一定要分析透徹,不能有“差不多”的思想——差多少算多呢?程序這東東,錯一個字母都不行呀。光看會了還差遠著呢,自己要能寫,而且能對例子進行擴展才行。
<4>內容詳實豐富,這一點上,首推MSDN啦,還有就是在網上能找到的微軟出的Visual
C++的叢書,希望譯的電子版,是wdl格式的。雖然MSDN是英文版,但其中的英文并不難——您盡可以相信我,因為在下的英文水平是奇爛無比的。 MSDN有兩種用法,一種是當字典用,因為內容全;一種是當消遣,沒事了看一個類,敲幾行代碼,看到那個MFC的繼承圖了嗎,挺好玩兒的,感覺像逛街—— 而且東西不要錢,help
yourself。
互聯網上的資源是非常非常豐富的,千萬不要錯過!好網站和下載站BB皆是。還有論壇、新聞組、在線QQ群……你問我有哪些?呵呵,遠在天邊近在眼前呀:)
三.內因與外因:“三心二意”和“高手朋友”你有嗎?
啊哦,我不是在開玩笑。“三心”是指決心,信心和耐心。決心來源于動機,說來好笑,我最初動機很簡單,大學時有個朋友,計算機系的,我總認為我比他聰明(我的天~~~~),他會VC我不會,我就想超過他,現在都畢業兩年了,最初的動機早已經不在了,而學習卻VC已經成了我的心愿——最關鍵的一點是我的愿望是寫自己的輸入法,而寫輸入法只能用Visual
C++去實現,所以我會有決心學好Visual
C++。至于信心,有兩次失敗完全是信心不足造成的,促成這次成功的信心說起來還挺傳奇:我去北京玩兒,回家的火車上一姓趙位老師看見我別著一個MCP的領章就過來跟我聊天,得知他是一位有著十多年VC開發經驗的程序員,敬意油然而生。聊天的過程中,趙老師給了我極大的鼓勵和支持——我問他像我這種 Wood
Head能不能在半年內入門VC,他告訴我,一定能,于是我就堅定了自己的信心,現在剛好是4個月,如果趙老師有機會看到這篇文章——我在這里謝謝您啦! (花絮:下車,兩個小時后我與女友分手了,是被甩呀同志們!隨后的一段日子里,一直與VC相伴……)
還要說說耐心:如果您已經看到這里了,說明您很有耐心(竟然能看到這里還沒有拂袖而去),耐心與個人的風格有關,沒耐心的人多半是懶人,懶人什么都做不成,學習VC就是不能懶,書懶得看,問題懶得問,英語懶得譯……或者是有點挫折就放棄,學好VC是沒指望了。我不知道別人怎樣,反正我是沒少受挫,其實有兩次離入門就那么一點點了,我放棄了……學VC要越挫越勇,學VC要肯定執著,Gogogo!
“二意”是指第一你要感覺學習VC有“意思”,二是你要感覺學習VC有“意義”。有意思,就是說你喜歡寫程序,“三心”的源動力來源于你對程序設計的熱愛,不喜歡編程的人可能能學好VB但絕學不好VC。有意義,就是說你要給自己一個理由:自己都不能給自己一個交待的事情是做不長久的。前面說過,我是為了寫自己的輸入法,解放中國人的雙手,這個理由夠純潔夠崇高,還有一個理由就是通過學習VC來礪練自己,成為一個真正的程序員。你可以有自己的理由,比如提高薪水或者取得認證云云,一定要有!這就像是給自己的“報酬”,沒有報酬只憑激情做事是任何事都做不長久的。
我小小的成功,有嚴重的原因是因為我有位“高手朋友”——楊W,他是個VC高手,大家會好奇地問:他教你寫什么呢?是MFC還是ATL或者是COM?呵呵,都不是,他從來沒教我寫過一行代碼,但他對我的每一次幫助都彌足珍貴,當我不知道從哪里查找類庫資源的時候,他告訴我:MSDN;當我不知道從哪里找到類的成員函數時,他告訴我:在頁面的左下角有一個class
member鏈接,當我問他能不能完成XXXX時,他說:別白費力氣了……在他的幫助下,我少走了很多彎路,這也正是高手朋友的可貴之處。在此,我要衷心地說一聲:謝謝!
并不是每位學習VC的朋友都有我這么好的運氣,如果你身邊沒有這樣的朋友也不用著急嗎,我這位好朋友可是經常出沒于CSDN的壇壇里,明白了?不過,提醒與我一樣的初學者:一定要做一個會問問題的人哦!怎么做一個會問題的人呢?概括一下就是:目的明確,言簡意賅,核心代碼,客氣謙虛。
四.VC入門隨筆
本人寫東西向來思緒凌亂、顛三倒四。剩下好多東西不知道寫到哪里,沒辦法了,只好叫“隨筆”咯。
……學習VC編程,首先要豎立一個“系統/全局觀”。無論是VB、C#、Delphi,寫程序的時候只需要考慮程序本身就行了,換句話說就是你不用考慮消息是如何映射和傳遞的。而VC寫程序就要多多少少考慮到這些東西。打個比方:以前用VB寫程序,就好像是在一座山上建一個亭子,山是山,亭子是亭子,我只管造亭子就是了;而用VC寫程序,還是這個亭子,那么你應該意識到,亭子是山的亭子,是山的一部分而不是一個孤立的建筑。“亭子”就是程序,“山”就是Windows系統,亭子的地基是山留給建筑的“接口”,也就是API了……
……VC相對VB入門難,一上來不是像VB那樣給個窗體從頭做起,而且AppWizard要分好幾步,每一步里還有一大堆不知所云的選項,不等生成一個程序就已經暈頭轉向了。怎么辦呢?一句話,從對話框程序入手,因為它最簡單,生成的類最少,而且相對是與VB編程最“像”的。在對話框程序里,你可以充分練習添加類和成員變量或者成員函數。……不過我有一點始終搞不明白,由易到難是對話框程序、單文檔程序、多文檔程序,在AppWizard里微軟為什么不按這個順序排列,非要倒著來呢?成心跟我們這些初學的做對!(國罵省去)……
……又是沒大寫……又是少分號……又是少一個等號……提醒VB轉過來的程序員,別總像我這么沒記性哦!
……還是提醒那些學習了VB或者是VB.NET/C#的DDMM,MFC的類雖然是面向對象的,但它沒有“屬性”這個概念地!不要指望有Me.TextBox1.Text="Hello
World!"這樣的語法,C/C++是函數型的語言,類已經把“屬性”封裝成了成員變量,那些私有的成員變量你看不到,只能通過函數來更改——this ->myTextBox.SetWindowText("Hello
World!");……190-823 117-202 1Z0-043 1z0-042
……暈,原來Win32程序和MFC程序不是一回事呀(看看,這就是一本爛書帶給我的,讓我一直以為Win32程序就是MFC程序,直到拜讀《深入淺出MFC》時才恍然大悟)……
……VC好還是VB好?(拜托,別再問這種無聊的問題了)……
……VC的確能做底層,但不是最底層;VC的確功能強大,但不是萬能的——拿手術刀切西瓜或者用菜刀動手術都不對……
……VC高手都是用記事本寫程序的:笑不笑由你……190-802 000-834 000-861
……VC程序員比VB程序員強:呵呵,毛主席說過,武器不是戰爭勝利的決定因素……
……在快速開發工具(RAD)中,控件與后臺代碼是捆綁在一起的,而MFC的“控件類”不一樣,它的“資源”(或者說是皮)與“類”(或者說是瓤)是分開的,要通過ClassWizard把它們“粘”起來……
……如果說C++是一種程序設計語言,那么Visual
C++中的C++語言不如叫“Windows語言”更合適——Visual
C++就是在編程Windows,用到的宏或者Windows數據類型和Windows結構數不勝數,做好心理準備哦!……
……我的天,那么長的函數或者結構都要一個字母一個字母寫呀!呵呵,按一下Ctrl+J看看發生了什么?我就奇怪了,幾乎沒看到有書上提醒我們的初學者要這樣去做。這可是著實嚇跑了不少初學者呢!(至少我就被嚇跑過)。器利工善,我們要把IDE用熟哦,微軟送的好禮物可不能浪費……
……很多書在添加完對新話框類之后都寫著要在主對話框類里手動添加對這個新類頭文件的引用,何必呢?用添加成員變量的方法添加這個新對話框類的實例,頭文件自動引用,一舉兩得。一句話:盡量多用Class
Wizard,能不手寫的地方就不手寫……
五.virtual BOOL LongWayToGo(void)
{
//頭一次寫文章,其中Bug肯定少不了,大家一起來DeBug。
//由于是入門級文章,如果有錯誤,很可能影響初學者學習,恐誤人子弟,有錯必糾!
//希望大家多提寶貴意見,幫助我前進,謝謝先!
//這是虛函數,留待有所得時續以后文。我還有很長的路……
return TRUE;
}
摘要: 很多人甚至市面上的一些書籍,都使用了void main( ) ,其實這是錯誤的。C/C++ 中從來沒有定義過void main( ) 。C++ 之父 Bjarne Stroustrup 在他的主頁上的 FAQ 中明確地寫著 The definition void main( ) { /* ... */ } is not and never has been C++, nor has it even been C.( void main( ) 從來就不存在于 C++ 或者 C )。下面我分別說一下 C 和 C++ 標準中對 main 函數的定義
閱讀全文
對于崇尚中庸之道的朋友,就不必理會這篇文章了。簡單說明一下目前網絡安裝的簡單過程:
安裝向導cbuilder2007trialsetup.exe檢測是否有.net
2.0環境,這個好辦,如果沒有安裝環境,在網上可以下載到并安裝。但是它的本體,全部安裝文件400多M,通過安裝向導的單線程進行下載,臨近每個文件結尾的時候還留下非常充裕的時間給你上wc,你需要上這么多次嗎?所以我強烈推薦我們的快車最新版,開8個線程真是牛啊。好了,廣告時間已過,開始我們的旅程。
7zip文件管理器,用于解壓7zip格式的文件:
http://www.skyuc.com/bbs/showthread.php?t=62&goto=nextoldest
.net 2.0環境所需要安裝的3個文件:
1)http://installers.codegear.com/prereq/radstudio/5.0/microsoft%20.net%20framework%202.0.7zip
2)http://installers.codegear.com.edgesuite.net/prereq/radstudio/5.0/microsoft%20jsharp%20runtime%202.0.7zip
3)http://installers.codegear.com.edgesuite.net/prereq/radstudio/5.0/microsoft%20.net%202.0%20english%20framework%20sdk%20x86.7zip
或
http://www.microsoft.com/downloads/info.aspx?na=90&p=&SrcDisplayLang=zh-cn&SrcCategoryId=&SrcFamilyId=fe6f2099-b7b4-4f47-a244-c96d69c35dec&u=http%3a%2f%2fdownload.microsoft.com%2fdownload%2fb%2fe%2fa%2fbea35549-7804-4e28-beef-a7d9d1675f4c%2fsetup.exe
在www.codegear.com上注冊,拿到你的安裝序列號,下載安裝向導cbuilder2007trialsetup.exe:
C++Builder 2007 Enterprise Trial
Serial Number: xxxxx
CDN Login
Name: xxx
First Name: xxx
Last Name: xxx
重要提示:注意下文的編號,如1)和(1)是不同的,并不是我寫錯了
需要特別注意的4個目錄:
1)2007安裝目錄,自定義:我的為J:\CodeGear\RAD Studio\5.0
2)示例文件目錄,自定義:J:\CodeGear\RAD Studio\5.0\Demos
3)安裝向導存儲2007安裝文件用到的臨時目錄,自定義:H:\download\bcb2007
4)安裝向導cbuilder2007trialsetup.exe解壓3)的安裝文件用到的臨時目錄,不可自定義,所以一般要保證c盤剩余空間在1.2g以上(安裝了.net
2.0之后,系統仍提示需要1.7g,實際上不要這么多,可以忽視它的提示)對于所有用戶使用方式安裝的為:C:\Documents and Settings\All
Users\ApplicationData\{2EB4C530-C94F-4893-ABDC-C1E05A89956E}
安裝的4個步驟:
(1)安裝.net 2.0環境的3個文件,網上遍地都是。如果是7zip格式的文件,你要先安裝7zip的解壓軟件
(2)運行cbuilder2007trialsetup.exe,設置各種目錄,直到它開始向3)的目錄下載并自動解壓到4)中,這是一個環繞地球80天的等待步驟,就讓它自動運行好了
(3)在(2)的同時,我們用快車之類的軟件先下載3)用到的所有文件約423M并放到3)中的目錄,下面的鏈接你可以手工一條條地輸入快車,也可以做成一個html文件批量導入,方法我就不說了,留給大家發揮:
http://installers.codegear.com/release/radstudio/11.0.2709.7128/vcldotnetruntimes.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/vclwin32runtimes.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/bcbwin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/bcbwin32%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/dunit.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/core.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/core%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/corex.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/corecx.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/transm
// 本文轉自 C++Builder 研究 -
http://www.ccrun.com/article.asp?i=1024&d=lmp27sogrifierc.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/corewin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/corewin32%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/xmlmapper.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/coredelphibcb.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/database.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/databasew32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/dbexplr.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/ibxw32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/ibxbcbwin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/ibxbcbwin32%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/dbgow32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/sqlbuilder.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/sqlbuilder%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/dbxcomponentsw32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/dbexpress.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/dbexpress%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/dbexpressx.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/unittesting.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/unittesting_bcb.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/htmldesigner.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/htmldesigner%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/ite.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/etm.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/itewin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/indywin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/indybcbwin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/intraweb.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/iwbcbwin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/sampleprograms.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/samplesbcbwin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/helpwin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/helpwin32%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/helpcommon.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/helpcommon%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/codevisualization.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/codevisualizationx.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/codevisualizationx%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/codevisualization_bcbw32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/internetcontrols.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/internetctrlswin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/internetctrlswin32x.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/soapwin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/webappdebugger.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/msofficecontrolscore.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/msofficebcbwin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/corba.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/codeguard.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/bde.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/bdecommon.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/bdecommon%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/bde_pro.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/bdecommon_pro.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/bdecommon_pro%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/databasedesktop.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/databasedesktop%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/imagefiles.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/sampledatafiles.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/rave%20reports%20installer.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/borland%20database%20engine%20professional%20english.7zip
(4)完成了(3)之后,停止(2)的運行,再重新運行(2),這時候它就不再下載,只會自動解壓3)中的所有文件并放到4)去。當然,(3)中的文件下漏幾個也無所謂,反正它會補下。它仍需要通過網絡驗證這些文件的有效性,所以這也是一個環游地球一圈的漫長時間。最后,提示你注冊,如果你沒有那個可愛的破解文件bds.exe(版本11.0.2709.7128),估計你會進不去。如果你今天才來,很不幸,http://www.ccrun.com/forum/forum_posts.asp?TID=6071提到的破解文件已經被刪除了,但是我成為了幸運者,所以我才能夠完成這篇文章。替換bds.exe,運行,啟動速度比起我無法忍受的turbo
c++ 2006 professional(和bds 2006是兄弟)快了很多。com支持已經存在了,非常好。
TTreeView是VCL中提供的樹列表控件,樹的每個節點是一個TTreeNode類,TTreeNode組件的屬性和方法可以參考Borland提供的幫助(雖然不如MSDN全面,但有總比沒有強)。實際應用中我們可能需要禁用某個節點(界面上反應的效果是:節點字體呈灰色顯示,節點無法選中等)。但是VCL沒有提供Node->Disable();或Node->Enable = false;這樣的功能,我們只好自己動手實現了。首先我們需要為每個節點設定一個標志,用來標識此節點是否可用,標識方法有很多,比如判斷節點的文本(Text),節點的絕對索引值(AbsoluteIndex),節點的索引(Index)加縮進(Indent)等,在本例中我們用節點的Data屬性作標識(一個void
*型數據,其實可以存放N多東西)。如果在你的應用中恰好用了Data屬性,就另外想個用來作標志的方法吧。:)
我們寫一個自定義函數,用來啟用/禁用一個節點:
void __fastcall CrnEnableTreeNode(
bool bEnable, TTreeNode *pNode)
{
pNode->Data = bEnable? NULL: (
void *)0xFFFF;
// 本文轉自 C++Builder 研究 -
http://www.ccrun.com/article.asp?i=1015&d=r2tf61
pNode->TreeView->Invalidate();
}
然后考慮如何達到禁用節點的效果,前面說了,我們只需實現這兩個效果:
1. 節點字體呈灰色顯示
2. 節點無法選中
節點字體呈灰色顯示可以通過TreeView的OnCustomDrawItem事件中的自繪實現,在設計時狀態,選中TreeView,Events選項卡雙擊OnCustomDrawItem事件,添加以下代碼:
void __fastcall TForm1::TreeView1CustomDrawItem(TCustomTreeView *Sender,
TTreeNode *Node, TCustomDrawState State,
bool &DefaultDraw)
{
if(
int(Node->Data) == 0xFFFF)
{
Sender->Canvas->Font->Color = clGray;
}
}
節點無法選中則可以通過TreeView的Changing事件來處理,在設計時狀態,選中TreeView,Events選項卡雙擊OnChanging事件,添加以下代碼:
void __fastcall TForm1::TreeView1Changing(TObject *Sender, TTreeNode *Node,
bool &AllowChange)
{
AllowChange = (
int(Node->Data) != 0xFFFF);
}
有以上的實現,效果基本就出來了:

測試代碼:
void __fastcall TForm1::Button1Click(TObject *Sender)
{
if(TreeView1->Selected)
CrnEnableTreeNode(
true, TreeView1->Selected);
}
void __fastcall TForm1::Button2Click(TObject *Sender)
{
CrnEnableTreeNode(
true, TreeView1->Items->Item[1]);
}
為看到比較好的效果,可在測試時展開所有節點為:
TreeView1->FullExpand();