在C語言中實現泛型編程

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段中。(斜體字表明還沒有考證的說法)java

  • 在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中就只能自行編寫函數進行定義了,在其餘語言中可使用現成的接口或者協議。
相關文章
相關標籤/搜索