2023年8月24日 星期四

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

struct在C語言中是一個很好用的關鍵字,基本就是要放不同型態的資料,但說到底,它其實就是表格上的標頭,在沒有實例化前,就只有標頭。以下說明三種狀況:

1.宣告struct真的只有標頭而已,你沒有實例化,就沒有辦法放資料,只是一個空白的表格。

2.實例化並且表格裡面只有放一組資料,這個就比較像物件,一個東西就只有一種屬性。

3.實例化並且表格裡面只有放多組資料,這就是我們平常看到的表格,我們若要設定規格書上的表格,用結構陣列可以輕鬆達成。

有了上面的觀念後,我們來說明繼承在現實生活怎麼實現,我們以表格來說明

繼承

想像公司就存在一些表格,之前維護表格的人離職了,或是主管要你優化原本的表格,無中生有最難了,一般的做法會去拿原本的表格,或是網路上找相關的表格來參考,這樣只要繼承原本表格的優點,再擴大為現有的需求,如下圖:

還有一種狀況很常見,公司提供的表格是基本款,不可能每一個部門就專門做一個表格,所以簡單的留下一個備註欄,可以讓底下各部門自由發揮,若只有簡單事項要備註,直接在備註欄填上資訊即可;若是備註欄不敷使用,底下的部門可以自製附件或表格,然後在備註欄寫下,附件#,這樣就會知道每一項其他需要知道的資訊,如下圖:

在「嵌入式Linux C語言程式設計實務」書中提到,實現繼承的兩種方式,分別為利用資料結構的包含實現繼承功能,以及利用私有指標實現繼承功能,專業上說得很拗口,實際上在生活上就是上面舉的例子。

* 利用資料結構的包含實現繼承功能

namePoint.h
struct NamedPoint;

struct NamedPoint* makeNamedPoint(double x, double y, char* name);
void setName(struct NamedPoint* np, char* name);
char* getName(struct NamedPoint* np);
namePoint.c ① namePoint結構體好像它是Point結構體的衍生結構體一樣,因為NamedPoint的前兩個欄位的順序與Point相同。值得注意的是,字元指標也需要依照字串長度,malloc一段再做strcpy。
#include "namedPoint.h"
#include <stdlib.h>

struct NamedPoint {
  double x,y;
  char* name;
};

struct NamedPoint* makeNamedPoint(double x, double y, char* name) {
  struct NamedPoint* p = malloc(sizeof(struct NamedPoint));
  p->x = x;
  p->y = y;
  p->name = malloc(strlen(name) + 1);
  strcpy(p->name, name);
  return p;
}
void setName(struct NamedPoint* np, char* name) {
  np->name = name;
}
char* getName(struct NamedPoint* np) {
  return np->name;
}
main.c 將NamedPoint參數(arguments)轉型為Point型態。在一個真正的OO語言中,這種向上轉型是隱式的(implicit)。
#include "point.h"
#include "namedPoint.h"
#include <stdio.h>
int main(int ac, char** av) {
  struct NamedPoint* origin = makeNamedPoint(0.0, 0.0, "origin");
  struct NamedPoint* upperRight = makeNamedPoint (1.0, 1.0, "upperRight");
  printf("distance=%f\n", distance((struct Point*) origin, 
                                   (struct Point*) upperRight));
}
另外一種包含的寫法,可以注意到是直接在新的struct包原本的struct,而在建構子上賦予初始值,就需要轉兩層,才能存取到x及y值。 namePoint.c ②
#include "namedPoint.h"
#include 

struct NamedPoint {
  struct Point point;
  char* name;
};

struct NamedPoint* makeNamedPoint(double x, double y, char* name) {
  struct NamedPoint* p = malloc(sizeof(struct NamedPoint));
  p->point.x = x;
  p->point.y = y;
  p->name = malloc(strlen(name) + 1);
  strcpy(p->name, name);
  return p;
}
void setName(struct NamedPoint* np, char* name) {
  np->name = name;
}
char* getName(struct NamedPoint* np) {
  return np->name;
}
此包含的方法,namePoint結構體是對Point結構體的擴充,增加了name成員,本質上是利用資料結構儲存的實際位置相同的特性。如下圖:

* 利用私有指標實現繼承功能

在原有的struct Point,加了一個void型別的指標,並名為private,就是像上面所提到的備註欄。此時不需要一個單獨的NamePoint資料結構,每一個生成的物件其區別在於私有資料指向的內容是不一樣的,所以可以任意擴充物件的特性。
struct Point {
  double x,y;
  void* priv;
};
私有資料定義如下:
struct NamedPointPriv {
  char* name;
};
下面程式碼要注意的是會為私有指標分配記憶體。 namePoint.c ③
struct NamedPoint* makeNamedPoint(double x, double y, char* name) {
  struct NamedPoint* p = malloc(sizeof(struct NamedPoint));
  p->x = x;
  p->y = y;
  p->priv = malloc(sizeof(struct NamedPointPriv));
  (p->priv)->name = malloc(strlen(name) + 1);
  strcpy((p->priv)->name, name);
  return p;
}
私有指標是一個地址,32位元系統中,就是一個32位元的整數,指標所佔用的儲存空間也是32位元的整數,若擴充部份的資訊放在一個32位元整數的儲存空間內,就不需要再在heap上分配(malloc)出另一塊空間。這就是上面所說的,若不用附件,簡單寫在備註欄即可,不用malloc,也代表現實生活中不用再拿一張白紙印附件表格。 namePoint.c ④
struct NamedPoint* makeNamedPoint(double x, double y, char* name) {
  struct NamedPoint* p = malloc(sizeof(struct NamedPoint));
  p->x = x;
  p->y = y;
  p->priv = 0;
  return p;
}

Uncle Bob在繼承的結論說:OO對於繼承有半分貢獻,可以做多重繼承,使資料結構的偽裝變得更方便。

在linux中的struct file,可以看到這兩個技巧。

struct file {
    struct list_head        f_list;
    struct dentry           *f_dentry;
    
    struct vfsmount         *f_vfsmnt;
    struct file_operations  *f_op;
    /*指向file_operations結構*/
    atomic_t                f_count;
    unsigned int            f_flags;

    mode_t                  f_mode;
    /*包含可讀可寫的權限資訊*/
    int                     f_error;
    loff_t                  f_pos;
    /*目前的讀寫位置*/
    struct fown_struct      f_owner;
    unsigned int            f_uid, f_gid;
    struct file_ra_state    f_ra;

    unsigned long           f_version;
    void                    *f_security;

    /* needed for tty driver, and maybe others */
    void                    *private_data;
    /*
    驅動程式可自行運用此指標,典型用法是讓他指向一塊私有資料區
    如果有用到這個,release時要記得釋放此指標的記憶體
    非常好用的功能
    */
#ifdef CONFIG_EPOLL
    /* Used by fs/eventpoll.c to link all the hooks to this file */
    struct list_head        f_ep_links;
    spinlock_t              f_ep_lock;
#endif /* #ifdef CONFIG_EPOLL */
    struct address_space    *f_mapping;
};

參考資料:

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

0 意見:

張貼留言