2023年8月23日 星期三

C語言使用物件導向(面向對象) - 上

嚴格說起來C語言不是物件導向的語言,一般所認知C就不能寫物件導向的程式嗎?[C 語言] 程式設計教學:物件導向程式入門有提到,「C程式設計藝術」這本書前半部都是介紹C,開始介紹C++後才引入物件導向的概念,讓初學者覺得:只有C++以後的語言才能用物件導向,C是沒有辦法的,很不幸的,我剛開始學C語言就是用這本螞蟻書。

看看下面一段程式碼:

#include <stdio.h>
#include <conio.h>

main()
{
    FILE *fp;
    char ch;
    fp = fopen("hello.txt", "w");
    printf("Enter data");
    while( (ch = getchar()) != EOF) {
          putc(ch, fp);
    }
    fclose(fp);
}

對初學者來說,fp是什麼?一個指標。為什麼是一個指標?fopen返回一個fp指標,看來是一個規定,我就死背。有open也有close,嗯,是對稱的,為什麼?我也背起來。putc代入的引數也有fp。這個fp好特別,為什麼可以有這樣的騷操作?

如果有學過C++語言,C++的初學都知道會有一個建構子,也會有一個解構子,也就是物件需要建立,結束也需要銷毀,中間可以對物件做不同的運算。上面的fp是一個物件嗎?fopen回傳一個handle,然後putc對handle操作運算,結束後用fclose對handle做關閉。若把handle看成是一個物件,fopen建立並回傳物件,putc對物件操作,fclose則把物件銷毀。這一切似乎就說得通。只是差在C++我不用拿這個handle,而C語言需要額外帶入這個handle值。

handle中文翻成控制碼,好神奇,是裡面有系統嗎?是誰分配一個神奇的控制碼?它是一個數字,這數字通常也不是一個流水號,他到底是什麼?

handle其實是一個記憶體位址而已,位址說到底也是一個數字,我只要知道這個記憶體位址,就會知道這個物件是放在哪。那這個記憶體位址是誰分配的?其實他也只是C語言而已,C語言也有一個語法是需要對稱呼叫的,想起來了嗎?若不做對稱處理的話可能造成問題,那就是malloc & free。

這個就要講到在1966年以前,Dahl和Nygaard將函式呼叫堆疊框架(stack frame)移到了累堆(heap)中,並發明了OO(Uncle Bob教你如何建構好的軟體架構)。從stack移到heap是一個關鍵的觀念,function裡面的區域變數都是放在stack中的,有什麼方法可以讓function已經返回,而記憶體空間還依舊存在的?除了該死盡量不要用的全域變數,就是使用malloc後,產生的一個記憶體位址,這個位址是存放於heap中,而這個位址,即為物件的handle。所以這個函式就變為constructor,裡面的區域變數則成為instance variables,內部巢狀函式成為method,若使用函式指標,則可以實現多型。

還記得物件導向的三大特性嗎?上段最後提到的多型(polymorphism),還有封裝(encapsulation)、繼承(inheritance)。C語言又如何實現這三大特性?

封裝

下面是用C++寫的一段以class封裝起來的程式碼:

point.h
class Point {
public:
  Point(double x, double y);
  double distance(const Point& p) const;
 
private:
  double x;
  double y;
};
point.cc
#include "point.h"
#include <math.h>

Point::Point(double x, double y)
: x(x), y(y)
{}

double Point::distance(const Point& p) const {
  double dx = x-p.x;
  double dy = y-p.y;
  return sqrt(dx*dx + dy*dy);
}

封裝就是將「資料」和「函式」封裝,讓資訊隱藏。上面的header檔中的class, public, 及private很明顯都是C++的關鍵字,在C語言中如何做到這樣?

其實在C語言就做到了「完美封裝」,即私有資料成員就放在.c中,若要嚴格禁止caller呼叫.c中的變數及函式,像是使用C++中的private關鍵字,在變數及函式前面加'static'即可達到;公有函式成員就在.h中,供caller使用,這就是C++裡面所用的public關鍵字,而class就簡單用struct取代。C++有一項規定破壞了「完美封裝」,因技術原因,C++編譯器需要在該類別的標頭檔中宣告一個類別的成員變數,而C語言不需要這麼做,他乖乖的放在.c中就好,有時我們會給caller函式庫檔及標頭檔,這樣封裝起來的struct,caller不會知道裡面有什麼成員。我們看看C語言的寫法:

point.h
struct Point;
struct Point* makePoint(double x, double y);
double distance (struct Point *p1, struct Point *p2);
point.c
#include "point.h"
#include <stdlib.h>
#include <math.h>

struct Point {
  double x,y;
};

struct Point* makepoint(double x, double y) {
  struct Point* p = malloc(sizeof(struct Point));
  p->x = x;
  p->y = y;
  return p;
}

double distance(struct Point* p1, struct Point* p2) {
  double dx = p1->x - p2->x;
  double dy = p1->y - p2->y;
  return sqrt(dx*dx+dy*dy);
}

header files宣告資料結構與函式;implementation files則是實作。可以注意到point.c中的struct Point,裡面的x,y即為attribute,makepoint即為constructor,distance即為method。在makepoint中,會使用malloc產生一塊struct Point*樣式及大小的記憶體位址,並回傳返回給call makepoint這支function的caller,這個位址即為handle,即代表物件。一句話說明,控制碼(handle)在C語言物件程式設計中用來描述或表示一個「物件」,本質是一個指向某個資料結構的指標。

Uncle Bob說:因為C++需要在標頭檔中宣告一個類別的成員變數,進而caller會知道成員變數的相關資訊,所以編譯器要阻止使用者對它們的存取,才會有加入 public, private和protected等關鍵字的事情。所以當初C++好像很好意的加了這些關鍵字,讓語言多了很多功能,原來才知道是一個不得不的原因,並且破壞C原有的完美封裝,無法將一個類別的宣告(declaration)和定義(definition)分離。他說OO對於封裝可說是一點貢獻都沒有。

所以搞清楚了嗎?不是C語言沒有封裝特性,是有,而且還是完美封裝。

參考資料:

Clean Architecture無瑕的程式碼 
嵌入式Linux上的C語言編程實踐 Embedded C by Michael Pont

0 意見:

張貼留言