作者邮箱:z_zhanghaobo@163.com
github相关: https://github.com/HaoboLongway/simple_examples
什么是函数?函数是完成特定任务的独立程序代码单元,那么我们为什么要使用函数呢?无疑,把函数当作构件块,可以有效地组织代码结构,就像如果要得到控制台输入的所有的整数之和,我们可能在main
函数中这样写:
int main(){
read_data(); //from the console
get_sum();
...
这里我们并没有关注read_data()
和get_sum()
的具体实现,而是从结构上分析如何设计,组织该程序,条理更为清晰。
从黑盒角度来看,我们给函数一个或多个参数(也可能没有),然后它做一些事情,最后有一个返回值(也可能没有),这就是大体上函数的“样子”。
下面我们开始关注函数的具体细节,从创建一个函数开始吧!
创建函数
先来看下面的例子
#include <iostream>
using namespace std;
void show_star(void); //(1)
int main(){
show_star(); //(2)
show_star();
show_star();
}
void show_star(void) //(3)
{
int i=0;
for(;i<40;i++){
cout<<"*";
}
cout<<'\n';
}
不难看出示例中定义了函数show_star()
,作用是在一行打印40个*
号,下面我们具体分析:
- 程序中有三处以注释备注了序号
1 2 3
,程序在三处均使用了show_star
标识符。具体地,- (1)注释处是函数原型(function prototype),这是为了告诉编译器函数
show_star()
的类型 - (2)注释处是函数调用(function call),表明在此处执行函数
- (3)注释处是函数定义(function definition),你要在这里明确指出函数要做什么
- (1)注释处是函数原型(function prototype),这是为了告诉编译器函数
- 函数与变量一样,有多种类型。任何程序在使用函数前均需要声明函数的类型,函数原型就保证了这一点,下面我们再具体对以上三个阶段加以介绍
-
函数原型
一般而言,函数原型指明了函数的返回值类型和函数接受的参数类型。格式是:
<返回类型> 函数名(<参数类型>, <参数类型>, ...);
需要注意以下几点:
- 函数原型的声明应当始终在调用前完成,最合乎规范的写法是在开头处将要用到的函数原型一并列出
- 检查函数返回值类型,函数接受的参数类型是否都在函数原型中指明,这些信息被称为函数的签名(signature),对于
show_star()
函数,其签名是没有返回值,也没有参数(均为void
) - 在声明含有形式参数的函数原型时,用逗号分隔的列表来指明参数的数量和类型,也可以省略变量名,例如
void print_star(int star_num);
和void print_star(int);
都是可以的.如果一个函数不接受参数,规范写法是使用关键字如`void show_star(void); - 如果一个函数被重载了,有必要把各个函数原型罗列出来,请看下面的例子:
...
int mult_power(int base, int power); //(1)
int mult_power(int base); //(2)
int main()
{
cout<<mult_power(4, 3)<<endl;
cout<<mult_power(4);
}
int mult_power(int base){
return base*base;
}
int mult_power(int base, int power){
int counter, result=1;
for(counter=0;counter<power;counter++){
result *= base;
}
return result;
}
这个程序能正常运行,也达到了预期的效果,代码中标注(1) (2)
注释行的函数原型缺一不可,它们告诉编译器如何处理传入实参不同的mult_power()
函数调用.
- 函数声明
声明一个函数同样需要注意函数的参数与函数的类型,其基本格式为
<函数类型> 函数名(<函数参数>, <函数参数>){
//具体代码块
}
函数声明也可以与函数原型一起出现,例如下面的例子,不过,我们不建议这种合起来写的方式,显式写出函数原型是一个好习惯。
当然对于内联函数(inline function),我们必须在文件开头写出,关于内联函数将在第三部分介绍
...
inline int fac(int n){ return (n<2)? 1:n*fac(n-1);}
int main()
{
for(int i=1;i<10;i++)
{
cout<<"The factorial of "<<i<<" is "<<fac(i)<<endl;
}
}
下面我们具体分析
-
函数类型
在代码块中,我们使用return
语句返回一个返回值(不过,如果我们使用在return
后没有值,如return;
,函数会终止,返回值是void
),带返回值的函数的类型应该与其返回值类型相同(否则会进行隐式的类型转化),没有返回值的函数声明为void
类型。 -
函数参数
从设计的角度考虑,我们有时需要向函数传参,有时则不必。刚才说到,如果没有参数在原型中就可以像void i_need_nothing_from_you(void)
,在定义函数的声明中则可以void i_need_nothing_from_you(){...}
,对于需要参数的函数,这些变量被称为形式参数(formal parameter),形式参数也是局部变量,这意味这在其他函数中可以使用同名变量而不起冲突.
函数参数还可以分为默认参数以及非默认参数,我们可以在函数中使用默认参数像是这样void print_num(int val, int base=10)
。
在圆括号中,我们将形参的类型以及变量名的列表一并写出。例如C风格复制字符串函数char * strcpy(char* to, const char* from);
,
求字符串长度函数int strlen(const char*);
,更多关于函数参数的信息将在本节的参数传递中介绍. -
函数调用
在函数调用中,实际参数(autual argument)提供了形参的值,例如如果我们定义了一个函数do_something()
,其函数原型是do_something(int matter);
,那么我们如果以do_something(1+2/3*3-1)
的方式调用它,那么实参也就是1+2/3*3-1
(值为0)提供了形参matter
的值,实际参数是主调函数赋给函数的具体值,无论如何,实际参数总被要求求值,然后该值被拷贝给被调函数相应的形参。
参数传递
参数传递是一个重点,前面说到了一些传递参数时注意的事项,例如实参的值被拷贝给被调函数相应的形参,这是一般情形,我们可以使用引用或者指针对传入的参数“就地修改”,传递的参数有传值与传引用两种分别,请看下面的例子
...
int main()
{
int i, j, k;
i=j=k=1;
cout<<"Previous value: "<<i<<setw(5)<<j<<setw(5)<<k<<endl;
fake_change(i);
ptr_change(&j);
ref_change(k);
cout<<setw(17)<<'|'<<setw(5)<<'|'<<setw(5)<<'|'<<endl;
cout<<setw(17)<<'|'<<setw(5)<<'|'<<setw(5)<<'|'<<endl;
cout<<"Present value: "<<i<<setw(5)<<j<<setw(5)<<k<<endl;
}
void fake_change(int i)
{
i++;
}
void ptr_change(int * i)
{
(*i)++;
}
void ref_change(int & i)
{
i++;
}
输出为
Previous value: 1 1 1
| | |
| | |
Present value: 1 2 2
不难见到这里fake_change()
是对原参数的副本进行了递增操作,而采用指针的函数ptr_change()
以及采用引用的函数ref_change()
都对原值进行了修改,引用的方式写起来更简便些。
使用引用十分简单,我们只需要加上&
符即可,编译器会根据上下文判断这是一个引用的声明还是取地址操作.
在函数定义中的参数列表中,形参可以被const
修饰,它使得形参的值在后来不能再改变,例如
...
int main()
{
char * epitaph =
"So we beat on,\nboats against the current, \nborne back ceaselessly into the past\n";
cout<<"The sentences are:\n"<<epitaph;
cout<<"The first sentence is:\n";
show_head(epitaph);
}
void show_head(const char *str)
{
while(*str != ','){
cout<<*str;
str++;
}
}
show_head()
输出的是epitaph
的首句,结果如下:
The sentences are:
So we beat on,
boats against the current,
borne back ceaselessly into the past
The first sentence is:
So we beat on
这里不需要对形参str
做出修改,使用const
声明一定程度上维护了程序的安全
关于从实参到形参的过程,还有一些细节值得注意,首先以下做法编译器会报错:
- 传入的参数个数与形参个数不符
- 传入的参数类型不能够转换为形参类型
这里面如果实参的类型与形参不符,那么编译器会隐式地进行类型转换,当然,不是所有的类型之间都可以进行转换。此外,还是小心传参为好,因为我们知道类型转换不总是合乎心意。
函数重载
有时我们希望能有“一个”函数做”多种“工作,这就需要函数重载,重载在C++中十分常见,例如加法只有一个符号也就是+
,而却可以用于整数,浮点数,双精度数,指针值等的加法。重载在函数中显然也是被允许的。
void print(int);
void print(float);
void print(const char*);
观察上面的函数原型,你会发现print()
被重载了(没错,就是这么简单)。事实上,两个print()
函数在某种意义上是相互类似的,不过,重载函数名从根本上来讲是一种记法上的方便,在特定的应用场景下,这种方便性就尤为重要,例如这里的print()
函数。
关于重载函数,很重要一点是,当print()
被调用时,编译器必须弄清究竟调用的是哪一个函数。这时编译器会将实际参数的类型与所有的重载函数形参类型相比较,基本想法是去调用其中的那个在参数上匹配得最好的函数.
匹配规则是这样的(级别由高到低):
- 准确匹配,例如调用'print(2.0f)'对应的函数原型即为
void print(float);
- 利用提升的匹配, 包括整数提升等,调用’print(false)
对应的函数原型即为
void print(int);,这里进行了从
bool到
int`的整数提升 - 标准转换,例如有
int
到double
的转换,double
到float
的转换,等等,这些转换的规则在标准库中已经指明了
这里,如果在某个调用中产生了歧义性,编译器会报错,假如有下面的语句
long int a = 5L; print(a);
报错信息是call of overloaded 'print(long int&)' is ambiguous
,因为long int
可以向int
也可以向float
转换,恰巧的是,这两种转换是同级的,所以产生了歧义。
下面是一个函数重载的例子,clear_up()
和print_ar()
都进行了重载
#define size 10
using namespace std;
void clear_ar(int *);
void clear_ar(float *); //对clear_up重载
void print_ar(int *);
void print_ar(float *); //对print_ar重载
void show(int ar1[], float ar2[]);
int main()
{
int test_ar[size] = {1, 3, 4, 8, 3, 7};
float ftest_ar[size] = {1, 3, 4, 8, 3, 7};
//Print it
show(test_ar, ftest_ar);
// clear up
clear_ar(test_ar);
clear_ar(ftest_ar);
cout<<"After clearing the array you can see that:"<<endl;
//Then print the array again.
show(test_ar, ftest_ar);
}
void clear_ar(int * ar){
int i=0;
for(;i<size;i++){
*(ar + i) = 0;
}
}
void clear_ar(float * ar){ //两个clear_ar函数,它们的逻辑是一致的
int i=0;
for(;i<size;i++){
*(ar + i) = 0;
}
}
void print_ar(int *ar){
int i=0;
for(;i<size;i++){
cout<<*(ar+i)<<' ';
}
cout<<endl;
}
void print_ar(float *ar){
int i=0;
for(;i<size;i++){
cout<<*(ar+i)<<' ';
}
cout<<endl;
}
void show(int ar1[], float ar2[]){
cout<<"Test_ar:\n";
print_ar(ar1);
cout<<"Ftest_ar:\n";
print_ar(ar2);
}
最后,该程序的输出与我们料想的一致
Test_ar:
1 3 4 8 3 7 0 0 0 0
Ftest_ar:
1 3 4 8 3 7 0 0 0 0
After clearing the array you can see that:
Test_ar:
0 0 0 0 0 0 0 0 0 0
Ftest_ar:
0 0 0 0 0 0 0 0 0 0
另外需要知道,在不同的非命名空间作用域里声明的函数不算是重载。(下一节主要介绍作用域的问题)
例如
void f(int);
void g()
{
void f(double);
f(1);
...
}
...
该程序中只有f(double)
函数原型在作用域里,所以对f()
的调用会把(int)1
转换为(double)1
,我们可以通过加入或者去除局部声明来取得所需要的行为,例如:
void f(int);
void g()
{
void f(double);
extern void f(int);
f(1);
...
}
...