首先,,這篇文章針對近期網(wǎng)友在ARX版塊的提問,很多都是在調(diào)用ARX函數(shù)或者設計自定義函數(shù)時出現(xiàn)的困惑,,為方便大家分析和理解問題,進而正確解決問題,,我將個人的一些理解寫成文字,希望對大家在做ARX程序設計時有所幫助,。同時,,這篇文章也為“ObjectARX程序設計入門(2)”作些準備工作。 這篇文章與普通的C/C++教材相比,,可能要深入得多,,閱讀時應該細心,。而對于未接觸過C語言的讀者來說,大概需要先閱讀一般的C++教材,。我的看法,,《C++編程思想》和《深入淺出MFC》一類的書對于初學者太過深入,,而類似《Visual C++ 6.0從入門到精通》的書籍主要篇幅在介紹VC軟件的使用方法而不是講解C++程序設計方法,,它們都不適宜作C++或ARX程序設計入門學習用書,。我個人學習C++使用的是南京大學出版社的書名就是《C++教程》,,它是為C程序員編寫的C++教材,全書僅130多頁,內(nèi)容淺顯但基本夠用,。不過那是上世紀90年代初出版的,現(xiàn)在大概不好找了,,不過類似的(比如說大學教材)我想書店里還是有的,。 文章中的大部分內(nèi)容是我個人的看法,,一般的C++書籍上找不到類似的說法與其比較,,對于其正確性,我沒有十足的把握,。各位網(wǎng)友可以對此進行討論或者批評,。(只是不要真的用磚頭砸,,那樣對于我英勇而忙碌的醫(yī)護人員太不尊重,,別再給他們添亂了。) C語言的函數(shù)入口參數(shù),,可以使用值傳遞和指針傳遞方式,,C++又多了引用(reference)傳遞方式。引用傳遞方式在使用上類似于值傳遞,,而其傳遞的性質(zhì)又象是指針傳遞,,這是C++初學者經(jīng)常感到困惑的。為深入介紹這三種參數(shù)傳遞方式,,我們先把話題扯遠些: 1、 C/C++函數(shù)調(diào)用機制及值傳遞: 在結(jié)構(gòu)化程序設計方法中,先輩們告訴我們,,采用“自頂向下,,逐步細化”的方法將一個現(xiàn)實的復雜問題分成多個簡單的問題來解決。而細化到了最底層,,就是“實現(xiàn)單一功能”的模塊,,在C/C++中,這個最小的單元模塊就是函數(shù),。然而,,這些單個的模塊(或者說函數(shù))組合起來要能完成一項復雜的功能,這就注定各個函數(shù)之間必然要有這樣或那樣的聯(lián)系(即耦合),。而參數(shù)耦合是各個函數(shù)之間最為常見的耦合方式,,也就是說,各個函數(shù)之間通常通過參數(shù)傳遞的方式來實現(xiàn)通訊,。 當我們設計或者調(diào)用一個函數(shù)時,,首先要注意的是函數(shù)的接口,也就是函數(shù)的參數(shù)和返回值,。調(diào)用一個函數(shù)就是將符合函數(shù)接口要求的參數(shù)傳遞給函數(shù)體,,函數(shù)執(zhí)行后返回一個值給調(diào)用者。(當然,,C/C++允許void類型的參數(shù)和返回值,。當返回值為void時,函數(shù)類似Basic的Sub子過程或者Pascal的Procedure過程,。) 函數(shù)的參數(shù)傳遞,,就是將在函數(shù)體外部已賦值(或者至少已經(jīng)定義并初始化)的變量通過函數(shù)接口傳遞到函數(shù)體內(nèi)部,。根據(jù)變量種類的不同,有不同的參數(shù)傳遞方式: 若傳遞的參數(shù)是一個類對象(包括象Int和float這樣的C/C++內(nèi)部數(shù)據(jù)類型),,這種傳遞方式為值傳遞,。C/C++這種以函數(shù)為主體的語言中,幾乎所有的功能都是通過函數(shù)調(diào)用來實現(xiàn)的,。<不是嗎,?你說C/C++運算符操作?還有變量聲明,?你先等等,,接下來我們就看看C++中這些操作是怎么實現(xiàn)的。>以下的C/C++代碼是如此的簡單,,可能你從未想過還有什么要分析的,,但它確實是函數(shù)值傳遞方式的典型例子。 float x = 0.254; 以上代碼編譯執(zhí)行時,,第一步float x,,即聲明一個實數(shù)變量。即將標志符x認為是一個實數(shù)變量,,并調(diào)用float類的初始化函數(shù),。當然你可能感覺不到它的存在,因為現(xiàn)在的CPU都直接支持浮點運算,,它只是一條匯編指令而已,。 初始化完成后,調(diào)用賦值函數(shù): x.operator = (0.254); 不要奇怪以上函數(shù)的寫法,,它實際上與 x = 0.254; 效果完全相同,,會產(chǎn)生同樣的匯編代碼。 該函數(shù)首先根據(jù)變量x的數(shù)據(jù)類型分配合適的內(nèi)存空間,,并將該內(nèi)存地址與標志符x關(guān)聯(lián),。然后將立即數(shù)0.254寫入分配的內(nèi)存。(這里借用匯編語言的術(shù)語,,立即數(shù)可以理解為程序已指定的具體數(shù)值,。)然而,賦值函數(shù)的設計者并不能獲知立即數(shù)0.254的數(shù)值,,調(diào)用該函數(shù)時就必須通過參數(shù)傳遞的方法將數(shù)值通知給函數(shù)體,。賦值函數(shù)接口大致是這樣: float float::operator = (register float a); 變量a是在CPU寄存器中使用的臨時變量。調(diào)用賦值函數(shù)時,,將0.254送到寄存器變量a中,,再將a值送到變量x所在的內(nèi)存位置中。以上函數(shù)的返回值用于類似這樣的鏈式表達式的實現(xiàn): x = y = z; 說了許多,好象十分復雜,,其實賦值操作僅僅只是兩條匯編代碼: mov AX, 0.254 mov [x], AX 事實上,,它之所以簡單,僅僅是因為float是CPU能直接處理的數(shù)據(jù)類型,。若以上代碼中不是float類型數(shù)據(jù)賦值,,而是更復雜的(比如說自定義)類型數(shù)據(jù),同樣的賦值操作盡管是相同的步驟,,但實際情況要復雜得多,。因為寄存器容量限制,可能變量a無法作為寄存器變量存放,,這樣即使是簡單的賦值操作也要為函數(shù)的臨時變量分配內(nèi)存并初始化,,在函數(shù)的返回時,,臨時變量又要析構(gòu)(或者說從內(nèi)存中釋放),,這也就是參數(shù)值傳遞方式的弱點之一:效率低。以后我們還可以看到,,值傳遞方式還有其力所不能及的時候,。 上面的代碼段中加法調(diào)用這樣的函數(shù),其參數(shù)傳遞方式同樣是值傳遞: float::operator + (float a, float b); 下面看一個稍微復雜的類,Complex復數(shù)類,。ObjectARX程序設計中使用的大部份對象類型都將比這個類復雜,。 class Complex 類的接口函數(shù)的參數(shù)仍然用值傳遞方式,。當執(zhí)行下列代碼中的加法和賦值操作時,,程序?qū)⒁啻螆?zhí)行Complex類的構(gòu)造函數(shù)和析構(gòu)函數(shù)。 Complex A(2.5, 3); Complex B(0.4, 2.5); Complex C = A + B; 最后一句代碼,,首先聲明一個Complex類對象C,,然后根據(jù)運算符優(yōu)先級,執(zhí)行加法運算,,將對象A,,B傳遞給加法函數(shù),這時C++調(diào)用Complex類的默認構(gòu)造函數(shù)聲明兩個臨時變量,,再調(diào)用默認的“拷貝構(gòu)造函數(shù)”采用位拷貝的方法將對象A,,B復制到臨時變量,加法操作返回時,,再將臨時變量析構(gòu),,返回值再用值傳遞方式傳遞給賦值函數(shù),。 從以上執(zhí)行過程可以看出,值傳遞方式效率低的關(guān)鍵在于臨時變量的建立和析構(gòu),。于是考慮,,因為在調(diào)用函數(shù)時該變量已經(jīng)在內(nèi)存中存在,將這個已經(jīng)存在的變量直接傳遞給函數(shù)體而不去聲明和拷貝臨時變量,。這樣,,臨時變量的構(gòu)造、拷貝,、析構(gòu)等工作都被省略,,從而大大提高了函數(shù)效率。這便是使用C/C++指針和引用傳遞機制的主要原因,。另外,,使用這樣的函數(shù)參數(shù)傳遞機制,在函數(shù)體內(nèi)部可以很輕易地修改變量的內(nèi)容,。(而使用值傳遞方式,,函數(shù)體內(nèi)部只能修改臨時變量,沒有辦法修改這些外部變量本身的值,。)這樣一方面增加了程序設計的靈活性,,同時也給程序帶來了安全隱患。當然,,我們可以使用const聲明防止變量的內(nèi)容在函數(shù)體內(nèi)部被修改,,但這需要編程者有良好的編程風格和編程習慣。在介紹函數(shù)參數(shù)的指針和引用傳遞方式之前,,先說一說指針和引用這兩個概念,。 2、指針和引用 在解釋指針和引用之前,,先看看普通變量是怎樣在內(nèi)存中存放的,。聲明變量后,編譯程序要維護一張包括各種標識符的表,。在這張表內(nèi),,每一個標識符,比如說變量名都應該有它的類型和在內(nèi)存中的位置,。 在這要進一步說明幾個問題,,這些問題可能涉及多個計算機專業(yè)領域,我也不想在這作深入介紹,,看不明白沒有關(guān)系,,不會影響您繼續(xù)閱讀這篇文章。 首先,C/C++的內(nèi)存分配有靜態(tài)分配和動態(tài)分配兩種機制,。靜態(tài)分配內(nèi)存是由編譯程序為標識符分配固定的內(nèi)存地址,,而動態(tài)分配機制是應用程序在進入內(nèi)存后再根據(jù)程序使用內(nèi)存的實際情況決定變量存放地址。這個話題非常復雜,,不過進行ObjectARX程序設計好象不必太在意內(nèi)存分配機制,,讓編譯程序和Windows去管這件事吧。而且內(nèi)存分配機制對于我們理解指針和引用不會造成影響,。 其次,,標識符可以標識變量,也可以標識函數(shù)入口,。從而它的類型可以是CPU能直接處理的內(nèi)部數(shù)據(jù)類型<例如int類型>,,也可以是用戶自定義類型,還可以是函數(shù)類型,。 另外,,由于標識符的類型不同,它占用內(nèi)存的大小也各有差異,。“在內(nèi)存中的位置”實際上指的是它占用的內(nèi)存塊的首地址,。對于80286以上的計算機<這句話是不是多余,?>,,內(nèi)存地址由基址(或段地址)加上偏移地址組成?;肥菓贸绦虮徽{(diào)入內(nèi)存時由操作系統(tǒng)分配,,當然,編譯程序把應用程序編譯成多個段,,從而要求操作系統(tǒng)對于不同的段分配不同的基址,。而編譯程序(哪怕是使用靜態(tài)地址分配)只能決定標識符存放的偏移地址,也就是說,,“在內(nèi)存中的位置”只是標識符占用內(nèi)存的第一個字節(jié)的偏移地址,。說了這么多,有一點需要記住,,無論是程序設計者還是編譯程序都無法確知變量的內(nèi)存中的實際位置,。 最后,這個標識符表要比上面說的復雜,,我只選擇了與目前討論的問題有關(guān)的內(nèi)容,。 好了,準備工作做了許多,,讓我們正式進入C/C++指針和引用的神秘世界,。 指針變量其實質(zhì)類似一個int整型變量。我們在源程序中這樣聲明一個指針變量: float *px; 此時,標識符px指示的內(nèi)存位置上存放的就是一個int類型整數(shù),,或者說,,通過變量px可以訪問到一個int類型整數(shù),并且這個整數(shù)與指針指向的數(shù)據(jù)類型<在此例中為float浮點數(shù)>無關(guān),。在ARX程序中,,甚至可以用這樣的方式打印一個指針變量: acutPrintf(“指針變量px的值為%d”, px); 當然,這個整數(shù)值到底意味著什么,,可以只有計算機(或者說操作系統(tǒng))自己知道,,因為這個值表示的是指針指向的數(shù)據(jù)在內(nèi)存中的位置。也就是說,,不應該將指針變量與普通int整型混淆,,例如,對指針進行四則運算將使用結(jié)果變得計算機和程序員都無法理解,,盡管編譯器允許你這樣做,。<實際上,計算數(shù)組下標就要使用指針的加法,。> 與普通變量不同,,若在程序中聲明指針變量的同時不進行初始化,系統(tǒng)會自動將指針變量初始化為NULL,。<NULL的值與0相同,,但好的編程風格是使用NULL而非0,以與普通int類型區(qū)別,。>而聲明普通變量,,系統(tǒng)僅為其分配內(nèi)存,而不做自動初始化,,從而未初始化的變量值是不可預測的,。當然,直接使用未初始化的指針決不是一個好程序(此時編譯器會發(fā)出警告信息),,其危害或隱患以后在說明內(nèi)存管理技術(shù)時再討論,。<若ObjectARX程序設計連載能堅持寫下去,我想會要涉及到內(nèi)存管理的,。> //float *px = 0.254;原文如此 float f = 0.254 ; float *px = &f ; 這是初始化同時賦值,,也可以使用new運算符進行初始化: float *px = new float; 這種初始化方式經(jīng)常用于不方便或不能直接賦值的復雜數(shù)據(jù)類型。 上述語句執(zhí)行時,,首先分配一塊可存放數(shù)據(jù)的內(nèi)存區(qū)域<大小與數(shù)據(jù)類型有關(guān)>,,若要同時賦值,就調(diào)用賦值函數(shù)將數(shù)值寫入剛分配的內(nèi)存中,。然后為標識符px分配一個int整型要占用的(通常為4字節(jié))內(nèi)存空間,,最后將分配的用于存放數(shù)據(jù)的內(nèi)存首地址寫入內(nèi)存,。 注意:使用new運算符初始化指針,指針變量使用結(jié)束后應該用delete運算符釋放其占用的內(nèi)存,。也就是說,,new運算符和delete運算符最好能成對使用。 指針的初始化可以在程序的任何位置進行,,<當然,,最好在使用它之前初始化。>比如: float x = 0.254; 上面最后一行代碼是將變量x的地址賦值給指針px,。以上初始化指針的方法效率相差無幾,讀者可自行分析,。&運算符稍后討論,。 下面看一個ObjectARX程序中最為普通的使用指針的代碼段:(注意,以下不是完整的代碼,,不能進行編譯或執(zhí)行,。) void Sample(void) 這段代碼不作深入分析,,請讀者注意pLine指針的聲明和初始化過程以及pLayerName指針的賦值過程。 注意到我們在上面初始化px指針時使用了&運算符,,即取地址運算符,,這也是使用引用的一般方法之一。引用是C++概念,,C++初學者容易將引用和指針混淆在一起。 以下代碼中,,m就是n的一個引用: int n; 編譯程序編譯以上代碼時,,在標識符表中添加一個int引用類型的標識符m,它使用與標識符n相同的內(nèi)存位置,。這樣對m的任何操作實際上就是對n的操作,,反之亦然。注意,,m既不是n的拷貝,,(這樣的話,內(nèi)存中應該的兩塊不同的區(qū)域,,存放著完全相同的內(nèi)容,。),,也不是指向n的指針,其實m就是n本身,,只不過使用了另外一個名稱而已,。 盡管指針和引用都是利用內(nèi)存地址來使用變量,它們之間還是有本質(zhì)的區(qū)別: 首先,,指針變量(包括函數(shù)調(diào)用時的臨時指針變量)在編譯和運行時要分配相當于一個int變量的內(nèi)存空間以存放指針變量的值,,盡管這個值表示的是指針指向的變量的地址。而引用與普通變量一樣,,標識符所指示的內(nèi)存位置就是變量存放位置,。這樣不僅不需要在內(nèi)存中分配一個int變量的內(nèi)存空間(盡管它可能微不足道),而且在使用中可以少一次內(nèi)存訪問,。<僅就內(nèi)存使用效率而言,,指針和引用所帶來的區(qū)別確實不大,完全可以不去在意它,。> 其次,,由于標識符表在填寫后就不能再被修改,因此引用在創(chuàng)建就必須初始化,,并且初始化后,,不能改變引用的關(guān)系。另外,,引用初始化時,,系統(tǒng)不提供默認設置,引用必須與合法的內(nèi)存位置相關(guān)聯(lián),。而這些特征對于指針而言都是不存在的,。指針可以在程序任何時刻初始化,初始化的指針在程序中也可以根據(jù)需要隨時改變所指向的對象,,(這只需要改寫指針變量的值就可以了,。)當然,未初始化的指針變量系統(tǒng)會初始化為NULL,,而NULL引用是非法的,。 下面看一段類似文字游戲的程序: int I = 6; 注意,由于引用關(guān)系不能被修改,,語句K = J;并不能將K修改為對J的引用,,只是修改了K的值。實際上,,聲明并初始化引用后,,可以把引用當作普通變量來使用,只不過在操作時會影響另外一個變量,。 以上代碼僅僅只是解釋引用的定義,,并不能體現(xiàn)引用的價值,。引用的主要功能在于函數(shù)的參數(shù)(或者返回值)的傳遞。 注:下圖應該是float *px = &x ;
3,、 函數(shù)參數(shù)的指針和引用傳遞機制 先看一下簡單的例子,。 void Func1(int x) //這個函數(shù)的參數(shù)使用值傳遞方式 以下代碼調(diào)用這些函數(shù): int n = 0; 以上代碼段中,,當程序調(diào)用Func1()函數(shù)時,,首先在棧(Stack)內(nèi)分配一塊內(nèi)存用于復制變量n。若變量n的類型復雜,,甚至重載了該類的默認拷貝構(gòu)造函數(shù): CMyClass(const CMyClass &obj); 這個過程可能會比較復雜,。<類的默認拷貝構(gòu)造函數(shù)使用“位拷貝”而非“值拷貝”,若類中包括指針成員,,不重載該函數(shù)幾乎注定程序會出錯,。關(guān)于這個問題以后再深入探討。> 程序進入函數(shù)Func1()體內(nèi)后,,操作的是棧中的臨時變量,,當函數(shù)結(jié)束(或者說返回)時,棧內(nèi)變量被釋放,。而對于函數(shù)Func1()來說的外部變量n并未起任何變化,,因此隨后的acutPrintf函數(shù)將輸出n = 0。 程序調(diào)用函數(shù)Func2()時,,在棧內(nèi)分配內(nèi)存用于存放臨時的指針變量x,。然后用&運算取得變量n的地址,并拷貝給臨時指針變量x作為x的值,。此時,,指針x就成了指向變量n的指針。在函數(shù)體內(nèi),,*x運算得到的是指針x指向的內(nèi)容,,即變量n。對*x操作實際上就是對n操作,。因此,,在函數(shù)Func2()中變量n的值起了變化。在分析Func2()函數(shù)時應該注意到,,臨時指針變量x要指向的內(nèi)存地址,也就是說變量x的“值”仍然是采用了值傳遞方式從函數(shù)外部(或者說函數(shù)調(diào)用者)獲得,,那么“值”也就應該具有值傳遞方式的特點,,它要在棧中復制臨時變量,它在函數(shù)體內(nèi)被修改不會影響到函數(shù)外部,。比如說,,在上面的代碼段中,,函數(shù)Func2()內(nèi)可以讓指針x指向另外的變量,但函數(shù)結(jié)束或返回后,,在函數(shù)外部是無法得到這樣的指向另外變量的指針,。 程序調(diào)用函數(shù)Func3()時,臨時變量x是一個變量n的引用,,此時變量x就是變量n本身,,對x操作的同時,外部變量n也起了變化,。實際上,,引用能做的事,指針也能做到,。 以上的代碼段確實簡單,,以至還不能充分顯示指針和引用在傳遞函數(shù)參數(shù)時的許多其他功能。下面我們設計這樣一個函數(shù),,函數(shù)需要兩個參數(shù),,在函數(shù)內(nèi)將兩個參數(shù)的值互換。由于值傳遞方式盡管能通過返回值賦值的方法修改一個參數(shù)值,,但不能同時修改兩個參數(shù)值,,因此這個函數(shù)不能使用值傳遞方式。使用指針傳遞方式,,函數(shù)可以寫成這樣: bool swap(int *x, int *y) 以下代碼調(diào)用該函數(shù): /*原文如此 int *a = 10; */ int a1 = 10 ; int b1 =15 ; int *a = &a1 ; int *b = &b1 ; 在以上代碼中,swap()函數(shù)設計成與常見的ARX函數(shù)一致的風格,,用一個bool類型返回函數(shù)執(zhí)行狀態(tài),。<在ARX中,這個返回值通常使用Acad::ErrorStatus類,。>在調(diào)用函數(shù)時,,由于變量a和b已經(jīng)聲明為指針,使用標識符a和b訪問的是int類型變量的內(nèi)存地址,。 使用引用傳遞參數(shù),,可以這樣設計swap()函數(shù): bool swap(int &x, int &y) 使用代碼swap(int a, int b)調(diào)用以上函數(shù)時,進入函數(shù)體內(nèi),,x,、y分別是變量a、b的引用,,對x,、y操作就是操作變量a、b,。函數(shù)返回后,,變量a,、b的值互相交換了。 注意:以上代碼只是交換兩個變量(或者指針指向的變量)的值,。即將變量a,、b(或指針a、b指向的變量)的修改為b,、a(或指針b,、a指向的變量)的值,而不是將指針a指向原來指針b指向的變量,。也就是說,,swap()函數(shù)調(diào)用前后,指針a和b的值(地址)并沒有發(fā)生任何變化,。(當然,,引用關(guān)系在任何時候都不能修改。)要修改指針的地址值,,應該使用指向指針的指針或者使用對指針的引用,。這樣設計和調(diào)用函數(shù): bool swap(int **x, int **y); //使用指向指針的指針傳遞參數(shù) 或者: bool swap(int *&x, int *&y); //使用對指針的引用傳遞參數(shù) 在以上的兩個swap()函數(shù)以交換兩個指針的值,,使指針a指向原來指針b指向的變量,,指針b指向原來指針a指向的變量。 最后,,我們看一個ARX程序中使用指針和引用傳遞參數(shù)的函數(shù)例子: AcDbDatabase *pDb = new AcDbDatabase(); 從ARX幫助中可以查看到,,getBlockTable()函數(shù)的原型是: Acad::ErrorStatus getBlockTable( AcDbBlockTable*& pTable, AcDb::OpenMode mode); 其中可以看到,函數(shù)的第一個參數(shù)是對一個AcDbBlockTable類型指針的引用,,從而可以在函數(shù)體內(nèi)部對指針pBlkTbl進行修改,,使之指向pDb指針指向的圖形數(shù)據(jù)庫的塊表 |
|