?? 第20章 雜項.txt
字號:
C語言編程常見問題解答
發(fā)表日期:2003年11月22日 作者:C語言之家整理 已經(jīng)有2500位讀者讀過此文
第20章 雜項(Miscellaneous)
本書試圖覆蓋有關(guān)C語言編程的每一個重要主題,并且希望所介紹的內(nèi)容都是有用的和可以理解的。但是,在這樣一本簡潔的書中,是不可能介紹象計算機這樣復雜的事物的每一個側(cè)面的。因此,本章將討論其它章中還未涉及到的一些雜項問題,并給出答案。
20.1 怎樣獲得命令行參數(shù):
每當你運行一個DOS或Windows程序時,都會生成一個程序段前綴(Program SegmentPrefix,簡稱PSP)。當DOS程序的裝入程序把程序復制到RAM中來執(zhí)行時,它先把256個字節(jié)分配給PSP,然后把可執(zhí)行代碼復制到緊接著PSP的內(nèi)存區(qū)域中。PSP中包含了DOS為了執(zhí)行一個程序所需要的各種各樣的信息,其中的一部分數(shù)據(jù)就是命令行。PSP中偏移量為128
的那個字節(jié)中存放著命令行中的字符個數(shù),接下來的127個字節(jié)中存放著命令行本身。這也正是DOS把你能在其提示行中輸入的字符個數(shù)限制在127個之內(nèi)的原因——因為它為命令行分配的存儲空間只有那么多。遺憾的是,PSP的命令行緩沖區(qū)中并沒有存放可執(zhí)行程序的名字——而只存放著在可執(zhí)行程序名后鍵入的字符(包括空格符)。例如,如果你在DOS提示行中鍵入以下命令:
XCOPY AUTOEXEC.BAT AUTOEXEC.BAK
假設(shè)XCOPY.EXE存放在c驅(qū)動器的DOS目錄下,則XCOPY.EXE的PSP命令行緩沖區(qū)中將包含以下信息:
AUTOEXEC.BAT AUTOEXEC.BAK
注意,命令行中緊接著"XCOPY"的空格符也被復制到PSP的緩沖區(qū)中。
除了不能在PSP中找到可執(zhí)行程序名外,PSP還有一個不足之處——在命令行中能看到的對于輸出或輸入的重定向,在PSP的命令行緩沖區(qū)中是無法看到的,也就是說,你無法從PSP中得知你的程序是否被重定向過。
到現(xiàn)在為止,你應該熟悉在C程序中可以通過argc和argv來獲取一些有關(guān)信息,但是,這些信息是怎樣從DOS的裝入程序傳給argv指針的呢?這是由程序的啟動代碼來完成的。啟動代碼在main()函數(shù)的第一行代碼之前被執(zhí)行,在其執(zhí)行期間,它調(diào)用一個名為__setargv()的函數(shù),把程序名和命令行從PSP和DOS環(huán)境中復制到mai‘n()函數(shù)的argv指針所指向的緩沖區(qū)中。你可以在xLIBCE.LIB文件中找到_setargv()函數(shù),對于Small,Medium和Large這三種存儲模式,這里的“x”分別為“S”,“M”和“L”。在生成可執(zhí)行程序時,上述庫文件會自動被連接進來。除了復制argv參數(shù)的內(nèi)容外,c的啟動代碼還要完成其它一些工作。當啟動代碼執(zhí)行完畢后,main()函數(shù)中的代碼就開始執(zhí)行了。
在DOS中的情況是這樣的,那么在Windows中的情況又是怎樣的呢?實際上,在Windows中的情況大致上和在DOS中的一樣。當執(zhí)行一個Windows程序時,與DOS的裝入程序一樣,Windows的裝入程序也會建立一個PSP,其中包含了與DOS的PSP中相同的信息。主要的區(qū)別是此時命令行被復制到lpszCmdLine參數(shù)中,它是WinMain()函數(shù)的參數(shù)表中的第三個(也是倒數(shù)第二個)參數(shù)。在Windows C的xLIBCEW.LIB庫文件中包含了啟動函數(shù)setargv(),它負責把命令行信息復制到lpszCmdLine緩沖區(qū)中。同樣,這里的“x”也表示程序所使用的存儲模式。在Quick c中,函數(shù)_setargv()包含在庫文件xLIBCEWQ.LIB中。
盡管DOS程序和Windows程序的命令行信息的管理方式基本相同,但是傳給你的C程序的命令行的格式在安排上稍有不同。在DOS中,啟動代碼獲得以空格符為分隔符的命令行后,就把每個參數(shù)轉(zhuǎn)換為其自身的以NULL為終止符的字符串。因此,你可把argv原型化為一個指針數(shù)組(char* argv[]),并通過從O到n這些下標值來訪問每個參數(shù),其中n等于命令行中的參數(shù)個數(shù)減去1。你也可以把argv原型化為一個指向指針的指針(char **argv),并通過增減argv的值來訪問每一個參數(shù)。
在Windows中,傳給c程序的命令行是一個LPSTR類型或char_far*類型,其中的每一個參數(shù)都用空格符隔開,就象你在DOS提示行中鍵入這些字符后所看到的那樣(實際上,在Windows中不可能真正鍵入這些字符,而是通過雙擊應用程序圖標這樣的方式來啟動一個程序)。為了訪問Windows命令行中的各個參數(shù),你必須人工地訪問lpszCmdLine所指向的存儲區(qū),并分隔存放在該處的參數(shù),或者使用strtok()這樣的函數(shù),每次處理一個參數(shù)。
如果你富于探索精神,你可以仔細地研究PSP本身,并從中獲取命令行信息。為此,你可以像下面這樣來使用DOS中斷21H(此處使用Microsoft C):
# include <stdio. h>
# incIude <dos. h>
main(int argc,char **argv)
{
union REGS regs ; / * DOS register access struct * /
char far * pspPtr; / * pointer to PSP * /
int cmdLineCnt; / *num of chars in cmd line * /
regs. h. ah=0x62; /*use DOS interrupt 62 *;
int86(0x21 ,®s,&egs) ; / *call DOS * /
FP-SEG(pspPtr) =regs. x. bx ; / *save PSP segment * /
FP_OFF(pspPtr)=0xS0; / * set pointer offset * /
/ * * pspPtr now points to the command-line count byte * /
cmdLineCnt== * pspPtr ;
需要注意的是,在Small存儲模式下,或者在只有一個代碼段的匯編程序中,由DOS返回到BX寄存器中的值就是程序代碼段的地址值;在Large模式的c程序中,或者在多個代碼段的匯編程序中,所返回的值是程序中包含PSP的那個代碼段的段地址值。如果你已經(jīng)建立了一個指向這個數(shù)據(jù)的指針,你就可以在程序中使用這個數(shù)據(jù)了。
請參見:
20.2 程序總是可以使用命令行參數(shù)嗎?
20.2 程序總是可以使用命令行參數(shù)嗎?
今天,通常你可以認為你的程序可以使用命令行參數(shù)。但是,在DOS 2.O版以前,存儲在PSP中的命令行信息與現(xiàn)在稍有不同(它不能從命令行中分離出輸入或輸出重定向數(shù)據(jù)),而
且由argv[O]所指向的數(shù)據(jù)中并不一定包含可執(zhí)行程序的路徑名。直到DOS發(fā)展到3.o版,它才提供了(或者說至少公開了)用來檢索PSP的中斷62H。因此,你至少可以認為,在運行DOS3.0或更高版本的PC上,你的程序總是可以獲得命令行參數(shù)的。
如果你的程序運行在DOS 3.0或更高的版本下,你基本上就可以任意處理命令行參數(shù)了,因為這些信息已存入棧中供你使用。顯然,適用于棧中數(shù)據(jù)的常規(guī)的數(shù)據(jù)操作規(guī)則同樣也適用于存入棧中的命令行參數(shù)。然而,如果你的編譯程序不提供argv參數(shù),例如當你用匯編語言或者某種不提供argv參數(shù)的編譯程序編寫程序時,真正的問題就出現(xiàn)了。在這種情況下,你將不得不自己找出檢索命令行參數(shù)的方法,而使用DOS中斷62H就是一種很方便的方法。
如果你要用DOS中斷62H來檢索指向命令行的指針,你必須明白該指針所指向的數(shù)據(jù)是供DOS使用的,并且正在被DOS使用。盡管你可以檢索這些數(shù)據(jù),但你不可以改變它們。如果在程序中需要隨時根據(jù)命令行參數(shù)作出決定,那么在使用這些數(shù)據(jù)之前應該先把它們復制到一個局部緩沖區(qū)中,這樣就可以隨意處理這些數(shù)據(jù),而不用擔心會與DOS發(fā)生沖突了。實際上,這種技巧同樣適用于帶argv參數(shù)的c程序。位于main()函數(shù)之外的函數(shù)需要使用命令行參數(shù)的情況并不少見,為了使這些函數(shù)能引用這些數(shù)據(jù),main()函數(shù)必須把這些數(shù)據(jù)存為全局型,或者通過(再次)入棧把它們傳遞給需要使用它們的函數(shù)。因此,如果你想使用命令行參數(shù),一種好的編程習慣就是把它們存入一個局部緩沖區(qū)中。
請參見:
20.1 怎樣獲得命令行參數(shù)?
20.3 “異常處理(exception handling)”和“結(jié)構(gòu)化異常處理(structured exception handling)”有什么區(qū)別?
總的來說,結(jié)構(gòu)化異常處理和異常處理之間的區(qū)別就是Microsoft對異常處理程序在實現(xiàn)上的不同。所謂的“普通”C++異常處理使用了三條附加的c++語句:try,catch和throw。這些語句的作用是,當正在執(zhí)行的程序出現(xiàn)異常情況時,允許一個程序(異常處理程序)試著找到該程序的一個安全出口。異常處理程序可以捕獲任何數(shù)據(jù)類型上的異常情況,包括C++類。這三條語句的實現(xiàn)是以針對異常處理的ISO WG21/ANSI X3J16 C++標準為基礎(chǔ)的,Microsoft C++支持基于這個標準的異常處理。注意,這個標準只適用于C++,而不適用于C。
結(jié)構(gòu)化異常處理是Microsoft c/c++編譯程序的一種功能擴充,它的最大好處就是它對C和C++都適用。Microsoft的結(jié)構(gòu)化異常處理使用了兩種新的結(jié)構(gòu):try—except和try-finally。這兩種結(jié)構(gòu)既不是ANSI c++標準的子集,也不是它的父集,而是異常處理的另一種實現(xiàn)(Microsoft會繼續(xù)在這方面努力的)。try—except結(jié)構(gòu)被稱為異常處理(exception handling),tryfinally結(jié)構(gòu)被稱為終止處理(termination handling)。try—except語句允許應用程序檢索發(fā)生異常情況時的機器狀態(tài),在向用戶顯示出錯信息時,或者在調(diào)試程序時,它能帶來很大的方便。在程序的正常執(zhí)行被中斷時,try—finally語句使應用程序能確保去執(zhí)行清理程序。盡管結(jié)構(gòu)化異常處理有它的優(yōu)點,但它也有缺點——它不是一種ANSI標準,因此,與使用ANSI異常處理的程序相比,使用結(jié)構(gòu)化異常處理的程序的可移植性要差一些。如果你想編寫一個真正的C++應用程序,那么你最好使用ANSI異常處理(即使用try,catch和throw語句)。
20.4 怎樣在DOS程序中建立一個延時器(delay timer)?
我們這些程序員很幸運,因為Microsoft公司的專家認為建立一個獨立于硬件的延時器是一個好主意。顯然,這樣做的目的在于,無論程序運行在什么速度的計算機上,都可產(chǎn)生一段固定的延遲時間。下面的程序說明了如何在DOS中建立一個延時器:
# include <stdio. h>
# include %dos. h>
# include <stdlib. h>
void main(int argc,char ** argv)
{
union REGS regs:
unsiged long delay;
delay = atoI(argv[1]) ; / * assume that there is an argument * /
/ * multiply by 1 for microsecond-granularity delay * /
/ * multiply by 1000 for millisecond-granularity delay * /
/ * multiply by 1000000 for second-granularity delay * /
delay * =1000000;
regs. x. ax = 0x8600 ;
regs. x. cx= (unsigned int )((delay & 0xFFFF0000L)>>16) ;
regs. x. dx = (unsigned int ) (delay & 0xFFFF)
int86 (0x15, ®s, ®s) ;
}
上例通過DOS中斷15H的86H功能來實現(xiàn)延時,延遲時間以微秒為單位,因此,上述延時程序認為你可能要使用一個非常大的數(shù)字,于是它分別用CX和DX寄存器來接受延時值的高16位和低16位。上述延時程序最多可以延時49.億微妙---大約等于1.2小時。
上例假設(shè)延時值以微秒為單位,它把延時值乘以一百萬,因此在命令行中可以輸入以秒為單位的延時值。例如,“delay 10”命令將產(chǎn)生10秒的延時。
請參見:
21.2 怎樣在Windows程序中建立一個延時器?
20.5 Kernighan和Ritchie是誰?
Kernighan和Ritchie是指Brian w.Kernighan和Dennis M.Ritchie兩人,他們是《C程序設(shè)計語言(The C Programming Language)》一書的作者。這是一本廣為人知的書,許多人都親切地稱之為“K&R手冊”、“白皮書”、“K&R圣經(jīng)”或其它類似的名字。這本書最初是由Prentice—Hall于1978年出版的。Dennis為運行在DEC PDP-11主機上的UNIX操作系統(tǒng)開發(fā)了c程序設(shè)計語言。70年代初,Dennis和Brian都在AT&T Bell實驗室工作,c和“K&R手冊”就是在那時推出的。從某種意義上來講,C是以Ken Thompson于1970年所寫的程序設(shè)計語言和Martin Richards于1969年所寫的BCPL語言為模型的。
20.6 怎樣生產(chǎn)隨機數(shù)?
盡管在計算機中并沒有一個真正的隨機數(shù)發(fā)生器,但是可以做到使產(chǎn)生的數(shù)字的重復率很低,以至于它們看起來是隨機的。實現(xiàn)這一功能的程序叫做偽隨機數(shù)發(fā)生器。
有關(guān)如何產(chǎn)生隨機數(shù)的理論有許多,這里不討論這些理論及相關(guān)的數(shù)學知識。因為討論這一主題需要整整一本書的篇幅。這里要說的是,不管你用什么辦法實現(xiàn)隨機數(shù)發(fā)生器,你都必須給它提供一個被稱為“種子(seed)”的初始值,而且這個值最好是隨機的,或者至少是偽隨機的。“種子”的值通常是用快速計數(shù)寄存器或移位寄存器來生成的。
在本書中,筆者將介紹c語言所提供的隨機數(shù)發(fā)生器的用法。現(xiàn)在的c編譯程序都提供了一個基于一種ANSI標準的偽隨機數(shù)發(fā)生器函數(shù),用來生成隨機數(shù)。Microsoft和Borland都是通過rand()和srand()函數(shù)來支持這種標準的,它們的工作過程如下:
(1)首先,給srand()提供一個“種子”,它是一個unsignde int類型,其取值范圍是從0到65,535 ;
(2)然后,調(diào)用rand(),它會根據(jù)提供給srand()的“種子”值返回一個隨機數(shù)(在0到32,767之間);
(3)根據(jù)需要多次調(diào)用rand(),從而不斷地得到新的隨機數(shù);
(4)無論什么時候,你都可以給srand()提供一個新的“種子”,從而進一步“隨機化"rand()的輸出結(jié)果。
這個過程看起來很簡單,問題是如果你每次調(diào)用srand()時都提供相同的“種子”值,那么你將會得到相同的“隨機”數(shù)序列。例如,在以17為“種子”值調(diào)用srand()之后,在你首次調(diào)用rand()時,你將得到隨機數(shù)94;在你第二次和第三次調(diào)用rand()時,你將分別得到26,602和30,017。這些數(shù)看上去是相當隨機的(盡管這只是一個很小的數(shù)據(jù)點集合),但是,在你再次以17為“種子”值調(diào)用srand()之后,在對rand()的前三次調(diào)用中,所得到的返回值仍然是94、
26,602和30,017,并且此后得到的返回值仍然是在對rand()的第一批調(diào)用中所得到的其余的返回值。因此,只有再次給srand()提供一個隨機的“種子”值,才能再次得到一個隨機數(shù)。
下面的例子用一種簡單而有效的辦法來產(chǎn)生一個相當隨機的“種子”值——當天的時間值。
# include <stdlib. h>
# include <stdio. h>
# include <sys/types. h>
# include <sys/timeb. h>
void main (void)
{
int i ;
unsigned int seedVal;
struct_timeb timeBuf ;
_ftime (&timeBuf) ;
seedVal = ( ( ( ( (unsigned int)timeBuf, time & 0xFFFF) +
(unsigned int)timeBuf, millitm) ^
(unsigned int)timeBuf, millitm) ;
srand ((unsigned int)seedVal) ;
for(i=O;i<lO;++i)
printf (" %6d\n" ,rand ( ) ) ;
}
上例先是調(diào)用_ftime()來檢索當前時間,并把它的值存入結(jié)構(gòu)成員timeBuf.time中,當前時間的值從1970年1月1日開始以秒計算。在調(diào)用了_ftime()之后,在結(jié)構(gòu)timeBuf的成員millitm中還存入了在當前那一秒已經(jīng)度過的毫秒數(shù),但在DOS中這個數(shù)字實際上是以百分之一秒來計算的。然后,把毫秒數(shù)和秒數(shù)相加,再和毫秒數(shù)進行一次異或運算。你可以對這兩個結(jié)構(gòu)成員施加更多的邏輯運算,以控制seedVal的取值范圍,并進一步加強它的隨機性,但上例所用的邏輯運算已經(jīng)足夠了。
注意,在前面的例子中,rand()的輸出并沒有被限制在一個指定的范圍內(nèi),假設(shè)你想建立一個彩票選號器,其取值范圍是從1到44。你可以簡單地忽略掉rand()所輸出的在該范圍之外的值,但這將花費許多時間去得到所需的全部(例如6個)彩票號碼。假設(shè)你已經(jīng)建立了一個令你滿意的隨機數(shù)發(fā)生器,它所產(chǎn)生的隨機數(shù)據(jù)范圍是從0到32,767(就象前文中提到過的那樣),而你想把輸出限制在1到44之間,下面的例子就說明了如何來完成這項工作:
int i ,k ,range ;
int rain, max ;
double j ;
min=1; /* 1 is the minimum number allowed */
max=44; /* 44 is the maximum number allowed * /
range=max-min; / * r is the range allowed; 1 to 44 * /
i=rand(); /* use the above example in this slot * /
/ * Normalize the rand() output (scale to 0 to 1) * /
/ * RAND_MAX is defined in stdlib, h * /
j= ((double)i/(double)RAND_MAX) ;
/ * Scale the output to 1 to 44 * /
i= (int)(j * (double)range) ;
i+ =min;
上例把輸出的隨機數(shù)限制在1到44之間,其工作原理如下:
(1)得到一個在O到RAND_MAX(32,767)之間的隨機數(shù),把它除以RAND_MAX,從而產(chǎn)生一個在0到1之間的校正值;
(2)把校正值乘以所需要的范圍值(在本例中為43,即44減去1),從而產(chǎn)生一個在O到43之間的值;
(3)把該值和所要求的最小值相加,從而使該值最終落在正確的取值范圍——1到44之內(nèi)。
你可以用不同的min和max值來驗證這個例子,你會發(fā)現(xiàn)它總是會正確地產(chǎn)生在新的rain和max值之間的隨機數(shù)。
20.7 什么時候應該使用32位編譯程序?
32位編譯程序應該在32位操作系統(tǒng)上使用。由32位編譯程序生成的32位程序比16位程序運行得更快,這正是任何32位的東西都很熱門的原因。
有那么多不同版本的Microsoft Windows,它們和哪種編譯程序組成最佳搭配呢?
Windows 3.1和Windows for Workgroups 3.11是16位的操作系統(tǒng);Microsoft Visual C++1.x是16位編譯程序,它所生成的程序能在Windows 3.1上運行。Microsoft Windows NT和Windows 95是32位操作系統(tǒng);Microsoft Visual C++2.o是32位編譯程序,它所生成的32位程序能在這兩種操作系統(tǒng)上運行。由Visual C++1.x生成的16位程序不僅能在Windows 3.1上運行,也能在Windows NT和Windows 95上運行。
然而,由Visual 2.O生成的32位程序無法在Windows 3.1上運行。這就給Microsoft提出了一個難題——它希望每個人都使用它的32位編譯程序,但是有6千萬臺計算機所使用的Windows版本無法運行32位程序。為了克服這個障礙,Microsoft設(shè)計了一個叫做Win32s的轉(zhuǎn)換庫,用來完成由32到16位的轉(zhuǎn)換,從而使由Visual C++生成的32位程序能在
Windows 3.1和Windows for Workgroups上運行。Win32s是專門為在Windows 3.1(和WFW)上運行而設(shè)計的,它不能用在Windows NT和Windows 95上,因為它們不需要為運行32位程序作什么轉(zhuǎn)換。有了Win32s,你就可以用Visual C++2.0生成一個既能在Windows 3.1(和WFW)上運行,又能在Windows NT上運行的程序了。
最后還有一個問題,即編譯程序本身的問題。Visual C++1.x是一個16位Windows程序,它既能在Windows 3.1上運行并生成16位程序,也能在Windows 95和Windows NT上運行并生成同樣的程序。但是,作為一個32位程序,Visual C++2.O無法在Windows 3.1上運行,即使裝上了Win32s也無法運行,因為出于方便的目的,Microsoft認為在啟動Visual C++2.O之前,你一定運行在Windows 95或Windows NT上。
總而言之,在Windows 3.1,Windows for Workgroups,Windows 95或Windows NT上運行Visual c++1.x(更新的版本為1.51)而生成的16位程序能在任何版本的Windows上運行。由Visual C++2.O生成的32位程序在Windows 95或Windows NT上運行得很快,但它在Windows 3.1(和WFW)下也能很好地運行。
對于Borland C/C++,Borland的Turbo C++3.1版是版本較新的16位編譯程序,Borland c++4.5版(注意該名稱中沒有"Turbo一詞)是32位Windows編譯程序。這兩種編譯程序都包含編譯程序、Borland的OWL C++類和一個出色的集成化調(diào)試程序。
20.8 怎樣中斷一個Windows程序?
沒有一個普通的方法可以中斷一個Windows程序,從而讓你去完成一項必需的任務(wù)(如果這就是你的目標的話)。Windows 3.x是一種不支持優(yōu)先調(diào)度的多任務(wù)操作系統(tǒng),換句話說,它是一種合作性(cooperative)的多任務(wù)操作系統(tǒng)——原因之一就是你無法簡單地從當前正在由處理器控制著的一個Windows程序中搶占一段時間。如果你看一下Windows API,你會發(fā)現(xiàn)PeekMessage()和WaitMessage()這樣一些函數(shù),在Microsoft的幫助文檔中有這樣一段介紹這些函數(shù)的話:
函數(shù)GetMessage(),PeekMessage()和WaitMessage()把控制權(quán)交給別的應用程序,允許別的應用程序運行的唯一方法就是使用這些函數(shù)。如果一個應用程序長時間不調(diào)用這些函數(shù),那么它就阻止了別的應用程序的運行。
盡管這樣,你還是可以中斷一個Windows程序的。一種辦法是在你的Windows程序中建立一個定時器,它每隔一段時間就會發(fā)出信號,只要你的程序處于激活的狀態(tài),那么在定時器發(fā)出信號的時候,你就能獲得時間來完成各種所需的工作。但是,從技術(shù)角度來講,這種方法并沒有中斷一個程序的處理過程——它只是應用了Windows的合作性多任務(wù)性能。如果你需要中斷一個Windows程序,你可以使用過濾函數(shù)(filter function,也叫做hook function,即鉤子函數(shù))。Windows中的過濾函數(shù)與DOS中的中斷處理程序相似(見20.12和20.17),你可用它“掛上(hook)”某個Windows事件,并在該事件發(fā)生時執(zhí)行相應的任務(wù)。實際上,你可以用一個使用了一個或多個可用的鉤子(hook)的過濾器來監(jiān)視Windows中的幾乎每一條消息。下面的例子使用了鍵盤鉤子,因為它要使你能隨時按一個特定的組合鍵來中斷一個程序。該例掛上了鍵盤事件鏈,并且在Ctrl+Alt+F6鍵按下時打開一個消息框。不管當前正在執(zhí)行的是哪一個應用程序,該例都能起作用。
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -