函数
一个C++程序是由若干个源程序文件构成,而一个源程序是由若干个函数构成。函数将一段逻辑封装起来便于复用。
从用户的角度看,函数分成两种:
- 库函数:标准函数,由C++系统提供。比如strcpy_s
- 用户自定义函数:需要用户定义后使用
函数的组成部分:
- 返回类型:一个函数可以返回一个值;
- 函数名称:这是函数的实际名称,函数名称和参数列表一起构成了
函数签名
,函数签名才是函数被调用时使用的真正的名字; - 参数:参数列表包含函数参数的类型、顺序、数量。参数是可选的,也就是说函数可能不包含参数。
函数重载(overload)与签名
- int test(int)
- int test(double a)
- int test(int a,double d)
以上三个函数构成函数重载:函数名称一样,但是函数参数不一样。
程序内部存储的是函数签名,正是由于函数签名不同,编译器才会区分出这些程序。
指向函数的指针和返回指针的函数
int(*p)(int);
//一个指针p,指向参数是int、返回值是int类型的函数
int test(int index);
p = test;
int result = (*p)(1);
每一个函数都占用一段内存单元,它们有一个一个起始地址,指向函数入口地址的指针称为函数指针。
一般形式:数据类型(*指针变量名)(参数列表),数据类型表示函数返回的数据类型
比如:int(*p)(int)
我们要区分返回指针的函数的区别
- int(*p)(int):指针,指向一个函数的入口地址
- int* p(int):函数,返回值是一个指针
int MaxValue(int x,int y)
{
return (x>y)?x:y;
}
int MinValue(int x,int y)
{
return (x<y)?x:y;
}
int add(int x,int y)
{
return x+y;
}
bool ProcessNum(int x,int y,int(*p)(int a,int b)) //函数指针仅仅把函数传递进来
{
cout<< p(x,y) << endl; //这个时候才真正调用了函数
return true;
}
int main()
{
int x = 10;
int y = 20;
//三个函数的返回值和参数列表都一样,所有都可以使用函数名传递
cout<<ProcessNum(x,y,MaxValue)<<endl; //参数是函数指针,直接传递函数名即可
cout<<ProcessNum(x,y,MinValue)<<endl;
cout<<ProcessNum(x,y,Add)<<endl;
}
上述例子中bool ProcessNum(int x,int y,int(*p)(int a,int b))中的参数是个函数指针,我们传递进去函数名后,真正调用函数的地方在这个函数体内部,我们把这种调用的方式称为回调函数
。我们无法控制函数什么时候调用,我们仅仅把函数给他,由它决定什么时候使用。
命名空间
软件开发过程中有可能会有多个程序员开发了相同函数签名的函数。就像一个班级里出现了同名的人。这时候可以用到命名空间的概念。
命名空间可以作为附加信息来区分不同库中相同名称的函数、类、变量等。命名空间即定义了上下文。本质上命名空间就是定义了一个范围。
关键词:using和namespace
//声明
int test(int a);
namespace mytest
{
int test(int a);
}
//定义
int test(int a)
{
return a;
}
namespace mytest
{
int test(int a)
{
return a+1;
}
}
//调用
int(*p)(int);
p = test;
int result = (*p)(1);
result = mytest::test(0);
内联函数
如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。
inline int MaxValue(int x,int y)//求最大数
{
return (x>y)?x:y;
}
引入内联函数的目的是为了解决程序中函数调用的效率问题,即用空间换时间。在以往的传统函数调用的过程中涉及到了call的过程,这里的汇编代码相对比较复杂。我们可以节省成本,直接把函数的执行体拿过来,copy到调用的位置,而把函数的参数、压栈出栈的过程全部忽略掉。
注意内联函数内部不能有太复杂的逻辑,编译器有时候会有自己的优化策略,所以内联函数不一定起作用。
在VS编译器中,右键项目属性,找到C/C++栏中的高级设置,会有调用约定选项。默认的是_cdecl方式,即C语言的调用方式。这种方式参数是从右到左的进栈过程,这些参数都存在栈内。我们可以换其他方式,这时候会有一些变化。
递归调用
数学归纳法
数学归纳法是整明当n等于任意一个自然数时某命题成立。整明步骤分2步:
- 整明当n=1时命题成立;
- 假设当n=m时命题成立,那么可以推导出在n=m+1时命题也成立(m代表任意自然数)
经典问题:斐波那契数列
1,1,2,3,5,8,13,21,34……
Fib(n) = 1 n=1、2
= Fib(n-1)+Fib(n-2) n>2
int Fib(int n)
{
if(n == 0)
return 0;
else if(n == 1)
return 1;
else
return Fib(n-1)+Fib(n-2); //函数自己调用自己
}
递归调用
函数自己调用自己称为递归调用。这个调用自己不是真的调用自己,而是参数有递进,不然就会是死循环了。
递归的四个基本法则:
- 基准情形:要有无须递归就能解出的情况(斐波那契数列问题中就是当n=0或1的时候,这时候不需要递归调用也能返回),这是递归退出的条件;
- 不断推进:每一次递归调用都必须使求解状况朝接近基准情形的方向推进;
- 设计法则:假设所有的递归调用都能运行;
- 合成效益法则:求解一个问题的同一个实例时,切记不要进行重复性工作,这就牵扯到下面所说的递归的问题
递归的问题
斐波那契数列求解的过程会有问题。递归运算中有大量的重复运算,对计算机有大量时间和空间上的浪费。这种算法在工程中是有很大的问题。n比较小的时候程序还能正常运行,一旦n值非常大,函数栈会变得极其复杂(类似于前序访问二叉树),整个程序就面临很大的问题甚至崩溃。
由此可见,使用递归来计算此类注入斐波那契数列问题并不是好主意。
递归的优化
递归是一种重要的编程思想,很多算法都包含这种思想(如归并排序),但是递归有/缺陷:
- 空间上需要开辟大量的栈空间
- 时间上可能需要大量重复运算
递归的优化:
- 尾递归:所有递归形式的调用都出现在函数的末尾;
- 使用循环替代;
- 使用动态规划,空间换时间。
//循环优化
//用一个循环等效于递归,拿一个中间变量来存储上次的值。避免了递归调用中不停的调用函数栈
int Fib2(int n)
{
if(n<2)
return n;
int n0 = 0,n1 = 1;
int temp;
for(int i=2;i<=n;i++)
{
temp = n0;
n0 = n1;
n1 = temp + n1;
}
return n1;
}
//尾递归优化
//仍然使用递归,但是递归仅在函数的末尾发生,即递归推进的过程放在最后完成
//传统递归中return的是个表达式,而这个尾递归中return的是个函数调用。所以传统递归的堆栈很复杂,需要保存多个函数的堆栈
//尾递归并没有保存太多的堆栈信息,编译器可以进行优化。
//从汇编层面来看,因为是最后一行代码做递归调用,寄存器没有必要把前面的空间信息进行保存。
//汇编代码中可以看到编译器把这个递归优化成了一个循环操作
int Fi3(int n,int ret0,int ret1)
{
if(n == 0)
return ret0;
else if(n == 1)
return ret1;
return Fi3(n-1,ret1,ret0+ret1);
}
//动态规划优化
//从传统递归可以看到我们进行了大量的重复计算,那么我们完全可以把第一次计算出来的值存起来
//后面再遇到这个计算直接查表即可
int myArr[1000]; //全局的数组,记录前1000个值。在全局区中默认初始化为全0
int Fi4(int n)
{
myArr[0] = 0;
myArr[1] = 1;
for(int i=2;i<=n;i++)
{
if(myArr[i] == 0) //没有被初始化
{
myArr[i] = myArr[i-1]+myArr[i-2];
}
}
return myArr[n];
}