不想錯過我的推送,記得右上角-檢視公眾號-
設為星標
,送你一顆小星星
不知道有多少人去了解過語言的發展史,早期C語言的語法功能其實比較簡單。隨著應用需求和場景的變化,C語言的語法功能在不斷升級變化。
雖然我們的教材有這麼一個結論:C語言是
面向過程
的語言,C++是
面向物件
的程式語言,但面向物件的概念是在C語言階段就有了,而且應用到了很多地方,比如某些作業系統核心、通訊協議等。
面向物件程式設計,也就是大家說的OOP(Object Oriented Programming)並不是一種特定的語言或者工具,
它只是一種設計方法、設計思想
,它表現出來的三個最基本的特性就是
封裝、繼承與多型
。
1、為什麼用C實現OOP
閱讀文字之前肯定有讀者會問這樣的問題:我們有C++面向物件的語言,為什麼還要用C語言實現面向物件呢?
C語言這種非面向物件的語言,同樣也可以使用面向物件的思路來編寫程式的。
只是用面向物件的C++語言來實現面向物件程式設計會更簡單一些,但是C語言的高效性是其他面向物件程式語言無法比擬的。
當然使用C語言來實現面向物件的開發
相對不容易理解
,這就是為什麼大多數人學過C語言卻看不懂Linux核心原始碼。
所以這個問題其實很好理解,只要有一定C語言程式設計經驗的讀者都應該能明白:
面向過程的C語言和麵向物件的C++語言相比,程式碼執行效率、程式碼量都有很大差異
。在效能不是很好、資源不是很多的MCU中使用C語言面向物件程式設計就顯得尤為重要。
2、所具備的條件
要想使用C語言實現面向物件,首先需要具備一些基礎知識。比如:(C語言中的)結構體、函式、指標,以及函式指標等,(C++中的)基類、派生、多型、繼承等。
首先,不僅僅是瞭解這些基礎知識,而是有一定的程式設計經驗,因為上面說了“面向物件是一種設計方法、設計思想”,如果只是停留在字面意思的理解,沒有這種設計思想肯定不行。
因此,不建議初學者使用C語言實現面向物件,特別是在真正專案中。建議把基本功練好,再使用。
利用C語言實現面向物件的方法很多,下面就來描述最基本的封裝、繼承和多型。
3、封裝
封裝就是把資料和函式打包到一個類裡面,
其實大部分C語言程式設計者都已近接觸過了。
C 標準庫中的 fopen(), fclose(), fread(), fwrite()等函式的操作物件就是 FILE。資料內容就是 FILE,資料的讀寫操作就是 fread()、fwrite(),fopen() 類比於建構函式,fclose() 就是解構函式。
這個看起來似乎很好理解,那下面我們實現一下基本的封裝特性。
這是 Shape 類的宣告,非常簡單,很好理解。一般會把宣告放到標頭檔案裡面 “Shape。h”。來看下 Shape 類相關的定義,當然是在 “Shape。c” 裡面。
再看下 main。c
編譯之後,看看執行結果:
整個例子,非常簡單,非常好理解。以後寫程式碼時候,要多去想想標準庫的檔案IO操作,這樣也有意識的去培養面向物件程式設計的思維。
4、繼承
繼承就是基於現有的一個類去定義一個新類
,這樣有助於重用程式碼,更好的組織程式碼。在 C 語言裡面,去實現單繼承也非常簡單,只要把基類放到繼承類的第一個資料成員的位置就行了。
例如,我們現在要建立一個 Rectangle 類,我們只要繼承 Shape 類已經存在的屬性和操作,再新增不同於 Shape 的屬性和操作到 Rectangle 中。
下面是 Rectangle 的宣告與定義:
我們來看一下 Rectangle 的繼承關係和記憶體佈局:
因為有這樣的記憶體佈局,所以你可以很安全的傳一個指向 Rectangle 物件的指標到一個期望傳入 Shape 物件的指標的函式中,就是一個函式的引數是 “Shape *”,你可以傳入 “Rectangle *”,並且這是非常安全的。這樣的話,基類的所有屬性和方法都可以被繼承類繼承!
輸出結果:
5、多型
C++ 語言實現多型就是使用虛擬函式。在 C 語言裡面,也可以實現多型。
現在,我們又要增加一個圓形,並且在 Shape 要擴充套件功能,我們要增加 area() 和 draw() 函式。但是 Shape 相當於抽象類,不知道怎麼去計算自己的面積,更不知道怎麼去畫出來自己。而且,矩形和圓形的面積計算方式和幾何影象也是不一樣的。
下面讓我們重新宣告一下 Shape 類:
看下加上虛擬函式之後的類關係圖:
5.1 虛表和虛指標
虛表(Virtual Table)是這個類所有虛擬函式的函式指標的集合。
虛指標(Virtual Pointer)是一個指向虛表的指標。這個虛指標必須存在於每個物件例項中,會被所有子類繼承。
在《Inside The C++ Object Model》的第一章內容中,有這些介紹。
5.2 在建構函式中設定vptr
在每一個物件例項中,vptr 必須被初始化指向其 vtbl。最好的初始化位置就是在類的建構函式中。事實上,在建構函式中,C++ 編譯器隱式的建立了一個初始化的vptr。在 C 語言裡面, 我們必須顯示的初始化vptr。
下面就展示一下,在 Shape 的構造函數里面,如何去初始化這個 vptr。
5.3 繼承 vtbl 和 過載 vptr
上面已經提到過,基類包含 vptr,子類會自動繼承。但是,vptr 需要被子類的虛表重新賦值。並且,這也必須發生在子類的建構函式中。下面是 Rectangle 的建構函式。
5.4 虛擬函式呼叫
有了前面虛表(Virtual Tables)和虛指標(Virtual Pointers)的基礎實現,虛擬呼叫(後期繫結)就可以用下面程式碼實現了。
這個函式可以放到。c檔案裡面,但是會帶來一個缺點就是每個虛擬呼叫都有額外的呼叫開銷。為了避免這個缺點,如果編譯器支援行內函數(C99)。我們可以把定義放到標頭檔案裡面,類似下面:
如果是老一點的編譯器(C89),我們可以用宏函式來實現,類似下面這樣:
看一下例子中的呼叫機制:
5.5 main.c
輸出結果:
6、總結
還是那句話,
面向物件程式設計是一種方法,並不侷限於某一種程式語言。
用 C 語言實現封裝、單繼承,理解和實現起來比較簡單,多型反而會稍微複雜一點,如果打算廣泛的使用多型,還是推薦轉到 C++ 語言上,畢竟這層複雜性被這個語言給封裝了,你只需要簡單的使用就行了。但並不代表,C 語言實現不了多型這個特性。
文章來源:嵌入式情報局