十. 函数
● 基本概念
函数 函数定义 function definition: return_type function_name ( parameter list ) { Body of function; } //函数体内先是变量的声明, 然后是语句; 这就样平时看到的主函数一样, 先有变量, 再有语句 函数调用 function call: function_name ( arguments list ); 函数原型: 函数原型也叫函数声明,还叫引用说明, 函数声明由函数返回类型、函数名和形参列表组成。形参列表必须包括形参类型,但是不必对形参命名。 函数声明 function declaration return_type function_name ( parameter list ); ※ 如果函数定义在先,调用在后,调用前可以不必声明; |
#include <iostream> using namespace std; void show_message(); //声明一个返回值类型为空(不返回函数值), 没有形参, 函数体只有一条输出语句的子函数, void show_age(); //声明一个返回值类型为空(不返回函数值), 没有形参的子函数, 函数体有变量定义和输出语句的子函数 void out(int x, int y); //声明一个返回值类型为空(不返回函数值), 但有两个形参的子函数(一些返回值类型为空的子函数不需要没有形参); 形参列表可以省略形参, 即int dec(int, int) int dec(int x, int y); //声明一个返回值为int型, 函数体只包含一条返回语句的子函数; 两个不同子函数的形参的标识符可以一样 int func(int n); //声明一个返回值类型为空, 有一个形参, 函数体为一个if...else if...else...语句的子函数 int main() { show_message(); //函数调用, 不需要实参 show_age(); out(10, 5); //不能写成cout<<out(10, 5)<<endl, 因为子函数out()的没有返回值, 无法传值给cout对象 cout<<dec(10, 5)<<endl; //调用子函数dec()以后, 返回值返回到 主函数main()调用dec()函数的位置 int a=10, b=5; cout<<dec(10, a+b)<<endl; // 实参可以是常量、变量或表达式,如这里的dec(10, a+b),但要求a和b有确定的值; 形参不能是表达式 //不直接给出fun()实参的值, 而是靠用户输入 int n; cout<<"input n: "<<endl; //如果没有这个提示, 就直接输入n的值 cin>>n; cout<<"\nthe result: "<<func(n)<<endl; return 0; } //子函数show_message() void show_message() { cout<<"Hello World!"<<endl; //就算只有一条语句, 也必须要用一对花括号 } //子函数show_age() void show_age() { int age=23; cout<<"age is: "<<age<<endl; } //子函数out() void out(int x,int y) { cout<<x+y<<endl; cout<<x-y<<endl; } //子函数dec() int dec(int x, int y) { return(x+y); } //子函数dec() int func(int n) { if(n>0) return 1; //n>0成立 else if (n==0) return 0; //n=0成立 else return -1; //n=-1成立 } |
#include <iostream> using namespace std; int add(int x) { return x; } int main() { int a=add(0); cout<<a<<endl; int b=add(a+10); cout<<b<<endl; return 0; } //函数的实参可以是表达式: |
● 栈帧
栈帧(Stack Frame)是栈上的内存片段,也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。其主要作用有:为局部变量和临时变量分配空间,传递参数,保存返回地址。 每个函数在栈上拥有独立的帧,这些帧是在函数调用时分配的。在任一时刻,寄存器ebp(帧指针)都指向帧的起始处,寄存器esp(栈指针)指向帧的结尾处,它们之间的部分是当前函数的栈帧。我们可以这样认为,ebp是局部的,作用于当前帧,而esp是全局的,作用于整个栈。 (1)ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。 (2)EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。 ebp是寄存器,前面加%表示取寄存器的值, 以此类推. 假设函数P(caller)调用函数Q(callee),一般来说在P中会执行如下操作:
举个例子:
由gcc编译之后的汇编代码如下:
个字节,但实际只需要8个字节,这是什么原因呢? |
● 函数的调用
函数的调用的执行过程: 第一步:函数调用; ② 对实参表从后向前,依次计算出实参表达式的值,并将值压栈。 ③ 转跳到函数体处。 第二步:函数体执行,即逐条运行函数体中语句的过程。 ④ 如果函数中还定义了变量,将变量压栈。 ⑤ 将每一个形参以栈中对应的实参值取代,执行函数体。 ⑥ 将函数体中的变量、保存在栈中的实参值,依次从栈中取出,以释放栈 第三步:返回,即返回到函数调用表达式的位置。 ⑦ 返回过程执行的是函数体中的return语句。 例如: int max(int x,int y) { int z; z=(x>y)?x:y; return z ; } 函数max()的详细过程是: //几个概念 调用函数时,要先把寄存器的值入栈,这个过程叫做函数调用时的现场保护. 函数执行完成后要出栈,叫做现场还原. 返回地址一般是紧邻函数调用语句的下一条语句的地址, 因为函数调用结束后程序要继续执行, 所以先把这个地址压入堆栈, 等函数调用结束以后, 把这个地址从堆栈里面弹出来, 然后执行. |
● 默认参数(default parameter)
//sample1.cpp #include <iostream> using namespace std; void OutputInfo(const char* pchData = "Hello!") //括号里的参数还可以写成(char* pchData = "Hello!"), 差别是?△ { cout << pchData << endl; //输出信息 } void main() { OutputInfo(); //利用默认值作为函数实际参数 OutputInfo("World!"); //直接传递实际参数 } //sample2.cpp #include <iostream> using namespace std; int add(int x=5, int y=6) { return x+y; } int main() { cout<<add(10,20)<<endl; cout<<add(10)<<endl; cout<<add()<<endl; return 0; } |
/*注意①默认参数不能出现在非默认参数的左边: int func(int x, int y=10, int z) //非法 int func(int x, int y, int z=10) //合法 ②形参的默认值可以是: 全局常量, 全局变量, 表达式, 函数调用, 但不能是局部变量, 如: void func1() { int k; void func2(int x=k); //k为局部变量, 不能作为形参的默认值 } */ |
● 函数和宏的区别
//带参函数的定义和调用 #include <iostream.h> int SQ(int); void main() { int i=1; //声明变量并初始化 while(i<=5) cout<<SQ(i++)<<"\t"; cout<<endl; } int SQ(int x) { return ((x)*(x)) } |
//带参宏 #define SQ(x) ((x)*(x)) #include <iostream.h> void main() { int i=1; while(i<=5) cout<<SQ(i++)<<"\t"; //主函数运行前, SQ(i++)被替换为((i++)*(i++)), 每次循环后,i的值会增加2, 因此只会做三次循环 cout<<endl; } |
● C/C++参数传递的3种方式
传值调用----数据传送是单向的----不影响原始的实参值; 地址调用----数据传送是双向的----修改原始的实参的值(在被调用函数中修改调用函数环境中的参数变量); 引用调用---数据传送是双向的----同上 |
#include <iostream.h> void swap1(int x, int y); //单向值传递 void swap2(int *x, int *y); //(双向)地址传递, 优先在C语言中使用 void swap3(int &x,int &y); // (双向)引用传递, 在C语言中使用(因为引用是C++的概念) int main() { int a=5,b=10; swap1(a,b); cout<<"a="<<a<<endl; cout<<"b="<<b<<endl; swap2(&a,&b); cout<<"a="<<a<<endl; cout<<"b="<<b<<endl; swap3(a,b); cout<<"a="<<a<<endl; cout<<"b="<<b<<endl; return 0; } void swap1(int x, int y) //如下三个子函数, 也可以定义为int型, 并返回0 { int tmp; tmp=x; x=y; y=tmp; cout<<"swap1: x="<<x<<endl; cout<<"swap1: y="<<y<<endl; } void swap2(int *x, int *y) { int tmp; tmp=*x; *x=*y; *y=tmp; cout<<"swap2: x="<<*x<<endl; cout<<"swap2: y="<<*y<<endl; } void swap3(int &x,int &y) { int tmp = x; x = y; y = tmp; cout<<"swap3: x="<<x<<endl; cout<<"swap3: y="<<y<<endl; } |
//把swap3独立出来, 那么swap2与swap3的效果就是一样的: #include <iostream.h> void swap3(int &x,int &y); // (双向)引用传递, 在C++语言中使用(因为引用是C++的概念) int main() { int a=5,b=10; swap3(a,b); cout<<"a="<<a<<endl; cout<<"b="<<b<<endl; return 0; } void swap3(int &x,int &y) { int tmp = x; x = y; y = tmp; cout<<"swap3: x="<<x<<endl; cout<<"swap3: y="<<y<<endl; } |
● 空类型函数
空类型函数: 凡不要求返回值的函数都应定义为空类型 & auto, 静态局部变量 |
#include<iostream.h> void AddOne() //形式参数列表为空, 这样就定义了不需要参数的函数,并且, 这个函数不要求返回值, 所以应定义为空类型 { auto int a=1; a+=1; cout<<a<<endl; } int main() { cout<<"第一次调用:"<<endl; AddOne(); cout<<"第二次调用:"<<endl; AddOne(); return 0; } //上面的空类型函数也可以写成: int AddOne() { auto int a=1; a+=1; cout<<a<<endl; return 0; } //但为了降低程序出错的几率,凡不要求返回值的函数都应该定义为空类型,所以最好不要这样定义 |
#include<iostream.h> void AddOne() //函数声明/函数原型, 如果有形参,要写形参类型和形参(形参可省略) void AddOne() { static int a=1; a+=1; cout<<a<<endl; } int main() { cout<<"第一次调用:"<<endl; //第一次调用后,虽然变量a的作用域和上例中的auto变量a一样,即都是在AddOne函数体类,但此时a保留了此次调用后留下的值 AddOne(); cout<<"第二次调用:"<<endl; AddOne(); return 0; } //在上面的程序中, 子函数的定义在主函数上面, 如果被定义在主函数下面, 那么在主函数上面要就要有函数声明(function declaration)/函数原型(function prototype), 如: #include<iostream.h> int main() { cout<<"第一次调用:"<<endl; //第一次调用后,虽然变量a的作用域和上例中的auto变量a一样,即都是在AddOne函数体类,但此时a保留了此次调用后留下的值 AddOne(); cout<<"第二次调用:"<<endl; AddOne(); return 0; } void AddOne() { static int a=1; a+=1; cout<<a<<endl; } |
● 返回值为空(void)类型的函数不能进行赋值和传值
返回值为空(void)类型的函数不能进行赋值和传值 |
//有如下空函数: void show_index() { int index=10; cout<<"Index is:"<<index<<end; } //下面的操作时不允许的 i=show_index(); //,非法语句, 返回值为空的函数不能进行赋值 set_index(show_index); //非法语句,返回值为空的函数不能进行值传递 |
● 空函数
空函数: 没有参数和返回值, 函数的作用域也为空 |
void set_work_space() {} //作用是在编程的开始阶段, 在将来准备扩展功能的地方写上一个空函数, 先占一个位置 int main() { set_work_space() return 0; } |
● 自定义并调用可变参数函数
#include <iostream> #include <STDARG.H> //需要包含该头文件 using namespace std; void OutputInfo(int num,...) //定义一个省略号参数的函数 { va_list arguments; //定义va_list类型变量 va_start(arguments,num); while(num--) //读取所有参数的数据 { char* pchData = va_arg(arguments,char*); //获取字符串数据 int iData = va_arg(arguments,int); //获取整型数据 cout<< pchData << endl; //输出字符串 cout << iData << endl; //输出整数 } va_end(arguments); } void main() { OutputInfo(2,"Beijing",2008,"Olympic Games",2008); //调用OutputInfo函数 } |
● 三种函数调用
一般函数调用, 函数的嵌套调用, 函数的递归调用 |
● 嵌套调用
嵌套调用: 在自定义函数中调用其它自定义函数 注意: 在C/C++语言中, 函数的定义都是互相平行、独立的,也就是说,即在定义函数时,一个函数体内不能定义另一函数;即嵌套定义是不允许的,但嵌套调用是允许的,即在一个函数体内可以调用另一函数,如我们常见的在main()函数内调用另一函数。 |
//函数嵌套调用的简单案例 #include <iostream.h> void show_message() { cout<<"The show_message function"<<endl; } void display() { show_message(); //嵌套调用, 即在子函数display()中调用另一个子函数show_message() } int main() { display(); //主函数调用子函数display(), 子函数display()调用另一子函数show_message() return 0; } |
一般函数调用: 函数的嵌套调用: |
//函数嵌套调用的复杂案例 #include <stdio.h> int dif(int x,int y,int z); int max(int x,int y,int z); int min(int x,int y,int z); void main() { int a,b,c,d; scanf("%d%d%d",&a,&b,&c); d=dif(a,b,c); printf("Max-Min=%d\n",d); } int dif(int x,int y,int z) { return max(x,y,z)-min(x,y,z); } int max(int x,int y,int z) { int r; r=x>y?x:y; return(r>z?r:z); } int min(int x,int y,int z) { int r; r=x<y?x:y; return(r<z?r:z); } |
● 递归调用
函数直接或间接的调用自身叫函数的递归调用, 即递归可以分为直接递归调用和间接递归调用。
注意: ① 递归一定要有出口。 ② 递归次数不宜过多,否则可能引起堆栈溢出。 ③ 无限循环不一定会崩溃, 但如果函数的递归没有终止条件, 系统会崩溃. |
#include <stdio.h> void func(int n) { if (n > 0) { func(n - 1); // 这一语句是func()函数运行的第一步,当一次调用的函数运行结束时,系统将释放这次调用时所占用的存储单元,程序的执行流程返回到上一层的调用点,同时取用当初进入该层时函数中的变量(在函数体里面)和形参所占用的存储单元中的数据. // 递推过程(剥卷心菜):func(10)→func(9)……func(1) // 回归过程(合卷心菜):1→2→……10 printf("n=%d\n", n); } else printf("n is less than or equal to 0.\n"); } void main() { func(10); } |
#include <stdio.h> void func(int n) { if (n > 0) { printf("n=%d\n", n); func(n - 1); } else printf("n is less than or equal to 0.\n"); } int main(void) { func(-10); func(0); func(10); return 0; } |
● 分析问题的两种方法:递推(迭代)和递归
分析问题的两种方法:递推(迭代)和递归 There are two approaches to writing repetitive algorithms: recursion and iteration (迭代和递归). ① 递归是重复调用函数自身实现循环。 ② 而迭代与普通循环的区别是:循环代码中参与运算的变量同时是保存结果的变量,当前保存的结果作为下一次循环计算的初始值。 |
版本一: ● 递推:从一个已知的事实出发,按一定的规律推出下一个事实。 通常利用迭代公式法,通过循环结构实现,循环的终值是问题的结果。 ● 递归:从要求的结果出发,归纳出后一个结果和前一个结果存在的关系,直到一个已知值(初值)为止(即反推法)。 这样我们就能设计一个函数(递归函数),使它不断使用下一级值调用自身,直到一个已知值(递归出口)。 版本二: 递归是首先自顶向下逐步拓展计算需求,最后自底向上计算, 即由f(n)拓展到f(1),再由f(1)逐步算回f(n) 迭代是直接自下向顶运算,由f(1)算到f(n)。 版本三: 递归(recursion): Any procedure(A) which, while being executed, either calls itself or calls another procedure (B),which in turn calls procedure(A).过程(A)执行时, 或调用其自身,或调用另一过程(B),而(B)又调用过程(A), 这种进程(process)称为递归。 经典案例: 阶乘, 汉诺塔, Fibonacci数列 迭代(iteration): a. The process of repeating a set of instructions a specified number of times or until a specific result is achieved. 重复一套指令的执行过程,执行特定的次数或直到得到一个特定的结果 b. One cycle of a set of instructions to be repeated: After ten iterations, the program exited the loop. 一个会被反复执行一套指令的循环. 加到100 int v=1; for(i=2;i<=100;i++) { v=v+i; } |
● 函数递归典型案例1: n的阶乘
递推法 |
#include<stdio.h> main() { int m=1,i,n; scanf("%d",&n); for(i=1;i<=n;i++) m=m*i; printf("the result is %10d\n",m); } |
递归法 |
递归函数设计的一般形式是: |
//形式一 递归函数名f(参数n,……) { if(n==初值) return…; else return含f(n-1)的表达式; } #include <stdio.h> int fac(int n) { if(n==0||n==1) return 1;/*递归出口*/ else return fac(n-1)*n; /*递归公式*/ } main() { int n, s; scanf("%d",&n); s=fac(n); printf("%d! =%15d\n",n,s); } |
//形式二 递归函数名f(参数n,……) { 定义一个保存结果的变量t; if(n==初值) 结果(t)=…; else 结果(t)=含f(n-1)的表达式; return(返回结果(t)); } #include <stdio.h> int fac(int n) { int t; //定义一个保存结果的变量t; if(n==0||n==1) t=1;/*递归出口*/ else t=fac(n-1)*n; /*递归公式*/ return(t); } main() { int n, s; scanf("%d",&n); s=fac(n); printf("%d! =%15d",n,s); } 递归程序分两个阶段执行: ① 调用:欲求fac(n)→先求fac(n-1)→fac(n-2) → … → fac(0) 若fac(0)已知,回推结束。 欲求fac(3) →fac(2) →fac(1) →fac(0) ,回推结束。 ② 回推:已知fac(0)→可求出fac(1)→fac(2)→ … → (n) 已知fac(0)→可求出fac(1)→fac(2) →fac(3) 次,递归调用了3次(考虑n=0的情况),到终止条件才有确定的值,然后再递推出每一次调用的值,最后得到所求的结果。
int fact(int n) { if(n<=1) return 1; else return n*fact(n-1); } |
● 函数递归典型案例2: Fibonacci数列问题(求前12项之和)
,610,987,1597,2584,4181,6765,10946,17711,28657,46368... 项是0,第1项是第一个1。这个数列从第2项开始,每一项都等于前两项之和。 在数学上,斐波纳契数列以如下被以递归的方法定义: |
//方法1: 用循环结构来实现 #include <stdio.h> void main() { long int f1,f2; int i; f1=1; f2=1; for(i=1;i<=20;i++) { printf("%12ld %12ld",f1,f2); if(i%2==0) printf("\n"); f1=f1+f2; f2=f2+f1; //每次循环打印两个数字 } } |
//方法2: 递归法 #include<stdio.h> int fun(int n) { if(n==1||n==2) return 1; if(n>2) return (fun(n-1)+fun(n-2)); } void main() { int i; for(i=1;i<=20;i++) { printf("%8d",fun(i)); if(i%5==0) //这两句是用来换行 printf("\n"); } } |
● 函数递归典型案例3: 反向输出一个整数(非数值问题)
//方法1:用循环结构来实现 #include<stdio.h> main() { int n; scanf("%d",&n); if(n<0) { n=-n; printf("-"); //如果n>=0, 这个符号就不会打印出来 } while(n!=0) { printf("%d", n%10); n=n/10; } printf("\n"); } |
:用递归函数来实现 /* 将反向输出一个正整数的算法归纳为: if(n为一位整数) 输出n; else { 输出n的个位数字; 对剩余数字组成的新 整数重复"反向输出" 操作; } */ #include<stdio.h> void main() { void printn(int x); int n; printf("Input n="); scanf("%d",&n); if(n<0) { n=-n; putchar('-'); } printn(n); printf("\n"); } void printn(int x) /*反向输出整数x*/ { if(x>=0 && x<=9) /*若x为一位整数*/ printf("%d",x); /*则输出整数x*/ else /*否则*/ { printf("%d",x%10); /*输出x的个位数字*/ printn(x/10); /*将x中的个位数字去掉,形成新的x后,继续递归操作*/ } } |
● 函数递归典型案例4: 汉诺塔(河内塔)问题
假设: 塔A有n个盘子 已知条件:若只有一个盘子,则是直接从A移到C(递归出口) 算法设计如下:
函数hanoi( int n, int a, int b, int c)设计: 1. 如果(n==1) 则A→C; 步 (1) 调用函数hanoi(n-1,a,c,b); (2) a→c; (3) 调用函数hanoi(n-1,b,a,c); |
#include<stdio.h> void hanoi(int n,char a,char b,char c) { if(n==1) printf("%c--->%c\n",a,c); else { hanoi(n-1,a,c,b); printf("%c--->%c\n",a,c); hanoi(n-1,b,a,c); } } main() { int m; printf("Input the number of disks:"); scanf("%d",&m); printf("The steps to moving%3d disks:\n",m); hanoi(m,'A','B','C'); } |
● 重载函数
重载函数: 多个函数的函数名相同, 但参数类型或参数个数不同(不是返回值类型不同) |
#include <iostream> using namespace std; int Add(int x ,int y) //定义第一个重载函数 { cout << "int add" << endl; //输出信息 return x + y; //设置函数返回值 } double Add(double x,double y) //定义第二个重载函数 { cout << "double add" << endl; //输出信息 return x + y; //设置函数返回值 } int main() { int a = Add(5,2); //调用第一个Add函数 float b = Add(10.5,11.4); //调用第二个Add函数 cout<<a<<endl; cout<<b<<endl; return 0; } |
● 内联函数
内联函数(inline function): 定义了一个内联函数后, 编译器会在调用该内联函数的地方展开这个函数的副本(copy), 这样可以减少函数调用带来的开销(在程序所在文件内移动指针寻找被调用函数地址带来的开销) |
#include<iostream.h> inline int integer_add (int x,int y); //声明该函数为内联函数 void main() { int result=integer_add(5, 10); cout<<result<<endl; } int integer_add(int x,int y) //也可以写成inline int integer_add(int x,int y),表明定义了一个内联函数 { return x+y; } |