第3课 - 函数的升级 - 上
一.内联函数的使用
1.C++推荐使用内联函数替代宏代码片段
2.C++使用关键字inline关键字声明内联函数
注意:inline关键字必须放在函数定义的地方,不能放在函数声明的地方,否则编译器会直接忽略内联请求
inline int func (int a, int b)
{
return (a < b ? a : b );
}
3.内联函数的具体说明
3.1 C++可以将一个函数进行内联编译
3.2 被C++编译器内联编译的函数叫做内联函数
3.3 内联函数在最终生成的代码中是没有定义的
3.4 C++编译器直接将函数体插入函数调用的地方
(例如当函数调用上面的func函数的时候会直接将该函数的函数体 (a < b ? a : b)插入调用的地方)
调用func (1,2)就相当于 1 < 2 ? 1 : 2
3.5 内联函数没有普通函数调用时的额外开销(压栈,跳转,返回)
注意:C++编译器不一定会准许函数的内联请求,如果不允许 !
3.6 内联函数是一种特殊的函数,具有普通函数的特征
4.内联函数与宏代码片段的区别
4.1 内联函数有编译器处理,直接将编译后的函数体插入到调用的地方,宏代码片段有预处理器处理,进行简单的文本替换,没有任何编译过程
4.2 效率上内联函数与宏代码片段一样,但是内联函数更安全。 具体见Source Example 4.1:
#include <iostream>
#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);
return 0;
}
当使用内联函数时,相当于 2 < 3 ? 2 : 3
输出结果a=2,b=3,c=2,正常。
当使用宏定义时,相当于 ++a < b ? ++a : b,即 3 < 3 ? 3 : 3
输出结果变为a=3,b=3,c=3,a自增了两次,并不是我们所想要的结果,这就是宏定义的副作用。
4.3 关于内联函数是否会被内联编译的具体说明
a.C++编译器能够进行编译优化,因此一些函数即使没有inline声明也可能被编译器内联编译
b.一些现代C++编译器提供了扩展语法,能够对函数进行强制内联,例如g++中的__attribute__((always_inline))属性
4.4 内联函数的深度示例
Source Example 4.4:
#include <iostream>
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
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);
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++编译器编译,得到汇编文件。
在main函数的汇编代码中
main:
pushq %rbp
.seh_pushreg %rbp
movq %rsp, %rbp
subq $48, %rsp
.seh_stackalloc 48
.seh_setframe %rbp, 48
.seh_endprologue
movl %ecx, 16(%rbp)
movq %rdx, 24(%rbp)
call __main
movl $2, %edx /* 将2压入栈中 */
movl $1, %ecx /* 将1压入栈中 */
call _Z8f_inlineii /* 调用f_inline函数 */
movl %eax, -4(%rbp)
movl $2, %edx /* 将2压入栈中 */
movl $1, %ecx /* 将1压入栈中 */
call _Z11g_no_inlineii /* 调用g_no_inline函数 */
movl %eax, -8(%rbp)
movl $0, %eax
addq $48, %rsp
popq %rbp
从上面的汇编代码就可以发现,f_inline函数并没有被内联编译,因为内联函数没有压栈,跳转,返回等开销
修改cpp文件,声明内联函数修改为
inline int f_inline(int a, int b)__attribute__((always_inline));即附加一个强制内联编译的属性
同样使用g++编译器进行编译得到汇编文件main函数如下
main:
pushq %rbp
.seh_pushreg %rbp
movq %rsp, %rbp
subq $48, %rsp
.seh_stackalloc 48
.seh_setframe %rbp, 48
.seh_endprologue
movl %ecx, 16(%rbp)
movq %rdx, 24(%rbp)
call __main /* f_inline函数体直接被插入到函数调用的地方 */
movl $1, -12(%rbp) /* 没有压栈,跳转,返回等开销 */
movl $2, -16(%rbp)
movl -12(%rbp), %eax
cmpl -16(%rbp), %eax
jge .L2
movl -12(%rbp), %eax
jmp .L3
.L2:
movl -16(%rbp), %eax
.L3:
movl %eax, -4(%rbp)
movl $2, %edx /* 将2压入栈中 */
movl $1, %ecx /* 将1压入栈中 */
call _Z11g_no_inlineii /* 调用g_no_inline函数 */
movl %eax, -8(%rbp)
movl $0, %eax
addq $48, %rsp
popq %rbp
ret
阅读代码发现并没有调用f_inline函数,很明显f_inline函数被内联编译了。
5.C++中内联函数的实现机制
当编译器编译到内联函数时,首先进行判断是否符合内联函数的要求,如果符合就会将内联函数放到符号表里面去。
(符号表就是C++在编译的过程中创建的,里面存放了一些名字,是编译器自己用的东西,不会进入最终生成的可执行程序)
当使用到内联函数,编译器首先会对函数的参数进行检查(预处理器不会这么做),如果参数正确,会在符号表里面将函数的函数体拿出来插入调用的地方。
6.C++中内联编译的限制
6.1 不能存在任何形式的循环语句
6.2 过多的条件判断语句(例如switch)
6.3 函数体不能过于庞大(函数体最好不要超过5句)
6.4 不能对函数进行取址操作(一旦对函数进行取址操作编译器就会拒绝对函数的内联编译)
6.5 函数的内联声明必须在调用语句之前,否则C++也可能会拒绝函数的内联编译
注意:编译器对内联函数的限制并不是绝对的,内联函数相对于普通函数的优势只是省去了函数调用时的开销(压栈,跳转,返回)。
因此,当函数的函数体执行开销远大于压栈,跳转,和返回所用的开销时,那么内联将没有意义。(编译器有可能会智能的判断是否有意义进行内联编译,从而决定是否进行内联编译)
二.函数的默认参数
1. 函数默认参数的使用
C++可以在函数声明时为函数提供一个默认值,但函数调用时没有指定这个参数的值,编译器会自动用默认值代替。
Source Example 1:
#include <iostream>
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
int mul(int x = 0);
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());
return 0;
}
int mul(int x)
{
return x * x;
}
输出结果如下:
当没有指定函数的参数时就会使用函数声明时指定的默认参数。
2. 函数默认参数的规则
2.1 只有参数列表后面部分的参数才可以提供默认参数值
一旦一个函数中开始使用默认参数值,那么这个参数后的所有参数都必须使用默认参数值。
例如f(int a, int b, int c)函数
一旦b开始使用默认参数值,那么c就必须使用默认参数值。
Source Example 2.1:
#include <iostream>
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
int add(int a, int b = 0, int c = 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));
return 0;
}
输出结果如下
一旦参数b使用了默认参数值,参数c就必须也使用默认参数值。
例如 调用add(2),参数b,c都使用了默认参数值,是合法的。
假设想要a=2,b使用默认参数值,c=1,调用的时候add(2,,1);很明显是不合法的。
因此 一旦有一个参数开始使用默认参数值,那么这个参数后面的参数都必须使用默认参数值。
三.函数的占位参数
1.占位参数定义
占位参数只有参数类型声明,而没有参数名的声明
Source Example 3.1:
#include <iostream>
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
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));
return 0;
}
输出结果为3
在调用func函数的时候必须提供三个参数,但是占位参数不会被使用。
2.占位参数的使用
占位参数可以与默认参数结合起来使用
Source Example 3.2.1:
#include <iostream>
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
int func (int a, int b, int = 0)
{
return a + b;
}
int main(int argc, char** argv) {
printf ("func(1,2,3) = %d\n", func(1,2));
return 0;
}
这样在调用的时候使用两个参数就合法了。
这么做的意义在于为以后程序的扩展留下线索,同时也是为了兼容C语言程序中可能出现的不规范的写法,具体见下例。
Source Example 3.2.2:
#include <stdio.h>
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
int func ()
{
return 1;
}
int main(int argc, char** argv) {
printf ("func()=%d\n", func());
printf ("func(1)=%d\n", func(1));
return 0;
}
这个C程序在C语言中是可以编译的过的,func没有指定形参,表示可以接受任意类型,个数的参数
但是放到C++中进行编译则编译不过。
一般直接在函数定义的地方加上占位参数,并且加上一个默认值就可以编译通过了int func (int = 0),避免修改很多次。