美文网首页
cpp函数(一):认识函数

cpp函数(一):认识函数

作者: 浩波千里 | 来源:发表于2019-05-22 17:57 被阅读0次

    作者邮箱: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函数:指向函数的指针

    相关文章

      网友评论

          本文标题:cpp函数(一):认识函数

          本文链接:https://www.haomeiwen.com/subject/hqprzqtx.html