C++的階段,我想根據(jù)C++的一些特有的特性分別寫一些專題,每個(gè)專題我都捎帶講一些語(yǔ)法,當(dāng)然不會(huì)很多,我還是會(huì)像C語(yǔ)言那樣,內(nèi)存結(jié)構(gòu)貫穿始終,有匯編就有真相……
本專題,我們講述封裝性。封裝性是C++的入門特性,要想學(xué)習(xí)C++語(yǔ)言,封裝性是首先要掌握的。下面我們進(jìn)入正題:
一、 類與對(duì)象
早在本系列第一節(jié)課(理解程序中的數(shù)據(jù))的時(shí)候講述過數(shù)據(jù)類型與數(shù)據(jù)的區(qū)別和聯(lián)系當(dāng)時(shí)得出的結(jié)論如下:
Ø 數(shù)據(jù)類型規(guī)定了數(shù)據(jù)的大小和表現(xiàn)形式
Ø 數(shù)據(jù)就是電腦中存放的數(shù)。
Ø 每個(gè)數(shù)據(jù)都有自己的地址,而這些地址可以有變量來(lái)代替
Ø 因此,數(shù)據(jù)通常存放于變量(或常量)中
這個(gè)結(jié)論在C++中仍然同樣適用,類就是我們自己定義的復(fù)雜的數(shù)據(jù)類型,而對(duì)象則就是由類聲明的變量。下面我們進(jìn)入純語(yǔ)法層面。
1、 類的定義方法
我相信,大家都還記得我在第一節(jié)課的時(shí)候講述的結(jié)構(gòu)體的課程,也相信大家沒有忘記怎么定義一個(gè)結(jié)構(gòu)體。下面我給出類的定義方法:
class CExample // 是不是很像定義一個(gè)結(jié)構(gòu)體
{
private: // 權(quán)限控制,相關(guān)內(nèi)容在下面的小節(jié)中詳細(xì)講述
int m_nFirstNum; // 定義成員變量。也叫屬性
int m_nSecNum;
public:
int GetSum(){return m_nFirstNum} // 成員函數(shù)
bool SetNum(int nFirst, int nSec)
{
m_nFirstNum = nFirst;
m_nSecNum = nSec ;
return true;
}
CExample(){m_nFirstNum = 0; m_nSecNum = 0;} //構(gòu)造函數(shù)
~CExample(){} // 空析構(gòu)
};
當(dāng)然,上面這個(gè)類的定義是不是很像定義一個(gè)結(jié)構(gòu)體?只不過多了個(gè)private和public還有一些函數(shù)。是的,C++里面,將結(jié)構(gòu)體升級(jí)了,結(jié)構(gòu)體里面可以有函數(shù)成員了,為了兼容,換了個(gè)關(guān)鍵字,當(dāng)然,上面的這個(gè)class完全可以改成struct,一點(diǎn)問題都沒有。
好奇的朋友會(huì)問:如果函數(shù)體的語(yǔ)句太多,邏輯復(fù)雜了,函數(shù)很大,那這個(gè)類豈不是很難看,太臃腫了吧。
是的,為了方便類的組織,也為了協(xié)調(diào)項(xiàng)目工程中文件的組織。上面的類還可以寫成如下的形式:
// .h文件中寫如下的聲明部分
class CExample // 是不是很像定義一個(gè)結(jié)構(gòu)體
{
private: // 權(quán)限控制,相關(guān)內(nèi)容在下面的小節(jié)中詳細(xì)講述
int m_nFirstNum; // 定義成員變量。也叫屬性
int m_nSecNum;
public:
int GetSum(); // 成員函數(shù)
bool SetNum(int nFirst, int nSec);
CExample(); //構(gòu)造函數(shù)
~CExample(); // 空析構(gòu)
};
// .cpp 文件中寫如下的定義及實(shí)現(xiàn)部分
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;
}
上面兩種寫法也是有區(qū)別的,第一種方法寫的函數(shù)具有Inline函數(shù)的特性。后一種則沒有。
2、 屬性和方法的使用
C++中定義一個(gè)對(duì)象跟定義一個(gè)函數(shù)沒有什么區(qū)別。
#include <stdio.h>
#include "Example.h"
int main(int argc, char* argv[])
{
CExample obj_Exp; // 定義一個(gè)對(duì)象
obj_Exp.SetNum(10, 20); // 調(diào)用一個(gè)方法
printf("%d+%d = %d\r\n", obj_Exp.GetFirstNum(),
obj_Exp.GetSecNum(),
obj_Exp.GetSum());
return 0;
}
由此,我們就可以通過一個(gè)函數(shù)間接的來(lái)操作我們的變量,用戶在給我們的變量賦值時(shí),我們可以通過Set函數(shù)來(lái)對(duì)輸入的內(nèi)容作檢測(cè),在獲取一個(gè)變量的內(nèi)容時(shí),我們可以通過一個(gè)函數(shù)來(lái)取得,這樣都提高了程序安全性。
從程序設(shè)計(jì)的角度來(lái)講,如果我們以類為單位編碼的話,每個(gè)模塊都是獨(dú)立的我們只要關(guān)注與本類相關(guān)操作,比如人這個(gè)類,它一般情況下有兩個(gè)眼睛、一個(gè)嘴巴等之類的屬性,人可以使用工具,可以行走,可以跳躍等方法。我們編寫的所有的函數(shù)都是針對(duì)這些展開的。而不用去關(guān)心誰(shuí)要使用這個(gè)類。因此,類/對(duì)象概念的加入,不單單是給編碼方式做了改變,主要是設(shè)計(jì)思路的改變,程序模塊化的改變等等。
二、 解析對(duì)象的內(nèi)存結(jié)構(gòu)
現(xiàn)在,我相信,如果習(xí)慣了我這種學(xué)習(xí)方式的朋友一定會(huì)很好奇,類定義對(duì)象的內(nèi)存格式是怎樣的,它是不是像一個(gè)普通變量那樣,或者是不是像一個(gè)結(jié)構(gòu)體變量那樣在內(nèi)存里連續(xù)的將各個(gè)成員組織到一起,它又是怎樣將各個(gè)成員變量還有函數(shù)綁定到一起的?變量和函數(shù)在一起它是怎么組織的?本小節(jié)讓我們來(lái)解決這些問題。
為節(jié)省篇幅,我們?nèi)耘f使用上面的代碼。我們用VC的調(diào)試器,調(diào)試這個(gè)代碼:

注意看我們的變量監(jiān)視區(qū),我們定義的對(duì)象的內(nèi)容跟結(jié)構(gòu)體成員的內(nèi)容格式差不多,(是按照定義的順序連續(xù)存放的,這點(diǎn)跟普通的局部變量不一樣,普通的局部變量在內(nèi)存中的順序與定義順序相反)內(nèi)存中只存放了成員變量,它并沒有標(biāo)出SetNum的位置,那它是怎么找到SetNum這個(gè)函數(shù)的呢?
根據(jù)我們先前調(diào)試C函數(shù)的經(jīng)驗(yàn),我們知道,函數(shù)的代碼是被放在可執(zhí)行文件的代碼區(qū)段中的。在這個(gè)代碼中,也有調(diào)用SetNum的代碼,我們?cè)敿?xì)的跟一下它的匯編代碼:
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)
這段代碼又給我們帶來(lái)了新的問題,我們只用類定義了一個(gè)對(duì)象(變量),它自動(dòng)的調(diào)用了一個(gè)函數(shù),根據(jù)注釋我們知道它調(diào)用的是構(gòu)造函數(shù)。我們跟進(jìn)去看下:
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 ; 保存寄存器環(huán)境
0040105A lea edi,[ebp-44h]
0040105D mov ecx,11h
00401062 mov eax,0CCCCCCCCh
00401067 rep stos dword ptr [edi] ; 將棧空間清為CC(Release編譯就沒有這部分代碼了。)
00401069 pop ecx
0040106A mov dword ptr [ebp-4],ecx ; 將 ECX中的內(nèi)容給局部變量
13: }
0040106D mov eax,dword ptr [ebp-4] ; 將ECX的內(nèi)容返回
00401070 pop edi
00401071 pop esi
00401072 pop ebx
00401073 mov esp,ebp
00401075 pop ebp
00401076 ret
這段代碼,首次看還真看不出個(gè)所以然來(lái),源碼的構(gòu)造函數(shù)中,我們什么都沒寫,是個(gè)空函數(shù),而這里做的是返回ECX的值,可是這個(gè)函數(shù)也沒有對(duì)ECX做什么特別的操作,而是直接使用進(jìn)函數(shù)時(shí)ECX的值。那只能說(shuō)明在調(diào)用這個(gè)函數(shù)前,ECX發(fā)生了變化。我們?cè)倩仡^看下調(diào)用構(gòu)造函數(shù)的代碼:
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對(duì)象的地址給了ECX,然后調(diào)用構(gòu)造返回的,也就是說(shuō),構(gòu)造的返回值是我們對(duì)象的首地址。哎,迷糊了,真搞不懂這是在干什么。先不管他,我們繼續(xù)看怎么調(diào)用的SetNum這個(gè)函數(shù)吧:
11: obj_Exp.SetNum(10, 20);
004011FC push 14h ; 傳遞參數(shù)
004011FE push 0Ah
00401200 lea ecx,[ebp-14h] ; 也有這句,還是把我們的對(duì)象首地址給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 ; 備份一下我們的對(duì)象首地址
31: m_nFirstNum = nFirst;
0040114D mov eax,dword ptr [ebp-4] ; 取出對(duì)象首地址
00401150 mov ecx,dword ptr [ebp+8] ; 取出nFirst參數(shù)
00401153 mov dword ptr [eax],ecx ; 給對(duì)象首地址指向的內(nèi)容賦值為nFirst的內(nèi)容
32: m_nSecNum = nSec ;
00401155 mov eax,dword ptr [ebp-4] ; 取出對(duì)象首地址
00401158 mov ecx,dword ptr [ebp+0Ch]; 取出nSec參數(shù)
0040115B mov dword ptr [eax+4],ecx ; 給對(duì)象首地址+4指向的你內(nèi)容賦值
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
我簡(jiǎn)要的注釋下來(lái)一下上面的代碼。通過分析上面的代碼,我們可以得出這樣的結(jié)論:
A、 函數(shù)通過ecx傳遞了我們對(duì)象的首地址。
B、 函數(shù)通過ecx傳遞的對(duì)象首地址定位對(duì)象的每個(gè)成員變量。
這樣,很明顯,ECX起到了傳遞參數(shù)的作用,這時(shí)ecx中的地址有個(gè)專業(yè)術(shù)語(yǔ),叫做this指針。
OK,這就是一個(gè)新的知識(shí)點(diǎn),我們成員函數(shù)的調(diào)用方式。
1、 成員函數(shù)的調(diào)用方式: __thiscall
記得在前面章節(jié)介紹函數(shù)時(shí),講過一些調(diào)用方式,但是沒有提到過這種調(diào)用方式。下面我做一個(gè)簡(jiǎn)要的總結(jié):
A、 參數(shù)也通過棧傳遞。
B、 它用一個(gè)寄存器來(lái)傳遞this指針。
C、 本條特性摘自《加密與解密》(第三版)非原文:
a) 對(duì)于VC++中傳參規(guī)則:
i. 最左邊兩個(gè)不大于4字節(jié)的參數(shù)分別用ECX和EDX傳參數(shù).
ii. 對(duì)于浮點(diǎn)數(shù)、遠(yuǎn)指針、__int64類型總是通過棧來(lái)傳遞的。
b) 對(duì)于BC++|DELPHI中的傳遞規(guī)則:
i. 最左邊三個(gè)不大于DWORD的參數(shù),依次使用EAX,ECX,EDX傳遞,其它多的參數(shù)依次通過PASCAL方式傳遞。
這樣,函數(shù)的地址還是在代碼區(qū)域,對(duì)象的內(nèi)存中只存放數(shù)據(jù)成員,當(dāng)我們要調(diào)用成員函數(shù)時(shí),就通過一個(gè)寄存器將函數(shù)操作的對(duì)象的首地址(也就是this指針)傳遞過去就可以了,傳遞不同的對(duì)象指針,就操作不同的數(shù)據(jù)。哈哈,太巧妙了。
2、 淺談構(gòu)造與析構(gòu)函數(shù)
OK,繼續(xù)調(diào)試代碼:
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) ; 調(diào)用GetSum函數(shù)
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) ; 調(diào)用析構(gòu)函數(shù)
00401246 mov eax,dword ptr [ebp-18h]
我們至始至終都沒有調(diào)用過構(gòu)造和析構(gòu)函數(shù)。但是,通過這次調(diào)我們知道,在創(chuàng)建一個(gè)對(duì)象(變量)的時(shí)候,我們的程序會(huì)自動(dòng)的調(diào)用我們的構(gòu)造函數(shù),在要出對(duì)象作用域的時(shí)候,會(huì)自動(dòng)的調(diào)用析構(gòu)函數(shù)。
這樣,我們很容易就能想象出,構(gòu)造和析構(gòu)的用途:構(gòu)造就做初始化對(duì)象的各個(gè)成員,申請(qǐng)空間等初始化工作。析構(gòu)就做一些釋放申請(qǐng)的空間啊之類的清理工作。
就這樣,C++將數(shù)據(jù)跟函數(shù)封裝到了一起,這樣我們每個(gè)類產(chǎn)生的對(duì)象都是一個(gè)獨(dú)立的個(gè)體,它有一個(gè)自己的運(yùn)作方式,幾乎完全獨(dú)立。在我們使用它的時(shí)候,根本不需要它是怎么實(shí)現(xiàn)了,只要知道怎么使用即可。
三、 淺談?lì)惖撵o態(tài)成員
通過前面幾節(jié)的學(xué)習(xí),我們大概的能理解類的封裝性及其運(yùn)作過程,但是,如果我們繼續(xù)深入的學(xué)習(xí)C++,我們很快就能發(fā)現(xiàn)一個(gè)問題:我們上面說(shuō)的所有的成員都是屬于對(duì)象的,也就是說(shuō),我們必須先通過類來(lái)定義一個(gè)對(duì)象才可以操作。但是有的時(shí)候,我們需要一些屬于類的成員,比如:人都有一個(gè)腦袋,這一個(gè)腦袋屬于人類共有的特性。不需要具體到哪一個(gè)人,我們都可以確定人只有一個(gè)腦袋。
放到類中也一樣,比如我們需要知道當(dāng)前這個(gè)類創(chuàng)建了幾個(gè)對(duì)象的時(shí)候,我們不必在創(chuàng)建一個(gè)新的對(duì)象只需要使用類的相關(guān)函數(shù)或者直接訪問類的某些屬性就可以了,而這些函數(shù)或者變量,它肯定不可能屬于某個(gè)對(duì)象,它應(yīng)該屬于這個(gè)類本身。
OK,下面就來(lái)體驗(yàn)一下靜態(tài)帶給我們的一些好處。同樣,我們將前面的代碼添加點(diǎn)兒東西(見Exp02):
// .h文件中
public:
static int m_nCount; // 統(tǒng)計(jì)產(chǎn)生對(duì)象的
static int print(const char *szFormat, ...); // 讓我們的類有自己的輸出函數(shù)
// .cpp文件中
int CExample::m_nCount = 0; // 初始化靜態(tài)成員變量
CExample::CExample()
{
m_nCount++; // 當(dāng)創(chuàng)建一個(gè)對(duì)象的時(shí)候,這個(gè)變量加1
}
CExample::~CExample()
{
if (m_nCount > 0)
{
m_nCount--; // 當(dāng)對(duì)象銷毀時(shí),這個(gè)變量減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);
}
好,有了這些,我們可以編寫如下的測(cè)試代碼:
#include "stdafx.h"
#include <stdio.h>
#include "Example.h"
int main(int argc, char* argv[])
{
CExample obj_Exp1;
CExample::print("當(dāng)前對(duì)象的數(shù)量為:%d\r\n", CExample::m_nCount);
if (1)
{
CExample obj_Exp2; // 該對(duì)象屬于if作用域,出了if,對(duì)象自動(dòng)銷毀
CExample::print("當(dāng)前對(duì)象的數(shù)量為:%d\r\n", CExample::m_nCount);
}
CExample::print("當(dāng)前對(duì)象的數(shù)量為:%d\r\n", CExample::m_nCount);
return 0;
}
我想大家應(yīng)該能想象出來(lái)運(yùn)行的結(jié)果:

好,我們調(diào)試一下這段程序:
11: CExample::print("當(dāng)前對(duì)象的數(shù)量為:%d\r\n", CExample::m_nCount);
004012EC mov eax,[CExample::m_nCount (0042ae6c)] ;這明顯告訴我們,靜態(tài)就是全局
004012F1 push eax
004012F2 push offset string "當(dāng)前對(duì)象的數(shù)量為:%d\r\n "
004012F7 call @ILT+30(CExample::print) (00401023) ; 調(diào)用該靜態(tài)函數(shù)沒有傳遞this指針
004012FC add esp,8
多了不用看了,通過這段代碼,我們很明顯就可以清楚,靜態(tài)變量,不屬于類對(duì)象,它存放于全局?jǐn)?shù)據(jù)區(qū),同全局變量在一個(gè)地方(更多關(guān)于靜態(tài)變量的相關(guān)說(shuō)明見我發(fā)的《static學(xué)習(xí)筆記》一文)。
靜態(tài)函數(shù),跟全局函數(shù)一樣,它雖然在源碼中書寫與類內(nèi),但是它其實(shí)就是一個(gè)全局函數(shù),不傳遞this指針,因此,在使用靜態(tài)函數(shù)時(shí)需要知道,靜態(tài)函數(shù)中不能調(diào)用其它普通的成員函數(shù)也不能引用普通的成員變量。但是反過來(lái),在其它的成員函數(shù)中可以調(diào)用靜態(tài)函數(shù)也可以使用靜態(tài)變量。
四、 打破類封裝性的棒槌 —— 友元
當(dāng)我們的工程總類越來(lái)越多的時(shí)候,我們很難避免類與類之間相互操作。倘若每個(gè)類之間相互操作都通過函數(shù)進(jìn)行,那可想我們編寫代碼是多么的繁瑣,想投機(jī)取巧走小路么?是的,C++提供了這種捷徑,讓我們直接對(duì)一個(gè)類的私有成員進(jìn)行操作——當(dāng)然,我極力反對(duì)這種做法,因?yàn)樗蚱屏祟惖姆庋b性。
我不推薦使用友元在這里仍然講述友元并不是我無(wú)聊也不是我寫它裝門面。因?yàn)槲覀冊(cè)俸竺鎸W(xué)習(xí)用全局函數(shù)運(yùn)算符的時(shí)候,不可避免的要使用到它。
聲明友元的關(guān)鍵字是friend,OK,看代碼,我們還是在上面的代碼中加點(diǎn)東西(見Exp03):
// Example.h
friend extern void ShowPrivMem(CExample tmpExpObj);
聲明ShowPrivMem函數(shù)是CExample類的友元函數(shù),這時(shí)ShowPrivMem中就可以隨便訪問CExample 類中任何一個(gè)成員了。
編寫測(cè)試代碼如下:
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;
}
當(dāng)然,這是將一個(gè)函數(shù)聲明為友元,將另外一個(gè)類聲明為自己的友元也是一樣的。由于沒有什么特別之處,我就不為此特別的寫一個(gè)代碼了。
另外,需要注意的是,將一個(gè)類聲明為自己的友元,并不是自己可以訪問那個(gè)類的私有成員而是被聲明為友元的類可以訪問自己的私有成員。注意順序別弄反了……。
五、 學(xué)習(xí)小結(jié)
本節(jié)講的東西很少,但是很關(guān)鍵,調(diào)試C++程序,摸清楚this指針對(duì)摸清程序結(jié)構(gòu),摸清程序數(shù)據(jù)格式都起著非常重要的作用。
學(xué)習(xí)本小節(jié)的方法,不是理解,而是多寫,多調(diào)試。
祝大家成功。