C++函数的重载
什么是函数重载
函数的重载能使我们定义多个同名函数,我们在调用时,编译器会根据函数特征标自动帮我们调用对应的函数。嗯,这个特性又是C++为了方便程序员,通过编译器帮我们干活,设计出来的一个东西。通过C和C++的对比,我们可以看到函数重载的这种特性是怎么提出来的。
实现原理
C的做法
假设我们使用C要实现一个打印数字的功能,数值可能有多种类型,于是我们要给每一种类型写一个函数。
void printnum_char(char) { } void printnum_short(short) { } void printnum_int(int) { } int main(void) { char a=1; short b=1024; int c=655360; printnum_char(a); printnum_short(b); printnum_int(c); }
这样做,有一些缺点:
- 我们在调用时,要时刻操心不同的参数类型,要选择正确的函数,如下面的例子,一不小心,就可能导致数据溢出。
//如果不小心做了如下调用,short类型的b传给了char类型的参数,发生了截断,与预期不符,当然现在的IDE,编译器都比较强大了,会报警告 printnum_char(b)
- 函数名包含了多余的区分信息,例如这里我以C语言的基本的数据类型做标记,这种信息和程序逻辑毫不相干。
C++的做法
void printnum(char) { } void printnum(short) { } void printnum(int) { } int main(void) { char a=1; short b=1024; int c=655360; printnum(a); printnum(b); printnum(c); }
C/C++都是强类型语言,因此变量在使用前类型已经明确了,这是由他们的语法规则确定的。那么一旦一个变量定义好,编译器就清楚变量的类型。如上,如果不同类型的变量作为参数传递给函数时,编译器也就有手段知道最佳匹配函数。具体实现原理可以看下上面C++代码的汇编代码:
g++ -S main.cpp -o main.asm
以下为汇编代码:
.file "main.cpp" .text .globl _Z8printnumc .type _Z8printnumc, @function _Z8printnumc: .LFB0: .cfi_startproc endbr64 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl %edi, %eax movb %al, -4(%rbp) nop popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size _Z8printnumc, .-_Z8printnumc .globl _Z8printnums .type _Z8printnums, @function _Z8printnums: .LFB1: .cfi_startproc endbr64 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl %edi, %eax movw %ax, -4(%rbp) nop popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE1: .size _Z8printnums, .-_Z8printnums .globl _Z8printnumi .type _Z8printnumi, @function _Z8printnumi: .LFB2: .cfi_startproc endbr64 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl %edi, -4(%rbp) nop popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE2: .size _Z8printnumi, .-_Z8printnumi .globl main .type main, @function main: .LFB3: .cfi_startproc endbr64 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $16, %rsp movb $1, -7(%rbp) movw $1024, -6(%rbp) movl $655360, -4(%rbp) movsbl -7(%rbp), %eax movl %eax, %edi call _Z8printnumc movswl -6(%rbp), %eax movl %eax, %edi call _Z8printnums movl -4(%rbp), %eax movl %eax, %edi call _Z8printnumi movl $0, %eax leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE3: .size main, .-main .ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0" .section .note.GNU-stack,"",@progbits .section .note.gnu.property,"a" .align 8 .long 1f - 0f .long 4f - 1f .long 5 0: .string "GNU" 1: .align 8 .long 0xc0000002 .long 3f - 2f 2: .long 0x3 3: .align 8 4:
可以看到,同样一个printnum函数,在汇编时,函数符号对应关系为:
_Z8printnumc <---> printnum(char) _Z8printnums <---> printnum(short) _Z8printnumi <---> printnum(int)
很明显c,s,i分别是char,short,int的开头字母。也就是说C++其实是通过编译器,帮我们按照C的方式实现了函数重载,把机械的事交给了工具(编译器)干。这样看来,函数的符号最终还是不一样的,编译器需要通过函数的原型,在汇编时给它重命名,以达到区分的目的,前面提到函数的特征标,就是用来提示编译器做区分的。特征标指:函数的参数类型,参数个数,参数顺序。观察一下汇编重命名重载函数的规律:
class A{}; class AB{}; void printnum(){} void printnum(int*){} void printnum(int){} void printnum(A){} void printnum(AB){} void printnum(int, int){} void printnum(int, char){} char printnum(int, int, int){return '0';}
反汇编后,可以得到如下对应关系:
_Z8printnumv <---> void printnum() _Z8printnumi <---> void printnum(int) _Z8printnumPi <---> void printnum(int*) _Z8printnum1A <---> void printnum(A) _Z8printnum2AA <---> void printnum(AB) _Z8printnumii <---> void printnum(int, int) _Z8printnumic <---> void printnum(int, char) _Z8printnumiii <---> char printnum(int, int, int)
规律就是基本数据类型取一个首字母,按照参数列表顺序拼接就行了,自定义的类型就取类型名字,前面加一个类型名字字符数。指针就类型前面加一个P,具体规则可以网上搜一搜。
可以看到一个又意思的现象,函数的返回值不被考虑在特征标里面,为什么?
一些注意点
一些看起来构成重构的例子,实际不构成:
void foo() char foo(){}
返回值不同,参数列表相同的情况,为什么?其实举一个反例就知道了。假定返回值构成重载条件,那么有如下函数定义和调用:
int main(void) { foo(); }
因为C/C++语法并未规定函数返回值必须要使用,因此即便函数有返回值,我也可以不用它,上面这个例子编译器就迷糊了,到底是调用void foo()
,还是char foo()
?
再看一下下面的列子:
void bar(int){} void bar(const int){} //提示重复定义,不构成重载 int main() { int a = 1; bar(a); }
以上面调用的例子来看,int a = 1;
即可以传递给第一个bar也可以传递给第二个bar,因为于将非const值赋给const变量是合法的,但反之则是非法的。
那是不是通过const修饰的就不构成重载呢?并不是,看下面一个例子:
void tar(int *){} void tar(const int *){} int main() { int a = 1; tar(&a); }
上面那个例子是传值,这里是传指针,仅仅这个区别,为什么这里会构成重载?按照上一个例子
-----------------------------以下内容待完善------------------------
此时你知道我想调用的是传值还是传引用?从语法上来说,都可以,所以产生了歧义,因此这种情况不构成重载。类似的,tar
的调用也会产生歧义。
因此记住一个规则,特征标不同,特征标指:参数类型,参数个数,参数顺序。只和参数有关。
比较晦涩的重载
class A { public: void foo() { std::cout << "non const foo" << std::endl; } void foo() const { std::cout << "const foo" << std::endl; } };
嗯,就问你吊不吊,这2个货构成重载。说好的特征标呢????
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
2016-01-06 ipc之消息队列