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 *
是相同的,這對於絕大多數型別來說都會出現錯誤。所以在使用的時候必須知道該泛型型別原來所佔的長度,我們需要一個名為mallocsize
的int
型別形參來告訴我們這個值,在計算指標偏移的時候乘以它。這就相當於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 中的一個概念去闡述。就像剛剛那個問題,既然我的快速排序是泛型的,那麼怎麼保證實際傳入泛型引數一定是可比較的呢?舉個例子,顯然
int
、float
、double
是可以進行比較的,char
使用ASCII編碼方案的比較我們也理解,String
型別甚至也是可以比較的。但是如果在其他語言中,物件之間如何進行比較呢?這就是個問題了。在C++中我們可以進行運算子過載,這樣就仍舊可以使用比較運算子,藉助運算子過載函式來完成。不過對於Java、Objective-C這種語言該怎麼辦?而且如果傳入的泛型引數沒有實現對應的運算子過載函式怎麼辦?這時候就要引入一個協議的概念,簡單來說就是,如果某個型別想要作為排序泛型函式的泛型引數,那你必須實現可比較的協議。這個協議在Swift語言中就稱為Comparable
,這樣的話在編譯的時候,編譯器才知道這個泛型引數是可以進行比較的,這樣才能完成我們的操作,否則的話就會出現錯誤。這就是泛型中的協議問題。
0x06 總結
- C語言的泛型程式設計以
void *
作為泛型型別,本質上是泛型指標。 - C語言的泛型程式設計需要知道一個泛型型別變數所佔的記憶體大小,這個可以通過
sizeof
求得並傳入泛型函式。 - C語言的泛型程式設計中要注意陣列的偏移問題,
void *
的預設偏移是1,對於絕大多數型別來說都是錯誤的,需要自行程式設計轉換。 - C語言的泛型程式設計中使用
memcpy
函式進行泛型變數的拷貝和賦值。 - C語言的泛型程式設計中也需要注意協議問題,但是C中就只能自行編寫函式進行定義了,在其他語言中可以使用現成的介面或者協議。