Copyright (慣C) 2017, 2019 宅色夫
void naive_transpose(int *src, int *dst, int w, int h)
{
for (int x = 0; x < w; x++)
for (int y = 0; y < h; y++)
*(dst + x * h + y) = *(src + y * w + x);
}
使用方式如下:
int *src = (int *) malloc(sizeof(int) * TEST_W * TEST_H);
int *out2 = (int *) malloc(sizeof(int) * TEST_W * TEST_H);
naive_transpose(src, out2, TEST_W, TEST_H);
這有什麼問題呢?
typedef struct matrix_impl Matrix;
struct matrix_impl {
float values[4][4];
/* operations */
bool (*equal)(const Matrix, const Matrix);
Matrix (*mul)(const Matrix, const Matrix);
};
static Matrix mul(const Matrix a, const Matrix b) {
Matrix matrix = { .values = {
{ 0, 0, 0, 0, }, { 0, 0, 0, 0, },
{ 0, 0, 0, 0, }, { 0, 0, 0, 0, },
},
};
for (int i = 0; i < 4; i++)
for (int j = 0; j < 4; j++)
for (int k = 0; k < 4; k++)
matrix.values[i][j] += a.values[i][k] * b.values[k][j];
return matrix;
}
int main() {
Matrix m = {
.equal = equal,
.mul = mul,
.values = { ... },
};
Matrix o = { .mul = mul, };
o = o.mul(m, n);
這樣的好處是:
Matrix m
,可以很容易指定特定的方法 (method),藉由 designated initializers,在物件初始化的時候,就指定對應的實作,日後要變更 (polymorphism) 也很方便
equal
和 mul
函式實作時都標註 static
,所以只要特定實作存在獨立的 C 原始程式檔案中,就不會跟其他 compilation unit 定義的符號 (symbol) 有所影響,最終僅有 API gateway 需要公開揭露This encourages immutability, cultivates pure functions, and makes things simpler and easier to understand. It also improves safety by eliminating the possibility of a NULL argument.
void drink_mix(Drink * const drink, Ingredient const ingr) {
assert(drink);
color_blend(&(drink->color), ingr.color);
drink->alcohol += ingr.alcohol;
}
Drink drink_mix(Drink const drink, Ingredient const ingr) {
return (Drink) {
.color = color_blend(drink.color, ingr.color),
.alcohol = drink.alcohol + ingr.alcohol
};
}
不過仍是不夠好,因為:
values
欄位仍需要公開,我們就無法隱藏實作時期究竟用 float
, double
, 甚至是其他自訂的資料表達方式 (如 Fixed-point arithmetic)m.mul()
就注定會失敗matrixA * matrixB
,對應程式碼為 matO = matA.mul(matB)
,但在上述程式碼中,我們必須寫為 Matrix o = m.mul(m, n)
,後者較不直覺2.
,如果初始化時配置記憶體,那要如何確保釋放物件時,相關的記憶體也會跟著釋放呢?若沒有充分處理,就會遇到 memory leaks延伸閱讀:
const char *lookup[] = {
[0] = "Zero",
[1] = "One",
[4] = "Four"
};
assert(!strcasecmp(lookup[0], "ZERO"));
也可變化如下:
enum cities { Taipei, Tainan, Taichung, };
int zipcode[] = {
[Taipei] = 100,
[Tainan] = 700,
[Taichung] = 400,
};
前述矩陣操作的程式,我們期望能導入下方這樣自動的處理方式:
struct matrix { size_t rows, cols; int **data; };
struct matrix *matrix_new(size_t rows, size_t cols) {
struct matrix *m = ncalloc(sizeof(*m), NULL);
m->rows = rows; m->cols = cols;
m->data = ncalloc(rows * sizeof(*m->data), m);
for (size_t i = 0; i < rows; i++)
m->data[i] = nalloc(cols * sizeof(**m->data), m->data);
return m;
}
void matrix_delete(struct matrix *m) { nfree(m); }
其中 nalloc
和 nfree
是我們預期的自動管理機制,對應的實作可見 nalloc
複製字串可用 strdup 函式:
char *strdup(const char *s);
strdup 函式會呼叫 malloc 來配置足夠長度的記憶體,當然,你需要適時呼叫 free 以釋放資源。 [heap]
strdupa 函式透過 alloca 函式來配置記憶體,後者存在 [stack],而非 heap,當函式返回時,整個 stack 空間就會自動釋放,不需要呼叫 free。
char *strdupa(const char *s);
alloca
function is not in POSIX.1.alloca()
在不同軟硬體平台的落差可能很大,在 Linux man-page 特別強調以下:RETURN VALUE
The alloca() function returns a pointer to the beginning of the allocated space. If the allocation causes stack overflow, program behaviour is undefined.
延伸閱讀:
在 C++11 的 STL,針對使用需求的不同,提供了三種不同的 smart pointer,分別是:
unique_ptr
unique_ptr
物件管理的 smart pointer;當 unique_ptr
物件消失時,就會自動釋放資源。shared_ptr
shared_ptr
共用一份資源的 smart pointer,內部會記錄這份資源被引用的次數(reference counter),只要還有引用該資源的 shared_ptr
物件存在、資源就不會釋放;只有當所有引用這份資源的 shared_ptr
物件都消失的時候,資源才會自動釋放。weak_ptr
shared_ptr
使用的 smart pointer,和 shared_ptr
的不同點在於 weak_ptr
不會增加資源的引用計數,也就是說的 weak_ptr
存在與否不影響資源會不會被釋放掉。這些 smart pointer 都是 class template 的形式,所以適用範圍很廣泛;它們都是被定義在 <memory>
標頭檔、在 std
這個 namespace 下。
Implementing smart pointers for C
#define autofree \
__attribute__((cleanup(free_stack)))
__attribute__ ((always_inline))
inline void free_stack(void *ptr) { free(*(void **) ptr); }
int main(void) {
autofree int *i = malloc(sizeof (int));
*i = 1;
return *i;
}
Smart pointers for the (GNU) C: Allocating a smart array and printing its contents before destruction:
#include <stdio.h>
#include <csptr/smart_ptr.h>
#include <csptr/array.h>
// @param ptr points to the current element
void print_int(void *ptr, void *meta) { printf("%d\n", *(int *) ptr); }
int main(void) {
// Destructors for array types are run on every
// element of the array before destruction.
smart int *ints = unique_ptr(int[5],
{5, 4, 3, 2, 1},
print_int);
/* Smart arrays are length-aware */
for (size_t i = 0; i < array_length(ints); ++i)
ints[i] = i + 1;
return 0;
}
__attribute__((cleanup))
。但函式回傳的 unbound 物件及函式參數屬性皆無支援 __attribute__((cleanup))
。使用案例:
void f(int m, int C[m][m]) {
double v1[m];
...
#pragma omp parallel firstprivate(C, v1)
...
}
Randy Meyers (chair of J11, the ANSI C committee) 的文章 The New C:Why Variable Length Arrays?,副標題是 "C meets Fortran, at long last."
一個特例是 Arrays of Length Zero,GNU C 支援,在 Linux 核心出現多次
延伸閱讀: Zero size arrays in C
-fplan9-extensions
可支援 Plan 9 C Compilers 特有功能typedef struct S {
int i;
} S;
typedef struct T {
S; // <- "inheritance"
} T;
void bar(S *s) { }
void foo(T *t) {
bar(t); // <- call with implict conversion to "base class"
bar(&t->S); // <- explicit access to "base class"
}
-fms-extensions
編譯選項。見 GCC Unnamed Fields&t->S
或 type cast (S*)t
。但若用 transparent union,即可透過更漂亮的語法來實作:typedef union TPtr TPtr;
union TPtr {
S *S;
T *T;
} __attribute__((__transparent_union__));
void foo(TPtr t) {
t.S->s_element;
t.T->t_element;
}
T* t;
foo(t); // T * can be passed in as TPtr without explicit casting
typedef enum GenericType GenericType;
typedef struct A A;
typedef struct B B;
enum GenericType {
TYPE_A = 0,
TYPE_B,
};
struct A {
GenericType type;
...
};
struct B {
GenericType type;
...
};
union GenericPtr {
GenericType *type;
A *A;
B *B;
} __attribute__((__transparent_union__));
void foo (GenericPtr ptr) {
switch (*ptr.type) {
case TYPE_A:
ptr.A->a_elements;
break;
case TYPE_B:
ptr.B->b_elements;
break;
default:
assert(false);
}
}
A *a;
B *b;
foo(a);
foo(b);
Cello 在 C 語言的基礎上,提供以下進階特徵:
可寫出以下風格的 C 程式:
/* Stack objects are created using "$" */
var i0 = $(Int, 5);
var i1 = $(Int, 3);
var i2 = $(Int, 4);
/* Heap objects are created using "new" */
var items = new(Array, Int, i0, i1, i2);
/* Collections can be looped over */
foreach (item in items) {
print("Object %$ is of type %$\n",
item, type_of(item));
}
typeof 允許我們傳入一個變數,代表的會是該變數的型態。舉例來說:
int a;
typeof(a) b = 10; // equals to "int b = 10;"
char s[6] = "Hello";
char *ch;
typeof(ch) k = s; // equals to "char *k = s;"
typeof
大多用在定義巨集上,因為在巨集裏面我們沒辦法知道參數的型態,在需要宣告相同型態的變數時,typeof
會是一個很好的幫手。
以 max 巨集為例:
#define max(a, b) \
({typeof(a) _a = a; \
typeof(b) _b = b; \
_a > _b ? _a : _b;} \
)
至於為什麼我們需要將 max 的巨集寫成這樣的形式呢?為何不可簡單寫為 #define max(a,b) (a > b ? a : b)
呢?
這樣的寫法會導致 double evaluation 的問題,顧名思義就是會有某些東西被執行 (evaluate) 過兩次。
試想如下情況:
#define max(a, b) (a > b ? a : b)
void doOneTime() { printf("called doOneTime!\n"); }
int f1() { doOneTime(); return 0; }
int f2() { doOneTime(); return 1; }
int result = max(f1(), f2());
實際執行後,我們會發現程式輸出竟有3 次 doOneTime
函式,但在 max
的使用,我們只期待會呼叫 2 次?
這是因為在巨集展開後,原本 max(f1(), f2())
會被改成這樣的形式
int result = (f1() > f2() ? f1() : f2());
為了避免這個問題,我們必須在巨集中先用變數把可能傳入的函式回傳值儲存下來,之後判斷就不要再使用一開始傳入的函式,而是是用後來宣告的回傳值變數。
#define container_of(ptr, type, member) \
__extension__({ \
const __typeof__(((type *) 0)->member) *__pmember = (ptr); \
(type *) ((char *) __pmember - offsetof(type, member)); \
})
一步步拆解。首先看到的是 __extension__
,是一個修飾字,用來防止 gcc 編譯器產生警告。
什麼情況下,我們會想編譯器產生的警告?在編譯階段,編譯器可能會提醒我們,程式使用到非 ANSI C 標準的語句,我們開發的程式在現在的編譯器可能可以過,但是用其他的編譯器可能就不會過了。
在這邊,出問題的地方應該是在開頭的 ({})
(braced-group within expression),實際編譯過後我們可以看到這樣的警告訊息
warning: ISO C forbids braced-groups within expressions [-Wpedantic]
這是 gcc 一個 extension,透過這種寫法可以讓我們的 macro 更安全。
如果不想要看見這個警告,可在最前面加上 __extension__
修飾字。
再來的話看到 __typeof__(((type *) 0)->member) * __pmember
這段
可以看到我們用 ((type*)0)->member
的型態再宣告一個新的指標 __pmember
。
(type*)0
代表的是一個被轉型成 type
型態的 struct,這裡我們不用理會是否為一個合法的位址,所以可以直接寫 0
就好。再來我們讓他指向 member
,因此 __typeof__(((type*)0)->member)
代表的便是 member
的資料型態。
再往下看到 (type *) ((char *) __pmember - offsetof(type, member));
這段比較好解釋,其實就只是把 member 的位址扣除 member 在整個 struct 裡面的偏移後,得到整個 struct 的開頭位址而已。
以圖像的方式會長這樣