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个货构成重载。说好的特征标呢????

posted @ 2024-01-06 18:46  thammer  阅读(9)  评论(0编辑  收藏  举报