• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            聚星亭

            吾笨笨且懶散兮 急須改之而奮進
            posts - 74, comments - 166, trackbacks - 0, articles - 0
              C++博客 :: 首頁 :: 新隨筆 :: 聯系 :: 聚合  :: 管理

                   C++的階段,我想根據C++的一些特有的特性分別寫一些專題,每個專題我都捎帶講一些語法,當然不會很多,我還是會像C語言那樣,內存結構貫穿始終,有匯編就有真相……

             

                   本專題,我們講述封裝性。封裝性是C++的入門特性,要想學習C++語言,封裝性是首先要掌握的。下面我們進入正題:

            一、       類與對象

            早在本系列第一節課(理解程序中的數據)的時候講述過數據類型與數據的區別和聯系當時得出的結論如下:

            Ø         數據類型規定了數據的大小和表現形式

            Ø         數據就是電腦中存放的數。

            Ø         每個數據都有自己的地址,而這些地址可以有變量來代替

            Ø         因此,數據通常存放于變量(或常量)中

             

            這個結論在C++中仍然同樣適用,類就是我們自己定義的復雜的數據類型,而對象則就是由類聲明的變量。下面我們進入純語法層面。

            1、 類的定義方法

            我相信,大家都還記得我在第一節課的時候講述的結構體的課程,也相信大家沒有忘記怎么定義一個結構體。下面我給出類的定義方法:

            class CExample   // 是不是很像定義一個結構體

            {

            private: // 權限控制,相關內容在下面的小節中詳細講述

                   int   m_nFirstNum;   // 定義成員變量。也叫屬性

                   int   m_nSecNum;

            public:

                   int   GetSum(){return  m_nFirstNum}  // 成員函數

                   bool  SetNum(int nFirst, int nSec)

            {

            m_nFirstNum  =  nFirst; 

            m_nSecNum  =  nSec ;

            return true;

            }

                   CExample(){m_nFirstNum = 0;  m_nSecNum = 0;}        //構造函數

                   ~CExample(){}                    // 空析構

            };

            當然,上面這個類的定義是不是很像定義一個結構體?只不過多了個privatepublic還有一些函數。是的,C++里面,將結構體升級了,結構體里面可以有函數成員了,為了兼容,換了個關鍵字,當然,上面的這個class完全可以改成struct,一點問題都沒有。

             

            好奇的朋友會問:如果函數體的語句太多,邏輯復雜了,函數很大,那這個類豈不是很難看,太臃腫了吧。

             

            是的,為了方便類的組織,也為了協調項目工程中文件的組織。上面的類還可以寫成如下的形式:

            // .h文件中寫如下的聲明部分

            class CExample   // 是不是很像定義一個結構體

            {

            private: // 權限控制,相關內容在下面的小節中詳細講述

                   int   m_nFirstNum;   // 定義成員變量。也叫屬性

                   int   m_nSecNum;

            public:

                   int   GetSum();  // 成員函數

                   bool  SetNum(int nFirst, int nSec);

                   CExample();   //構造函數

                   ~CExample();               // 空析構

            };

             

            // .cpp 文件中寫如下的定義及實現部分

            CExample::CExample()

            {

            }

            CExample::~CExample()

            {

            }

             

            int CExample::GetFirstNum()

            {

                return m_nFirstNum;

            }

             

            int CExample::GetSecNum()

            {

                return m_nSecNum;

            }

             

            bool CExample::SetNum(int nFirst, int nSec)

            {

            m_nFirstNum  =  nFirst; 

            m_nSecNum  =  nSec ;

            return true;

            }

             

            int CExample::GetSum()

            {

                   return m_nFirstNum+m_nSecNum;

            }

            上面兩種寫法也是有區別的,第一種方法寫的函數具有Inline函數的特性。后一種則沒有。

            2、 屬性和方法的使用

                                 C++中定義一個對象跟定義一個函數沒有什么區別。

            #include <stdio.h>

            #include "Example.h"

             

            int main(int argc, char* argv[])

            {

                CExample obj_Exp;       // 定義一個對象

                obj_Exp.SetNum(10, 20); // 調用一個方法

             

                printf("%d+%d = %d\r\n", obj_Exp.GetFirstNum(),

                                      obj_Exp.GetSecNum(),

                                      obj_Exp.GetSum());

             

                   return 0;

            }

                   由此,我們就可以通過一個函數間接的來操作我們的變量,用戶在給我們的變量賦值時,我們可以通過Set函數來對輸入的內容作檢測,在獲取一個變量的內容時,我們可以通過一個函數來取得,這樣都提高了程序安全性。

             

                   從程序設計的角度來講,如果我們以類為單位編碼的話,每個模塊都是獨立的我們只要關注與本類相關操作,比如人這個類,它一般情況下有兩個眼睛、一個嘴巴等之類的屬性,人可以使用工具,可以行走,可以跳躍等方法。我們編寫的所有的函數都是針對這些展開的。而不用去關心誰要使用這個類。因此,類/對象概念的加入,不單單是給編碼方式做了改變,主要是設計思路的改變,程序模塊化的改變等等。

            二、       解析對象的內存結構

                   現在,我相信,如果習慣了我這種學習方式的朋友一定會很好奇,類定義對象的內存格式是怎樣的,它是不是像一個普通變量那樣,或者是不是像一個結構體變量那樣在內存里連續的將各個成員組織到一起,它又是怎樣將各個成員變量還有函數綁定到一起的?變量和函數在一起它是怎么組織的?本小節讓我們來解決這些問題。

             

                   為節省篇幅,我們仍舊使用上面的代碼。我們用VC的調試器,調試這個代碼:

                  

                   注意看我們的變量監視區,我們定義的對象的內容跟結構體成員的內容格式差不多,(是按照定義的順序連續存放的,這點跟普通的局部變量不一樣,普通的局部變量在內存中的順序與定義順序相反)內存中只存放了成員變量,它并沒有標出SetNum的位置,那它是怎么找到SetNum這個函數的呢?

             

                   根據我們先前調試C函數的經驗,我們知道,函數的代碼是被放在可執行文件的代碼區段中的。在這個代碼中,也有調用SetNum的代碼,我們詳細的跟一下它的匯編代碼:

            10:       CExample obj_Exp;

            004011ED   lea         ecx,[ebp-14h]

            004011F0   call        @ILT+15(CExample::CExample) (00401014)

            004011F5   mov         dword ptr [ebp-4],0

            11:       obj_Exp.SetNum(10, 20);

            004011FC   push        14h

            004011FE   push        0Ah

            00401200   lea         ecx,[ebp-14h]

            00401203   call        @ILT+0(CExample::SetNum) (00401005)

             

                   這段代碼又給我們帶來了新的問題,我們只用類定義了一個對象(變量),它自動的調用了一個函數,根據注釋我們知道它調用的是構造函數。我們跟進去看下:

            11:   CExample::CExample()

            12:   {

            00401050   push        ebp

            00401051   mov         ebp,esp

            00401053   sub         esp,44h

            00401056   push        ebx

            00401057   push        esi

            00401058   push        edi

            00401059   push        ecx                                ; 保存寄存器環境

            0040105A   lea         edi,[ebp-44h]

            0040105D   mov         ecx,11h

            00401062   mov         eax,0CCCCCCCCh        

            00401067   rep stos    dword ptr [edi]    ; 將棧空間清為CCRelease編譯就沒有這部分代碼了。)

            00401069   pop         ecx                        

            0040106A   mov         dword ptr [ebp-4],ecx    ; ECX中的內容給局部變量

            13:   }

            0040106D   mov         eax,dword ptr [ebp-4]    ; ECX的內容返回

            00401070   pop         edi

            00401071   pop         esi

            00401072   pop         ebx

            00401073   mov         esp,ebp

            00401075   pop         ebp

            00401076   ret

                   這段代碼,首次看還真看不出個所以然來,源碼的構造函數中,我們什么都沒寫,是個空函數,而這里做的是返回ECX的值,可是這個函數也沒有對ECX做什么特別的操作,而是直接使用進函數時ECX的值。那只能說明在調用這個函數前,ECX發生了變化。我們再回頭看下調用構造函數的代碼:

            10:       CExample obj_Exp;

            004011ED   lea         ecx,[ebp-14h]

            004011F0   call        @ILT+15(CExample::CExample) (00401014)

            004011F5   mov         dword ptr [ebp-4],0

                   哈哈,它是把我們obj_Exp對象的地址給了ECX,然后調用構造返回的,也就是說,構造的返回值是我們對象的首地址。哎,迷糊了,真搞不懂這是在干什么。先不管他,我們繼續看怎么調用的SetNum這個函數吧:

            11:       obj_Exp.SetNum(10, 20);

            004011FC   push        14h                        ; 傳遞參數

            004011FE   push        0Ah

            00401200   lea         ecx,[ebp-14h]           ; 也有這句,還是把我們的對象首地址給ECX

            00401203   call        @ILT+0(CExample::SetNum) (00401005)

            29:   bool CExample::SetNum(int nFirst, int nSec)

            30:   {

            00401130   push        ebp

            00401131   mov         ebp,esp

            00401133   sub         esp,44h

            00401136   push        ebx

            00401137   push        esi

            00401138   push        edi

            00401139   push        ecx

            0040113A   lea         edi,[ebp-44h]

            0040113D   mov         ecx,11h

            00401142   mov         eax,0CCCCCCCCh

            00401147   rep stos    dword ptr [edi]

            00401149   pop         ecx

            0040114A   mov         dword ptr [ebp-4],ecx    ; 備份一下我們的對象首地址

            31:       m_nFirstNum  =  nFirst;

            0040114D   mov         eax,dword ptr [ebp-4]    ; 取出對象首地址

            00401150   mov         ecx,dword ptr [ebp+8]    ; 取出nFirst參數

            00401153   mov         dword ptr [eax],ecx        ; 給對象首地址指向的內容賦值為nFirst的內容

            32:       m_nSecNum  =  nSec ;

            00401155   mov         eax,dword ptr [ebp-4]    ; 取出對象首地址

            00401158   mov         ecx,dword ptr [ebp+0Ch]; 取出nSec參數

            0040115B   mov         dword ptr [eax+4],ecx    ; 給對象首地址+4指向的你內容賦值

                      return true;

            0040115E   mov         al,1          ; 返回1

            34:   }

            00401160   pop         edi

            00401161   pop         esi

            00401162   pop         ebx

            00401163   mov         esp,ebp

            00401165   pop         ebp

            00401166   ret         8

                   我簡要的注釋下來一下上面的代碼。通過分析上面的代碼,我們可以得出這樣的結論:

            A、 函數通過ecx傳遞了我們對象的首地址。

            B、 函數通過ecx傳遞的對象首地址定位對象的每個成員變量。

                   這樣,很明顯,ECX起到了傳遞參數的作用,這時ecx中的地址有個專業術語,叫做this指針。

             

                   OK,這就是一個新的知識點,我們成員函數的調用方式。

            1、 成員函數的調用方式: __thiscall

                   記得在前面章節介紹函數時,講過一些調用方式,但是沒有提到過這種調用方式。下面我做一個簡要的總結:

            A、 參數也通過棧傳遞。

            B、 它用一個寄存器來傳遞this指針。

            C、 本條特性摘自《加密與解密》(第三版)非原文:

            a)         對于VC++中傳參規則:

                                                                               i.              最左邊兩個不大于4字節的參數分別用ECXEDX傳參數.

                                                                             ii.              對于浮點數、遠指針、__int64類型總是通過棧來傳遞的。

            b)        對于BC++|DELPHI中的傳遞規則:

                                                                               i.              最左邊三個不大于DWORD的參數,依次使用EAX,ECX,EDX傳遞,其它多的參數依次通過PASCAL方式傳遞。

             

                   這樣,函數的地址還是在代碼區域,對象的內存中只存放數據成員,當我們要調用成員函數時,就通過一個寄存器將函數操作的對象的首地址(也就是this指針)傳遞過去就可以了,傳遞不同的對象指針,就操作不同的數據。哈哈,太巧妙了。

            2、 淺談構造與析構函數

                                 OK,繼續調試代碼:

            13:       printf("%d+%d = %d\r\n", obj_Exp.GetFirstNum(),

            14:                                obj_Exp.GetSecNum(),

            15:                                obj_Exp.GetSum());

            00401208   lea         ecx,[ebp-14h]

            0040120B   call        @ILT+30(CExample::GetSum) (00401023)             ; 調用GetSum函數

            00401210   push        eax

            00401211   lea         ecx,[ebp-14h]

            00401214   call        @ILT+5(CExample::GetSecNum) (0040100a)

            00401219   push        eax

            0040121A   lea         ecx,[ebp-14h]

            0040121D   call        @ILT+10(CExample::GetFirstNum) (0040100f)

            00401222   push        eax

            00401223   push        offset string "%d+%d = %d\r\n" (0042501c)

            00401228   call        printf (00401290)

            0040122D   add         esp,10h

            16:

            17:       return 0;

            00401230   mov         dword ptr [ebp-18h],0

            00401237   mov         dword ptr [ebp-4],0FFFFFFFFh

            0040123E   lea         ecx,[ebp-14h]

            00401241   call        @ILT+20(CExample::~CExample) (00401019) ; 調用析構函數

            00401246   mov         eax,dword ptr [ebp-18h]

                   我們至始至終都沒有調用過構造和析構函數。但是,通過這次調我們知道,在創建一個對象(變量)的時候,我們的程序會自動的調用我們的構造函數,在要出對象作用域的時候,會自動的調用析構函數。

             

                   這樣,我們很容易就能想象出,構造和析構的用途:構造就做初始化對象的各個成員,申請空間等初始化工作。析構就做一些釋放申請的空間啊之類的清理工作。

             

                   就這樣,C++將數據跟函數封裝到了一起,這樣我們每個類產生的對象都是一個獨立的個體,它有一個自己的運作方式,幾乎完全獨立。在我們使用它的時候,根本不需要它是怎么實現了,只要知道怎么使用即可。

            三、       淺談類的靜態成員

                   通過前面幾節的學習,我們大概的能理解類的封裝性及其運作過程,但是,如果我們繼續深入的學習C++,我們很快就能發現一個問題:我們上面說的所有的成員都是屬于對象的,也就是說,我們必須先通過類來定義一個對象才可以操作。但是有的時候,我們需要一些屬于類的成員,比如:人都有一個腦袋,這一個腦袋屬于人類共有的特性。不需要具體到哪一個人,我們都可以確定人只有一個腦袋。

             

                   放到類中也一樣,比如我們需要知道當前這個類創建了幾個對象的時候,我們不必在創建一個新的對象只需要使用類的相關函數或者直接訪問類的某些屬性就可以了,而這些函數或者變量,它肯定不可能屬于某個對象,它應該屬于這個類本身。

             

                   OK,下面就來體驗一下靜態帶給我們的一些好處。同樣,我們將前面的代碼添加點兒東西(見Exp02:

                   // .h文件中

            public:

                static int m_nCount; // 統計產生對象的

                static int print(const char *szFormat, ...);    // 讓我們的類有自己的輸出函數

                   // .cpp文件中

            int CExample::m_nCount = 0; // 初始化靜態成員變量

            CExample::CExample()

            {

                m_nCount++;        // 當創建一個對象的時候,這個變量加1

            }

             

            CExample::~CExample()

            {

                if (m_nCount > 0)

                {

                    m_nCount--;   // 當對象銷毀時,這個變量減1

                }

            }

            /************************************************************************/

            /*讓我們的CExample可以打印自己的信息

            /*支持多參,同printf用法相同

            /************************************************************************/

            int CExample::print(const char *szFormat, ...)

            {

                if (!szFormat)

                {

                    return 0;

                }

             

                va_list   pArgs;

                char      szBuffer[256 * 15] = {0};

                va_start(pArgs, szFormat);  

                vsprintf(szBuffer, szFormat, pArgs);

                va_end(pArgs);

             

                printf(szBuffer);

                return strlen(szFormat);

            }

             

                          好,有了這些,我們可以編寫如下的測試代碼:

            #include "stdafx.h"

            #include <stdio.h>

            #include "Example.h"

             

            int main(int argc, char* argv[])

            {

                CExample obj_Exp1;

                CExample::print("當前對象的數量為:%d\r\n", CExample::m_nCount);

             

                if (1)

                {

                    CExample obj_Exp2; // 該對象屬于if作用域,出了if,對象自動銷毀

                    CExample::print("當前對象的數量為:%d\r\n", CExample::m_nCount);

                }

             

                CExample::print("當前對象的數量為:%d\r\n", CExample::m_nCount);

             

                   return 0;

            }

                          我想大家應該能想象出來運行的結果:

                         

                          好,我們調試一下這段程序:

            11:       CExample::print("當前對象的數量為:%d\r\n", CExample::m_nCount);

            004012EC   mov         eax,[CExample::m_nCount (0042ae6c)]       ;這明顯告訴我們,靜態就是全局

            004012F1   push        eax

            004012F2   push        offset string "當前對象的數量為:%d\r\n "

            004012F7   call        @ILT+30(CExample::print) (00401023)    ; 調用該靜態函數沒有傳遞this指針

            004012FC   add         esp,8

                多了不用看了,通過這段代碼,我們很明顯就可以清楚,靜態變量,不屬于類對象,它存放于全局數據區,同全局變量在一個地方(更多關于靜態變量的相關說明見我發的《static學習筆記》一文)。

             

                靜態函數,跟全局函數一樣,它雖然在源碼中書寫與類內,但是它其實就是一個全局函數,不傳遞this指針,因此,在使用靜態函數時需要知道,靜態函數中不能調用其它普通的成員函數也不能引用普通的成員變量。但是反過來,在其它的成員函數中可以調用靜態函數也可以使用靜態變量。

            四、       打破類封裝性的棒槌 —— 友元

                   當我們的工程總類越來越多的時候,我們很難避免類與類之間相互操作。倘若每個類之間相互操作都通過函數進行,那可想我們編寫代碼是多么的繁瑣,想投機取巧走小路么?是的,C++提供了這種捷徑,讓我們直接對一個類的私有成員進行操作——當然,我極力反對這種做法,因為它打破了類的封裝性。

             

                   我不推薦使用友元在這里仍然講述友元并不是我無聊也不是我寫它裝門面。因為我們再后面學習用全局函數運算符的時候,不可避免的要使用到它。

             

                   聲明友元的關鍵字是friendOK,看代碼,我們還是在上面的代碼中加點東西(Exp03):

            // Example.h

                   friend extern void ShowPrivMem(CExample tmpExpObj);

                   聲明ShowPrivMem函數是CExample類的友元函數,這時ShowPrivMem中就可以隨便訪問CExample 類中任何一個成員了。

             

                          編寫測試代碼如下:

            void ShowPrivMem(CExample tmpExpObj)

            {

                printf("%d+%d = %d\r\n", tmpExpObj.m_nFirstNum,

                                         tmpExpObj.m_nSecNum,

                                         tmpExpObj.GetSum());

            }

             

            int main(int argc, char* argv[])

            {

                CExample obj_Exp;

                obj_Exp.SetNum(10, 20);

             

                ShowPrivMem(obj_Exp);

             

                   return 0;

            }

                   當然,這是將一個函數聲明為友元,將另外一個類聲明為自己的友元也是一樣的。由于沒有什么特別之處,我就不為此特別的寫一個代碼了。

             

                   另外,需要注意的是,將一個類聲明為自己的友元,并不是自己可以訪問那個類的私有成員而是被聲明為友元的類可以訪問自己的私有成員。注意順序別弄反了……。

            五、       學習小結

                   本節講的東西很少,但是很關鍵,調試C++程序,摸清楚this指針對摸清程序結構,摸清程序數據格式都起著非常重要的作用。

             

                   學習本小節的方法,不是理解,而是多寫,多調試。

             

                   祝大家成功。

            Feedback

            # re: 笨鳥先飛學編程系列-淺析C++的封裝性[未登錄]  回復  更多評論   

            2010-03-12 08:59 by dophi
            贊一個,懂點匯編確實對理解c++的對象模型很有幫助啊
            亚洲女久久久噜噜噜熟女| 国内精品久久久久久久coent| 狠狠色丁香婷婷久久综合五月| 亚洲精品无码久久久久| 丁香五月综合久久激情| 久久综合偷偷噜噜噜色| 国产精品久久久久无码av| 精品久久国产一区二区三区香蕉| 国内精品伊人久久久久妇| 久久婷婷五月综合97色一本一本| 狠狠色综合久久久久尤物| 亚洲国产精品一区二区久久hs| 丁香久久婷婷国产午夜视频| 久久婷婷激情综合色综合俺也去| 国内精品伊人久久久久影院对白| 久久久老熟女一区二区三区| 欧美精品丝袜久久久中文字幕 | 91精品国产91久久久久福利| 人人狠狠综合久久亚洲高清| 亚洲天堂久久精品| 精品无码久久久久久尤物| 久久婷婷色综合一区二区 | 久久人妻少妇嫩草AV蜜桃| 女人香蕉久久**毛片精品| 久久香蕉超碰97国产精品| 欧美精品九九99久久在观看| 久久精品成人免费观看97| 9999国产精品欧美久久久久久| 久久精品国产久精国产思思| 久久亚洲熟女cc98cm| 久久久久亚洲国产| 婷婷久久综合九色综合绿巨人| 999久久久国产精品| 午夜不卡888久久| 久久成人影院精品777| 97久久精品午夜一区二区| 久久亚洲AV成人无码国产| 久久综合给久久狠狠97色| 久久青青草原亚洲av无码app| 亚洲精品国产美女久久久| 亚洲国产欧洲综合997久久|