大家都知道 C++ 等面向對象的語言支持函數重載,C++ 實現函數重載很大程度上依賴與編譯器對函數名的 Mangling(損壞,破壞),即 C++ 的源代碼被編譯後同名的重載函數名字會被破壞,一般是在原函數名前後加上特定的字符串,以區分不同重載函數,然後在調用的時候根據參數的不同選擇合適的函數,如下代碼說明了編譯器是如何處理普通函數重載的:
#include <iostream>
usingnamespacestd;
intfunc(void)
{
cout<<"func without parameters"<<endl;
}
intfunc(intia)
{
cout<<"func with one int parameter:"<<endl;
cout<< ia <<endl;
}
intfunc(intia,floatfb)
{
cout<<"func with one int parameter and one float parameter"<<endl;
cout<< ia <<endl;
cout<< fb <<endl;
}
intmain()
{
func();
func(5);
func(5,5.0);
}
g++ -S ./t.cc
編譯後生成匯編代碼可能如下:
.file"t.cc"
.local _ZStL8__ioinit
.comm _ZStL8__ioinit,1,1
.section .rodata
.LC0:
.string"func without parameters"
.text
.globl _Z4funcv
.type _Z4funcv, @function
_Z4funcv:
.LFB966:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset8
.cfi_offset5, -8
movl %esp, %ebp
.cfi_def_cfa_register5
subl $24, %esp
movl $.LC0,4(%esp)
movl $_ZSt4cout, (%esp)
call_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
movl $_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_,4(%esp)
movl %eax, (%esp)
call_ZNSolsEPFRSoS_E
leave
.cfi_restore5
.cfi_def_cfa4,4
ret
.cfi_endproc
.LFE966:
.size _Z4funcv, .-_Z4funcv
.section .rodata
.LC1:
.string"func with one int parameter"
.text
.globl _Z4funci
.type _Z4funci, @function
_Z4funci:
.LFB967:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset8
.cfi_offset5, -8
movl %esp, %ebp
.cfi_def_cfa_register5
subl $24, %esp
movl $.LC1,4(%esp)
movl $_ZSt4cout, (%esp)
call_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
movl8(%ebp), %edx
movl %edx,4(%esp)
movl %eax, (%esp)
call_ZNSolsEi
movl $_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_,4(%esp)
movl %eax, (%esp)
call_ZNSolsEPFRSoS_E
leave
.cfi_restore5
.cfi_def_cfa4,4
ret
.cfi_endproc
.LFE967:
.size _Z4funci, .-_Z4funci
.section .rodata
.align4
.LC2:
.string"func with one int parameter and one float parameter"
.text
.globl _Z4funcif
.type _Z4funcif, @function
_Z4funcif:
.LFB968:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset8
.cfi_offset5, -8
movl %esp, %ebp
.cfi_def_cfa_register5
subl $24, %esp
movl $.LC2,4(%esp)
movl $_ZSt4cout, (%esp)
call_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
movl $_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_,4(%esp)
movl %eax, (%esp)
call_ZNSolsEPFRSoS_E
movl8(%ebp), %eax
movl %eax,4(%esp)
movl $_ZSt4cout, (%esp)
call_ZNSolsEi
movl $_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_,4(%esp)
movl %eax, (%esp)
call_ZNSolsEPFRSoS_E
movl12(%ebp), %eax
movl %eax,4(%esp)
movl $_ZSt4cout, (%esp)
call_ZNSolsEf
movl $_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_,4(%esp)
movl %eax, (%esp)
call_ZNSolsEPFRSoS_E
leave
.cfi_restore5
.cfi_def_cfa4,4
ret
.cfi_endproc
.LFE968:
.size _Z4funcif, .-_Z4funcif
.globl main
.type main, @function
main:
.LFB969:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset8
.cfi_offset5, -8
movl %esp, %ebp
.cfi_def_cfa_register5
andl $-16, %esp
subl $16, %esp
call_Z4funcv
movl $5, (%esp)
call_Z4funci
movl $0x40a00000, %eax
movl %eax,4(%esp)
movl $5, (%esp)
call_Z4funcif
movl $0, %eax
leave
.cfi_restore5
.cfi_def_cfa4,4
ret
.cfi_endproc
可以看到,func 的三個版本重載函數在編譯後名字都被破壞了,編譯器將他們重命名為了 _Z4funcv, _Z4funci, _Z4funcif, (g++ 編譯器可能根據函數參數類型為函數名加上了與參數類型相關的特定後綴,如func(void) 變成了 _Z4funcv, func(int) 變成了_Z4funci , func(int, float)變成了 _Z4funcif ),然後在調用各個版本的func()時,編譯器根據參數類型的不同選擇合適的重載函數,如調用 func() 其實是調用了 _Z4funcv, 調用 func(5, 5.0)實際上是調用了 _Z4funcif等。
但是,在很多情況下,利用可變參數可以實現 C 語言的函數重載的,POSIX 接口中定義的 open 函數就是一個非常好的例子,
#include <sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
intopen(constchar*pathname,intflags);
intopen(constchar*pathname,intflags, mode_t mode);
第二個 open 函數的第三個參數用來表明創建文件的權限,所以這就是 C 實現函數重載活生生的例子 :-)
那麼如何實現 C 的函數重載呢,比較通用的做法是利用 C 的可變參數va_args:
#include <stdarg.h>
voidva_start(va_list ap, last);
type va_arg(va_list ap, type);
voidva_end(va_list ap);
voidva_copy(va_list dest, va_list src);
以下是一個簡單的例子,"重載"了兩個函數,第一個函數是兩個參數,第二個函數帶了三個函數,其中第三個函數是可選的,
#include <stdio.h>
#include<stdarg.h>
voidva_overload2(intp1,intp2)
{
printf("va_overload2 %d %d\n", p1, p2);
}
voidva_overload3(intp1,intp2,intp3)
{
printf("va_overload3 %d %d %d\n", p1, p2, p3);
}
staticvoidva_overload(intp1,intp2, ...)
{
if(p2 ==3)
{
va_list v;
va_start(v, p2);
intp3 = va_arg(v,int);
va_end(v);
va_overload3(p1, p2, p3);
return;
}
va_overload2(p1, p2);
}
那麼調用的時候可以如下調用:
#include <stdio.h>
intmain()
{
va_overload(1,2);
va_overload(1,2,3);
return0;
}
除了根據參數個數實現重載以外,還可以實現參數類型的重載(typeof),這主要是利用了 GCC 的內置函數,__builtin_types_compatible_p()和__builtin_choose_expr(),
例如:
structs1
{
inta;
intb;
doublec;
};
structs2
{
longlonga;
longlongb;
};
voidgcc_overload_s1(structs1 s)
{
printf("Got a struct s1: %d %d %f\n", s.a, s.b, s.c);
}
voidgcc_overload_s2(structs2 s)
{
printf("Got a struct s2: %lld %lld\n", s.a, s.b);
}
//warning: dereferencing type-punned pointer will break strict-aliasing rules
#definegcc_overload(A)\
__builtin_choose_expr(__builtin_types_compatible_p(typeof(A),structs1),\
gcc_overload_s1(*(structs1 *)&A),\
__builtin_choose_expr(__builtin_types_compatible_p(typeof(A),structs2),\
gcc_overload_s2(*(structs2 *)&A),(void)0))
或者一個更高級的寫法:
voidgcc_type_overload_aux(inttypeval, ...)
{
switch(typeval)
{
case1:
{
va_list v;
va_start(v, typeval);
structs1 s = va_arg(v,structs1);
va_end(v);
gcc_overload_s1(s);
break;
}
case2:
{
va_list v;
va_start(v, typeval);
structs2 s = va_arg(v,structs2);
va_end(v);
gcc_overload_s2(s);
break;
}
default:
{
printf("Invalid type to 'gcc_type_overload()'\n");
exit(1);
}
}
}
#definegcc_type_overload(A)\
gcc_type_overload_aux(\
__builtin_types_compatible_p(typeof(A),structs1) *1\
+ __builtin_types_compatible_p(typeof(A),structs2) *2\
, A)
另外兩種用 C 實現函數重載的方法可以是利用宏和預處理,以及函數指針,只不過具體的重載方式也要根據特定的應用場景來決定。
不過,C 實現函數重載需要開發人員自己編寫很多額外的代碼,門檻稍微高了,這也使得 C 語言不太適合用函數重載方式來編寫規范的應用程序接口。
所以,以後別人如果問你,C 可不可以實現函數重載,你就不能說“C 是不支持函數重載的”,而應該回答:“看情況看心情看應用場景咯 :-)“。