別誤會,今天不是要寫我對象的......
這篇文章主要是聊聊我對于編程語言中「對象」的一些簡單認識,Go!
一、面向過程 VS 面向對象
為什么 C 叫面向過程(Procedure Oriented)的語言,而 Java、C++ 之類叫面向對象(Object Oriented)呢?
之前聽到一個有趣的說法:
在 C 語言中我們是這樣寫代碼的:
function_a(yyy);
function_b(xxx);
從左往右看過去,最先看到的是函數(shù),也就是 Procedure,故叫做「Procedure Oriented」。
而在 Java 這類語言我們通常是這樣的:
Worker?worker?=?new?Woker("小北");
worker.touchFish("5分鐘");
worker.coding("1小時");
第一眼看到的就是一個個的對象,所以叫做面向對象「Object Oriented」。
回到正題,在 C 語言,「數(shù)據(jù)」和「操作數(shù)據(jù)的函數(shù)」是互相分開的,你并不知道數(shù)據(jù)和函數(shù)之間有什么關聯(lián),這在語言層面上是不支持的。
在 C 語言中,編程就是將一堆以功能為核心導向的函數(shù)進行組合,依次調用這些函數(shù)就可以了。
這就叫面向過程,其實和我們思考問題的方式是吻合的,比如要實現(xiàn)一個貪吃蛇,那面向過程的設計思路就是首先分析問題的步驟:
1、開始游戲
2、隨機生成食物
3、繪制畫面
4、接收輸入并改變方向
5、判斷是否碰到墻壁和食物等
6、...
而用面向對象的思路則是:
首先,將整個游戲拆解為一個個的實體:蛇、食物、障礙物、規(guī)則系統(tǒng)、動畫系統(tǒng)。
然后分別去實現(xiàn)這些實體應該具有的功能(即成員函數(shù)),然后你還要考慮不同實體之間如何交互和傳遞消息,說白了就是調用關系和傳參。
比如規(guī)則系統(tǒng)接收蛇、食物、障礙物作為參數(shù),可以判定是否吃到食物或者碰到墻壁。
動畫系統(tǒng)則可以接收蛇、食物、障礙物等作為參數(shù),然后在屏幕上動態(tài)的顯示出來。
這樣做的好處便是,可以利用面向對象有封裝、繼承、多態(tài)性的特性,設計出低耦合的系統(tǒng),使系統(tǒng)更加靈活、更加易于維護。
好了,上面這段大概可以看做八股文,你分別用 C 和 Java/C++ 寫過程序自然知道二者區(qū)別,沒寫過,我在這說高內聚、低耦合也沒啥用。
二、那么對象是如何實現(xiàn)的呢?
對象的本質就是一堆的屬性(成員變量)和一系列的方法(成員函數(shù))組成,在講這個之前,先補充說明一個「函數(shù)指針」。
我們都知道函數(shù)在 C/C++、Java 這類語言中都不是一等公民,一等公民的意思就是能夠像其它整數(shù)、字符串變量一樣,可以被賦值或者作為函數(shù)參數(shù)、返回值等。
但是在 JS、Python 這類動態(tài)語言中,函數(shù)卻是一等公民,可以作為參數(shù)、返回值等等。
究其原因,這類語言底層實現(xiàn)中,一切東西皆是對象,函數(shù)、整數(shù)、字符串、浮點數(shù)都是對象,函數(shù)才因此具備同其它基本類型一樣的一等公民的身份。
但是!在 C/C++ 中函數(shù)雖然是二等公民, 但我們可以通過函數(shù)指針來變相的實現(xiàn)將函數(shù)用于變量賦值、函數(shù)參數(shù)、返回值場景。
三、函數(shù)指針是啥?
我們知道普通變量申明后,編譯器就會自動分配一塊適合的內存,那么函數(shù)也是同樣的,編譯的時候會將一個函數(shù)編譯好,然后放在一塊內存中。
(上面這段說法實際很不準確,因為編譯器不會分配內存,編譯好的代碼也是以二進制的形式放在磁盤上,只有程序開始運行時才會加載到內存)
如果我們把函數(shù)的首地址也存儲在某個指針變量里,就可以通過這個指針變量來調用所指向的函數(shù)了,這個存儲函數(shù)首地址的特殊指針就叫做「函數(shù)指針」。
比如有一個函數(shù)int func(int a);
我們如何申明一個可以指向func的函數(shù)指針呢?
int (*func_p)(int);
看起來有點奇怪,其實函數(shù)指針變量的聲明格式如同函數(shù)func的聲明一樣,只不過把 func換成了 (*func_p)罷了。
為什么要括號呢?因為不要括號的話int *func_p(int);就是申明一個返回指針的函數(shù)了,括號就是為了避免這種歧義。
我們來多看幾個函數(shù)指針的申明吧:
int?(*f1)(int);?//?傳入int,返回int?
void?(*f2)(char*);?//傳入char指針,沒有返回值?
double*?(*f3)(int,?int);?//傳遞兩個整數(shù),返回?double指針
來看一個函數(shù)指針的具體用處吧:
#?include?
typedef?void?(*work)()?Work;?//?typedef?定義一種函數(shù)指針類型
void?xiaobei_work()?{
?printf("小北工作就是寫代碼");
}
void?shuaibei_work()?{
?printf("帥北工作就是摸魚")
}
void?do_work(Work?worker)?{
??worker();
}
int?main(void)
{
??Work?x_work?=?xiaobei_work;
??Work?s_work?=?shuaibei_work;
??do_work(x_work);
??do_work(s_work);
??return?0;
}
輸出:
小北工作就是寫代碼
帥北工作就是摸魚
其實這里有點為了用函數(shù)指針而用了,不過大家應該體會到了,函數(shù)指針最大的優(yōu)點就是將函數(shù)變量化了。
我們可以將函數(shù)作為參數(shù)傳遞給其它函數(shù),那么這里其實就有了多態(tài)的雛形,我們可以傳遞不同的函數(shù)來實現(xiàn)不同的行為。
void qsort(void* base, size_t num, size_t width, int(*compare)(const void*,const void*))
這是 C 標準庫中 qsort 函數(shù)的申明,它最后一個參數(shù)就要求傳入一個函數(shù)指針,這個函數(shù)指針負責比較兩個 element。
因為兩個元素的比較方式只有調用者才知道,所以這里需要以函數(shù)指針的形式告訴 qsort 如何去判定兩個元素的大小。
好了,函數(shù)指針就簡單介紹到這里,接下來回到主題,對象。
四、對象
那么在 C 語言中如何簡單模擬一個對象呢?
當然只能靠結構體啦,而成員函數(shù)就可以通過函數(shù)指針來實現(xiàn),其它的比如訪問控制、繼承等我們暫時不考慮。
struct?Animal?{
????char?name[20];
????void?(*eat)(struct?Animal*?this,?char?*food);?//?成員方法?eat
????int?(*work)(struct?Animal*?this);????//?成員方法?工作
};
但是eat和work都還沒有任何具體實現(xiàn),所以我們可以在一個初始化函數(shù)中構造這個 Animal 對象。
void?eat(struct?Animal*?this,?char?*food)?{
????printf("%s?在吃?%s\n",?this->name,?food);
};
void?work(struct?Animal*?this)?{
????printf("%s?在工作\n",?this->name);
}
struct?Animal*?Init(const?char?*name)?{
????struct?Animal?*animal?=?(struct?Animal?*)malloc(sizeof(struct?Animal));
????strcpy(animal->name,?name);
????animal->eat?=?eat;
????animal->work?=?work;
????return?animal;
}
在Init函數(shù)內部我們就完成了“成員函數(shù)”的賦值和一些初始化工作,并且給 eat和work兩個函數(shù)指針都綁定了具體的實現(xiàn)。
接下來我們可以使用一下這個對象:
int?main()?{
?struct?Animal?*animal?=?Init("小狗");
?animal->eat(animal,?"牛肉");
?animal->work(animal);
?return?0;
}
輸出:
小狗 在吃 牛肉
小狗 在工作
為什么明明animal調用的eat方法卻還要把animal當參數(shù)傳遞給eat方法呢,難道eat不知道是哪一個Animal調用的它嗎?
確實不知道,對象其實就是在內存中一段有意義的區(qū)域,每一個不同的對象都有各自的內存位置。
而他們的成員函數(shù)卻存放在代碼段,而且只會存在一份副本。
所以animal->eat(...)調用方式和直接調用eat(...),效果完全等同,那個animal存在的意義就是讓你從面向過程轉變?yōu)槊嫦驅ο笏伎?,將方法調用轉變?yōu)閷ο箝g消息傳遞。
所以當調用成員函數(shù)的時候,我們還需要傳入一個參數(shù) this,用來指代當前是哪個對象在調用。
由于 C 語言不支持面向對象,所以我們需要手動將 animal 作為參數(shù)傳遞給 eat、work 函數(shù)。
如果是在 C++ 這種面向對象的語言中,我們直接不用手動傳遞這個參數(shù),就像下面這樣:
animal->eat(“牛肉”);
animal->work();
實際上這是編譯器幫我們去做這個事,上面這兩行代碼,經過編譯器之后會變成下面這個樣子:
eat(animal,?"牛肉");
work(animal);
然后,編譯器還會在編譯階段默默地將 this 作為成員函數(shù)的一個形參添加到參數(shù)列表。
并且哪個對象調用的方法,那個對象就會被當做參數(shù)賦值給this。
學習 Java 的的同學也一定對這個this非常熟悉吧,Java 中和 C++ 中的 this 基本都是一樣的作用。
或者說,幾乎所有的面向對象語言,都會存在一個類似的機制,來將調用對象隱式的傳遞給成員函數(shù),比如 Python 中的對象定義:
class?Stu:
???def?__init__(self,?name,?age):
??????self.name?=?name
??????self.age?=?age
??????
???def?displayStu(self):
??????print?"Name?:?",?self.name,??",?Age:?",?self.age
可以看到每個成員函數(shù)第一個參數(shù)都必須叫self,這個self實際上就是和this是一樣的作用。
只有這樣,當你在成員函數(shù)內訪問成員變量的時候,編譯器才知道你訪問的是哪一個對象。
誒,別忙,按照這樣說,那豈不是,如果我在成員函數(shù)內不訪問任何成員變量,就不需要傳遞這個this指針?
或者說可以傳遞一個空指針?
理論上確實成立,并且在 C++ 中也是可行的,比如下面這段代碼:
class?Stu{
public:
????void?Hello()?{
?????cout?<"hello?world"?<endl;
????}
private:
????char?*name;
????int?age;
????float?score;
};
由于,在 Hello 函數(shù)中沒有用到任何成員變量,所以我們甚至可以這樣玩:
Stu?*stu?=?new?Stu;
stu->Hello();?//?正常對象,正常調用
stu?=?NULL;
stu->Hello()?//?雖然?stu?為?NULL,但是依然不會發(fā)送運行時錯誤
這里實際上可以這樣看:
stu->Hello(); 等價于Hello(NULL);
由于在 Hello 函數(shù)內部,沒有使用任何的成員變量,所以就不需要用 this 指針去定位成員變量的內存位置,在這種情況下,調用對象為不為 NULL 其實是不重要的。
但是如果 Hello 函數(shù)訪問了成員變量,比如:
void?Hello()?{
?cout?<"Hello?"?<this->name?<endl;
}
這里需要用到 this 去訪問 name 成員變量, 那么就會導致運行時程序發(fā)生 coredump,因為我們訪問了一個 NULL 地址,或者說是基于 NULL 偏移一定位置的地址,這段空間絕對是沒有訪問權限的。
之前,恰好也有位同學在群里問了這個問題:
這個問題解釋就和上面的一樣,但是這個結論不能推廣到其它語言,比如 Java、Python,這些語言的虛擬機一般會做一些額外的檢查,比如判斷調用對象是否是空指針等,是的話就會觸發(fā)空指針異常。
而 C++ 就真的是很純粹的編譯成匯編,只要從匯編層面能跑通,那就沒問題,所以才能利用這個“奇技淫巧”。
那寫這篇文章得目的呢,就是想讓大家對「對象」有一個具體的認識,最好是明白對象在內存中或者 JVM 中是如何布局的。
我以前就會覺得對象挺神奇的,一堆的功能,后來才后知后覺,不就是一個結構體再加上編譯器的語法糖嗎
好啦,對象的秘密都給大家解開了,不給我點亮一個在看么?
免責聲明:本文內容由21ic獲得授權后發(fā)布,版權歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!





