c++(three)
函数
函数的定义与使用
- 所有的高级语言都有子程序的概念,实现模块的划分
- C和C++中的子程序体现为函数
- 一个C++程序由一个主函数和若干其他函数构成
- 函数分为:标准库函数和用户自定义函数
函数的定义
函数是面向对象设计中的基本抽象单元,是对功能的抽象
函数声明的语法形式
函数的定义(函数的类型和返回值)
形式参数表
函数的返回值
C++语言把函数返回值的数据类型规定为该函数的数据类型。因此,函数返回值的类型一定要与函数的类型相同
-
由return语句给出,例如:
return 0
-
无返回值的函数(void类型),不必写return语句
函数的定义(说明)
所有函数都是平行的,即在定义函数时是相互独立的,C++和C不允许函数嵌套定义,即在函数体中在定义一个函数是非法的(与PASCAL不同)
C++程序的执行从main函数开始,调用其他函数后回到main函数,在main函数中结束整个程序的运行。main函数是系统定义的
例:
int main(){
float add(float x,float y); //函数声明
float a,b,c;
cin>>a>>b;
c=add(a,b);
cout<<c<<endl;
}
float add(float x,float y){ //函数首部
float z; //函数体
z=x+y;
return z;
}
//float add(float x,float y){}为函数定义
float add(float x,float y){ //函数首部
int z; //float改为int //函数体
z=x+y;
return z; //出错
}
函数定义的一般形式
获取参数并返回值
int bigger(int a,int b){
return(a>b)?a:b;
}
获取参数但不返回值
void delay(long a){
for(int i=1;i<=a;i++);
}
没有获取参数但返回值
int geti(void){ //键盘获取一个整数
int x;
cout<<"please input a integer:\n";
cin>>x;
return x;
}
没有获取参数也不返回值
void message(void) //在屏幕上显示一个消息
{
cout<<"This is a message.\n";
}
函数的调用
调用形式
函数名(实参列表)
调用的方式
函数语句:
func(); //此时函数可以无返回值
函数表达式
if((x=func())==somevalue) //函数做参数
// func()必须有一个明确的返回值
函数参数
m=max(a,max(b,c));
函数声明和函数原型
-
一个函数调用另一个函数(被调函数)的条件:
点用函数之前必须首先对被调函数进行声明
-
函数声明(declaration)
- 是一条程序语句,包含函数名,返回类型和形式参数列表
- 作用是把函数的名字、返回类型以及形参的类型、个数和顺序通知编译系统,以便在调用该函数时按此进行对照检查(例如函数名是否正确、实参和形参的类型和个数是否一致)
函数声明和函数原型(说明)
-
函数声明的形式:
1.类型标识符 函数名(参数类型1,参数类型2,... ...); 2.类型标识符 函数名(参数类型1 参数名1,参数类型2 参数名2,... ...);
-
函数声明的说明
-
第1种是基本形式,只包含参数的类型
-
为了阅读,也允许加上参数名
-
以下三种声明形式等价
float add(float,float); float add(float a,float b); float add(float x,float y);
-
函数原型(function prototype)
在C++和C中,函数声明就是函数原型
-
函数声明和函数定义的区别
- “定义”指对函数功能的确定,包括指定函数名,返回类型,形参及其类型,函数体,它是一个完整,独立的函数单位
- “声明”只是把函数的原型通知编译系统
- 注意:声明函数时,函数原型预定义函数时函数首部在写法上必须一致,既函数名,返回类型,参数个数,参数类型和参数顺序必须相同,否则产生编译错误。
-
函数原型的作用
在编译阶段对函数调用的合法性进行全面检查
main(){ //编译从上到下逐条进行 float add(float x,float y); //如果没有函数声明,当编译“c=add(a,b)”时,编译系统不知道add是不是函数名,也无法判断实参的类型和个数是否正确,无法进行正确性检查。有可能导致运行时则错误 //在函数调用之前声明了函数原型,编译系统就会根据函数原型对函数调用的合法性进行全面检查,和函数原型不匹配的函数调用就会导致编译出错 c=add(a,b); ... } float add(float x,float y){ ... }
-
函数声明的位置
char letter(char,char); float f(float,float); int i(int,int); //如果在所有函数定义之前,在函数的外部已做了函数声明,那么函数原型在本文件中任何地方有效,即在本文件中任何地方都可以按照函数原型调用相应的函数 main() {...} char letter(char c1,char c2) {...} float f(float x,float y) {...} int i(int x,int y) {...}
函数在被调用之前必须声明或定义,如同变量使用之前必须先定义
-
函数调用的执行过程
函数调用的图例
嵌套调用
函数调用的内部机制
函数调用的过程就是栈空间的操作过程
- 建立被调函数的栈空间
- 保护主调函数的运行状态和返回地址
- 传递参数
- 将控制转变为被调函数
栈
- 是一种数据结构
- 是后进先出(Last In First Out)的线性表
栈
只允许在一端插入和删除的顺序表,插入和删除的一端称为栈顶,另一端称为栈底
特点:后进先出
函数调用时栈的建立过程
函数调用的返回
递归调用
再调用一个子程序或函数的过程之中出现直接或间接调用该子程序或函数本身,称为过程的递归调用
递归调用的形式
-
直接递归
void fun1(void){ ... fun1(); ... } Long fib(int x){ if(x>2) return(fib(x-1)+fib(x-1)); //直接递归 else return 1; }
-
间接递归
例:
int fn1(int a){ int b; b=fn2(a+1); //间接递归 //... } int fn2(int s){ int c; c=fn1(s-1); //间接递归 //... }
递归过程的两个阶段
-
递推
-
回归
例:求n!
分析:计算n!的公式如下
$n!= \begin{cases}1,(n=0)\n(n-1)!,(n>0)\end{cases}$
这是一个递归实现的公式,应该用递归函数实现
long Factorial(long n){
if(n==0)
return 1;
else
return n*Factorial(n-1);
}
求解阶乘n!的过程
三点认识
-
对于一个较为复杂的问题,如果能够分解成几个相对简单的且解法相同或类似的子问题时,只要解决了这些子问题,那么原问题就迎刃而解了,这就是递归求解。
例:4!=4*3!
-
递归结束条件
当分解后的子问题可以直接解决时,就停止分解。可以直接求解的问题叫做递归结束条件
如:0!=1
-
定义的结构
递归定义的函数可以简单地用递归过程来编程求解。递归直接反映了定义的结构
递归的条件
须有完成函数任务的语句
#include<iostream>
void count(int val) //递归函数可以没有返回值
{
if(val>1)
count(val-1);
cout<<"ok:"<<val<<endl;
//此语句完成函数任务
}
一个确定是否能避免递归调用的测试
例:上例代码中,语句“if(val>1)”便是一个测试
一个递归调用语句
例:上例代码中,“count(val-1);”
先测试,后递归调用
例:
#include<iostream>
void count(int val)
{
count(val-1); //无限递归下去
if(val>1) //该语句无法达到
cout<<"ok:"<<val<<endl;
}
自学(汉诺塔问题)
有三根针A、B、C。A针上有N个盘子,大的在下,小的在上,要求把这N个盘子从A针移到C针,在移动过程中可以借助B针,每次只允许移动一个盘,且在移动过程中在三根针上都保持大盘在下,小盘在上。
分析:将n个盘子从A移到C针可以分解为下面三个步骤
-
将A上n-1个盘子移到B针上(借助C针)
-
把A针上剩下的一个盘子移到C针上
-
将n-1个盘子从B针移到C针上(借助A针)
事实上,上面是三个步骤包含两种操作:
- 将多个盘子从一个针移到另一个针上,这是一个递归的过程。hanoi函数实现
- 将1个盘子从一个针上移到另一个针上用move函数实现
如图:
代码实现:
#include<iostream>
#include<strcalss>
void Hanoi(int n,string A,string B,string C) //解决汉诺塔问题的算法
{
if(n==1)
cout<<A<<"to"<<C<<endl;
else{
Hanoi(n-1,A,C,B);
cout<<"move"<<A<<"to"<<C<<endl;
Hanoi(n-1,B,A,C);
}
}
过程:
函数的参数传递机制(传递参数值)
函数定义中指定的形参,在函数未被调用时,并不占有存储单元,在函数被调用时才在栈中为形参分配存储单元,并将实参与形参结合
实参可以是常量,变量,表达式
实参类型必须与形参相同
传递时是传递参数值(Call by value),即单向传递,将实参的值传递给形参,形参值的改变对实参不起作用
举例:
#include<iostream>
void Swap(int a,int b);
int main()
{
int x(5),y(10);
cout<<"x="<<x<<"y="<<y<<endl;
Swap(x,y);
cout<<"x="<<x<<"y="<<y<<endl;
return 0;
}
void Swap(int a,int b)
{
int t;
t=a;
a=b;
b=t;
}
引用
概念
- 引用是一个变量或对象的别名
- 通过引用名与通过被引用的变量名访问变量的效果一样
- 声明一个引用时,必须同时对它进行初始化,使它指向一个已存在的对象
- 一旦一个引用被初始化后,就不能改为指向其它对象
一个引用,从它诞生之时起,就必须确定是那个变量的别名,而且始终只能作为这一变量的别名,不能另作他用
引用的建立
建立引用时,先写上目标类型,后跟引用运算符”&“,然后是引用的名字。引用能使用任何合法的变量名
例:引用一个整型变量
int someInt;
int& rInt = someInt;
程序的建立和使用引用
例:
#include<iostream>
void main(){
int intOne;
int& rInt=intOne;
intOne=5;
cout<<"intOne:"<<intOne<<endl;
cout<<"rInt:"<<rInt<<endl;
rInt=7;
cout<<"intOne:"<<intOne<<endl;
cout<<"rInt:"<<rInt<<endl;
}
//运行结果
//intOne:5
//rInt:5
//intOne:7
//rInt:7
函数的参数传递(用引用做形参)
-
引用运算符只在声明的时候使用,它放在类型名后面
int& rInt=intOne;
-
任何其它”&“的使用都是地址操作符:
例:
int *ip=&intOne; cout<<&ip;
-
为了提高可读性,不应在同一行上同时声明引用,指针和变量
int &rInt,sa;
-
与指针类似,下面三种声明引用的方法都是合法的。
int &rInt;
-
引用可以作为形参
void swap(int &a,int &b) { ... }
引用作为形参时,用实参来初始化形参。引用类型的形参就通过形式结合,成为实参的别名,对形参的任何操作就会作用于实参。
例:输入两个整数交换后输出
#include<iostream>
void Swap(int& a, int& b);
int main( )
{
int x(5), y(10);
cout<<"x="<<x<<" y="<<y<<endl;
Swap(x,y);
cout<<"x="<<x<<" y="<<y<<endl;
return 0;
}
void Swap(int& a, int& b)
{
int t;
t=a;
a=b;
b=t;
}
//运行结果
//x=5,y=10
//x=1-,y=5
注意:
- 引用不能绑定常量
- 引用一旦初始化,其值就不能修改
- 数组不能定义引用
小结
值调用和引用调用的比较
- 值调用是在发生函数调用时,给形参分配存储单元,并用实参来初始化形参(将实参的值直接传递给形参)。这一过程是参数值的单向传递过程,一旦形参获得了值,便与实参脱离关系。以后无论形参怎样改变,都不会影响到实参。
- 引用调用将引用作为形参,在发生函数调用时,实参初始化形参,形参就成为实参的一个别名,对形参的任何操作就直接作用于实参。
内联函数
-
为何使用内联函数(内嵌函数)
-
函数调用需要建立栈内存环境,进行参数传递,并产生程序执行转移,需要时间和空间的开销。如果有的函数被频繁调用,则耗时很长,降低执行效率
-
将规模较小又使用频繁的函数定义为内联函数,在编译时就会将函数体的代码嵌入到主调函数内的每一个调用语句处,节省了参数传递,控制转移等开销
例:
#include<iostream> using namespace std; int isnumber(char); //函数声明 int main(){ char c; while((c=cin.get())!='\n') //读入一个字符给变量并于’\n‘作比较 { if(isnumber(c)) //调用一个小函数 cout<<"You entered a digit \n"; else cout<<"You entered a non-digit \n"; } } int isnumber(char ch) //函数定义 { return(ch>='0'&&ch<='9')?1:0; }
-
为了提高效率,可将程序改为:
#include<iostream> void main(){ char c; while((c=cin.get())!='\n'){ if((ch>='0'&&ch<='9')?1:0) cout<<"You entered a digit \n"; else cout<<"You entered a non-digit \n"; } }
-
解决办法
将isnumber()函数声明为inline
inline int isnumber(char); inline int isnumber(char c){ return(ch>='0'&&ch<='9')?1:0; }
-
-
声明和定义时使用关键字inline
inline 类型说明符 被调函数名 (含类型说明的形参表)
例:
#include<iostream> inline int isnumber(char); //inline函数声明 void main() { char c; while((c=cin.getc())!='\n') { if(isnumber(c)) //调用一个小函数 cout<<"You entered a digit \n"; else cout<<"You entered a non-digit \n"; } } int isnumber(char ch) //此处无inlinr,视为inline { return(ch>='0'&&ch<='9')?1:0; }
-
先声明后调用
-
内联函数的声明必须出现在内联函数第一次被调用之前
例:下面的代码不会像预计的那样被编译
#include<iostream> int isnumber(char); //此处无inline void main() { char c; while((c=cin.getc())!='\n') { if(isnumber(c)) //调用一个小函数 cout<<"You entered a digit \n"; else cout<<"You entered a non-digit \n"; } } int isnumber(char ch) //此处为inline { return(ch>='0'&&ch<='9')?1:0; }
-
内联函数声明与使用
-
内联函数的函数体限制
- 内联函数体内不能有循环语句和switch语句
- 递归函数(自己调用自己的函数)是不能被用来做内联函数的
- 内联函数体只适合只有1~5行的小函数
- 对内联函数不能进行异常接口声明
- 使用内按函数可以节省运行时间,但却增加了目标程序的长度
举例:
#include<iostream> inline int max(int a,int b,intc){ if(b>a) a=b; if(c>a) a=c; return a; } void main(){ int i=7,j=10,k=25,m; m=max(i,j,k); //编译系统遇见函数调用max(i,j,k)时,就用max函数体的代码代替max(i,j,k),同时将实参代替形参。这样,max(i,j,k)就被置换成: // if ( j>i ) i = j ; // if ( k>i ) i = k; // m=i; cout<<m<<endl; }
带默认形参值的函数(默认形参值的作用)
-
默认形参值
-
一般情况下,实参个数应该与形参个数相同。C++允许实参和形参个数不同,办法是在形参列表中对形参指定默认值。
-
调用时如给出实参,则用实参初始化形参,否则采用预先给出的默认形参值。
-
c++可以给函数定义默认参数值。 通常要为函数的每个参数给定对应的实参
例:
void delay(int loops); //函数声明 void delay(int loops) //函数定义 { if(loops==0) return; for(int i=0;i<loops;i++); }
-
有时需要用相同的实参反复调用delay()函数。c++可以给参数定义默认值。
例:
void delay(int loops=1000);
-
默认形参值得说明次序
-
默认形参值必须按从右向左顺序声明,并且在默认形参值的右面不能有非缺省形参值的参数。因为调用时实参初始化形参是从左向右的顺序。
例:
int add(int x,int y=5,int z=6); //正确 int add(int x=1,int y=5,int z); //错误 int add(int x=1,int y,int z=6); //错误
缺省形参值与函数的调用位置
默认形参值在函数声明中提供,当既有声明又有定义时,定义中不允许默认参数。如果函数只有定义,则默认形参值才可以在函数定义中提供。
缺省形参值得作用域
在相同的作用域内,缺省形参值的说明应保持唯一,但如果在不同的作用域内,允许说明不同的缺省形参。
注:
- 在相同的作用域内,即使前后定义的值相同也不行;
- 如果一个函数在定义之前又有原型声明,默认形参值需要在原型中给出,定义中不能再出现默认形参值。
例:
int add(int x=1,int y=2); void main(void) { int add(int x=3,int y=4); add(); //使用局部缺省形参值(实现3+4) } void fun(void){ ... add(); //使用全局缺省形参值(实现1+2) }
函数重载
如果没有重载机制,即使是完全相同的操作,但是针对不同的数据类型,就需要定义名称完全不同的函数。
例如定义加法函数,必须对整数加法和浮点数加法使用不同的函数名:
int iadd(int x,int y)
{
int z;
z=x+y;
return z;
}
int fadd(float x,float y)
{
float z;
z=x+y;
return z;
}
-
C++允许功能相近的函数在相同的作用域内以相同函数名定义, 从而形成重载。方便使用,便于记忆。
-
若干函数,名称相同,但形参个数或类型不同,编译器根据实参和形参的类型及个数的最佳匹配,自动确定调用那一个函数,这就是函数重载。
例:
//形参类型不同 int add(int x,int y); float add(float x,float y); //形参个数不同 int add(int x,int y); int add(int x,int y,int z);
注意事项
- 重载函数的形参必须不同: 个数不同或类型不同。
- 编译程序将根据实参和形参的类型及个数的最佳匹配来自动选择调用哪一个函数
- 不要将不同功能的函数声明为重载函数,以免出现调用结果的误解、混淆。这样不好:
例:编写三个名为add的重载函数,分别实现两整数相加,两实数相加和两个复数相加的功能
#include<iostream.h>
struct complex
{
double real;
double imaginary;
};
void main(void)
{
int m, n;
double x, y;
complex c1, c2, c3;
int add(int m, int n);
double add(double x, double y);
complex add(complex c1, complex c2);
cout<<"Enter two integer: ";
cin>>m>>n;
cout<<"integer"<<m<<'+'<<n<<"=“ <<add(m,n)<<endl;
cout<<"Enter two real number: ";
cin>>x>>y;
cout<<"real number "<<x<<'+'<<y<< "= "<<add(x,y) <<endl;
cout<<"Enter the first complex number: ";
cin>>c1.real>>c1.imaginary;
cout<<"Enter the second complex number: ";
cin>>c2.real>>c2.imaginary;
c3=add(c1,c2);
cout<<"complex number (" <<c1.real<< ','
<< c1.imaginary <<")+("<<c2.real<<','
<<c2.imaginary<<")=("<<c3.real<<','
<<c3.imaginary<<")\n";
}
int add(int m, int n)
{ return m+n; }
double add(double x, double y)
{ return x+y; }
complex add(complex c1, complex c2)
{
complex c;
c.real=c1.real+c2.real;
c.imaginary=c1.imaginary+c2.imaginary;
return c;
}
//运行结果
//Enter two integer: 3 5
//integer 3+5=8
//Enter two real number: 2.3 5.8
//real number 2.3+5.8= 8.1
//Enter the first complex number: 12.3 45.6
//Enter the second complex number: 56.7 67.8
//complex number (12.3,45.6)+(56.7,67.8)=(69,113.4)
重载函数需要注意以下五个问题
- 函数的返回类型不能成为判断依据
- 如果在两个函数的参数列表中只有缺省实参不同,那么它们也不会被看作是重载函数。
- 采用typedef定义的类型也不能作为判断依据。
- 采用const来修饰按值传递的参数也意味着不改变实参的数值,所以不能被看作是重载函数
- 重载函数中所有的函数定义都必须在同一个域中,不同类域中的函数也不能构成重载函数
重载函数的选择
重载函数被调用时,系统会根据指定的实参在多个同名的函数中选择一个最合适的函数,通常分为三个步骤:
- 在重载函数集合中找出可行函数,可行函数的参数个数要和调用时的实参的个数相同,或者多一些缺省的参数,可行函数的参数类型要和调用时实参的类型相同,或者可以转换。
- 在可行函数中,寻找精确匹配的函数,精确匹配是指实参和形类的类型和个数完全相同,也指从数组到指针的类型转换,从函数到指针的类型转换。
- 依据数据类型转换规则,找到可匹配函数。类型转换被分成三个等级:提升转换、标准转换和自定义转换。
如果通过上述三步仍然无法找到任何可匹配函数,那么该函数的调用就是二义的。
函数模板
代码重用
- 按不同方式重复使用已有的代码
- 代码必须通用,不受数据类型的限制,这是参数化程序设计
- 模板有可以使用和操作任何数据类型的通用代码构成,其中将所使用的数据类型说明为参数
函数模板可以用来创建一个通用功能的函数,以支持多种不同形参,进一步简化重载函数的函数体设计。
函数模板的定义形式
template<类型形式参数表>
类型名 函数名(形式参数表)
{
//函数体
}
如果形式参数包含基本数据类型,加前缀typename
template <typename T>T func(T x)
如果形式参数表包含类类型名,加前缀class template T func(T x)
template <class T> 或 template <typename T>
类型名 函数名(形式参数表)
{
函数体
}
例:求绝对值函数的模板
#include<iostream.h>
template<typename T>
T abs (T x)
{ return x<0?-x:x; }
void main( )
{ int n=-5;
double d=-5.5;
cout<<abs(n)<<endl;
cout<<abs(d)<<endl;
}
当类型参数的含义确定后,编译器将以函数模板为样板,生成一个函数:
int abs (int x)
{ return x<0?-x:x; }
对于调用表达式abs(d),由于实参d为double类型,所以推导出模板中类型参数T为double。接着,编译器将以函数模板为样板
double abs (double x)
{ return x<0?-x:x; }
函数模板的说明
-
函数模板
- 函数模板的定义不是一个实实在在的函数,编译系统不为其产生任何执行代码。该定义只是对函数的描述,表示它每次能处理在类型形式参数表中说明的数据类型
- 当编译系统发现一个函数调用:func(实参表),将根据实参表中的类型,确认是否匹配函数模板中对应的形式参数表,然后生成一个函数。该函数的函数体与函数模板中的函数体相同,而形式参数表的类型则以实参表的实际类型为依据。该函数称为模板函数。
-
函数模板与模板函数的区别
函数模板是模板的定义,定义中用到通用类型参数; 模板函数是实实在在的函数定义,由编译系统遇见具体的函数调用时生成,具有执行代码。
-
对上例的说明
-
编译器从调用abs( )时实参的类型,推导出函数模板的类型参数。例如,对于调用表达式abs(n),由于实参n为int类型,所以推导出模板中类型参数T为int。
-
当编译器发现调用函数模板时,就创建一个模板函数。在上例中,当编译器发现abs(n)调用用时,产生一个如下的函数定义,生成其程序代码:
int abs(int x){ return x<0?-x:x; }
当发现abs(d)调用时,产生如下的函数定义,也生成其程序代码:
double abs(double x){ return x<0?-x:x; }
-
函数模板实例化
隐式实例化
#include <iostream>
using namespace std;
//定义函数模板
template<typename T>
T add(T t1, T t2)
{
return t1 + t2;
}
int main()
{
cout << add(1, 2) << endl; //传入int类型参数
cout << add(1.2, 3.4) << endl; //传入double类型参数
system("pause");
return 0;
}
显式实例化
#include <iostream>
using namespace std;
template<typename T>
T add(T t1, T t2)
{
return t1 + t2;
}
template int add<int>(int t1, int t2); //显式实例化为int类型
int main()
{
cout << add<int>(10, 'B') << endl; //函数模板调用
cout << add(1.2, 3.4) << endl;
system("pause");
return 0;
}
使用函数模板注意事项
- 函数模板中每一个类型参数在函数参数表中必须至少使用一次
- 在全局域中声明的模板参数同名对象、函数或类型、在函数模板中将被隐藏。
- 函数模板中定义声明的对象或类型不能与模板参数同名
- 模板参数名在同一模板参数表中只能使用一次,但可以在多个函数模板声明或定义之间重复使用。
使用C++系统函数
-
模板参数名在同一模板参数表中只能使用一次,但可以在多个函数模板声明或定义之间重复使用。
例如:
求平方根函数(sqrt),求绝对值函数(abs)等
-
使用系统函数是要包含相应的头文件
使用函数之前必须先声明函数原型。系统函数的原型存在于不同的头文件中。用#include指令嵌入相应的头文件,就可以使用系统函数。
例如:
#include<math>