面向?qū)ο?a target="_blank">編程(OOP),是一種設(shè)計(jì)思想或者架構(gòu)風(fēng)格。OO語(yǔ)言之父Alan Kay,Smalltalk的發(fā)明人,在談到OOP時(shí)是這樣說(shuō)的:
OOP應(yīng)該體現(xiàn)一種網(wǎng)狀結(jié)構(gòu),這個(gè)結(jié)構(gòu)上的每個(gè)節(jié)點(diǎn)“Object”只能通過(guò)“消息”和其他節(jié)點(diǎn)通訊。每個(gè)節(jié)點(diǎn)會(huì)有內(nèi)部隱藏的狀態(tài),狀態(tài)不可以被直接修改,而應(yīng)該通過(guò)消息傳遞的方式來(lái)間接的修改。
以本段話作為開(kāi)場(chǎng),展開(kāi)本文的探討。
1. 面向?qū)ο蟮膶?shí)質(zhì)
1.1 面向?qū)ο缶幊淌菫榱私鉀Q什么問(wèn)題?
大部分程序員都學(xué)過(guò)c語(yǔ)言,尤其是嵌入式工程師,可能只會(huì)c語(yǔ)言。
c語(yǔ)言是一項(xiàng)典型的面向過(guò)程的語(yǔ)言,一切都是流程。簡(jiǎn)單的單片機(jī)程序可能只有幾行,多的也不過(guò)幾百行。這時(shí)一個(gè)人的能力是完全可以走通整個(gè)代碼,但引入操作系統(tǒng)后,事情就變得復(fù)雜了。
進(jìn)程調(diào)度、內(nèi)存管理等各種功能使得代碼量劇增,一個(gè)簡(jiǎn)單的RTOS實(shí)時(shí)操作系統(tǒng)都達(dá)到了上萬(wàn)行代碼,這個(gè)時(shí)候,走通也不是不行,就是比較吃力。
但Linux代碼量就完全不是正常人能讀完的,一秒鐘讀一行代碼,每天讀12小時(shí),也需要幾年才能讀完Linux的源碼,這還僅僅是讀完,而不是理解。
再舉一個(gè)簡(jiǎn)單的例子
小公司往往只有幾個(gè)人,大家在一起干活,你完完全全可以看到每個(gè)人在干什么。
在大公司中呢?幾千、上萬(wàn)人的公司中,你要去弄清楚,每個(gè)人在干嘛,這是完全不可能的。于是就有了部門(mén),各部門(mén)區(qū)分職責(zé),各司其職,你可能不知道單個(gè)人的工作細(xì)節(jié),但你只需要知道,銷(xiāo)售部負(fù)責(zé)賣(mài),研發(fā)部負(fù)責(zé)研發(fā),生產(chǎn)部負(fù)責(zé)生產(chǎn)......
面向?qū)ο缶幊桃鉀Q的根本問(wèn)題就是將程序系統(tǒng)化組織起來(lái),從而方便構(gòu)建龐大、復(fù)雜的程序。
1.2 編程語(yǔ)言和面向?qū)ο缶幊痰年P(guān)系
很多人往往把編程語(yǔ)言和面向?qū)ο舐?lián)系起來(lái),比如c語(yǔ)言是面向過(guò)程的語(yǔ)言,c++是面向?qū)ο蟮恼Z(yǔ)言,其實(shí)不完全準(zhǔn)確。
面向?qū)ο笫且环N設(shè)計(jì)思想,用c語(yǔ)言也可以完全實(shí)現(xiàn)面向?qū)ο?,用c++等語(yǔ)言寫(xiě)出的程序可能是也面向過(guò)程而非對(duì)象。
c語(yǔ)言實(shí)現(xiàn)面向?qū)ο笠粋€(gè)最明顯的例子就是Linux內(nèi)核,可以說(shuō)是完完全全采用了面向?qū)ο蟮脑O(shè)計(jì)思想,它充分體現(xiàn)了多個(gè)相對(duì)獨(dú)立的組件(進(jìn)程調(diào)度、內(nèi)存管理、文件系統(tǒng)……)之間相互協(xié)作的思想。盡管Linux內(nèi)核是用C語(yǔ)言寫(xiě)的,但是他比很多用所謂OOP語(yǔ)言寫(xiě)的程序更加OOP。
這里用c++說(shuō)明一下,為什么面向?qū)ο笳Z(yǔ)言也會(huì)寫(xiě)出面向過(guò)程的程序:
本段適合有一些c++基礎(chǔ)的朋友,不會(huì)c++可跳過(guò)不看。
比如一個(gè)計(jì)算總價(jià)的程序,無(wú)非是數(shù)目*單價(jià)
#include< iostream >
using namespace std;
class calculate{
public:
double price;
double num;
double result(void){
return price*num;
}
};
int main ()
{
calculate a;
a.price=1;
a.num=2;
cout<
增加一個(gè)功能,雙11,打八折,你會(huì)怎么寫(xiě)?
#include< iostream >
using namespace std;
class calculate{
public:
double price;
double num;
int date;
double result(void){
if(date==11)
return price*num*0.8;
else
return price*num;
}
};
int main ()
{
calculate a;
a.price=1;
a.num=2;
cout< "please input the date:"<
如果這樣寫(xiě),就是典型的面向過(guò)程思想,為什么?如果再加一個(gè)雙12打七折,按照這個(gè)思路怎么寫(xiě)?再在calculate類(lèi)里面加一個(gè)if else 判斷一下,如果來(lái)個(gè)過(guò)年打5折呢?再再在calculate類(lèi)里面加一個(gè)if else 判斷一下。我們?cè)賮?lái)看一下面向?qū)ο蟮乃枷朐撛趺磳?xiě):
#include< iostream >
using namespace std;
class calculate{
public:
double price;
double num;
virtual double result(void){
}
};
class normal:public calculate{
public:
double result(void){
return price*num;
}
};
class discount:public calculate{
public:
double result(void){
return price*num*0.8;
}
};
int main ()
{
calculate *a;
int date;
cout< "please input the date:"<
利用了繼承和多態(tài),把雙11抽象出一個(gè)單獨(dú)的類(lèi),繼承自calculate類(lèi),把平時(shí)normal也抽象出一個(gè)單獨(dú)的類(lèi),繼承自calculate類(lèi)。在子類(lèi)中提供result的實(shí)現(xiàn)。
如果來(lái)個(gè)雙12,該怎么寫(xiě)?再寫(xiě)一個(gè)雙12的類(lèi),繼承自calculate類(lèi)并實(shí)現(xiàn)自己的result計(jì)算。
有朋友可能疑惑了,你這在主函數(shù)main中不還是要進(jìn)行if else判斷嗎,和第一種有什么區(qū)別?
區(qū)別就在于,我添加新需求的時(shí)候不再需要修改原來(lái)的代碼,(原來(lái)的代碼指計(jì)算的核心部分)充分吸收了原代碼的特性。
當(dāng)我不需要某個(gè)功能時(shí),我把對(duì)應(yīng)的類(lèi)刪了就行,靈活、擴(kuò)展性強(qiáng)。
這里看著沒(méi)什么差別是因?yàn)檫@代碼簡(jiǎn)單,當(dāng)實(shí)現(xiàn)一個(gè)復(fù)雜的功能時(shí),代碼經(jīng)過(guò)測(cè)試后,就不應(yīng)該去動(dòng)他了,第一種方法不斷修改核心部分,帶來(lái)很大的隱患,而且若原來(lái)的代碼復(fù)雜度高,修改難度會(huì)很高。
這里要特別強(qiáng)調(diào),簡(jiǎn)單用面向?qū)ο缶幊陶Z(yǔ)言寫(xiě)代碼,程序也不會(huì)自動(dòng)變成面向?qū)ο?,也不一定能得到面向?qū)ο蟮母鞣N好處
所以面向?qū)ο笾卦谒枷?,而非編程語(yǔ)言,在第二節(jié)中,我將談?wù)劊琹inux內(nèi)核是如何用c語(yǔ)言體現(xiàn)面向?qū)ο笏枷氲摹?/p>
1.3 面向?qū)ο笫侵阜庋b、繼承、多態(tài)嗎?
其實(shí)從1.1舉例大公司的例子和1.2中舉例兩個(gè)c++程序不同的例子中,就可以看出來(lái),封裝、繼承、多態(tài)只是面向?qū)ο缶幊讨械奶匦?,而非核心思想?/p>
面向?qū)ο缶幊套罡镜囊稽c(diǎn)是屏蔽和隱藏
每個(gè)部門(mén)相對(duì)的獨(dú)立,有自己的章程,辦事方法和規(guī)則等。獨(dú)立性就意味著“隱藏內(nèi)部狀態(tài)”。比如你只能說(shuō)申請(qǐng)讓某部門(mén)按照章程辦一件事,卻不能說(shuō)命令部門(mén)里的誰(shuí)誰(shuí)誰(shuí),在什么時(shí)候之前一定要辦成。這些內(nèi)部的細(xì)節(jié)你看不見(jiàn),也管不著。
應(yīng)當(dāng)始終記住一件事情,面向?qū)ο笫菫榱朔奖銟?gòu)建復(fù)雜度高的大型程序。
2. Linux內(nèi)核中面向?qū)ο笏枷氲捏w現(xiàn)
2.1 封裝
封裝的定義是在程序上隱藏對(duì)象的屬性和實(shí)現(xiàn)細(xì)節(jié),僅對(duì)外公開(kāi)接口,控制在程序中屬性的讀和修改的訪問(wèn)級(jí)別;將抽象得到的數(shù)據(jù)和行為(或功能)相結(jié)合,形成一個(gè)有機(jī)的整體,也就是將數(shù)據(jù)與操作數(shù)據(jù)的源代碼進(jìn)行有機(jī)的結(jié)合,形成“類(lèi)”,其中數(shù)據(jù)和函數(shù)都是類(lèi)的成員。
面向?qū)ο笾械姆庋b,把數(shù)據(jù),和方法(函數(shù))放到了一起。
在c語(yǔ)言中,定義一個(gè)變量,int a,可以再定義很多函數(shù)fun1,fun2,fun3.
通過(guò)指針,這些函數(shù)都能對(duì)a修改,甚至這些函數(shù)都不一定與a在同一個(gè).c文件中,這樣就特別混亂。
但是我們也可以進(jìn)行封裝:
struct file {
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
spinlock_t f_lock;
enum rw_hint f_write_hint;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
略去一部分
}
例如Linux內(nèi)核中的struct file,里面有file的各種屬性,還包含了file_operrations結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體就是對(duì)file的一堆操作函數(shù)
struct file_operations {
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
int (*open) (struct inode *, struct file *);
略去一部分
}
file_operations結(jié)構(gòu)體里是一堆的函數(shù)指針,并不是真正的操作函數(shù),這是為了實(shí)現(xiàn)多態(tài)。
實(shí)際上這個(gè)例子也很像繼承,struct file繼承了struct file_operations的一切,但我會(huì)用其他例子來(lái)更好體現(xiàn)繼承。
2.2 繼承
特殊類(lèi)(或子類(lèi)、派生類(lèi))的對(duì)象擁有其一般類(lèi)(或稱(chēng)父類(lèi)、基類(lèi))的全部屬性與服務(wù),稱(chēng)作特殊類(lèi)對(duì)一般類(lèi)的繼承。
從繼承的思想和目的來(lái)看,就是讓子類(lèi)能夠共享父類(lèi)的數(shù)據(jù)和方法,同時(shí)又能在父類(lèi)的基礎(chǔ)上定義擴(kuò)展新的數(shù)據(jù)成員和方法,從而消除類(lèi)的重復(fù)定義,提高軟件的可重用性。
c語(yǔ)言中一個(gè)鏈表結(jié)構(gòu)如下:
struct A_LIST {
data_t data; // 不同的鏈表這里的data_t類(lèi)型不同。
struct A_LIST *next;
};
Linux 內(nèi)核中有一個(gè)通用鏈表結(jié)構(gòu):
struct list_head {
struct list_head *next, prev;
);
可以把這個(gè)結(jié)構(gòu)體看作一個(gè)基類(lèi),對(duì)它的基本操作是鏈表節(jié)點(diǎn)的插入,刪除,鏈表的初始化和移動(dòng)等。其他數(shù)據(jù)結(jié)構(gòu)(可看作子類(lèi))如果要組織成雙向鏈表,可以在鏈表節(jié)點(diǎn)中包含這個(gè)通用鏈表對(duì)象(可看作是繼承)。
同上面的例子,我們只需要聲明
struct A_LIST {
data_t data;
struct list_head *list;
};
鏈表的本質(zhì)就是一個(gè)線性序列,其基本操作為插入和刪除等,不同鏈表間的差別在于各個(gè)節(jié)點(diǎn)中存放的數(shù)據(jù)類(lèi)型,因此把鏈表的特征抽象成這個(gè)通用鏈表,作為父類(lèi)存在,具體不同的鏈表則繼承這個(gè)父類(lèi)的基本方法,并擴(kuò)充自己的屬性。
通用鏈表其作為一個(gè)連接件,只對(duì)本身結(jié)構(gòu)體負(fù)責(zé),而不需要關(guān)注真正歸屬的結(jié)構(gòu)體。正如繼承的特性,父類(lèi)的方法無(wú)法操作也不需要操作子類(lèi)的成員。
關(guān)于鏈表結(jié)構(gòu)的宿主指針獲取方法,
獲取結(jié)構(gòu)類(lèi)型TYPE里的成員MEMBER 在結(jié)構(gòu)體內(nèi)的偏移
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)- >MEMBER)
通過(guò)指向成員member的指針ptr獲取該成員結(jié)構(gòu)體type的指針
#define container_of(ptr, type, member) ({
const typeof(((type *)0)- >member)*__mptr = (ptr);
(type *)((char *)__mptr - offsetof(type, member)); })
**2.3 多態(tài) **
Linux中多態(tài)最明顯的例子就是字符設(shè)備應(yīng)用程序和驅(qū)動(dòng)程序之間的交互,應(yīng)用程序調(diào)用open,read,write等函數(shù)打開(kāi)設(shè)備即可操作,而并不關(guān)心open,read,write是如何實(shí)現(xiàn),這些函數(shù)的實(shí)現(xiàn)在驅(qū)動(dòng)程序之中,而不同設(shè)備的open、read、write函數(shù)各不相同,實(shí)現(xiàn)與多態(tài)中的運(yùn)行時(shí)多態(tài)一樣的功能。
過(guò)程簡(jiǎn)化其實(shí)就是不同的驅(qū)動(dòng)程序?qū)崿F(xiàn)
struct file_operations drv_opr1
struct file_operations drv_opr2
struct file_operations drv_opr3
而應(yīng)用程序運(yùn)行時(shí)根據(jù)設(shè)備號(hào)找到對(duì)應(yīng)的struct file_operations,并將指針指向他,即可調(diào)用對(duì)應(yīng)的struct file_operations里的open,read,write函數(shù)(實(shí)際過(guò)程比這復(fù)雜)。
一個(gè)帶有面向?qū)ο箅r形的c程序:
#include< stdio.h >
double normal_result(double price,double num)
{
return price * num;
}
double discount_result(double price,double num)
{
return price * num * 0.8;
}
struct calculate{
double price;
double num;
double (*result)(double price,double num);
};
int main ()
{
struct calculate a;
int date;
a.price=1;
a.num=2;
printf("please input the date:n");
scanf("%d",&date);
if(date==11)
a.result=discount_result;
else
a.result=normal_result;
printf("%lfn",a.result(a.price,a.num));
return 0;
}
評(píng)論
查看更多