restrict 和 inline 在C語言中的使用,但是后者可能還能帶來些許理解上的便利,開啟 -O3 優(yōu)化是一個很不錯的選擇。inline 的作用還是在于和 static 一起使用,讓小函數(shù)盡可能的減小開銷甚至消除函數(shù)開銷。restrict 最重要的還是在于編譯器的優(yōu)化上。編譯器能夠為我們的程序提供優(yōu)化,這是眾所周知的,但是編譯器是如何優(yōu)化的,知道的人少之又少,其中有一些優(yōu)化是建立在編譯器能夠理解你的代碼,或者說編譯器要認為你的代碼是可以被優(yōu)化的情況下,才會采取優(yōu)化措施:
什么是指針別名?
void tmp_plus(int * a, int * b)
{
for(int i = 0; i < b_len;++i)
*a += b[i];
}
這段代碼中,a, b 是兩個被傳入的指針,編譯器對他們毫無所知,也不知道a是否在b的范圍之內,故無法對其做出最大程度上的優(yōu)化,這會導致什么結果呢?也就是,每依次循環(huán)過后,*a的結果都會寫回到主存當中去,而不是在寄存器里迅速進行下一次增加!
或者有的聰明的編譯器可以將其擴展成if ... else的加長版形式來避免寫回操作。
但是如果我們增加了restrict
void tmp_plus(int * restrict a, int * restrict b) ...
這就是告訴編譯器,這兩個指針是完全不相干的,你可以放心的優(yōu)化,不會出錯。
但是在這里有一些小的問題,那就是C++并不支持這個關鍵字,這會導致什么后果?
Visual Studio下編程的時候會發(fā)現(xiàn)使用restrict關鍵字是會產生編譯錯誤的,無論你使用 .c 還是 .cpp,難道說不支持嗎?實際上不是,主流的編譯器都對這個關鍵字有自己的實現(xiàn)__restrict__restrict__volatile,當時對其的解釋就是讓編譯器不對其進行優(yōu)化的意思,這里再說清楚一點
volatile int i = 0;i 的時候,是從它的內存位置讀取,每次對它操作完畢后,將結果寫回它的內存位置,而不是將其優(yōu)化保存在寄存器內。volatile的功能,內存屏障是由操作系統(tǒng)提供的功能,目的是防止由于某些優(yōu)化,導致的指令重排的效果。在常見C中,數(shù)組是這樣的。
int arr_1[3];
int arr_2[] = {1, 2, 3}; /* 創(chuàng)建三個元素的數(shù)組 */
C99之后,可以使用一種叫做 復合文字(Compound Literal)的機制來做到更多的事情,最簡單的就是創(chuàng)建匿名數(shù)組(看著有點像C++11引進的 Lambda匿名函數(shù)):
int *ptoarr = (int[]){1, 2, 4}; /* 之后可以使用 ptoarr 操作 */
ptoarr[2] = 0;
printf("The Third number is : %d", ptoarr[2]);
輸出: $ The Third number is : 0
當然,這種機制并不是只能如此使用,稍微高級一點的應用是,可以傳遞數(shù)組了,無論是按參數(shù)傳遞還是返回值。
int *test_fun(int most[], int length){
for(int i = 0;i < length;++i)
most[i] = i;
return (int []){most[0], most[1], most[2], most[3]...};/* so on */
}
// main
test_fun((int []){6,6,6,6,6}, 5);
這也是自從更新了C99標準以后,可以講某個整體進行返回的例子,也包括結構體:
typedef struct compond{
int value;
int number;
int arrays[10];
}compond;
//假設有test_fun函數(shù)返回該結構體
...
return (combond){
1, // 給value
2, // 給number
{most[0], most[1], most[2], most[3]...}}; //給arrats
當然也可以構造完成之后再返回實體,不過這么做不如上面寫的效果好,原因前方已經提過。
稍微修改一下結構體,又是另一番情況:
typedef struct compond{
int value;
int number;
int arrays[]; /* 這里不再顯式聲明大小,也就無法構造實體 */
}compond;
這個方式很像前方提到的 前橋和彌的 越界結構體 的例子,只不過這個是一個在C標準允許的情況下,而前橋和彌則是利用一些C語言標準的漏洞達到目的。
在使用這種結構體的時候,首先要為其動態(tài)分配好空間,之后通過指針進行操作,也增建了內存泄漏的風險,所以仁者見仁智者見智了:
compond* ptocom = malloc(sizeof(compond) + num_you_want * sizeof(int));
/* 這樣就成功分配了足夠的空間 */
ptocom->arrays[0] = some_number;
...
free(ptocom);
ptocom = NULL;
這其實并不是這種機制的目的,我覺得這種復合文字機制的最大用處還是在于消除艱澀難懂的函數(shù)調用
例如有一個函數(shù)的參數(shù)列表及其之長,我們就應該考慮使用新機制結合結構體,來對這個函數(shù)重新修飾一番:
int bad_function(double price, double count, int number,
int sales, Date sale_day, Date in_day,
String name, String ISBN, String market_name,
); /* 實現(xiàn)省略 */
這種函數(shù),在陌生的他人拿到之后,一定頭疼不已,可以對它進行一些處理,來減輕使用時候的苦惱:
/* 首先使用宏進行包裹 */
#define good_function(...) {\
/* 使用這個宏作為接口,可傳入不限個數(shù)的參數(shù) */
接下來定義一個結構體,用于參數(shù)的接收。
/* 接收參數(shù)的結構體 */
typedef struct param{
double price; /* 銷售價格 */
double count; /* 折扣 */
int number; /*總數(shù)量*/
int sales; /*銷售數(shù)量*/
Date sale_day; /* 銷售日期 */
Date in_day; /* 進貨日期 */
String name; /* 貨物名稱 */
String ISBN; /* ISBN號 */
String market_name; /* 銷售市場 */
}param;
/* 并配上文檔說明每個參數(shù)的作用 */
其次繼續(xù)完成宏
/* 此時將函數(shù)的聲明改為: */
int bad_function(param input);
/* 宏 */
#define good_function(...) {\
bad_function((param){__VA_ARGS__});\
}
這就完成了包裹
使用的時候:
good_function(.price = 199.9, .count = 0.9,
.number = 999, .sale = 20 /*and so on*/)
也可以在宏利使用默認參數(shù),以此來減少一些不必要的工作量,達到像其他高級語言一樣的函數(shù)默認參數(shù)的功能。當然如果不添加默認的值,則會按照標準將其值初始化為 0 或者 NULL.
#define good_function(...) {\
bad_function((param{.price = 100.0, .count = 1.0, __VA_ARGS__})); \
/* 假設想要設置默認價格為100, 默認折扣為 1.0 */\
}
較之C89(C90)的提取可變宏參數(shù)要來的更加靈活及"高效"。
至于 __VA_ARGS__ 宏的較為官方的用法,前人之述備矣,就不在這里記錄了。
只看名字就能明白這是C語言支持泛型的兆頭。
好像很有意思
不過某些地方依舊有些限制,比如對于選擇函數(shù)方面。
/* -std=c11 */
void print_int(int x) {printf("%d\n", x);}
void print_double(double x) {printf("%f\n", x);}
void print(){printf("Or else, Will get here\n");}
#define CHOOSE(x) _Generic((x),\
int : print_int,\
double : print_double,\
default : print)(x)
調用它
int main(void)
{
CHOOSE(11.0); /* 11.000000 */
CHOOSE(11.0f); /* Or else, Will get here */
return 0;
}
缺點就在于,: 后面無法真正的調用函數(shù),而是只能寫上函數(shù)名或者函數(shù)指針, 當然為了突破這一點可以使用宏嵌套來間接實現(xiàn)這一點,但是歸根結底,無法在 : 后面調用函數(shù)。
#define CHOOSE(X) _Generic((x), \
int : prinf("It is Int")\
double : printf("It is double"))(x)
/* Compile Error! */
這樣做會導致編譯錯誤,編譯器會告訴你 CHOOSE并不是一個函數(shù)或者函數(shù)指針,看起來錯誤很無厘頭,實際上一想,你要是在 : 之后調用了函數(shù),那么左后一個括號該如何自處,唯一的辦法就是返回函數(shù)指針:
typedef void (*void_p_double)(double);
typedef void (*void_p_int)(int);
void print_detail_double(double tmp){
printf("The Double is %f\n", tmp);
}
void print_detail_int(int tmp){
printf("The Int is %d\n", tmp);
}
void_p_int print_int(){
printf("It is a Int! ");
return print_detail_int;
}
void_p_double print_double() {
printf("It is a Double! ");
return print_detail_double;
}
void print_default(){printf("Nothing Matching !\n");}
#define CHOOSE(x) _Generic((x),\
int : print_int(x),\
double : print_double(x),\
default : print_default)(x)
調用:
CHOOSE(11); /* It is a Int The Int is 11 */
CHOOSE(11.0); /* It is a Double The Double is 11.000000 */
CHOOSE(11.Of); /* Nothing Matching ! */
choose(11l); /* Nothing Matching ! */
對于宏而言,最新的編譯器支持,
#program once, 將這個放在頭文件中,就代表該頭文件只編譯一次,也就是說,可以替代原有的老式#ifdef的三段式保護,具體編譯器支持請查詢各編譯器。
但是現(xiàn)在,在這個C99(C11)世紀,早已經打破這個局限,無論是從程序員編寫的語法角度看,亦或者是從編譯器的優(yōu)化角度看,都不在需要特地的將一個實體表示為指針進行返回。
combine* ret_struct(combine* other){
/* 這里的參數(shù)也是指針,因為當時并不允許直接給結構體進行賦值 */
int value = other->filed_value;
/* SomeThing to do */
combine* p_local_ret_com = malloc(sizeof(combine));
/* 一系列安全檢查 */
return p_local_ret_com;
這在當下自然也是可以的,而且會有不錯的性能,但是。但是這也是C語言最令人詬病的地方,你卻深深的踏了進去。
盡量少用 malloc(calloc, realloc) 之類的內存操作函數(shù),是現(xiàn)代C編程的一個指標,在這個函數(shù)中,我們沒有辦法保證分配出去的內存能夠回收(因為就這個函數(shù)而言并沒有回收這個內存),雖然現(xiàn)代計算機(非特殊機器)的內存已經不在乎那幾十個甚至幾百個中等結構體的內存泄漏,但是內存泄露依然是C語言最嚴重的問題,沒有之一。
我們該做的就是盡量減少風險的發(fā)生率:
combine ret_struct(combine other){
/* C99之后,我們就開始允許直接給結構體賦值,
意味著可以直接返回結構體了 */
combine loc_ret_com; /* 如果沒有復合的結構體成員的話,各成員會自動初始化為0,不必擔心初始化問題 */·
/* Do SomeThing to 'loc_ret_com' with 'other' */
...
return loc_ret_com;
}
/* main */
int main(void)
{
combine preview = {...};
combine action = ret_struct(preview);
return 0;
}
這么做的目的自然是為了讓我們的風險降到最低,讓系統(tǒng)棧幫我們管理內存,包括創(chuàng)建->使用->回收,這個過程(就像被其他語言所津津樂道的GC機制,實際上C語言程序員可以選擇自己實現(xiàn)一個垃圾回收機制,在本系列的最后面可能會做一個簡易的回收機制供大家參考,但是首先讓我們看完風景,再用一個實際程序串聯(lián)起來后,再去考慮GC)不需要你來操心。
但是這真的是最好的形式了嗎?
讓我們回想一下C語言在調用函數(shù)的時候發(fā)生的某些事情,因為最開始的我們是從 main 函數(shù)的調用開始我們的程序.
ret_structmain的位置以及ret_struct的各種參數(shù)等等,其中有一個東西就是返回地址
ret_struct之后我們能夠順利的返回main調用它的位置繼續(xù)執(zhí)行= =static對象,即便是static或者全局變量情況也是一樣只是不符合這個假設結論罷了),所以在函數(shù)結束后,??臻g被回收,它就被默認的銷毀了(可以參考前橋和彌的書里有這個的解釋,實際上值并沒有真正被銷毀了,但是不允許再用,否則視為非法),但是我們是怎么接收到函數(shù)的返回值的?復制副本->銷毀本地的原身->將這個副本的值賦給外部接收的變量(沒有則銷毀)->銷毀副本這有什么問題,難道還有更好的方法?
那自然有啊
return對象的情況下,是可以省去副本的操作的也就是說:
/*改寫上方代碼*/
combine ret_struct(combine other){
other->filed_value = ...;
/* SomeThing to other */
return (combine){ .filed_value = other->filed_value
...};
}
如果這么寫,編譯器就知道,喲!你是想要把這個對象放到外邊使用是吧,那我懂了,就直接找到外邊接收這個值得變量地址,不再創(chuàng)建副本(其實還是創(chuàng)建,只不過不再銷毀而已),而是在那個變量地址中寫入這個對象。
C++同樣適用