編程人的「對象」長啥樣?

2020-12-18 CSDN

作者 | 編程指北 責編 | 張文

頭圖 | CSDN 下載自視覺中國

別誤會,今天要寫的不是我對象,畢竟我還沒有......

這篇文章主要是聊聊我對於程式語言中「對象」的一些簡單認識,

面向過程 VS 面向對象

為什麼 C 叫面向過程(Procedure Oriented)的語言,而 Java、C++ 之類叫面向對象(Object Oriented)呢?

之前聽到一個有趣的說法:

在 C 語言中我們是這樣寫代碼的:

function_a(yyy);function_b(xxx);

從左往右看過去,最先看到的是函數,也就是 Procedure,故叫做 Procedure Oriented。

而在 Java 這類語言我們通常是這樣的:

Worker worker = new Woker("小北");worker.touchFish("5分鐘");worker.coding("1小時");

第一眼看到的就是一個個的對象,所以叫做面向對象 Object Oriented。

回到正題。在 C 語言中,數據和操作數據的函數是互相分開的,你並不知道數據和函數之間有什麼關聯,這在語言層面上是不支持的。

在 C 語言中,編程就是將一堆以功能為核心導向的函數進行組合,依次調用這些函數就可以了。

這就叫面向過程,其實和我們思考問題的方式是吻合的。比如要實現一個貪吃蛇遊戲,那面向過程的設計思路就是首先分析問題的步驟:

開始遊戲隨機生成食物繪製畫面接收輸入並改變方向判斷是否碰到牆壁和食物等...而用面向對象的思路則是:

首先,將整個遊戲拆解為一個個的實體:蛇、食物、障礙物、規則系統、動畫系統。

其次,分別去實現這些實體應該具有的功能(即成員函數),同時你還要考慮不同實體之間如何交互和傳遞消息。說白了就是調用關係和傳參。

比如規則系統接收蛇、食物、障礙物作為參數,可以判定是否吃到食物或者碰到牆壁。

動畫系統則可以接收蛇、食物、障礙物等作為參數,然後在屏幕上動態的顯示出來。

這樣做的好處是:可以利用面向對象有封裝、繼承、多態性的特性,設計出低耦合的系統,使系統更加靈活、更加易於維護。

好了,上面這段大概可以看做八股文。你分別用 C 和 Java/C++ 寫過程序自然知道二者區別。不然我說再多的高內聚、低耦合也沒啥用。

對象如何實現?

對象的就是由一堆的屬性(成員變量)和一系列的方法(成員函數)組成。在講這個之前,先補充說明一個函數指針。

我們都知道函數在 C/C++、Java 這類語言中都不是一等公民,一等公民的意思就是能夠像其它整數、字符串變量一樣,可以被賦值或者作為函數參數、返回值等。但是在 JS、Python 這類動態語言中,函數卻是一等公民,可以作為參數、返回值等等。

究其原因,這類語言底層實現中,一切東西皆是對象。函數、整數、字符串、浮點數都是對象,函數才因此具備同其它基本類型一樣的一等公民的身份。

在 C/C++ 中,函數雖然是二等公民, 但我們可以通過函數指針來變相的實現將函數用於變量賦值、函數參數、返回值場景。

函數指針是啥?

我們知道普通變量申明後,編譯器就會自動分配一塊適合的內存。函數也是同樣的,編譯的時候會將一個函數編譯好,然後放在一塊內存中。

(上面這段說法實際很不準確,因為編譯器不會分配內存,編譯好的代碼也是以二進位的形式放在磁碟上,只有程序開始運行時才會加載到內存)

如果我們把函數的首地址也存儲在某個指針變量裡,就可以通過這個指針變量來調用所指向的函數了,這個存儲函數首地址的特殊指針就叫做函數指針

比如有一個函數 int func(int a);

我們如何申明一個可以指向 func 的函數指針呢?

int (*func_p)(int);

看起來有點奇怪,其實函數指針變量的聲明格式和同函數 func 的聲明一樣,只不過把 func 換成(*func_p)罷了。

為什麼要括號呢?因為不要括號的話 int *func_p(int); 就是申明一個返回指針的函數了,括號就是為了避免這種歧義

我們來多看幾個函數指針的申明吧:

int (*f1)(int); // 傳入int,返回int void (*f2)(char*); //傳入char指針,沒有返回值 double* (*f3)(int, int); //傳遞兩個整數,返回 double指針

來看一個函數指針的具體用處吧:

# include<stdio.h>typedefvoid(*work)() Work; // typedef 定義一種函數指針類型voidxiaobei_work(){printf("小北工作就是寫代碼");}voidshuaibei_work(){printf("帥北工作就是摸魚")}voiddo_work(Work worker){ worker();}intmain(void){ Work x_work = xiaobei_work; Work s_work = shuaibei_work; do_work(x_work); do_work(s_work);return0;}

輸出:

小北工作就是寫代碼帥北工作就是摸魚

其實這裡有點為了用函數指針而用了,不過大家應該體會到了,函數指針最大的優點就是將函數變量化了。

我們可以將函數作為參數傳遞給其它函數,於是就有了多態的雛形。我們可以傳遞不同的函數來實現不同的行為。

voidqsort(void* base, size_t num, size_t width, int(*compare)(constvoid*,constvoid*))

這是 C 標準庫中 qsort 函數的申明,它最後一個參數就要求傳入一個函數指針,這個函數指針負責比較兩個 element。

因為兩個元素的比較方式只有調用者才知道,所以這裡需要以函數指針的形式告訴 qsort 如何去判定兩個元素的大小。

好了,函數指針就簡單介紹到這裡,接下來回到主題,對象。

對象

那麼在 C 語言中如何簡單模擬一個對象呢?

當然只能靠結構體啦,而成員函數就可以通過函數指針來實現,其它的比如訪問控制、繼承等我們暫時不考慮。

struct Animal {char name[20];void (*eat)(struct Animal* this, char *food); // 成員方法 eatint (*work)(struct Animal* this); // 成員方法 工作};

但是 eat 和 work 都還沒有任何具體實現,所以我們可以在一個初始化函數中構造 Animal 對象。

voideat(struct Animal* this, char *food) { printf("%s 在吃 %s\n", this->name, food);};voidwork(struct Animal* this) { printf("%s 在工作\n", this->name);}struct Animal* Init(constchar *name) {struct Animal *animal = (struct Animal *)malloc(sizeof(struct Animal)); strcpy(animal->name, name); animal->eat = eat; animal->work = work;return animal;}

在 Init 函數內部我們就完成了「成員函數」的賦值和一些初始化工作,並且給 eat 和 work 兩個函數指針都綁定了具體的實現。

接下來我們可以使用一下這個對象:

intmain() {struct Animal *animal = Init("小狗"); animal->eat(animal, "牛肉"); animal->work(animal);return0;}

輸出:

小狗在吃牛肉小狗在工作

為什麼明明 animal 調用的 eat 方法,卻還要把 animal 當參數傳遞給 eat 方法呢,難道 eat 不知道是哪一個 Animal 調用的它嗎?

答案是確實不知道。對象其實就是在內存中一段有意義的區域,每一個不同的對象都有各自的內存位置

而他們的成員函數卻存放在代碼段,並且只會存在一份副本。

所以 animal->eat(...)調用方式和直接調用 eat(...),效果完全等同,那個animal 存在的意義就是讓你從面向過程轉變為面向對象思考,將方法調用轉變為對象間消息傳遞。

所以當調用成員函數的時候,我們還需要傳入一個參數 this,用來指代當前是哪個對象在調用。

由於 C 語言不支持面向對象,所以我們需要手動將 animal 作為參數傳遞給 eat、work 函數。

如果是在 C++ 這種面向對象的語言中,我們直接不用手動傳遞這個參數,就像下面這樣:

animal->eat(「牛肉」);animal->work();

實際上這是編譯器幫我們去做這個事,上面這兩行代碼,經過編譯器之後會變成下面這個樣子:

eat(animal, "牛肉");work(animal);

然後,編譯器還會在編譯階段默默地將 this 作為成員函數的一個形參添加到參數列表。

並且哪個對象調用的方法,那個對象就會被當做參數賦值給 this。

學習 Java 的的同學也一定對這個this非常熟悉吧,Java 中和 C++ 中的 this 基本都是一樣的作用。

或者說,幾乎所有的面向對象語言,都會存在一個類似的機制,來將調用對象隱式的傳遞給成員函數,比如 Python 中的對象定義:

classStu:def__init__(self, name, age):self.name = nameself.age = agedefdisplayStu(self): print "Name : ", self.name, ", Age: ", self.age

可以看到每個成員函數第一個參數都必須叫 self,這個 self 實際上就是和 this是一樣的作用。

只有這樣,當你在成員函數內訪問成員變量的時候,編譯器才知道你訪問的是哪一個對象。

誒,別忙,按照這樣說,那豈不是,如果我在成員函數內不訪問任何成員變量,就不需要傳遞這個 this 指針?

或者說可以傳遞一個空指針?

理論上確實成立,並且在 C++ 中也是可行的,比如下面這段代碼:

classStu{public:voidHello(){cout << "hello world" << endl; }private:char *name;int age;float score;};

由於,在 Hello 函數中沒有用到任何成員變量,所以我們甚至可以這樣玩:

Stu *stu = new Stu;stu->Hello(); // 正常對象,正常調用stu = NULL;stu->Hello() // 雖然 stu 為 NULL,但是依然不會發送運行時錯誤

這裡實際上可以這樣看:

stu->Hello(); 等價於Hello(NULL);

由於在 Hello 函數內部,沒有使用任何的成員變量,所以就不需要用 this 指針去定位成員變量的內存位置,在這種情況下,調用對象為不為 NULL 其實是不重要的。

但是如果 Hello 函數訪問了成員變量,比如:

voidHello(){cout << "Hello " << this->name << endl;}

這裡需要用到 this 去訪問 name 成員變量,那麼就會導致運行時程序發生 coredump,因為我們訪問了一個 NULL 地址,或者說是基於 NULL 偏移一定位置的地址,這段空間絕對是沒有訪問權限的。

恰好之前也有位同學在群裡問了這個問題:

這個問題的解釋就和上面的一樣,但是這個結論不能推廣到其它語言,比如 Java、Python。這些語言的虛擬機一般會做一些額外的檢查,比如判斷調用對象是否是空指針等,是的話就會觸發空指針異常。

而 C++ 就真的是很純粹的編譯成彙編,只要從彙編層面能跑通,那就沒問題,所以才能利用這個「奇技淫巧」。

那寫這篇文章的目的,就是想讓大家對「對象」有一個具體的認識,最好是明白對象在內存中或者 JVM 中是如何布局的。

我以前就會覺得對象挺神奇的,一堆的功能,後來才後知後覺,這不就是一個結構體再加上編譯器的語法糖嗎

相關焦點

  • 如何給女朋友解釋什麼是面向對象編程?
    對於我來說,外賣軟體就是個對象,我再面向他「做飯」,其實是他幫我做的。額、你說的也對,也不對。在這個場景中,確實可以把外賣軟體當做是「對象」。其實,在面向對象編程中,拋棄了函數,想要實現一個功能不再是通過函數的疊加調用實現的了。而是通過對象。對象就是對事物的一種抽象描述。現實世界中的事物,都可以用「數據」和「能力」來描述。
  • 體脂率低於10%的腹肌長啥樣?快舔屏
    提到好身材大多數人腦海中浮現出的大概是這樣的▼>那麼問題來了體脂率低於10%的腹肌到底長啥樣當躺下來腹肌絲毫不減宛如大峽谷般震撼「腹肌躺」名副其實▼
  • 成年人也可接受編程培訓,編程速成訓練營「Le Wagon」獲 1900 萬美元
    2013 年,「Le Wagon」在法國起步,創業的起點為成人編程教育市場,是一家專注於產品研發的編程速成訓練營。目前,「Le Wagon」編程訓練營已擴張到亞洲、中東、美洲和非洲地區。2019 年,隨著深圳分校的即將開業,「Le Wagon」的分校達到了 30 家。
  • 當男朋友話:「冇男人會鍾意你咁樣」…
    ,呢個世界冇兩個同樣嘅人,你同人相處當然要做適當嘅改變,如果唔系,就只有你快樂,人哋難受,亦好難天長地久。不過有D事就系應該寸步不讓,因為,果D就系組成「你」嘅重要部份,又根本上對佢冇實際影響嘅事。比如話,你嘅外型。除非話你已經瘦到影響健康,或者肥到影響市容,如果唔系你鍾意自己點樣嘅身型都同人哋無關;你鍾意曬黑或美白,健美或纖細,著黑絲或長褲,長發或短髮,果D都系你嘅自由。
  • 36氪首發|「編程貓」再獲13億元D輪融資,成為國內少兒編程領域最大...
    36氪獲悉,在線少兒編程平臺「編程貓」在今日宣布完成13億元D輪融資,由霸菱亞洲旗下基金領投,中信證券投資、金石投資、優山資本、溫氏資本、遠洋資本、大灣區基金、中銀國際等跟投,高瓴資本、招銀國際、中銀集團旗下渤海中盛、粵科鑫泰等老股東持續跟投。
  • 月壤長啥樣
    月壤長啥樣 嫦五帶回「土特產」開箱啦 來源:南方都市報     2020年12月20日
  • 問卷丨中國誠通吉祥物應該長啥樣?
    問卷丨中國誠通吉祥物應該長啥樣?請您抽出寶貴的10分鐘時間,通過問卷告訴我們,中國誠通的吉祥物應該長啥樣?長按識別或用手機掃描下方二維碼,進入問卷若有更多寶貴意見和建議,請發郵件至viatus@qq.com,郵件標題:中國誠通吉祥物建議。感謝您的支持!
  • 好想看看月壤長啥樣!
    好想看看月壤長啥樣!原標題:《好想看看月壤長啥樣!》
  • 「應聲蟲」長啥樣?
    【華夏影像診斷中心】注重專業內涵建設,擁有醫學影像界排名前三的網站www.dic120.com排名前五的微信訂閱號dic120排名第一的學習超級QQ群是醫學影像人的三棲平臺 「應聲蟲」長啥樣?(張克雲整理)宋史《藝文志》中記載:有一個叫楊勔的人得了一種怪病,每當說話時,腹內即有回答的聲音,數年之後,回答聲音逐漸增大,好似兩人對面相談。楊勔雖四處求醫,但無法治癒。一天楊勔外出途中遇見一個叫陳羅漢的道士,兩人相談之時,楊勔將詳情告之。陳道士說:「你腹內生有應聲蟲,若長久不治,還要傳染給妻子和兒女。
  • 95後眼中的「國潮」長啥樣?
    6月22日19點,由第一財經商業數據中心(CBNData)旗下全新潮流態度IP「潮MOODS」發起的「漲潮計劃」線下啟動會在上海啟動。李寧在2018紐約時裝周秋冬秀場展示的「悟道」系列潮品 | 圖源:網絡95後眼中的「國潮」究竟有啥與眾不同?
  • 獵奇的「一般向」動畫「神樣DOLLS」情報初公開
    獵奇的「一般向」動畫「神樣DOLLS」情報初公開 動漫 178動漫頻道 ▪ 2011-02-17 15:04:04
  • 為什麼相親對象都這麼「奇葩」?背後的原因找到了
    「吃著飯呢,他竟然還開始跟我說起開公司的經歷了?!如果不是他公司臨時有事先走,我都不知道多久才能結束……」「更氣人的是啥你知道嗎?他回去後竟然跟他家人說覺得我太老了?我一個還沒跨入三十門檻的人,竟然被一年過三十的男人嫌老?」電話那頭的人越說情緒越激動,聲音也漸漸提高了八個度。
  • 「世界上最好的程式語言」,剛剛度過了25周歲生日
    「只要是用過 PHP 的程式設計師,都知道兩件事:第一,這是一種糟糕的語言,如果有其他選擇,自己絕不會再使用 PHP;第二,歷史上一些非常成功的項目都在用 PHP。」Fine,這兩點並不矛盾。作為一門程式語言,PHP 是有很多的缺陷,但和 PHP 環境具備的優點相比,這些缺陷也就不值一提了。
  • 80%的人都不知道流行語「SNS」到底是啥意思?
    寫真(しゃしん)を投稿(とうこう)してみんなで見(み)たり、友達(ともだち)とチャットしたり、同(おな)じ趣味(しゅみ)を持(も)つ人(ひと)たちとの交流(こうりゅう)を楽しんだりもできます。還可以發點兒照片兒啊,和朋友聊聊天兒啊,交個朋友啥滴。[「SNS」にはどんな種類(しゅるい)があるの?]那什麼樣的遠件兒是SNS呢?
  • 爸媽介紹的相親對象為什麼總那麼奇葩?背後的原因找到了!
    「吃著飯呢,他竟然還開始跟我說起開公司的經歷了?!如果不是他公司臨時有事先走,我都不知道多久才能結束……」「更氣人的是啥你知道嗎?他回去後竟然跟他家人說覺得我太老了?我一個還沒跨入三十門檻的人,竟然被一年過三十的男人嫌老?」電話那頭的人越說情緒越激動,聲音也漸漸提高了八個度。
  • 直男斬、直女斬分別長啥樣?看看秦牛正威和孟美岐就知道了!
    比如在一眾仙女中,最早奪得「直男斬」稱號的蔡卓宜▼ 所以,今天就來聊聊「直男斬」「直女斬」型分別具有哪些特點?你更適合朝哪個方向發展?如何調節自己的異性緣和同性緣?
  • 讓孩子也能掌握「造物之術」,魔趣童心編程積木上手體驗
    無縫拼接「大顆粒積木」一改傳統玩具靜止不動的痛點傳統的大顆粒積木可以發揮孩子們的想像空間,通過想像力可以創造出汽車、飛機等等,但是遇到一個痛點問題是創造的成果靜止不動,只有觀賞的作用非常無趣。「可編程化」加持可玩性和樂趣大大增加魔趣童心編程積木麻雀雖小,但五臟俱全,特別是具備編程功能。
  • 開山:想讓你在西餐廳喝白酒,新中式「先鋒廠牌」長啥樣?|新物種觀察
    民間一直有「儀狄造酒」、「杜康造酒」等說法,幾千年源遠流長足使其地位不可撼動。除本身的功能價值外,社交屬性在白酒身上表現得更為明顯,宴飲、聚會都不可或缺。但近年來8090後們似乎對白酒興趣索然,傳統白酒第一次遭遇代際消費「滑鐵盧」。今天的「新物種觀察」介紹的就是一款將傳統美學與國際表達結合,主打年輕用戶的「先鋒精釀」——開山。
  • Nature 封面:科學家用「重編程」逆轉生命之鐘,恢復小鼠喪失的視覺
    關於衰老,Peter Brian Medawar 提出了兩種解釋:一是生物學角度的「先天衰老」,即衰老是生物必經的過程;二是物理學角度的「衰老磨損」,即衰老是一種累積效應。在第一種解釋之下,衰老和死亡由物種進化決定,目的是為年輕一代騰出空間——我們的生命進程中有一個「主時鐘」,倒數著我們能自然存活的時間。在世人看來,衰老這件事情,雖然很無奈,但也無法改變。
  • 寫Python 代碼不可不知的函數式編程技術
    近來,越來越多人使用函數式編程(functional programming)。因此,很多傳統的命令式語言(如 Java 和 Python)開始支持函數式編程技術。本文對 Python 中的函數式編程技術進行了簡單的入門介紹。本文適合對函數式編程有基本了解的讀者。