在C语言中模拟含有默认参数的函数

在C语言中模拟含有默认参数的函数

nonoob write

写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
nonoob@nonoobPC$  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
nonoob@nonoobPC$  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
nonoob@nonoobPC$  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
nonoob@nonoobPC$  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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值