在C語言中實現泛型程式設計

霧花_小路發表於2019-05-12

0x00 泛型程式設計概述

  • 泛型程式設計是一個非常常見的程式設計方式。主要目的是實現靜態聯編,使得函式可以接受不同型別的引數,並且在編譯的時候確定正確的型別。
  • 很多語言都對泛型程式設計提供了支援,比如在C++中可以使用函式模版和類模版來實現泛型程式設計;在Java、Objective-C或者C#等單根繼承的語言中,也可以使用類似java.lang.Object、NSObject等型別進行程式設計。在具有型別推斷功能(比如Swift)的程式語言中,更是可以直接使用泛型程式設計。
  • 不過C語言是高階語言程式設計的基礎語言,那如何在C語言中實現泛型程式設計,確實是一個問題。首先C語言不支援函式過載,不支援模版型別,所以實現起來確實比較困難。

0x01 泛型指標(void *)簡介

  • void *是C語言中的一種型別,大家都知道在大多數程式語言中,void型別都代表所謂的空型別,比如一個函式的返回一個空型別void ,這是很常見的用法。

注意:返回值為void 並不是沒有返回值,而是代表返回空型別,這就是你仍然可以在這些函式中使用return語句的原因。只有一些語言的建構函式和解構函式才沒有返回值,在這些函式中,不可以使用return語句,他們是有顯著的不同的,Objective-C是一門獨特的語言,它的類的初始化方法是一個普通方法,返回值是instancetype(當前類的指標型別)型別。

  • void *可能就稍微鮮為人知一些,void *在C語言中可以表示人任意型別的指標。畢竟對於記憶體單元的地址而言,所謂它儲存的資料型別,只是每次取出的位元組數不同而已,這些記憶體單元的地址本身並沒有什麼不同。下面會更好的體現這句話的含義。
  • void *的大小和普通型別的指標一樣,總是一個字,具體的大小因機器的字長而異,例如對於32位機器是4個位元組,對於64位機器是8個位元組。

我沒有考證過16位的8086機器上指標的大小,因為8086的地址是20位的,這個有興趣的話可以回去試一試。

個人認為指標的大小仍然是16位,因為20位是實體地址,而實體地址是由段地址和偏移地址計算出的,在彙編之後C語言的指標可能只是變成相對於段地址的偏移地址,畢竟對於8086而言資料一般總是在DS段中,而程式碼一般總是在CS段中。(斜體字代表尚未考證的說法)

  • 在C語言中,其他普通型別的指標可以自動轉換為void *型別,而void *型別一般只能強制轉換為其他普通型別的指標,否則會出現警告或錯誤。
  • 有一個特別大的坑就是關於所謂void *指向陣列的情況,這裡直接上程式碼解釋了。
void Swap(void *array, int x, int y, int mallocsize) {
    void *temp = malloc(mallocsize);
    memcpy(temp, array+mallocsize*x, mallocsize);
    memcpy(array+mallocsize*x, array+mallocsize*y, mallocsize);
    memcpy(array+mallocsize*y, temp, mallocsize);
    free(temp);
}
  • 這是一個比較經典的交換函式,藉助的是臨時變數temp,但是這個函式是泛型的,對於memcpy的使用稍後會介紹。需要注意的是,array指向一個陣列的話,不能直接用&array[x]或者array+x獲得指向第x個元素的地址,因為void *型別預設的指標偏移量是1,和char *是相同的,這對於絕大多數型別來說都會出現錯誤。所以在使用的時候必須知道該泛型型別原來所佔的長度,我們需要一個名為mallocsizeint型別形參來告訴我們這個值,在計算指標偏移的時候乘以它。這就相當於C++程式設計中的模版型別定義或者Java中的泛型引數了。
  • 同時要注意對於void *型別的指標,任何時候都不可以對其進行解引用運算(或者在課堂上老師習慣叫做“取內容”?),原因是顯然的:void型別的變數並不合法。所以如果想進行解引用運算,必須先將其轉換為普通型別的指標。用於陣列的時候還需要注意解引用運算子的優先順序是高於加法的,所以要加括號,比如這樣:
int a = *(array + mallocsize * x);
  • 這句程式碼完美的體現了C語言程式設計的醜陋。

0x02 sizeof運算子簡介

  • sizeof運算子相信學過C語言的朋友都不會陌生,但是sizeof是一個運算子估計就沒多少人知道了,返回的型別是size_t型別。sizeof運算子返回某個型別所佔用的空間大小。這裡只說一點就是,如果對一個指標型別或者陣列名(實際上陣列名就是指標常量嘛)求sizeof的話,返回結果總是一個字(見上面所述)。而對一個結構體型別求sizeof,並不是簡單的將結構體中各個型別的sizeof求和得到,而是要涉及到記憶體對齊問題,這裡不多做介紹了,詳細瞭解可以訪問:如何理解 struct 的記憶體對齊? – 知乎

0x03 memcpy函式簡介

  • memcpy是一個經常和void *配合使用的函式,其函式原型為:
void * memcpy(void *, const void *, size_t);
  • 所屬的標頭檔案為string.h,大家也看出來了,這個函式本身就是以void *型別作為引數和返回值,其實也很好理解,就是一個賦值的過程,進行記憶體拷貝。把第二形參指向的記憶體拷貝到第一形參,拷貝的位元組數由第三形參指定。當然了第三個引數一般通過sizeof運算子求出,這裡就不舉例子了。返回值我沒有研究過,也沒用過,如果有知道的朋友可以評論區交流。

0x04 C語言中實現泛型程式設計

  • 說了這麼多,還沒提到泛型程式設計。不過前面也提的差不多了,總體思想就是使用void *型別當作泛型指標,然後再輔以類似於mallocsize的引數指定所佔記憶體大小,所佔記憶體大小通過sizeof運算子求得,如果需要進行賦值的話,利用memcpy函式完成,下面就直接給一個例子出來,是泛型的快速排序演算法,說明這些問題:
#ifndef Compare_h
#define Compare_h

#include <stdio.h>
#include "JCB.h"

int IsGreater(void *x, void *y);
int IsGreaterOrEqual(void *x, void *y);
int IsSmaller(void *x, void *y);
int IsSmallerOrEqual(void *x, void *y);

#endif /* Compare_h */
//
//  Compare.c
//  Job-Dispatcher
//
//  Created by 路偉饒 on 2017/11/16.
//  Copyright © 2017年 路偉饒. All rights reserved.
//

#include "Compare.h"

int IsGreater(void *x, void *y) {
    return *(int *)x > *(int *)y;
}
int IsGreaterOrEqual(void *x, void *y) {
    return *(int *)x >= *(int *)y;
}
int IsSmaller(void *x, void *y) {
    return *(int *)x < *(int *)y;
}
int IsSmallerOrEqual(void *x, void *y) {
    return *(int *)x <= *(int *)y;
}
//
//  QuickSort.h
//  Job-Dispatcher
//
//  Created by 路偉饒 on 2017/11/16.
//  Copyright © 2017年 路偉饒. All rights reserved.
//

#ifndef QuickSort_h
#define QuickSort_h

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "Compare.h"

void QuickSort(void *array, int left, int right, int mallocsize);

#endif /* QuickSort_h */
//
//  QuickSort.c
//  Job-Dispatcher
//
//  Created by 路偉饒 on 2017/11/16.
//  Copyright © 2017年 路偉饒. All rights reserved.
//

#include "QuickSort.h"

void Swap(void *array, int x, int y, int mallocsize) {
    void *temp = malloc(mallocsize);
    memcpy(temp, array+mallocsize*x, mallocsize);
    memcpy(array+mallocsize*x, array+mallocsize*y, mallocsize);
    memcpy(array+mallocsize*y, temp, mallocsize);
    free(temp);
}

int QuickSortSelectCenter(int l, int r) {
    return (l+r)/2;
}
int QuickSortPartition(void *array, int l, int r, int mallocsize) {
    int left = l;
    int right = r;
    void *temp = malloc(mallocsize);
    memcpy(temp, array+mallocsize*right, mallocsize);
    while (left < right) {
        while ( IsSmallerOrEqual(array+mallocsize*left, temp) && left < right) {
            left ++;
        }
        if (left < right) {
            memcpy(array+mallocsize*right, array+mallocsize*left, mallocsize);
            right--;
        }
        while ( IsGreaterOrEqual(array+mallocsize*right, temp) && left < right) {
            right--;
        }
        if (left < right) {
            memcpy(array+mallocsize*left, array+mallocsize*right, mallocsize);
            left ++;
        }
    }
    memcpy(array+mallocsize*left, temp, mallocsize);
    return left;
}

void QuickSort(void *array, int left, int right, int mallocsize) {
    if (left>=right) {
        return;
    }
    int center = QuickSortSelectCenter(left, right);
    Swap(array, center, right, mallocsize);
    center = QuickSortPartition(array, left, right, mallocsize);
    QuickSort(array, left, center-1, mallocsize);
    QuickSort(array, center+1, right, mallocsize);
}
  • 這裡留了一個懸念,明明可以直接比較的,為什麼還要這麼麻煩使用好多函式完成,也就是關於Compare.h的用處的問題,下面會揭曉答案。

0x05 泛型的協議問題

  • 剛剛那個問題就涉及到了一個泛型的協議問題,我這裡是借用了Objective-C 中的一個概念去闡述。就像剛剛那個問題,既然我的快速排序是泛型的,那麼怎麼保證實際傳入泛型引數一定是可比較的呢?舉個例子,顯然intfloatdouble是可以進行比較的,char使用ASCII編碼方案的比較我們也理解,String型別甚至也是可以比較的。但是如果在其他語言中,物件之間如何進行比較呢?這就是個問題了。在C++中我們可以進行運算子過載,這樣就仍舊可以使用比較運算子,藉助運算子過載函式來完成。不過對於Java、Objective-C這種語言該怎麼辦?而且如果傳入的泛型引數沒有實現對應的運算子過載函式怎麼辦?這時候就要引入一個協議的概念,簡單來說就是,如果某個型別想要作為排序泛型函式的泛型引數,那你必須實現可比較的協議。這個協議在Swift語言中就稱為Comparable,這樣的話在編譯的時候,編譯器才知道這個泛型引數是可以進行比較的,這樣才能完成我們的操作,否則的話就會出現錯誤。這就是泛型中的協議問題。

0x06 總結

  • C語言的泛型程式設計以void *作為泛型型別,本質上是泛型指標。
  • C語言的泛型程式設計需要知道一個泛型型別變數所佔的記憶體大小,這個可以通過sizeof求得並傳入泛型函式。
  • C語言的泛型程式設計中要注意陣列的偏移問題,void *的預設偏移是1,對於絕大多數型別來說都是錯誤的,需要自行程式設計轉換。
  • C語言的泛型程式設計中使用memcpy函式進行泛型變數的拷貝和賦值。
  • C語言的泛型程式設計中也需要注意協議問題,但是C中就只能自行編寫函式進行定義了,在其他語言中可以使用現成的介面或者協議。

相關文章