cpp函数(一):认识函数

作者邮箱: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. 函数原型
    一般而言,函数原型指明了函数的返回值类型和函数接受的参数类型。格式是:
<返回类型> 函数名(<参数类型>, <参数类型>, ...);

需要注意以下几点:

  • 函数原型的声明应当始终在调用前完成,最合乎规范的写法是在开头处将要用到的函数原型一并列出
  • 检查函数返回值类型,函数接受的参数类型是否都在函数原型中指明,这些信息被称为函数的签名(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()函数调用.


  1. 函数声明
    声明一个函数同样需要注意函数的参数与函数的类型,其基本格式为
<函数类型> 函数名(<函数参数>, <函数参数>){
//具体代码块
}

函数声明也可以与函数原型一起出现,例如下面的例子,不过,我们不建议这种合起来写的方式,显式写出函数原型是一个好习惯。
当然对于内联函数(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;
    }
}

下面我们具体分析

  1. 函数类型
    在代码块中,我们使用return语句返回一个返回值(不过,如果我们使用在return后没有值,如return;,函数会终止,返回值是void),带返回值的函数的类型应该与其返回值类型相同(否则会进行隐式的类型转化),没有返回值的函数声明为void类型。
  2. 函数参数
    从设计的角度考虑,我们有时需要向函数传参,有时则不必。刚才说到,如果没有参数在原型中就可以像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*);,更多关于函数参数的信息将在本节的参数传递中介绍.
  3. 函数调用
    在函数调用中,实际参数(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);,这里进行了从boolint`的整数提升
  • 标准转换,例如有intdouble的转换,doublefloat的转换,等等,这些转换的规则在标准库中已经指明了
    这里,如果在某个调用中产生了歧义性,编译器会报错,假如有下面的语句
    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);
    ...
}
...

其他文章

cpp函数:认识函数
cpp函数:生命周期与作用域
cpp函数:指向函数的指针

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容