3. C++中的函数

一、常量与宏回顾

C++中的const常量可以替代宏常数定义,如:
               const int A = 3;  ->  #define A 3

C++中是否有解决方案替代宏代码片段呢?替代宏代码片段就可以避免宏的副作用!

二、C++中的函数

1. 内联函数

                1. C++中推荐使用内联函数替代宏代码片段;
                2. C++中使用inline关键字声明内联函数;

//3-1.cpp
#include <stdio.h>

#define FUNC(a, b) ((a) < (b) ? (a) : (b))

inline int func(int a, int b)
{
    return a < b ? a : b;
}

int main(int argc, char *argv[])
{
    int a = 1;
    int b = 3;
    
    int c = func(++a, b);
    //int c = FUNC(++a, b);
    
    printf("a = %d\n", a);
    printf("b = %d\n", b);
    printf("c = %d\n", c);
    
    printf("Press enter to continue ...");
    getchar();	
    return 0;
}

运行结果如下:

注:内联函数声明时inline关键字必须和函数定义结合在一起,否则编译器会直接忽略内联请求。下面的demo中func函数就一定不会是内联函数。

#include <stdio.h>

inline int func(int a, int b);

int main(int argc, char *argv[])
{
    int a = 1;
    int b = 3;
    
    int c = func(++a, b);
    
    printf("a = %d\n", a);
    printf("b = %d\n", b);
    printf("c = %d\n", c);
    
    printf("Press enter to continue ...");
    getchar();	
    return 0;
}

int func(int a, int b)
{
    return a < b ? a : b;
}

内联函数的定义以及和普通函数的区别:

     1. C++编译器可以将一个函数进行内联编译;
            2. 被C++编译器内联编译的函数叫做内联函数;
            3. 内联函数在最终生成的代码中是没有定义的;
            4. C++编译器直接将函数体插入函数调用的地方;
            5. 内联函数没有普通函数调用时的额外开销(压栈,跳转,返回)

注:C++编译器不一定准许函数的内联请求!

        内联函数是一种特殊的函数,具有普通函数的特征(参数检查,返回类型等);
        内联函数是对编译器的一种请求,因此编译器可能拒绝这种请求;
        内联函数由编译器处理,直接将编译后的函数体插入调用的地方;
        宏代码片段由预处理器处理,进行简单的文本替换,没有任何编译过程;

现代C++编译器能够进行编译优化,因此一些函数即使没有inline声明,也可能被编译器内联编译;另外,一些现代C++编译器提供了扩展语法,能够对函数进行强制内联;如:g++中的__attribute__((always_inline))属性。

//main.cpp
#include <stdio.h>

inline int f_inline(int a, int b);
int g_no_inline(int a, int b);

int main(int argc, char *argv[])
{
    int r1 = f_inline(1, 2);
    int r2 = g_no_inline(1, 2);
    
    printf("Press enter to continue ...");
    getchar();	
    return 0;
}

int f_inline(int a, int b)
{
	return a < b ? a : b;
}

int g_no_inline(int a, int b)
{
	return a < b ? a : b;
}

通过下面的命令单步编译main.cpp,将产生一个main.s的汇编文件。 

g++ -S main.cpp -o main_no_inline.s
	#main_no_inline.s
    .file	"main.cpp"
	.section	.text$getchar,"x"
	.linkonce discard
	.globl	_getchar
	.def	_getchar;	.scl	2;	.type	32;	.endef
_getchar:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$24, %esp
	movl	__imp___iob, %eax
	movl	4(%eax), %eax
	leal	-1(%eax), %edx
	movl	__imp___iob, %eax
	movl	%edx, 4(%eax)
	movl	__imp___iob, %eax
	movl	4(%eax), %eax
	testl	%eax, %eax
	js	L2
	movl	__imp___iob, %eax
	movl	(%eax), %edx
	movb	(%edx), %al
	movzbl	%al, %eax
	leal	1(%edx), %ecx
	movl	__imp___iob, %edx
	movl	%ecx, (%edx)
	jmp	L3
L2:
	movl	__imp___iob, %eax
	movl	%eax, (%esp)
	call	__filbuf
L3:
	leave
	ret
	.def	___main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
LC0:
	.ascii "Press enter to continue ...\0"
	.text
	.globl	_main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
	pushl	%ebp
	movl	%esp, %ebp
	andl	$-16, %esp
	subl	$32, %esp
	call	___main
	movl	$2, 4(%esp)
	movl	$1, (%esp)
	call	__Z8f_inlineii
	movl	%eax, 28(%esp)
	movl	$2, 4(%esp)
	movl	$1, (%esp)
	call	__Z11g_no_inlineii
	movl	%eax, 24(%esp)
	movl	$LC0, (%esp)
	call	_printf
	call	_getchar
	movl	$0, %eax
	leave
	ret
	.section	.text$_Z8f_inlineii,"x"
	.linkonce discard
	.globl	__Z8f_inlineii
	.def	__Z8f_inlineii;	.scl	2;	.type	32;	.endef
__Z8f_inlineii:
	pushl	%ebp
	movl	%esp, %ebp
	movl	8(%ebp), %eax
	cmpl	12(%ebp), %eax
	jge	L6
	movl	8(%ebp), %eax
	jmp	L7
L6:
	movl	12(%ebp), %eax
L7:
	popl	%ebp
	ret
	.text
	.globl	__Z11g_no_inlineii
	.def	__Z11g_no_inlineii;	.scl	2;	.type	32;	.endef
__Z11g_no_inlineii:
	pushl	%ebp
	movl	%esp, %ebp
	movl	8(%ebp), %eax
	cmpl	12(%ebp), %eax
	jge	L9
	movl	8(%ebp), %eax
	jmp	L10
L9:
	movl	12(%ebp), %eax
L10:
	popl	%ebp
	ret
	.def	__filbuf;	.scl	2;	.type	32;	.endef
	.def	_printf;	.scl	2;	.type	32;	.endef

通过分析main_no_inline.s发现,f_inline函数有普通函数调用时的额外开销(压栈,跳转,返回),即内联请求没有被满足。我们可以通过g++中扩展的属性进行强制的内联。

//main.cpp
#include <stdio.h>

inline int f_inline(int a, int b) __attribute__((always_inline));
int g_no_inline(int a, int b);

int main(int argc, char *argv[])
{
    int r1 = f_inline(1, 2);
    int r2 = g_no_inline(1, 2);
    
    printf("Press enter to continue ...");
    getchar();	
    return 0;
}

int f_inline(int a, int b)
{
	return a < b ? a : b;
}

int g_no_inline(int a, int b)
{
	return a < b ? a : b;
}

通过命令进行编译:g++ -S main.cpp -o main_inline.s

    #main_inline.s
	.file	"main.cpp"
	.section	.text$getchar,"x"
	.linkonce discard
	.globl	_getchar
	.def	_getchar;	.scl	2;	.type	32;	.endef
_getchar:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$24, %esp
	movl	__imp___iob, %eax
	movl	4(%eax), %eax
	leal	-1(%eax), %edx
	movl	__imp___iob, %eax
	movl	%edx, 4(%eax)
	movl	__imp___iob, %eax
	movl	4(%eax), %eax
	testl	%eax, %eax
	js	L2
	movl	__imp___iob, %eax
	movl	(%eax), %edx
	movb	(%edx), %al
	movzbl	%al, %eax
	leal	1(%edx), %ecx
	movl	__imp___iob, %edx
	movl	%ecx, (%edx)
	jmp	L3
L2:
	movl	__imp___iob, %eax
	movl	%eax, (%esp)
	call	__filbuf
L3:
	leave
	ret
	.def	___main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
LC0:
	.ascii "Press enter to continue ...\0"
	.text
	.globl	_main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
	pushl	%ebp
	movl	%esp, %ebp
	andl	$-16, %esp
	subl	$32, %esp
	call	___main
	movl	$1, 20(%esp)
	movl	$2, 16(%esp)
	movl	20(%esp), %eax
	cmpl	16(%esp), %eax
	jge	L5
	movl	20(%esp), %eax
	jmp	L6
L5:
	movl	16(%esp), %eax
L6:
	movl	%eax, 28(%esp)
	movl	$2, 4(%esp)
	movl	$1, (%esp)
	call	__Z11g_no_inlineii
	movl	%eax, 24(%esp)
	movl	$LC0, (%esp)
	call	_printf
	call	_getchar
	movl	$0, %eax
	leave
	ret
	.globl	__Z11g_no_inlineii
	.def	__Z11g_no_inlineii;	.scl	2;	.type	32;	.endef
__Z11g_no_inlineii:
	pushl	%ebp
	movl	%esp, %ebp
	movl	8(%ebp), %eax
	cmpl	12(%ebp), %eax
	jge	L8
	movl	8(%ebp), %eax
	jmp	L9
L8:
	movl	12(%ebp), %eax
L9:
	popl	%ebp
	ret
	.def	__filbuf;	.scl	2;	.type	32;	.endef
	.def	_printf;	.scl	2;	.type	32;	.endef

通过分析main_inline.s汇编文件发现,此时并没有发现f_inline函数。这就说明了f_inline函数被内联编译了。前面有讲到过C++编译器直接将内联函数的函数体插入函数调用的地方。main_inline.s中哪些汇编代码又是f_inline函数的函数体呢?下面的汇编代码就是f_inline函数的函数体。

	movl	$1, 20(%esp)
	movl	$2, 16(%esp)
	movl	20(%esp), %eax
	cmpl	16(%esp), %eax
	jge	L5
	movl	20(%esp), %eax
	jmp	L6

C++中内联编译的限制:

      1. 不能存在任何形式的循环语句
             2. 不能存在过多的条件判断语句
             3. 函数体不能过于庞大
             4. 不能对函数进行取址操作
             5. 函数内联声明必须在调用语句之前

编译器对于内联函数的限制并不是绝对的,内联函数相对于普通函数的优势只是省去了函数调用时压栈,跳转和返回的开销。因此,当函数体的执行开销远大于压栈,跳转和返回所用的开销时,那么内联将无意义。

C++中内联函数的实现机制

当C++编译器编译程序,遇到通过inline关键字声明的函数的时候,就明白该函数在请求内联编译。因此C++编译器就会检查该函数体,如果符合内联函数的要求,C++编译器就会将它加入到C++编译器自己的符号表中。符号表是C++编译器编译程序的时候生成的一张表,是编译器自己用的。编译生成的可执行文件中没有包含符号表的信息。当C++编译器遇到函数调用的时候,首先进行函数参数类型的检查,当发现符号表中有对应的函数时,就会将符号表中的函数体替换到函数调用的位置。这也说明了,当C++编译内联函数后,在生成的汇编代码中看不到内联函数的声明以及内联函数定义,只有内联函数的函数体。

2. 函数默认参数

C++中可以在函数声明时为参数提供一个默认值,当函数调用时没有指定这个参数的值,编译器会自动用默认值代替。

#include <stdio.h>

int mul(int x = 3);

int main(int argc, char *argv[])
{
    printf("mul(2) = %d\n", mul(2));
    printf("mul(-2) = %d\n", mul(-2));
    printf("mul() = %d\n", mul());
    
    printf("Press enter to continue ...");
    getchar();	
    return 0;
}

int mul(int x)
{
    return x * x;
}

运行结果如下:

  • 函数定义中是否可以出现参数的默认值?              编译器报错
  • 当函数声明和定义中的参数默认值不同时会发生什么?  编译器报错

函数默认参数的规则
               1. 只有参数列表后面部分的参数才可以提供默认参数值;
               2. 一旦在一个函数调用中开始使用默认参数值,那么这个参数后的所有参数都必须使用默认参数值;

#include <stdio.h>

int add(int a, int b = 0, int c = 0, int d = 0)
{
    return a + b + c;
}

int main(int argc, char *argv[])
{
    printf("add(2) = %d\n", add(2));
    printf("add(1, 2) = %d\n", add(1, 2));
    printf("add(1, 2, 3) = %d\n", add(1, 2, 3));
    
    printf("Press enter to continue ...");
    getchar();	
    return 0;
}

运行结果如下:

3. C++中函数占位参数

             在C++中可以为函数提供占位参数。占位参数只有参数类型声明,而没有参数名声明。一般情况下,在函数体内部无法使用占位参数。

#include <stdio.h>

int func(int a, int b, int)
{
    return a + b;
}

int main(int argc, char *argv[])
{
    printf("func(1, 2, 3) = %d\n", func(1, 2, 3));//必须传递三个参数
    
    printf("Press enter to continue ...");
    getchar();	
    return 0;
}

运行结果如下:

C++支持这样的函数占位参数有什么意义?

        可以将占位参数与默认参数结合起来使用
               意义
                     1. 为以后程序的扩展留下线索
                     2. 兼容C语言程序中可能出现的不规范写法

在C语言中不等价。void func();表示可以接受任意多个的参数。void func(void);表示不接受参数。

C++中等价。

#include <stdio.h>

int func(int a, int b, int = 0)//当看到int = 0,就说明这个函数以后可能会修改
{
    return a + b;
}

int main(int argc, char *argv[])
{
    printf("func(1, 2) = %d\n", func(1, 2));
    
    printf("Press enter to continue ...");
    getchar();	
    return 0;
}

运行结果如下:

下面我们再看一个例子,说明兼容C语言程序中可能出现的不规范写法的程序。

#include <stdio.h>

int func()
{
    return 1;
}

int main(int argc, char *argv[])
{
    printf("func() = %d\n", func());
    printf("func(1) = %d\n", func(1));
    
    printf("Press enter to continue ...");
    getchar();	
    return 0;
}

因为C语言中,定义一个函数如果没有任何参数,默认是可以传递任意个实参的。因此上述程序通过C编译器编译可以通过。

同样的程序,通过C++编译呢?

通过C++编译器编译,就一个地方报错,我们修改报错的地方,不在传入参数就好,但如果有几十个地方或者上百个地方都调用了呢?C++为了兼容C代码,提供了函数占位符参数。我们只要修改函数定义的地方就能正常编译运行了,不需要改多个地方。

#include <stdio.h>

int func(int = 0)
{
    return 1;
}

int main(int argc, char *argv[])
{
    printf("func() = %d\n", func());
    printf("func(1) = %d\n", func(1));
    
    printf("Press enter to continue ...");
    getchar();	
    return 0;
}

编译运行结果如下:

小结:

             1. C++中可以通过inline声明内联函数
             2. 内联函数在编译时直接将函数体插入函数调用的地方
             3. inline只是一种请求,编译器不一定允许这种请求
             4. 内联函数省去了普通函数调用时压栈,跳转和返回的开销
             5. C++中在声明函数的时候指定参数的默认值
             6. C++可以声明占位符参数,占位符参数一般用于程序扩展和对C代码的兼容
 

4. C++中的函数重载

                函数重载(Function Overload),用同一个函数名定义不同的函数,当函数名和不同的参数搭配时函数的含义不同。

#include <stdio.h>
#include <string.h>

int func(int x)
{
    return x;
}

int func(int a, int b)
{
    return a + b;
}

int func(const char* s)
{
    return strlen(s);
}

int main(int argc, char *argv[])
{
    int c = 0;
    
    c = func(1);
    
    printf("c = %d\n", c);
    
    c = func(1, 2);
    
    printf("c = %d\n", c);
    
    c = func("12345");
    
    printf("c = %d\n", c);
    
    printf("Press enter to continue ...");
    getchar();	
    return 0;
}

运行结果:

函数重载至少满足下面的一个条件:

              1. 参数个数不同
              2. 参数类型不同
              3. 参数顺序不同

#include <stdio.h>
#include <string.h>

int func(int x)
{
    return x;
}

int func(int a, int b)
{
    return a + b;
}

int func(const char* s)
{
    return strlen(s);
}

int func(int a, const char* s)
{
    return a;
}

int func(const char* s, int a)
{
    return strlen(s);
}

int main(int argc, char *argv[])
{
    int c = 0;
    
    c = func("ab", 1);
    
    printf("c = %d\n", c);
    
    printf("Press enter to continue ...");
    getchar();	
    return 0;
}

运行结果如下:

当函数默认参数遇上函数重载会发生什么?

#include <stdio.h>
#include <string.h>

int func(int a, int b, int c = 0)
{
    return a * b * c;
}

int func(int a, int b)
{
    return a + b;
}

int main(int argc, char *argv[])
{
    int c = 0;
    
    c = func(1, 2); // 存在二义性,调用失败,编译不能通过 
    
    printf("c = %d\n", c);
    
    printf("Press enter to continue ...");
    getchar();	
    return 0;
}

运行结果如下:

编译器调用重载函数的准则:

              将所有同名函数作为候选者
              尝试寻找可行的候选函数
                   1. 精确匹配实参;
                   2. 通过默认参数能够匹配实参;
                   3. 通过默认类型转换匹配实参;
              匹配失败
                   1. 最终寻找到的可行候选函数不唯一,则出现二义性,编译失败。
                   2. 无法匹配所有候选者,函数未定义,编译失败。

函数重载的注意事项:

               1. 重载函数在本质上是相互独立的不同函数;
               2. 重载函数的函数类型是不同的;
               3. 函数返回值不能作为函数重载的依据;

注:函数重载是由函数名和参数列表决定的。

函数重载与函数指针

               当使用重载函数名对函数指针进行赋值时
                     1. 根据重载规则挑选与函数指针参数列表一致的候选者
                     2. 严格匹配候选者的函数类型与函数指针的函数类型(包括返回值类型和参数类型)

#include <stdio.h>
#include <string.h>

int func(int x) // int(int a)
{
    return x;
}

int func(int a, int b)
{
    return a + b;
}

int func(const char* s)
{
    return strlen(s);
}

typedef int(*PFUNC)(int a); // int(int a)

int main(int argc, char *argv[])
{
    int c = 0;
    PFUNC p = func;
    
    c = p(1);
    
    printf("c = %d\n", c);
    
    printf("Press enter to continue ...");
    getchar();	
    return 0;
}

上面程序中哪个函数将被调用?运行结果如下:

注意:

  • 函数重载必然发生在同一个作用域中;
  • 编译器需要用参数列表或函数类型进行函数选择;
  • 无法直接通过函数名得到重载函数的入口地址;

三、C++和C的相互调用

在项目中融合C++和C代码是实际工程中不可避免的。虽然C++编译器能够兼容C语言的编译方式,但C++编译器会优先使用C++的方式进行编译。可以利用extern关键字强制让C++编译器对代码进行C方式编译。

1. C++调用C编写的函数

//main.cpp
#include <stdio.h>

extern "C"
{
#include "add.h"
}


int main()
{
    printf("1 + 2 = %d\n", add(1, 2));
    
    return 0;
}
//add.c
#include "add.h"

int add(int a, int b)
{
    return a + b;
}
//add.h
int add(int a, int b);

通过下面的命令进行编译运行:

2. C调用C++编写的函数

//main.c
#include <stdio.h>
#include "add.h"

int main()
{
    printf("1 + 2 = %d\n", add(1, 2));
    
    return 0;
}
//add.cpp

extern "C"
{

#include "add.h"

int add(int a, int b)
{
    return a + b;
}

}
//add.h
int add(int a, int b);

通过下面的命令进行编译运行:

统一的解决方案

                 __cplusplus是C++编译器内置的标准宏定义
                 __cplusplus的意义:让C代码即可以通过C编译器的编译,也可以在C++编译器中以C方式编译


#ifdef __cplusplus
extern "C" {
#endif

//函数声明或函数定义

#ifdef __cplusplus
}
#endif

注:C++编译器不能以C的方式编译多个重载函数

#include <stdio.h>
#include <string.h>


#ifdef __cplusplus
extern "C" {
#endif

int func(int a, int b)
{
    return a + b;
}

int func(const char* s)
{
    return strlen(s);
}

#ifdef __cplusplus
}
#endif

int main(int argc, char *argv[])
{
    printf("Press enter to continue ...");
    getchar();	
    return 0;
}

运行结果:

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值