如何實現 C 的函數重載


大家都知道 C++ 等面向對象的語言支持函數重載,C++ 實現函數重載很大程度上依賴與編譯器對函數名的 Mangling(損壞,破壞),即 C++ 的源代碼被編譯后同名的重載函數名字會被破壞,一般是在原函數名前后加上特定的字符串,以區分不同重載函數,然后在調用的時候根據參數的不同選擇合適的函數,如下代碼說明了編譯器是如何處理普通函數重載的:

#include <iostream>
using namespace std;

int func(void)
{
    cout << "func without parameters" << endl;
}

int func(int ia)
{
    cout << "func with one int parameter: " << endl;
    cout << ia << endl;
}

int func(int ia, float fb)
{
    cout << "func with one int parameter and one float parameter" << endl;
    cout << ia << endl;
    cout << fb << endl;
}

int main()
{

    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_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    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_restore 5
    .cfi_def_cfa 4, 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_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    subl    $24, %esp
    movl    $.LC1, 4(%esp)
    movl    $_ZSt4cout, (%esp)
    call    _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
    movl    8(%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_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
.LFE967:
    .size    _Z4funci, .-_Z4funci
    .section    .rodata
    .align 4
.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_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    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
    movl    8(%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
    movl    12(%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_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
.LFE968:
    .size    _Z4funcif, .-_Z4funcif
    .globl    main
    .type    main, @function
main:
.LFB969:
    .cfi_startproc
    pushl    %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    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_restore 5
    .cfi_def_cfa 4, 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>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

第二個 open 函數的第三個參數用來表明創建文件的權限,所以這就是 C 實現函數重載活生生的例子 :-)

那么如何實現 C 的函數重載呢,比較通用的做法是利用 C 的可變參數va_args:

#include <stdarg.h>

void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);
void va_copy(va_list dest, va_list src);

以下是一個簡單的例子,"重載"了兩個函數,第一個函數是兩個參數,第二個函數帶了三個函數,其中第三個函數是可選的,

#include <stdio.h>
#include <stdarg.h>

void va_overload2(int p1, int p2)
{
    printf("va_overload2 %d %d\n", p1, p2);
}

void va_overload3(int p1, int p2, int p3)
{
    printf("va_overload3 %d %d %d\n", p1, p2, p3);
}

static void va_overload(int p1, int p2, ...)
{
    if (p2 == 3)
    {
        va_list v;
        va_start(v, p2);
        
        int p3 = va_arg(v, int);
        va_end(v);
        va_overload3(p1, p2, p3);
        
        return;
    }

    va_overload2(p1, p2);
}

那么調用的時候可以如下調用:

#include <stdio.h>
int main()
{
    va_overload(1, 2);
    va_overload(1, 2, 3);
    return 0;
}

除了根據參數個數實現重載以外,還可以實現參數類型的重載(typeof),這主要是利用了 GCC 的內置函數,__builtin_types_compatible_p()__builtin_choose_expr(),

例如:

struct s1
{
    int a;
    int b;
    
    double c;
};

struct s2
{
    long long a;
    long long b;
};

void gcc_overload_s1(struct s1 s)
{
    printf("Got a struct s1: %d %d %f\n", s.a, s.b, s.c);
}

void gcc_overload_s2(struct s2 s)
{
    printf("Got a struct s2: %lld %lld\n", s.a, s.b);
}

// warning: dereferencing type-punned pointer will break strict-aliasing rules
#define gcc_overload(A)\
    __builtin_choose_expr(__builtin_types_compatible_p(typeof(A), struct s1),\
        gcc_overload_s1(*(struct s1 *)&A),\
    __builtin_choose_expr(__builtin_types_compatible_p(typeof(A), struct s2),\
        gcc_overload_s2(*(struct s2 *)&A),(void)0))

或者一個更高級的寫法:

void gcc_type_overload_aux(int typeval, ...)
{
    switch(typeval)
    {
        case 1:
        {
            va_list v;
            va_start(v, typeval);
    
            struct s1 s = va_arg(v, struct s1);
        
            va_end(v);
            
            gcc_overload_s1(s);
        
            break;
        }
        
        case 2:
        {
            va_list v;
            va_start(v, typeval);
    
            struct s2 s = va_arg(v, struct s2);
        
            va_end(v);
            
            gcc_overload_s2(s);
        
            break;
        }
        
        default:
        {
            printf("Invalid type to 'gcc_type_overload()'\n");
            exit(1);
        }
    }
}

#define gcc_type_overload(A)\
    gcc_type_overload_aux(\
        __builtin_types_compatible_p(typeof(A), struct s1) * 1\
        + __builtin_types_compatible_p(typeof(A), struct s2) * 2\
        , A)

另外兩種用 C 實現函數重載的方法可以是利用宏和預處理,以及函數指針,只不過具體的重載方式也要根據特定的應用場景來決定。

不過,C 實現函數重載需要開發人員自己編寫很多額外的代碼,門檻稍微高了,這也使得 C 語言不太適合用函數重載方式來編寫規范的應用程序接口。

所以,以后別人如果問你,C 可不可以實現函數重載,你就不能說“C 是不支持函數重載的”,而應該回答:“看情況看心情看應用場景咯 :-)“。

 參考鏈接:http://locklessinc.com/articles/overloading/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM