1. 具體和抽象
具體:客觀存在著的或在認(rèn)識中反映出來的事物的整體,是具有多方面屬性、特點(diǎn)为流、關(guān)系的統(tǒng)一;
抽象:從具體事物中被抽取出來的相對獨(dú)立的各個方面、屬性让簿、關(guān)系等敬察。
以 Person
為例:“pmst”,“numbbbbb”尔当,“MM”等都是客觀存在的莲祸,稱之為具體;然后我們抽取共同的特性:姓名,性別居凶,年齡和介紹自己等(當(dāng)然這是極小虫给、極小的一部分)。
這個資料可以進(jìn)行學(xué)習(xí)參考:
? ? ? ?C語言實現(xiàn)面向?qū)ο缶幊蹋篬http://www.makeru.com.cn/live/1392_1051.html?s=45051
2. C 語言抽象的雛形
先用 C 語言抽象侠碧,實現(xiàn)如下:
typedef struct Person Person;
typedef void (*Method)(Person *my_self);
typedef struct Person {
char name[12];
int age;
int sex;
Method behavior1; // 行為1
} Person;
void selfIntroducation(Person *my_self) {
printf("my name is %s,age %d,sex
%d\n",my_self->name,my_self->age,my_self->sex);
}
int main(int argc, const char * argv[]) {
// 1
Person *pmst = (Person *)malloc(sizeof(Person));
// 1.1
strcpy(pmst->name, "pmst");
pmst->age = 18;
pmst->sex = 0;
// 2
pmst->behavior1 = selfIntroducation;
// 3
pmst->behavior1(pmst);
return 0;
}
int抹估,float,struct 等類型在編譯之后轉(zhuǎn)變成對內(nèi)存地址的訪問弄兜,比如 1 中的 pmst 指針在調(diào)用 malloc 方法后返回分配的地址為
0x12345678药蜻,會標(biāo)識占 sizeof(Person) 個字節(jié);pmst->age = 18 其實是對 0x12345678 偏移 12
字節(jié)內(nèi)存的賦值瓷式,占4個字節(jié);
函數(shù)在編譯之后放置在代碼段,入口為函數(shù)指針;
pmst->behavior1(pmst); 先取到 0x12345678 偏移 20 字節(jié)內(nèi)存的值————函數(shù)指針语泽,然后 call
命令調(diào)用
編譯之后代碼都變成了對內(nèi)存地址的訪問贸典,稱之為靜態(tài)綁定;那么該如何實現(xiàn) Runtime
運(yùn)行時的動態(tài)訪問呢?比如在UI界面上(ps:Terminal那種古老的輸入輸出方式也是OK的)輸入一個類的名稱以及調(diào)用方法名稱,緊接著我們要實例化一個該類的對象踱卵,然后調(diào)用方法廊驼。
3. C 語言實現(xiàn)動態(tài)性
3.1 運(yùn)行時如何實現(xiàn)抽象->具體
想要運(yùn)行時隨心所欲地將抽象轉(zhuǎn)變成具體,就需要在內(nèi)存中保存一份對抽象的描述惋砂,這里的描述并非指 typedef struct Person
{...}Person 定義 ———— 這是靜態(tài)的妒挎,而是開辟一塊內(nèi)存加載一份 json 抽象描述:
{
"Name": "Person",
"VariableList":[
{
"VarName":"name",
"Type":"char[]",
"MemorySize":12,
},
{
"VarName":"age",
"Type":"int",
"MemorySize":4,
},
{
"VarName":"sex",
"Type":"int",
"MemorySize":4,
},
],
"MethodList":[
{
"name":"selfIntroducation",
"methodAddress":0x12345678
},
]
}
關(guān)于這串json描述,可以是在編譯階段生成的西饵,運(yùn)行時使用 char *description 加載到堆內(nèi)存上酝掩,需要時通過對應(yīng)的 Key 取到
Value:例如 Key=Name 可以取到類名,Key=VariableList 可以取到變量列表眷柔,Key=MethodList
可以取到方法列表期虾。這里可能需要有個小小的Parser解析器。
3.2 二次抽象驯嘱,生成類對象
倘若每次用到時就要進(jìn)行一次 char *description 信息 parser
解析镶苞,性能這關(guān)都過不去,正確做法是解析成某個數(shù)據(jù)結(jié)構(gòu)鞠评,保存到堆內(nèi)存中:
// 偽代碼如下
typedef struct Variable {
char *name;
char *type;
int memorySize;
}Variable;
typedef struct Method {
char *name;
void (*callAddress)(void *my_self);// 顯然這里多參傳入 當(dāng)然這些暫時不考慮
}Method;
typedef struct Class {
char *className;
/// Variable List 是一個數(shù)組 類型為 Variable
Variable *variableList;
/// Method List 也是一個數(shù)組 類型為 Method
Method *methodList
}
上述是最簡單的抽象定義宾尚,現(xiàn)在我們將解析json信息,然后分配內(nèi)存填充信息(抽象->具體)的過程谢澈,首先 Class
是一個抽象概念,抽象出類名御板、變量列表和方法列表等信息狞尔,但此刻我們開辟了一塊內(nèi)存填充信息———— 客觀存在了哲银,稱之為對象(通常我們稱之為class
object,類對象)
//////////////偽代碼如下/////////////////
//////////////////////////////////////////
// parse person json proccess get result
///////////////////end////////////////////
// 分配一塊內(nèi)存
Class *personClsObject = (Class *)malloc(sizeof(Class));
strcpy(personClsObject->className, "Person");
personClsObject->variableList = (Variable *)malloc(sizeof(Variable) *
3);// 有3個變量
Variable *nameVar = (Variable *)malloc(sizeof(Variable));
strcpy(nameVar->name, "name");
strcpy(nameVar->type, "char[]");
nameVar->memorySize = 12;
//... ageVar & sexVar 生成
personClsObject->variableList[0] = nameVar;
personClsObject->variableList[1] = ageVar;
personClsObject->variableList[2] = sexVar;
// 同理生成一個個Method 然后填充到 personClsObject->methodList;
你、我磨总、他是客觀存在的稱之為對象,進(jìn)一步抽象出了姓名利术、性別和年齡三個方面邦鲫,使用 struct Person
結(jié)構(gòu)體定義,之前說了編譯之后不存在所謂的結(jié)構(gòu)體杈抢,都會轉(zhuǎn)而變成對內(nèi)存地址的訪問;我們換了種思路数尿,又抽象出了一個 Class
結(jié)構(gòu)體,然后分配一塊具體客觀存在的內(nèi)存保存信息惶楼,即 personClsObject 類對象(class
object)右蹦,然后將所有變量信息存儲到variableList诊杆,方法信息存儲到 methodList。
舉一反三何陆,如果繼續(xù)定義typedef struct Animal晨汹,typedef struct Car
等一系列的類,那么必定也會各自在堆內(nèi)存上生成有且僅有一個 AnimalClsObject 贷盲、CarClsObject 類對象!
3.2 使用類對象來生成實例對象
上文說到內(nèi)存中保存了一份對 Person
抽象描述淘这,即PersonClsObject類對象,包含類名稱巩剖,變量列表铝穷,方法列表等信息。此刻進(jìn)一步具體到現(xiàn)實生活中某個具體的人球及,生成 “pmst”
博主我氧骤,"numbbbbb" 幫主梁杰,“mm”靈魂畫師吃引,有種God創(chuàng)世的趕腳筹陵。這一個個都是現(xiàn)實存在的,即實例對象————自然要分配一塊內(nèi)存給各自镊尺,填充
name名字朦佩,sex性別,age年齡庐氮。
[圖片上傳失敗...(image-3d0165-1519970136607)]
///////////// 以下為偽代碼 ////////////////////
// 可以遍歷 personClsObject 中variableList所有變量
// 取到每個變量所占的內(nèi)存大小memorySize语稠,累加得到總的需要分配的內(nèi)存大小
int size = 0;
for variable in personClsObject->variableList {
size = variable->memorySize;// 當(dāng)然這里肯定要考慮內(nèi)存對齊問題
}
Person *pmst = (Person *)malloc(size); // 分配內(nèi)存 得到指針0x10000000
Person *numbbbb = (Person *)malloc(size); // 分配內(nèi)存 得到指針0x10001000
Person *MM = (Person *)malloc(size); // 分配內(nèi)存 得到指針0x10002000
note:
這里只為實例變量分配了內(nèi)存,章節(jié)2中我們還包含一個8字節(jié)的函數(shù)指針弄砍,那么問題來了仙畦,現(xiàn)在我們該如何調(diào)用selfIntroducation函數(shù)呢?
3.3 實例對象和類對象
因為我們在內(nèi)存中保存了一份對 Person
的抽象描述,在運(yùn)行時就知道Person包含哪些允許調(diào)用的函數(shù)名稱音婶,函數(shù)類型以及位于代碼段的函數(shù)入口地址慨畸。
章節(jié) 2 中使用了 pmst->behavior1(pmst) 調(diào)用方式 :先取到函數(shù)指針,然后把實例對象自身指針作為傳參傳入調(diào)用∫率剑現(xiàn)在有了
personClsObject 我們又該如何實現(xiàn)這種調(diào)用呢?
/// 偽代碼如下
/// C語言函數(shù)返回類型為函數(shù)指針寫法如下:
/// ps:當(dāng)然也可以先typedef 然后替換返回類型寸士,
void (*findMethod(char *methodName))(void *myself) {
for method in personClsObject->methodList {
if methodName == method->name {
return method->callAddress;
}
}
return NULL;
}
可以看到我們會通過傳入函數(shù)名稱,遍歷類對象中的方法列表進(jìn)行匹配碴卧,找到函數(shù)指針弱卡,接下去就是和章節(jié)2調(diào)用一樣。
void (*call)(void *) = findMethod("selfIntroducation");
call(pmst);
現(xiàn)在的運(yùn)行時動態(tài)性方案存在很多缺陷住册,隨便舉幾點(diǎn):
實例對象會有很多個婶博,但是對應(yīng)的類對象有且僅有一個,因為類對象是一份抽象描述界弧,一個足矣凡蜻。但是你會發(fā)現(xiàn)實例對象和類對象并沒有聯(lián)系在一起搭综,導(dǎo)致我們得到一個實例對象無法運(yùn)行時得知其屬于什么類(對應(yīng)哪個
class object 類對象)!這也是后面我們要解決的;
存在太多的硬編碼,比如 findMethod 寫死了是從 personClsObject 中去遍歷方法列表
小總結(jié):1.實例對象允許很多個划栓,但是對應(yīng)的類對象有且僅有一個兑巾,運(yùn)行時保存在堆上;2.類對象是一份抽象描述,我們可以在運(yùn)行通過查詢類對象忠荞,拿到關(guān)于類的信息蒋歌,比如第一個變量名稱,占字節(jié)數(shù)委煤,變量類型等等堂油,拿到這些信息可以幫助我們實際訪問實例對象指針指向內(nèi)存中的數(shù)據(jù)啦!——————
因為我們知道字節(jié)偏移和變量類型。
3.4 改進(jìn):實例對象關(guān)聯(lián)類對象
3.3節(jié)中我們僅考慮只有一個personClsObject碧绞,并且在 findMethod 查詢函數(shù)中也硬編碼寫死了是從 Person
類對象方法列表中遍歷匹配府框,現(xiàn)在開始加入不同的類對象,findMethod 只需要新增一個入?yún)⒓纯?
/// 分離硬編碼部分讥邻,傳入 `Class *classObject` 不同的類對象
void (*findMethod(Class *classObject, char *methodName))(void *myself)
{
for method in classObject->methodList {
if methodName == method->name {
return method->callAddress;
}
}
return NULL;
}
/// 現(xiàn)在調(diào)用方式改為:
void (*call)(void *) = findMethod(personClsObject,
"selfIntroducation");
call(pmst);
但是這樣實現(xiàn) findMethod 的前提是知道 pmst 這個實例對象對應(yīng)的類對象為
personClsObject迫靖,單純拿到指向?qū)嵗龑ο髢?nèi)存的指針(0x1000 0000)顯然信息不足:
[圖片上傳失敗...(image-e11e38-1519970136607)]
試想知曉一個實例對象的指針 0x1000,0000,指針類型為 void * 兴使,我們可以訪問這塊內(nèi)存的數(shù)據(jù)系宜,但是問題來了:
這個實例對象到底占幾個字節(jié)呢?
內(nèi)存布局怎樣————比如第一個成員類型是Int,要讀入4個字節(jié)发魄,ps:這里可能要考慮內(nèi)存對齊問題;
我們依舊不知道這個實例對象對應(yīng)的類對象是哪個盹牧,或者說類對象所占的內(nèi)存地址是多少。
正如第三點(diǎn)指出励幼,問題根本在于我們的實例對象沒有綁定類對象的內(nèi)存地址!這樣問題就很好解決了汰寓,我們只需在內(nèi)存頭部“塞入”類對象的指針就OK了,假設(shè)
personClsObject 類對象地址為0x2000 0000
————————————————— ——————————————————————————————————————————————
| 0x2000 0000 -|--------------->| "Person"(char *className) |
|_______________ | |____________________________________________|
| "pmst" | | 0x2000 1000(Variable *variableList) |
| 26 | |____________________________________________|
| 0 | | 0x2000 2000(Method *methodList) |
————————————————— |____________________________________________|
其中 0x2000,1000 0x02000,2000 都是指針苹粟,分別指向變量列表和方法列表內(nèi)存地址踩寇。
這樣的結(jié)構(gòu)意味著要修改 Person 的結(jié)構(gòu)體:
typedef struct Person {
Class *clsObject;
char name[12];
int age;
int sex;
} Person;
////////// 偽代碼(前提我們已經(jīng)得到了person 類對象) /////////////
int size = 0;
for variable in personClsObject->variableList {
size = variable->memorySize;// 當(dāng)然這里肯定要考慮內(nèi)存對齊問題
}
Person *pmst = (Person *)malloc(size + 8); // 因為多了一個指針,32位平臺占4字節(jié)
64位平臺占8字節(jié)
pmst->clsObject = personClsObject;
//...
這樣就可以解決我們之前的問題了六水,給一個實例變量的指針,我們先取到內(nèi)存首地址開始的8個字節(jié)辣卒,解釋成 Class *
指針指向了我們的類對象掷贾,愉快地獲取想要的所有信息。
3.5 關(guān)于實例對象
不過問題來了荣茫,上述實現(xiàn)必須在抽象出來的數(shù)據(jù)結(jié)構(gòu)頂部插入一個 Class *clsObject 想帅,如下:
typedef struct Person {
Class *clsObject; // 指向 personClsObject 類對象
char name[12];
int age;
int sex;
} Person;
typedef struct Car {
Class *clsObject; // 指向 carClsObject 類對象
char brand[12];
int color;
int EngineDisplacement;
//...
} Car;
//... 還有其他很多抽象類定義
不同類的實例對象聲明如下:
// Person 實例對象:pmst numbbbb
Person *pmst = (Person *)malloc(person_size);
pmst->clsObject = personClsObject;
Person *numbbbb = (Person *)malloc(person_size);
numbbbb->clsObject = personClsObject;
// Car 實例對象:pmst's bmw & benz 以下表示客觀存在的兩輛車
Car *bmw_pmst = (Car *)malloc(car_size);
bmw_pmst->clsObject = carClsObject;
Car *benz_pmst = (Car *)malloc(car_size);
benz_pmst->clsObject = carClsObject;
盡管 pmst numbbbb bmw_pmst benz_pmst 都是指針,但是指向類型分別是 Person 和 Car
結(jié)構(gòu)體啡莉,那么在此種情況下港准,我們使用能夠使用一種統(tǒng)一的方式來定義一個實例對象呢?
觀察上述實例對象聲明以及抽象類的定義旨剥,我們找出相同點(diǎn):數(shù)據(jù)結(jié)構(gòu)頂部都為 Class *clsObject 指針。
struct object {
Class *clsObject;
};
struct object *pmst = (Person *)malloc(person_size)
pmst->clsObject = personClsObject;
struct object *bmw_pmst = (Car *)malloc(car_size);
bmw_pmst->clsObject = carClsObject;
Person 和 Car 后面的成員浅缸,我們無法使用 -> 訪問了轨帜,轉(zhuǎn)而變成查詢各自的類對象中 variableList
變量列表————變量類型和地址偏移量。這樣可以間接訪問pmst這個實例指向內(nèi)存內(nèi)容了(當(dāng)然內(nèi)存前8個字節(jié)保存的是類對象指針)衩椒。
至于為什么能這么做蚌父,先來說說C語言實現(xiàn)變長結(jié)構(gòu)體。
struct Data
{
int length;
char buffer[0];
};
結(jié)構(gòu)體中毛萌,length 其實表示分配的內(nèi)存大小苟弛,而buffer是一個空數(shù)組,可理解為占位名稱罷了;buffer的真實地址緊隨 Data
結(jié)構(gòu)體包含數(shù)據(jù)之后阁将,可以看到這個結(jié)構(gòu)體僅占4個字節(jié)膏秫,倘若我們在malloc時候分配了100個字節(jié),那么剩下100-4=96個字節(jié)就是屬于 buffer
數(shù)組做盅,非常巧妙不是嗎?
char str[10] = "pmst demo";
Data *data = (Data *)malloc(sizeof(Data) + 10);
data->length = 10;
memcpy(data->data,str, 10);
回歸正題缤削,現(xiàn)在我們可以使用 struct object 結(jié)構(gòu)來統(tǒng)一指向我們的實例對象了,但是這并不意味著我們不需要Person類
Car類的定義言蛇,只不過現(xiàn)在抽象數(shù)據(jù)結(jié)構(gòu)體的頂部不需要嵌入 Class *clsObject了僻他。
3.6 改寫實例對象的分配方式
前文demo中是這么實例化一個對象的:
/// ...
Person *pmst = (Person *)malloc(person_size)
pmst->clsObject = personClsObject;
/// ...
PersonClsObject 知道 Person 的一切。
以 (Person *)malloc(person_size) 方式實例化一個對象腊尚,首先是要拿到 personClsObject 對象吨拗,然后遍歷
variableList 累加所有變量占的內(nèi)存得到 person_size,最后調(diào)用 malloc 方法開辟內(nèi)存返回指針婿斥。
分配一塊內(nèi)存給person實例對象 這種行為屬于person類對象的職責(zé)劝篷,因此將這種行為添加到 personClsObject 類對象的
methodList 中(其實細(xì)細(xì)想來,是不太恰當(dāng)?shù)拿袼蓿筮€會繼續(xù)改進(jìn))娇妓,命名為 mallocInstance。
/// 簡單修改下定義 真實定義結(jié)構(gòu)名稱改為了 `Person_IMP`
typedef struct object Person;
struct Person_IMP {
char name[12];
int age;
int sex;
}
/// 增加一個分配內(nèi)存的方法 注意傳參為實例對象 對于當(dāng)前方法來說應(yīng)該傳NULL
(struct object *)mallocIntance(void *myself) {
/// 偽代碼
int size = 0;
for variable in personClsObject->variableList {
size = variable->memorySize;// 當(dāng)然這里肯定要考慮內(nèi)存對齊問題
}
return (struct object *)malloc(size);
}
personClsObject->methodList[xx]=mallocInstance;
/// 分配內(nèi)存改寫如下:
void (*mallocIntance)(void *) = findMethod(personClsObject,
"mallocIntance");
Person *pmst = mallocIntance(NULL); //
之前是要傳一個實例對象進(jìn)去活鹰,為了方便操作哈恰,但是分配內(nèi)存比較特殊,要知道此刻連實例都不存在!
3.8 對象調(diào)用函數(shù)方式的思考
實例對象函數(shù)調(diào)用過程: findMethod 傳入對應(yīng)的類對象和函數(shù)名稱志群,遍歷 methodList
找到匹配項返回函數(shù)指針着绷,傳入實例對象指針調(diào)用即可,譬如之前person實例調(diào)用自我介紹方法的demo锌云。
/// 現(xiàn)在調(diào)用方式改為:
void (*call)(void *) = findMethod(personClsObject,
"selfIntroducation");
call(pmst);
那么問題來了荠医,實例對象的屬性變量如何修改呢?比如name,age和sex。現(xiàn)在已經(jīng)不能像最開始之前那樣直接訪問內(nèi)存地址進(jìn)行修改了,盡管personClsObject知道這些變量的信息:變量名稱彬向,類型以及所占內(nèi)存大小兼贡。
其實解決方案也很簡單,既然不能直接訪問變量娃胆,間接總可以把!
Any problem in computer science can be solved by anther layer of
indirection.
現(xiàn)在為每個屬性變量都創(chuàng)建一個讀寫方法遍希,通過調(diào)用這兩個方法來修改實例對象內(nèi)存中的變量值(Note:前8個字節(jié)保存的是類對象地址)
void setName(void *my_self, char *newName) {}
char *getName(void *my_self) {}
注意到不同函數(shù)的傳參個數(shù)也不同,如 selfIntroducation 傳參僅 void *my_self一個缕棵,而 setName
方法傳參個數(shù)為2孵班。這其實是ok,Method封裝的是個函數(shù)指針(占4或8個字節(jié))招驴,指向某塊代碼段的地址篙程,C語言又是支持可變參數(shù)的函數(shù),原理自行g(shù)oogle關(guān)鍵字"C語言
函數(shù) 可變參數(shù)"别厘。
這里講下我的理解虱饿,函數(shù)其實就是封裝好的代碼塊,編譯之后轉(zhuǎn)成一串指令保存在代碼段触趴。
關(guān)于函數(shù)調(diào)用:正常的調(diào)用方式
functionName(variable1,variable2,variable3)氮发,編譯器會把functionName替換成函數(shù)地址(0x12345678),匯編可能是使用類似
call 0x12345678 來調(diào)用函數(shù);
關(guān)于函數(shù)入?yún)崿F(xiàn):variable1 variable2 variable3冗懦,應(yīng)該是會調(diào)用 push 指令一個個入棧(這里注意是先 push
variable1 還是 push variable3 是由ABI決定的!)
如果說函數(shù)是指令爽冕,那么棧就是給函數(shù)提供數(shù)據(jù)的源!函數(shù)實現(xiàn)是一串指令,使用push和pop操作棧上的數(shù)據(jù)披蕉,拿上面的函數(shù)入?yún)碚f颈畸,我們就使用 push
命令將variable1 variable2 variable3壓到棧里,其中 ebp 寄存器指向當(dāng)前函數(shù)調(diào)用上下文的棧 base
address没讲,而esp寄存器則是根據(jù)push和pop改變指針地址眯娱,一開始ebp和esp指針都是指向 base address。
[圖片上傳失敗...(image-f28fc2-1519970136607)]
上面就是簡單的一個調(diào)用方式爬凑,至于variable1這些函數(shù)入?yún)⑷绾稳♂憬桑瑧?yīng)該是依靠 ebp+ offset得到。
當(dāng)然如果是學(xué)習(xí)C語言的話也可以參考下面的資料
C語言編程基礎(chǔ)
[http://www.makeru.com.cn/live/1758_311.html?s=45051
結(jié)構(gòu)體普及與應(yīng)用
[http://www.makeru.com.cn/live/5413_1909.html?s=45051
C語言玩轉(zhuǎn)鏈表