1. 程式人生 > >在C語言中模擬含有預設引數的函式

在C語言中模擬含有預設引數的函式

寫C++程式碼的時候總想當然以為C中也有含預設引數的函式這種玩意兒(值得注意的是Java也不支援C#支援,Scala這種奇葩支援是不足為奇的),然後在編譯下面這段程式碼之後顏面掃盡TwT

?
default_args.c
1 2 3 4 5 6 7 8 9 #include "default_args.h" void printString(const char* msg,int size,int style){ printf("%s %d %d\n",msg,size,style); } int main(){ printString("hello"); printString(
"hello",12); printString("hello",12,bold); }
?
default_args.c
1 2 3 4 5 #include<stdio.h> enum{ plain=0,italic=1,bold=2 }; void printString(const char* msg,int size=18,int style=italic);
?
1 2 3 4 [email protected]$  clang default_args.c -o default_args In file included from default_args.c:1:
./default_args.h:12:42: error: C does not support default arguments ...

clang果然是人性化的編譯器,還會告訴我們真實的原因;不像gcc只會報出一堆慕名奇妙的error資訊,讀者不妨自己嘗試一下,這裡就不吐槽了。至於如果我們的目的在於只要編譯通過的話,那完全可以無節操地把這段程式碼當成C++程式碼,然後用clang++或g++來搞定這一切;最多隻是會報出一個warning(而如果把default_args.c換成default_args.cpp的話連clang++都不報任何警告):

?
1 clang: warning: treating
'c' input as 'c++' when in C++ mode, this behavior is deprecated

傳說中程式設計師只關心error而不管warning,那大可就此打住併到stackoverflow的這個thread上灌水一番。不過如果是那種閒著無聊且非常執著的話(或者是那種沒法用C++而只能選擇C的情況,抑或是那種考慮到在其他C的原始檔中用到printString()函式的情況),那不妨往下看。一個很容易想到的解決方案自然是過載函數了(先不管效率)。在default_args.c刪掉函式中的預設值並新增下面這段:

?
1 2 3 void printString(const char *msg,int size){ printString(msg,size,italic); }

但卻是:

?
1 2 [email protected]$  clang override_args.c -o override_args override_args.c:14:6: error: conflicting types for 'printString'

又一次顏面掃盡,C原來連過載函式都不支援>_<,弱爆了。沒轍了嗎?就不能猥瑣地模擬一下然後讓C語言程式設計師也享受一下預設引數的快感嗎?macro!C程式設計師的必殺技,一個被C++程式設計師吐槽無數的招數:-(,但卻是一個很優雅的解決方案^_^

?
macro.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include<stdio.h> enum{ plain=0,italic=1,bold=2 }; void printString(const char* message, int size, int style) { printf("%s %d %d\n",message,size,style); } #define PRINT_STRING_1_ARGS(message)              printString(message, 18, italic) #define PRINT_STRING_2_ARGS(message, size)        printString(message, size, italic) #define PRINT_STRING_3_ARGS(message, size, style) printString(message, size, style) #define GET_4TH_ARG(arg1, arg2, arg3, arg4, ...) arg4 #define PRINT_STRING_MACRO_CHOOSER(...) \ GET_4TH_ARG(__VA_ARGS__, PRINT_STRING_3_ARGS, \ PRINT_STRING_2_ARGS, PRINT_STRING_1_ARGS, ) #define PRINT_STRING(...) PRINT_STRING_MACRO_CHOOSER(__VA_ARGS__)(__VA_ARGS__) int main(int argc, char * const argv[]) { PRINT_STRING("Hello, World!"); PRINT_STRING("Hello, World!", 12); PRINT_STRING("Hello, World!", 12, bold); return 0; }

看到這麼一坨程式碼,估計沒幾個人喜歡——巨集這種對把原始碼看成白盒的程式設計師實在不友好的東西,然而卻讓所有的C程式設計師大受其益。不妨看一下NULL的定義:

?
1 #define NULL ((void *)0)

閒話不扯了,看看前段程式碼是怎麼回事;畢竟子曾曰過:舉一隅不以三隅反,則不復也。

macro本身也不是什麼見不得人的東西,說到底就是方便程式設計師偷懶的,在實現的最終目的上和函式沒有本質區別。這裡需要注意的是__VA_ARGS__這個東東。其洋名叫Variadic Macros,就是可變引數巨集,在這裡是配合“...”一起用的。可變參函式想必C程式設計師都不陌生,就是沒吃過豬肉也見過豬跑是吧,比如printf;這裡也有個tutorial。我們在這裡需要知道的是巨集定義(define)處的“...”是可以和巨集使用(use)處的多個引數一起匹配的。下面以PRINT_STRING("Hello, World!", 18);為例說明是怎麼展開的。

首先"Hello,World!", 12匹配PRINT_STRING_MACRO_CHOOSER(...)中的"...",於是被擴充套件成:

?
1 PRINT_STRING_MACRO_CHOOSER("Hello, World!", 12)("Hello, World!", 12);

PRINT_STRING_MACRO_CHOOSER("Hello, World!",12)又被擴充套件成

?
1 GET_4TH_ARG("Hello, World!", 12, PRINT_STRING_3_ARGS, PRINT_STRING_2_ARGS, PRINT_STRING_1_ARGS, )

所以整條語句被擴充套件成了

?
1 GET_4TH_ARG("Hello, World!", 12, PRINT_STRING_3_ARGS, PRINT_STRING_2_ARGS, PRINT_STRING_1_ARGS, )("Hello, World!", 12);

接下來看到的是匹配#define GET_4TH_ARG(arg1,arg2,arg3,arg4, ...)arg4的情況,"Hello,World!"匹配args1,12匹配arg2PRINT_STRING_3_ARGS匹配arg3PRINT_STRING_2_ARGS匹配arg4,而其餘, PRINT_STRING_1_ARGS, 的部分匹配了“...”,所以經過這一番擴充套件變成了

?
1 PRINT_STRING_2_ARGS("Hello, World!", 12);

即為

?
1 printString("Hello, World!", 12,1);

這樣一番折騰終於見到廬山真面目了。當然我們可以用gnu cpp檢視一下預處理的結果是不是這樣的(一般來講C和C++用preprocessor是一樣的)。

?
1 2 3 4 5 6 7 ... int main(int argc, char * const argv[]) { printString("Hello, World!", 18, italic); printString("Hello, World!", 12, italic); printString("Hello, World!", 12, bold); return 0; }

這也解釋了為什麼說用macro的解決方案是優雅的。不妨再看看生成的llvm的ir形式:

?
macro.ll
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 [email protected]$  clang macro.c -S -o - -emit-llvm ; ModuleID = 'macro.c' target datalayout = "e-p:32:32:32-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:32:64-f32:32:32-f64:32:64-v64:64:64-v128:128:128-a0:0:64-f80:32:32-n8:16:32-S128" target triple = "i386-pc-linux-gnu" @.str = private unnamed_addr constant [10 x i8] c"%s %d %d\0A\00", align 1 @.str1 = private unnamed_addr constant [14 x i8] c"Hello, World!\00", align 1 define void @printString(i8* %message, i32 %size, i32 %style) nounwind { %1 = alloca i8*, align 4 %2 = alloca i32, align 4 %3 = alloca i32, align 4 store i8* %message, i8** %1, align 4 store i32 %size, i32* %2, align 4 store i32 %style, i32* %3, align 4 %4 = load i8** %1, align 4 %5 = load i32* %2, align 4 %6 = load i32* %3, align 4 %7 = call i32 (i8*, ...)* @printf(i8* getelementptr inbounds ([10 x i8]* @.str, i32 0, i32 0), i8* %4, i32 %5, i32 %6) ret void } declare i32 @printf(i8*, ...) define i32 @main(i32 %argc, i8** %argv) nounwind { %1 = alloca i32, align 4 %2 = alloca i32, align 4 %3 = alloca i8**, align 4 store i32 0, i32* %1 store i32 %argc, i32* %2, align 4 store i8** %argv, i8*** %3, align 4 call void @printString(i8* getelementptr inbounds ([14 x i8]* @.str1, i32 0, i32 0), i32 18, i32 1) call void @printString(i8* getelementptr inbounds ([14 x i8]* @.str1, i32 0, i32 0), i32 12, i32 1) call void @printString(i8* getelementptr inbounds ([14 x i8]* @.str1, i32 0, i32 0), i32 12, i32 2) ret i32 0 }

很清爽的程式碼,令人心曠神怡吧。

廢了這麼大的力氣才做了這麼點事,還不如不用“預設引數”呢是吧?但是當把這個寫成庫的時候,或者以後要經常使用的話這就方便多了,且不容易出錯!

為了無聊起見,再看看default_org.h+default_org.c用clang++/g++編譯得到的llvm的ir:

?
default_args_cpp.ll
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 [email protected]$  clang++ default_args.c -S -o - -emit-llvm clang: warning: treating 'c' input as 'c++' when in C++ mode, this behavior is deprecated ; ModuleID = 'default_args.c' target datalayout = "e-p:32:32:32-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:32:64-f32:32:32-f64:32:64-v64:64:64-v128:128:128-a0:0:64-f80:32:32-n8:16:32-S128" target triple = "i386-pc-linux-gnu" @.str = private unnamed_addr constant [10 x i8] c"%s %d %d\0A\00", align 1 @.str1 = private unnamed_addr constant [6 x i8] c"hello\00", align 1 define void @_Z11printStringPKcii(i8* %msg, i32 %size, i32 %style) { %1 = alloca i8*, align 4 %2 = alloca i32, align 4 %3 = alloca i32, align 4 store i8* %msg, i8** %1, align 4 store i32 %size, i32* %2, align 4 store i32 %style, i32* %3, align 4 %4 = load i8** %1, align 4 %5 = load i32* %2, align 4 %6 = load i32* %3, align 4 %7 = call i32 (i8*, ...)* @printf(i8* getelementptr inbounds ([10 x i8]* @.str, i32 0, i32 0), i8* %4, i32 %5, i32 %6) ret void } declare i32 @printf(i8*, ...) define i32 @main() { %1 = alloca i32, align 4 store i32 0, i32* %1 call void @_Z11printStringPKcii(i8* getelementptr inbounds ([6 x i8]* @.str1, i32 0, i32 0), i32 18, i32 1) call void @_Z11printStringPKcii(i8* getelementptr inbounds ([6 x i8]* @.str1, i32 0, i32 0), i32 12, i32 1) call void @_Z11printStringPKcii(i8* getelementptr inbounds ([6 x i8]* @.str1, i32 0, i32 0), i32 12, i32 2) %2 = load i32* %1 ret i32 %2 }

從這裡的IR中我們至少可以得到兩點資訊:

  • C++編譯得到的函式名和C編譯得到的不一樣(事實上是很不一樣,可以參見name mangling),使用c++filt之後我們可以看到C++中的printString的簽名實際上是void @printString(char const*, int, int)(i8* %msg, i32 %size, i32 %style)而不再是void @printString(i8* %message, i32 %size, i32 %style)。同時這也解釋了為何在C中不會有函式的(靜態)過載(沒有OO自然動態過載更無從說起)——假設C有函式過載的話,會生成三個同名的函式,而C中呼叫函式時僅僅根據符號表中的函式名,這樣就會造成混亂。【TODO:動態過載實現機理】
  • 編譯得到的程式碼中是看不到任何預設建構函式的資訊的(同樣連enum的資訊也沒有了),3條call指令中我們得到的只不過是對應下面原始碼的指令(也沒有生成三個簽名不同但名字相同的函式printString())。
?
1 2 3 ​printString("hello",18,1); printString("hello",12,1); printString("hello",12,2);

到此為止,正文結束。下面貼搗鼓的一段含巨集的程式碼~~

?
foobar.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include<stdio.h> #include<stdarg.h> #define LOGSTRING(fm,...) printf(fm,__VA_ARGS__) #define MY_DEBUG(format,...) fprintf(stderr,NEWLINE(format),##__VA_ARGS__); #define NEWLINE(str) str "\n" #define GCC_DBG(format,args...) fprintf(stderr,format,##args) #define DEBUG(args) (printf("DEBUG: "), printf args) #define STRING(str) #str #define NULL 3 int main(int argc,char**argv){ LOGSTRING("Hello %s %s\n","Hong""xu","Chen"); MY_DEBUG("my debug") GCC_DBG("gcc dbg\n"); int n = 0; if (n != NULL) DEBUG(("n is %d\n", n)); puts(STRING(It really Compiles!)); return 0; }

相關推薦

C語言模擬含有預設引數函式

寫C++程式碼的時候總想當然以為C中也有含預設引數的函式這種玩意兒(值得注意的是Java也不支援而C#支援,Scala這種奇葩支援是不足為奇的),然後在編譯下面這段程式碼之後顏面掃盡TwT ? default_args.c 1 2 3 4 5 6 7 8 9 #include "default_

c語言關於變長引數函式的原理

     printf函式是在串列埠資訊列印中和串列埠命令列介面功能實現中經常用的函式。呼叫方式一般是這樣子的printf("cmd =%s\r\rn", cmd_str),printf("vol=%dmV, current=%dmA.", vol,current)

c語言的二級指標做函式引數

1.用指標做函式引數申請動態記憶體的問題 //如果函式引數是指標,不能用一級指標做函式引數實現申請動態記憶體 void getMemory(char *p, int num) { p = (char *)malloc(sizeof(char)*num); } void

C語言如何將陣列作為函式引數傳遞

今天覆習到c語言的陣列,看到這麼一個問題: 現在,你的程式要讀入兩個多項式,然後輸出這兩個多項式的和,也就是把對應的冪上的係數相加然後輸出。 程式要處理的冪最大為100。 輸入格式: 總共要輸入兩個多項式,每個多項式的輸入格式如下: 每行輸入兩個數字,第一個表

C語言的結構體內嵌函式

1.內嵌函式定義舉例:經過真實測試         在函式中宣告定義結構體 #include "fun_in_struct.h"int main(int argc, char **argv) { //定義結構體指標或者結構體變數,分別用->和

C++學習筆記——名稱空間&預設引數&函式過載&引用

C++學習筆記——名稱空間&預設引數&函式過載&引用 C++: 1.解決C語言中設計不好或者使用不是很方便的語法—>優化 2.增加新的語法特性 注:extern “C”:在C++工程中,將程式碼按照C語言的風格來編譯 C++關鍵字

C語言求字串長度的函式my_strlen()的幾種實現方法

C語言中求字串長度的函式的幾種實現方法 1.最常用的方法是建立一個計數器,判斷是否遇到‘\0’,不是’\0’指標就往後加一。 int my_strlen(const char *str) { assert(str != NULL);//此句判段str是否為空指標(事實上這條語

c語言自定義了一個函式,在main呼叫時提示找不到識別符號

解決方案一: 把定義的函式放在,main函式之前。 void f() { printf("Hello"); } main() { f(); } 解決方案二: 在main函式之前宣告。 void f(); main() { f

【linux CC語言常用的幾個函式的總結【一】

1、memset函式 定義變數時一定要進行初始化,尤其是陣列和結構體這種佔用記憶體大的資料結構。在使用陣列的時候經常因為沒有初始化而產生“燙燙燙燙燙燙”這樣的野值,俗稱“亂碼”。每種型別的變數都有各自的初始化方法,memset() 函式可以說是初始化記憶體的“萬能函式”,通常為新申請的記憶體進行初始化工作。

【linux CC語言常用的幾個函式的總結【二】

3、fgets 雖然用 gets() 時有空格也可以直接輸入,但是 gets() 有一個非常大的缺陷,即它不檢查預留儲存區是否能夠容納實際輸入的資料,換句話說,如果輸入的字元數目大於陣列的長度,gets 無法檢測到這個問題,就會發生記憶體越界,所以程式設計時建議使用 fgets()。fgets() 的原型為

C語言兩個反正切函式atan與atan2的區別

我們可以使用正切操作將角度轉變為斜率,那麼怎樣利用斜率來轉換為角度呢?可以利用斜率的反正切函式將他轉換為相應的角度.as中有兩個函式可以計算反正切,我們來看一下. 1、as中Math.atan() Math.atan()接受一個引數:用法如下: angel=Math.atan(slope)  angel為一

C語言常用的檔案操作函式

C函式庫中檔案操作函式: (1)fopen:開啟檔案 函式原型:FILE* fopen(char *path, char *mode); 函式引數:path----開啟檔名及其路徑      mode----r w a …… 函式返回:成功則返回指向該流的檔案指標,失敗則返回NULL並把錯誤存在errno中

c語言結構體在子函式的用法

在c語言中,結構體是一種很常用的資料結構,但是要用好卻又有許多疑惑。 假設下面這個是我們要用到的結構體 typedef struct DATA data; struct DATA{ int *A; data *next; data *last; } 首先來看一下這兩個句子

C語言關於獲取時間的函式,包括如果獲取微妙、毫秒級時間

  功 能:將時間格式化,或者說:格式化一個時間字串。我們可以使用strftime()函式將時間格式化為我們想要的格式。   原 型:size_t strftime(char *strDest,size_t maxsize,const char *format,const struct tm *timept

C語言利用封裝好的函式實現英文字母的大小寫轉換

在C語言中,利用tolower和toupper兩個函式實現英文字母的大小寫之間的轉換 範例1:將s字串內的小寫字母轉換成大寫字母 #include <ctype.h> int main(

C語言函式引數傳遞的兩種方式

問題及程式碼: /* *完成日期:2018.10.2 * *問題描述:c語言函式引數傳遞的兩種方式 * */ #include <stdio.h> void swap1(int x, int y); //對交換函式myswap1的提前宣告 (傳值

[C]C語言函式實現返回引數二進位制 1 的個數

通過C語言程式將十進位制數轉化成二進位制數,然後求出二進位制數中1的個數。 下面用三種方法來實現。來 方法一: 除2取餘法。對一十進位制數,用2輾轉相除至結果為1,將餘數和最後的1從下向上倒序寫就是對應的二進位制。 例如:十進位制數302轉化成二進位制。 302

C語言模擬實現strchr函式.即在一個字串查詢一個字元第一次出現的位置並返回

//模擬實現strchr函式.即在一個字串中查詢一個字元第一次出現的位置並返回 #include <stdio.h> //#include <string.h> #includ

C語言指標變數作為函式引數和一般變數作為函式引數的區別

函式的引數不僅可以是整型、浮點型、字元型等資料,還可以是指標型別。它的作用是將一個變數的地址傳送到另一個函式中。 指標變數作為函式引數和一般變數作為函式引數是有區別的,對於這種區別初學者一般都很迷惑。下面我將就一個簡單的例子來說明一下它們的區別。看透以後也許也就不那麼疑惑了。

c語言模擬實現strchr函式,功能:在一個字串查詢一個字元第一次出現的位置,如果沒有出現返回NULL

// 模擬實現strchr函式,功能:在一個字串中查詢一個字元第一次出現的位置,如果沒有出現返回NULL #include <stdio.h> #include <assert.h> char const* my_strchr(char cons