zoukankan      html  css  js  c++  java
  • c++primer笔记六、函数

    6.1函数基础

    一个典型的函数定义包括:返回类型、函数名字、0个或多个形参组成的列表、函数体。
    函数执行的操作在语句块中说明,成为函数体。
    通过圆括号()调用运算符来执行。

    局部静态对象:定义成static
    在函数调用时,只初始化一次,函数结束仍然有效。

    函数声明

    函数只能定义一次,但是可以声明多次。
    函数声明无需函数体,用分号替代。
    函数的三要素(返回类型,函数名,形参类型)描述了函数的接口,说明了调用该函数所需的全部信息。函数声明也称函数原型。

    建议在头文件中声明,在源文件中定义。

    分离式编译

    允许把程序分割到几个文件,每个文件独立编译。

    factMain调用fact,生产可执行文件
    编译的过程如下:
    $ CC factMain.cc fact.cc #generates factMain.exe
    $ CC factMain.cc fact.cc -o #generates main.exe
    

    如果修改了其中一个源文件,只需要重新编译改动的文件。
    分离式编译产生一个.obj或.o文件,含义是该文件包含对象代码。

    接下来编译器负责把对象文件链接在一起形成可执行文件
    实际编译过程:

    $ CC -c factMain.cc             #generates factMain.o
    $ CC -c fact.cc                 #generates fact.o
    $ CC factMain.o fact.o          #generates factMain.exe or a.out
    $ CC factMain.o fact.o -o main  #geneartes main or main.exe
    

    6.2参数传递

    每次调用函数都会重新创建形参,并用传入的实参对形参进行初始化,即拷贝。
    如果形参是引用类型,则实参被引用传递,函数被传引用调用。
    如果形参是实参的拷贝,则成为值船体,函数被传值调用。

    指针形参

    执行指针拷贝操作时,拷贝的时指针的值,拷贝之后两个指针时不同的。

    void reset(int *ip)
    {
        *ip = 0;        //改变ip所指对象的值
        ip = 0;         //改变了ip的局部拷贝,实参未改变。
    }
    
    int i = 42;
    reset(&i);      //i = 0;改变的是i的值,不会改变i的地址
    
    传引用参数
    void reset(int &i)
    {
        i = 0;      //改变了i所引对象的值
    }
    int j = 42;
    reset (j);      // j = 0;
    

    拷贝大的类型或容器比较低效,有的类型(IO)还不支持拷贝,函数只能引用形参访问改类型对象。
    如果函数要返回多个值,一个好方法就是给函数一个额外的引用参数,令其保存结果。

    const形参和实参

    首先要注意顶层const的使用

    const int ci = 42;      //不能改变ci,const是顶层的
    int i = ci;             //拷贝时可以忽略顶层const
    int * const p = &i;     //const是顶层的,p无法再赋值
    *p = 0;                 //通过p改变对象内容是允许的,i = 0
    

    当用实参初始化形参时会忽略顶层const。
    如果形参有const,传入的常量对象或者非常量对象都是可以的。

    指针或引用参与const
    void reset(int &i)
    {
        i = 0;      //改变了i所引对象的值
    }
    
    int i = 0;
    const int ci = i;
    string::size_type ctr = 0;
    reset(&i);          //可以
    reset(&ci);         //错误,不能指向const
    reset(i);           //可以
    reset(ci);          //错误
    reset(42);          //错误,不能把普通字面值作为引用
    reset(ctr);         //错误,类型不匹配
    
    尽量使用常量引用

    如果把引用不设置未常量,容易误导人以为可以修改实参。
    同时会限制参数,因此如果不需要修改就使用常量引用。

    数组形参

    数组有两个性质:1、不允许拷贝。2、使用数组时会转换成指针。
    因此无法值传递使用数组参数,只能用类似形式。

    //三个函数等价,每个函数都有const int*类型的形参
    void print(const int*)
    void print(const int[]])
    void print(const int[10]])  //这里的维度表示期望含有的元素,实际不一定
    

    调用时

    int i = 0;
    int j[2] = {0, 1}
    print(&i);      //正确,&i的类型时int*
    print(j);       //正确,j转换成int*指向j[0]
    

    因此一开始不知道数组的确切尺寸

    1、使用标记指定数组长度

    管理数组实参的一种方法是要求数组本身包含一个结束标记,如C风格字符串,最后有一个空字符表示结束。

    适用于有明显结束标记的数组

    void print(const char *cp)
    {
        if(cp)              //CP不是空指针
            while(*cp)      //只有指针所指不是空字符
                count << *cp++  //打印
    }
    
    2、使用标准库规范

    传递指向数组首元素和伪元素的指针。

    void print(const int *beg; const int *end)
    {
        while(beg != end)
            count << *beg << endl;
    }
    
    调用时传入两个指针
    int j [2] = {0,1};
    print(begin(j),end(j));
    
    3、显式传递数组大小的参数

    实参专门定义一个数组大小的形参
    只要size不超过实际数组大小,就是安全的。

    void print(const int ia[]; size_t size)
    {
        for(size_t i = 0; i != size; ++i)
            cout << ia[i] << endl;
    }
    
    数组形参和const

    当函数不需要对数组读写时,形参应该是指向const的指针。只有函数需要改变元素值时,才把形参定义指向非常量的指针。和指针类似。

    数组引用形参

    形参可以是数组的引用。(括号不可少)

    void print(int (&arr)[10])
    {
        for (auto elem : arr)
            cout << elem << endl;
    }
    
    函数体只能作用于大小为10的数组
    
    传递多维数组

    处理的是数组的数组,首元素本身是一个数组,指针是一个指向数字的指针。

    void print(int (*matrix)[10], int rowSize)
    
    等价定义
    void print(int matrix[][10], int rowSize)
    
    main:处理命令行选项

    有时需要给main传递实参,一种常见的情况是用户通过设置一组选项来确定函数要执行的操作。
    例如:假定main函数位于可执行文件prog内,我们可以向程序传递下面的选项:

    prog -d -o ofile data0
    

    命令行通过两个(可选的)形参传递给main函数

    int main(int argc, char *argv[])
    

    第一个形参表示数组中字符串的数量。
    第二个形参是一个数组,它的元素是指向C风格字符串的指针。

    也可以定义成
    int main(int argc, char **argv0)
    其中argv指向char*  
    

    当实参传递给main后,argv的第一个元素指向程序的名字或者一个空字符串。接下来的元素一次传递命令行提供的实参。最后一个指针之后的元素值保证为0.

    上述命令行为例
    argv[0] = "prog";
    argv[1] = "-d";
    argv[2] = "-o";
    argv[3] = "ofile";
    argv[4] = "data";
    argv[5] = 0;
    
    含有可变形参的函数

    为了编写能处理不同数量实参的函数,C++ 11有两种主要方法:
    1、如果所有实参类型相同,可以传递一个名为initializer_list的标准库函数。
    2、如果实参类型不同,可以编写一种特殊函数,即可变参数模板。

    C++还有一种特殊的形参类型(省略符),可以传递可变数量的实参。
    这种功能一般只用于与C函数交互的接口程序。

    initializer_list形参

    表示某种特定类型的值的数组。
    提供的操作有:

    initializer_list<T> lst //默认初始化;T类型元素的空列表
    initializer_list<T> list{a,b,c}//lst的元素数量和初始值一样多;lst的元素是对应初始值的副本;列表中的元素是const
    lst2(lst) ;// 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;
    lst2 = lst; //拷贝后,原始列表和副本共享元素
    lst.size(); //数量
    lst.begin();    //首元素指针
    lst.end();  //尾元素下一个位置的指针。
    

    和vector一样,initializer_list也是模板类型,需要说明所含元素类型。不过initializer_list对象中的元素永远是常量,无法修改。

    initializer_list<int> li;
    

    如下编写输出错误信息的函数,使其可以作用于可变函数的实参:

    void error_msg(initializer_list<string> il)
    {
        for (auto beg = li.begin(); beg != il.end(); ++beg)
        {
            cout << *beg << "";
        }
        cout << endl
    }
    

    如果想向initializer_list形参中传递一个值,要用花括号。

    error_msg({"functionX", expected, actual})
    

    含有initializer_list形参的函数也可以同时拥有其他形参。

    省略符形参

    是为了c++程序访问某些特殊的C代码设置的,这些代码使用了varargs的标准库。
    通常省略符形参不应用其他目的。
    只能出现在形参列表最后一个位置

    void foo(parm_list, ...);
    

    6.3、返回类型和return语句

    无返回值函数

    只能用在void函数中。void的函数最有一句后面会隐式地执行return。
    一般用法是中间位置提前退出,类似break。

    有返回值函数

    只要函数类型不是void,则必须返回一个对应的值。
    函数返回的是一个拷贝的临时对象。
    函数也可以返回引用

    const string &shorterString(const string &s1. const string &s2)
    {
        retrun s1.size() <= s2.size() ? s1 : s2;
    }
    
    不要返回局部对象的引用

    函数完成后,所占用的存储空间就释放了,因此局部变量的引用就不在有效。

    引用返回左值

    调用一个返回引用的函数得到左值,其他返回类型得到右值。

    char &get_val(string &str, string::size_type ix)
    {
        return str[ix];
    }
    get_val(s, 0) = 'A';
    

    如果返回的是常量引用就不能赋值。

    列表初始化返回值

    c++11允许返回花括号包围的值的列表。

    vector<string> process()
    {
        //expected和actual是string
        if(expected.empty())
            return{};
        else if (expected == actual)
            return {"functionX", "okay"};
        else
            return {"functionX", expected, actual}
    }
    
    主函数main的返回值

    允许main函数没有return(有一个隐藏的retrun 0)
    main函数的返回值看作状态指示器,返回0表示成功,其他表示失败。
    为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量。

    int main()
    {
        if(some_failure)
            return EXIT_FAILURE;
        else
            return EXIT_SUCCESS;
    }
    
    递归

    函数调用自身就是递归。
    递归函数必须有一个路径是不包含递归调用的,否则就会死循环。

    返回数组指针

    数组不能拷贝,因此函数不能返回数组,只能返回指针或引用。

    声明一个返回数组指针的函数
    int (*func(int i))[10];
    
    使用尾置返回类型

    c++简化方法,使用尾置返回类型
    fun函数返回指针,指针指向了10个整数的数组

    auto func(int i) -> int(*)[10]
    
    使用decltype

    如果知道函数返回的指针将指向哪个数组,就可以使用decltype来声明返回类型。

    int odd[] = {1,3,5,7,9};
    int even[] = {0,2,4,6,8};
    decltype(odd) *arrPtr(int i)
    {
        return (i % 2) ? &odd : &even;
    }
    

    arrPtr使用decltype表示它的返回类型是指针,并且所指对象与odd类型一致。因为odd是数组,所以arrPtr返回一个指向含有5个整数的数组的指针。

    6、4函数重载

    统一作用域内的几个函数名字相同但是形参列表不同,称之为重载。

    有顶层const的形参和没有的无法区分

    Record lookup(Phone);
    Record lookup(const Phone); //重复声明错误
    
    

    如果形参是指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现重载,此时const是底层的。

    Record lookup(Phone*);  //函数作于指向Phone的指针
    Record lookup(const Phone*);    //函数作用于指向常量的指针   
    
    const_cast和重载

    当函数的实参不是常量时,得到的结果是一个普通的引用,就可以使用const_cast

    string &shorterString(string &s1, string &s2)
    {
        auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2));
        return const_cast<string&>(r);
    }
    

    先把实参强制转成const的引用,然后调用函数,返回对const string的引用,再将其转换回一个普通的string&,是安全的。

    调用重载的函数

    把函数调用与一组重载函数中的某一个关联起来,叫函数匹配,也叫重载确定。
    调用重载函数有3种可能的结果:
    1、找到一个最佳匹配的函数
    2、找不到任何一个匹配的,编译器发出无匹配的错误
    3、有多于一个函数可以匹配也会发生错误,称为二义性调用

    重载和作用域

    函数只会调用最近的作用域中的重载函数。

    6、5特殊用途语言特性

    默认实参

    定义成如下

    string screen(sz ht = 24; sz wid = 80; char backgrnd = ' ')
    

    一旦每个形参被赋予了初值,剩下的都必须有默认值。
    实际调用时只能省略尾部的实参。

    window = screen(66)
    window = screen('?')        /实际调用screen('?', 80,' ')
    
    默认实参声明

    通常放在头文件,且一个函数只声明一次。
    每个形参只能被赋予一次默认值,如果后续声明为了添加默认实参,也是允许的。

    默认实参初始值

    局部变量不能作为默认实参,除此之外只要表达式的类型能转换成形参所需要的类型,该表达式就能作为默认实参。

    sz wd = 80;
    char def = '';
    sz ht();
    string screen(sz = ht(); sz = wd; char = def);
    
    如果:
    void f2()
    {
        def = '*;           //更新了传递的值
        sz wd = 100;        //隐藏了外层的wd,但与传递的wd无关
        window = screen();  //调用screen(ht(), 80, '*');
    }
    
    内联函数和constexpr

    调用函数有一系列工作,会比较慢

    内联函数可以避免函数调用的开销

    在函数返回类型前加上inline,声明成内联函数

    inline const string &shorterString(const string &s1, const string &s2)
    {
        return s1.size() <= se.size() ? s1 : s2;
    }
    

    一般用于优化规模较小,流程直接,频繁调用的函数。很多编译器不支持内联递归函数。

    constexpr函数

    指能用于常量表达式的函数。
    定义时候的约定:
    1、函数的返回类型及所有形参类型都是字面值类型
    2、函数体中必须有且只有一个return

    constexpr int new_sz() {return 42;}
    constexpr int foo = new_sz();
    

    在编译中直接替换成结果值,constexpr函数被隐式地指定为内联函数。
    constexpr函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作。

    把内联函数和constexpr函数放在头文件

    内联函数和constexpr函数可以多次定义,但是它的多个定义必须完全一致。

    调试帮助

    程序可以包含一下用于调试的代码,在发布时要屏蔽,用到两个预处理功能。assert和NDEBUG

    assert

    是一种预处理宏,即一个预处理变量。使用一个表达式作为条件

    assert(expr);
    

    如果表达式为假,assert输出信息并终止程序;否则什么也不做。
    定义在cassert头文件中,名字由预处理器管理,因此不需要提供using声明。
    常常用于检查“不能发生”的条件。

    NDEBUG

    assert的行为依赖于NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。默认状态下没定义。

    可以使用#define定义NDEBUG,从而关闭调试状态。
    很多编译器都提供了一个命令行选项是我们可以预处理变量

    $ CC -D NDEBUG main.c # use /D with the Microsoft compiler
    

    这条命令等价于在开头写#define NDEBUG

    还可以使用NDEBUG编写自己的调试代码,如果NDEBUG没定义,将指向#ifndef和#endif之间的代码

    void print(const int io[], sizt_t size)
    {
    #ifndef NDEBUG
        cerr << __func__ << ": array size is" << size << endl;
    #endif
    }
    

    __func__输出当前函数的名字。
    编译器为每个函数都定义了一些名字

    __FILE__        //存放文件名的字符串字面值
    __LINE__        //存放当前行号的整型字面值
    __TIME__        //存放文件编译时间的字符串字面值
    __DATE_-        //存放文件编译日期的字符串字面值
    

    6、6函数匹配

    有时候重载函数太多,不容易选择。

    确定候选函数和可行函数

    函数匹配第一步是选本次调用对应的重载函数集。
    两个特征:1、同名 2、声明在调用点可见。
    第二步考察实参,然后选出可行函数。
    两个特征:1、形参与调用提供的实参个数相等。2、类型也相同
    第三步:寻找最佳匹配。逐一检查提供的实参类型,寻找最匹配的可行函数。

    void f(int);
    void f(double, double = 3.14);
    f(3.14);        //调用第二个
    
    含有多个形参的函数匹配

    如果有且只有一个函数满足下列条件则匹配成功:
    1、函数的每个实参匹配都不劣于其他可行函数需要的匹配。
    2、至少有一个实参的匹配优于其他可行函数的匹配。
    如果没有函数可以,就是错误的,会报错。

    void f(int, int);
    void f(double, double = 3.14);
    f(3, 3.14)      //直接报错,有二义性。
    
    实参类型转换

    为了确定最近匹配,编译器将实参类型到形参类型的转换分了几个等级:
    1、精确匹配(类型相同、转换成对应指针、添加顶层const或删除顶层const)
    2、通过const转换实现的匹配
    3、类型提升
    4、算数转换或指针转换
    5、通过类类型转换

    需要算数提升和转换的匹配

    如果有两个函数,优先算术提升

    void ff(int);
    void ff(short);
    ff('a');        //提升为int调用int的
    

    算术类型转换的级别都一样

    void manip(long);
    void manip(float);
    manip(3.14);        //错误,二义性调用
    
    函数匹配和const实参

    如果重载函数的区别在于引用类型的形参是否引用了const,或者指针类型的形参是否指向const,那么就会根据实参是否是变量来决定使用哪个。
    如果传入const对象,所以唯一可行的函数是常量引用函数。
    如果传入普通对象,两个函数都可调用,优先使用非常量版本的函数。

    6、7函数指针

    指向的是函数而非对象。

    bool (*pf)(const string &, const string &);
    

    pf是一个指向函数的指针,该函数的参数是两个const string引用,返回值是bool。

    使用函数指针

    把函数名作为一个值使用时,函数自动转换成指针

    pf = lengthCompare;        //pf指向这个函数
    pf = &LengthCompare;    //等价的,取地址符时可选的
    

    调用时可以直接使用,不需要解引用

    boo1 b1 = pf("Hello", "goodbye");
    boo1 b2 = (*pf)("Hello", "goodbye");
    bool b3 = lengthCompare("Hello", "goodbye");
    //三者等价
    

    函数指针可以赋值为nullptr或者0,表示没有指向函数。
    指向时函数的返回类型,形参类型必须精确匹配。

    重载函数的指针

    使用重载函数时,指针类型必须与重载函数中的某一个精确匹配

    void ff(int*);
    void ff(unsigned int);
    void (*pf1)(unsigned int) = ff;
    void (*pf2)(int) = ff;
    void (*pf3)(int*) = ff;
    
    函数指针形参

    可以直接把函数作为实参使用,自动转换成指针

    void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));
    useBigger(s1, s2, lengthCopmare);
    

    直接使用函数指针类型很冗长,用类别别名和decltype可以简化函数指针

    typedef bool Func(const string&, const string&);
    typedef decltype(lengthCompare) Func2;      //等价类型,是函数类型
    
    
    typedef bool (*Func2)(const string&, const string&);
    typedef decltype(lengthCompare) *Func2;      //等价类型,是指向函数的指针类型
    
    然后可以如下重新声明函数
    void useBigger(const string &s1, const string &s2, Func);   //编译器自动将Func表示的函数类型转换成指针。
    void useBigger(const string &s1, const string &s2, Func2);
    
    返回指向函数的指针

    一种方法是使用别名

    using F = int(int*, int);       //F是函数
    using PF = int(*)(int* ,int);    //F是指针
    
    PF fl(int);     //正确,PF是指针,fl返回指向函数的指针
    F fl(int);      //错误,F是函数,不能返回一个函数
    F *fl(int);     //正确,显式返回
    
    完整的如下:
    int (*fl(int))(int*, int);
    fl是个函数,fl返回一个指针,指针指向的函数返回int。
    
    使用尾置返回
    auto fl(int) -> int (*)(int*, int);
    
    将auto和decltype用于函数指针类型

    如果明确知道返回的函数是哪一个,就使用decltype简化。
    将decltype作用于某个函数时,本身返回函数类型而非指针,因此要加上*

    size_type sumLength(const string&, const string&);
    decltype(sumLength) * getFun(const string &);
    
  • 相关阅读:
    螺旋矩阵算法
    shell脚本编程的10个最佳实践
    时间字符串转长整形数
    python的发音
    wget使用技巧
    History命令用法15例
    14位格式时间字符串
    Spring MVC
    Android SQLite数据储存方式
    MYSQL命令大全
  • 原文地址:https://www.cnblogs.com/aqq2828/p/14073372.html
Copyright © 2011-2022 走看看