面向對象編程(OOP),是一種設計思想或者架構風格。OO語言之父Alan Kay,Smalltalk的發明人,在談到OOP時是這樣說的:
OOP應該體現一種網狀結構,這個結構上的每個節點“Object”只能通過“消息”和其他節點通訊。每個節點會有內部隱藏的狀態,狀態不可以被直接修改,而應該通過消息傳遞的方式來間接的修改。
以本段話作為開場,展開本文的探討。
1. 面向對象的實質
1.1 面向對象編程是為了解決什么問題?
大部分程序員都學過c語言,尤其是嵌入式工程師,可能只會c語言。
c語言是一項典型的面向過程的語言,一切都是流程。簡單的單片機程序可能只有幾行,多的也不過幾百行。這時一個人的能力是完全可以走通整個代碼,但引入操作系統后,事情就變得復雜了。
進程調度、內存管理等各種功能使得代碼量劇增,一個簡單的RTOS實時操作系統都達到了上萬行代碼,這個時候,走通也不是不行,就是比較吃力。
但Linux代碼量就完全不是正常人能讀完的,一秒鐘讀一行代碼,每天讀12小時,也需要幾年才能讀完Linux的源碼,這還僅僅是讀完,而不是理解。
再舉一個簡單的例子
小公司往往只有幾個人,大家在一起干活,你完完全全可以看到每個人在干什么。
在大公司中呢?幾千、上萬人的公司中,你要去弄清楚,每個人在干嘛,這是完全不可能的。于是就有了部門,各部門區分職責,各司其職,你可能不知道單個人的工作細節,但你只需要知道,銷售部負責賣,研發部負責研發,生產部負責生產......
面向對象編程要解決的根本問題就是將程序系統化組織起來,從而方便構建龐大、復雜的程序。
1.2 編程語言和面向對象編程的關系
很多人往往把編程語言和面向對象聯系起來,比如c語言是面向過程的語言,c++是面向對象的語言,其實不完全準確。
面向對象是一種設計思想,用c語言也可以完全實現面向對象,用c++等語言寫出的程序可能是也面向過程而非對象。
c語言實現面向對象一個最明顯的例子就是Linux內核,可以說是完完全全采用了面向對象的設計思想,它充分體現了多個相對獨立的組件(進程調度、內存管理、文件系統……)之間相互協作的思想。盡管Linux內核是用C語言寫的,但是他比很多用所謂OOP語言寫的程序更加OOP。
這里用c++說明一下,為什么面向對象語言也會寫出面向過程的程序:
本段適合有一些c++基礎的朋友,不會c++可跳過不看。
比如一個計算總價的程序,無非是數目*單價
#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<
增加一個功能,雙11,打八折,你會怎么寫?
#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:"<
如果這樣寫,就是典型的面向過程思想,為什么?如果再加一個雙12打七折,按照這個思路怎么寫?再在calculate類里面加一個if else 判斷一下,如果來個過年打5折呢?再再在calculate類里面加一個if else 判斷一下。我們再來看一下面向對象的思想該怎么寫:
#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:"<
利用了繼承和多態,把雙11抽象出一個單獨的類,繼承自calculate類,把平時normal也抽象出一個單獨的類,繼承自calculate類。在子類中提供result的實現。
如果來個雙12,該怎么寫?再寫一個雙12的類,繼承自calculate類并實現自己的result計算。
有朋友可能疑惑了,你這在主函數main中不還是要進行if else判斷嗎,和第一種有什么區別?
區別就在于,我添加新需求的時候不再需要修改原來的代碼,(原來的代碼指計算的核心部分)充分吸收了原代碼的特性。
當我不需要某個功能時,我把對應的類刪了就行,靈活、擴展性強。
這里看著沒什么差別是因為這代碼簡單,當實現一個復雜的功能時,代碼經過測試后,就不應該去動他了,第一種方法不斷修改核心部分,帶來很大的隱患,而且若原來的代碼復雜度高,修改難度會很高。
這里要特別強調,簡單用面向對象編程語言寫代碼,程序也不會自動變成面向對象,也不一定能得到面向對象的各種好處
所以面向對象重在思想,而非編程語言,在第二節中,我將談談,linux內核是如何用c語言體現面向對象思想的。
1.3 面向對象是指封裝、繼承、多態嗎?
其實從1.1舉例大公司的例子和1.2中舉例兩個c++程序不同的例子中,就可以看出來,封裝、繼承、多態只是面向對象編程中的特性,而非核心思想。
面向對象編程最根本的一點是屏蔽和隱藏
每個部門相對的獨立,有自己的章程,辦事方法和規則等。獨立性就意味著“隱藏內部狀態”。比如你只能說申請讓某部門按照章程辦一件事,卻不能說命令部門里的誰誰誰,在什么時候之前一定要辦成。這些內部的細節你看不見,也管不著。
應當始終記住一件事情,面向對象是為了方便構建復雜度高的大型程序。
2. Linux內核中面向對象思想的體現
2.1 封裝
封裝的定義是在程序上隱藏對象的屬性和實現細節,僅對外公開接口,控制在程序中屬性的讀和修改的訪問級別;將抽象得到的數據和行為(或功能)相結合,形成一個有機的整體,也就是將數據與操作數據的源代碼進行有機的結合,形成“類”,其中數據和函數都是類的成員。
面向對象中的封裝,把數據,和方法(函數)放到了一起。
在c語言中,定義一個變量,int a,可以再定義很多函數fun1,fun2,fun3.
通過指針,這些函數都能對a修改,甚至這些函數都不一定與a在同一個.c文件中,這樣就特別混亂。
但是我們也可以進行封裝:
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內核中的struct file,里面有file的各種屬性,還包含了file_operrations結構體,這個結構體就是對file的一堆操作函數
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結構體里是一堆的函數指針,并不是真正的操作函數,這是為了實現多態。
實際上這個例子也很像繼承,struct file繼承了struct file_operations的一切,但我會用其他例子來更好體現繼承。
2.2 繼承
特殊類(或子類、派生類)的對象擁有其一般類(或稱父類、基類)的全部屬性與服務,稱作特殊類對一般類的繼承。
從繼承的思想和目的來看,就是讓子類能夠共享父類的數據和方法,同時又能在父類的基礎上定義擴展新的數據成員和方法,從而消除類的重復定義,提高軟件的可重用性。
c語言中一個鏈表結構如下:
struct A_LIST {
data_t data; // 不同的鏈表這里的data_t類型不同。
struct A_LIST *next;
};
Linux 內核中有一個通用鏈表結構:
struct list_head {
struct list_head *next, prev;
);
可以把這個結構體看作一個基類,對它的基本操作是鏈表節點的插入,刪除,鏈表的初始化和移動等。其他數據結構(可看作子類)如果要組織成雙向鏈表,可以在鏈表節點中包含這個通用鏈表對象(可看作是繼承)。
同上面的例子,我們只需要聲明
struct A_LIST {
data_t data;
struct list_head *list;
};
鏈表的本質就是一個線性序列,其基本操作為插入和刪除等,不同鏈表間的差別在于各個節點中存放的數據類型,因此把鏈表的特征抽象成這個通用鏈表,作為父類存在,具體不同的鏈表則繼承這個父類的基本方法,并擴充自己的屬性。
通用鏈表其作為一個連接件,只對本身結構體負責,而不需要關注真正歸屬的結構體。正如繼承的特性,父類的方法無法操作也不需要操作子類的成員。
關于鏈表結構的宿主指針獲取方法,
獲取結構類型TYPE里的成員MEMBER 在結構體內的偏移
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)- >MEMBER)
通過指向成員member的指針ptr獲取該成員結構體type的指針
#define container_of(ptr, type, member) ({
const typeof(((type *)0)- >member)*__mptr = (ptr);
(type *)((char *)__mptr - offsetof(type, member)); })
**2.3 多態 **
Linux中多態最明顯的例子就是字符設備應用程序和驅動程序之間的交互,應用程序調用open,read,write等函數打開設備即可操作,而并不關心open,read,write是如何實現,這些函數的實現在驅動程序之中,而不同設備的open、read、write函數各不相同,實現與多態中的運行時多態一樣的功能。
過程簡化其實就是不同的驅動程序實現
struct file_operations drv_opr1
struct file_operations drv_opr2
struct file_operations drv_opr3
而應用程序運行時根據設備號找到對應的struct file_operations,并將指針指向他,即可調用對應的struct file_operations里的open,read,write函數(實際過程比這復雜)。
一個帶有面向對象雛形的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;
}
評論
查看更多