6.2函数基础

:::info —个典型的函数(function)定义包括以下部分:返回类型(return type)、函数名字、由0个或多个形参(parameter)组成的列表以及函数体。 :::

  • 调用运算符

我们通过调用运算符(calloperator)来执行函数。调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针

形参和实参

实参是形参的初始值 尽管实参与形参存在对应关系,但是并没有规定实参的求值顺序。编译器能以任意可行的顺序对实参求值。

作用域

:::info 在C++语言中,名字有作用域,对象有生命周期(lifetime)。理解这两个概念非常重要。
:::

局部静态对象

局部静态对象(local static object)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁

  1. sizetcountcalls(){
  2. static size_t ctr=0;//调用结束后,这个值仍然有效
  3. return ++ctr;
  4. }
  5. int main(){
  6. for(size_t i=0;i!=10;++i)
  7. cout<<count_calls()<<endl;
  8. return 0;
  9. }

函数声明

  • 函数只能定义一次,但可以声明多次
  • C++语言支持所谓的分离式编译(separatecompilation)0分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译。

6.2 参数传递

:::info 形参初始化的机理与变量初始化一样 :::

返回值

使用引用返回额外信息

一个函数只能返回一个值,然而有时函数需要同时返冋多个值,引用形参为我们一次返回多个结果提供了有效的途径。

  • 使用引用而非拷贝 :::info 拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型括IO类型在内)报本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。 :::

const形参和实参

忽略掉形参的顶层const可能产生意想不到的结果

  1. void fcn(const int i){/*fen能够读取i,但是不能向i写值/}
  2. void fcn(int i) {/*...*/}//错误:重复定义了fen(int)
  3. //因为顶层的const被忽略,实际上这两个都可以看作是输入参数是int i

我们可以使用非常量初始化一个底层const对象,但是反过来不行;同时一个普通的引用必须用同类型的对象初始化。

尽量使用常量引用

把函数不会改变的形参定义成(普通的)引用是一种比较常见的错误,这么做带给函数的调用者一种误导,即函数可以修改它的实参的值。此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型。就像刚刚肴到的,我们不能把const对象、字面值或者需要类型转换的对象传递给普通的引用形参。

  1. bool is_empty(string &s){
  2. return s.empty
  3. }//not good
  4. bool is_empty(const string &s){
  5. }//good

数组形参

:::info 数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:

  • 不允许拷贝数组
  • 以及使用数组时(通常)会将其转换成指针 :::

  • 当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。

  • 尽管不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式

    1. void print(const int*);
    2. void print(const int[]);
    3. void print(const int[10]);

    管理指针形参有三种常用的技术

  • 使用标记指定数组长度

第一种方法是要求数组本身包含一个结束标记

  • 使用标准库规范

    管理数组实参的第二种技术是传递指向数组首元素和尾后元素的指针,这种方法受到了标准库技术的启发

  • 显式传递一个表示数组大小的形参

    第三种管理数组实参的方法是专门定义一个表示数组大小的形参,在c程序和过去的C++程序中常常使用这种方法。

    数组引用形参

    C++语言允许将变量定义成数组的引用,基于这样的道理,形参也可以是数组的引用。此时,引用形参绑定到对应的实参上,也就是绑定到数组上

    1. void print(const (&arr)[10]){}

    main处理命令行选项

    有时我们确实需要给main传递实参,一种常见的情况是用户通过设置一组选项来确定函数所要执行的操作。

    1. int main(int argc, char* argv[])
  • 第二个形参argv是一个数组,它的元素是指向C风格字符串的指针

  • 第一个形参argc表示数组中字符串的数量。

含有可变形参的函数

为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要的方法:

  • 如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型;如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓可变参数模板
  • C++还有一种特殊的形参类型(即省略符),可以用它传递可变数量的实参。本节将简要介绍省略符形参,不过需要注意的是,这种功能一般只用于与C函数交互的接口程序。

:::info initializer_list也是一种模板类型。定义initializer_list对象时,必须说明列表中所含元素的类型 :::

  • 如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用initializer_list类型的形参
  • 和vector不一样的是,initializer_list对象中的元素永远是常量值,我们无法改变initializerlist对象中元素的值。 :::tips 省略符形参是为了便rC++程序访问某些特殊的c代码而设置的 :::

6.3 返回类型和return语句

函数一般有着两种返回值:

  1. return;
  2. return expression;

没有返冋值的return语句只能用在返回类型是void的函数中。返回void的函数不要求非得有return语句,因为在这类函数的最后一句后面会隐式地执行return。

值是如何返回的

返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。

  • 如果函数返回引用,则该引用便是它所引对象的一个别名
    :::danger 不要返回局部对象的引用或者指针
    函数完成后,它所占用的存储空间也随之被释放掉。因此,函数终止意味着局部变量的引用将指向不再有效的内存区域 :::

    返回类类型的函数和调用运算符

    和其他运算符-样,调用运算符也有优先级和结合律。调用运算符的优先级和点运算符和箭头运算符相同,并且符合左结合律,因此如果函数返回类、指针、引用,那么我们也可以通过.->运算符调用

    1. auto sz=shorterString(si,s2).size();

    引用返回左值

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

  • 可以像使用其他左值那样来使用返回引用的函数的调用,特别是,我们能为返回类型是非常量引用的函数的结果赋值

列表初始化返回值

C++11新标准规定,函数可以返回花括包围的值的列表。类似于其他返回结果,此处的列表也用来对表示函数返回的临时量进行初始化。

  1. vector<string> process(){
  2. /*
  3. */
  4. return {"Error","Expression"};
  5. }

递归

如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数(recursive function)。

返回数组指针

因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用。虽然从语法上来说,要想定义一个返回数组的指针或引用的函数比较烦琐,但是有一些方法可以简化这一仟务,其中最直接的方法是使用类型别名

使用类型别名

  1. typedef int arrT[10]
  2. using arrT=int[10]//两种方式等价

使用尾置返回类型

在C++11新标准中还行一种可以简化上述func声明的方法,就是使用尾置返回类型(trailing return type)。任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或者数组的引用。

  1. auto func(int i)->int(*)[10];

使用decltype

还有一种情况,如果我们知道函数返回的指针指向哪个数组,就可以使用decltype关键字声明返回类型。

  1. int odd[]={1,3,5,7,9}
  2. decltype(odd) *arrt(int i){
  3. //
  4. }

6.4 函数重载

如果同一作用域内有多个名字相同但是形参不同的函数,我们称之为重载(over loaded)函数。

重载和const形参

顶层const不影响传入函数的对象。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来:

  1. void record(Phone)
  2. void record(const Phone)//无效
  3. void record(Phone*)
  4. void record(Phone* const)//无效

另一方面,如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const是底层的

  1. void record(Phone*)
  2. void record(const Phone*)

const_cast和重载

之前将const_cast在重载中最有用,这里举例子说明
可以通过const_cast来利用const修饰的函数来重载非常量的函数

  1. const string& shorterString(const string &sl,const string &s2){
  2. return si.size()<=s2.size()?si:s2;
  3. }
  4. string &shorterString(string &sl,string &s2){
  5. auto&r=shorterString(const_cast<conststring&>(si),const_cast<conststring&>(s2));
  6. return const_cast<string&>(r);
  7. }

首先将它的实参强制转换成对const的引用,然后调用了shorterString函数的const版本。const版本返回对conststring的弓I用,这个引用事实上绑定在了某个初始的非常量实参上。因此,我们可以再将其转换回一个普通的strings,这显然是安全的。

调用重载函数

函数匹配(function matching)是指一个过程,在这个过程中我们把函数调用与一组重载函数中的某一个关联起来,函数匹配也叫做重载确定(over load resolution)。编译器首先将调用的实参与重载集合中每一个函数的形参进行比较,然后根据比较的结果决定到底调用哪个函数。

重载和作用域

对于刚接触C++的程序员来说,不太容易理淸作用域和重载的关系。其实,重载对作用域的一般性质并没有什么改变:如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实休。在不同的作用域中无法重载函数名

6.5 特殊用途的语言特性

默认实参

:::info 某些函数有这样一种形参,在函数的很多次调用中它们都被赋予一个相同的值,此时,我们把这个反复出现的值称为函数的默认实参(default argument)。调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。 :::

  • 我们可以为一个或多个形参定义默认值,不过需要注意的是,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
  • 当设计含有默认实参的函数时,其中一项任务是合理设置形参的顺序,尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面。
  • 不过有一点需要注意,在给定的作用域中一个形参只能被赋予一次默认实参。换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。

    1. string screen(int,int,char=' ');
    2. string screen(int,int, char='x');//wrong 重复定义
    3. string screen(int,int =10,char);//right

    内联函数

    把这种规模较小的操作定义成内联函数有很多好处

  • 然而,使用shorterString函数也存在一个潜在的缺点:调用函数一般比求等价表达式的值要慢一些。

  • 将函数指定为内联函数(inline),通常就是将它在坷个调用点上“内联地”展幵
  • 内联机制用于优化规模较小、流程直接、频繁调用的函数。

    constexpr函数

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

    (constexpr function)是指能用于常量表达式的函数。定义constexpr函数的方法与其他函数类似,不过要遵循几项约定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句

  • 我们允许constexpr函数的返回值并非一个常量

  • 内联函数和constexpr函数可以在程序中多次定义。对于某个给定的内联函数或者constexpr函数来说,它的多个定义必须完全一致。基于这个原因,内联函数和constexpr函数通常定义在头文件中。

    调试帮助

    :::info 基本思想是,程序可以包含一些用于调试的代码,但是这些代码只在开发程序时使用。当应用程序编写完成准缶发布时,要先屏蔽掉调试代码。这种方法用到两项预处理功能:assertNDEBUG。 ::: assert是一种预处理宏(preprocessormarco)。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。assert宏使用一个表达式作为它的条件。assert宏常用于检查“不能发生”的条件。

  • assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查。

6.6 函数匹配

当几个重载函数的形参数量相等以及某些形参的类型可以由其他类型转换得来时,函数匹配就不那么容易了。

候选函数

函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数(candidate function)。候选函数具备两个特征:一是与被调用的函数同名,二是其声明在调用点可见。

可行函数

可行函数也有两个特征:一是其形参数量与本次调用提供的实参数量相等,二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型

实参的类型转换

为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级
image.png

6.7 函数指针

函数指针指向的足函数而非对象。和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关

使用函数指针

  1. bool lengthCompare(const string&,const string&);
  2. bool (*pf)(const string&,const string&)//函数指针

:::info 当我们把函数名作为一个值使用时,该函数自动地转换成指针。 ::: 我们还能直接使用指向函数的指针调用该函数,无须提前解引用指针

  1. bool b1=pf("Abc","Abc");
  2. bool b2=(*pf)("hello","world");

重载函数的指针

编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配

函数指针形参

:::info 和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。此时,形参看起来是函数类型,实际上却是当成指针使用 ::: 两种等价声明:

  1. void useBigger(const string&sl,const string&s2, bool pf(const string&,const string&));
  2. void useBigger(const string&sl,const string&s2, bool (*pf)(const string&,const string&));

我们可以直接把函数作为实参使用,此时它会自动转换成指针;
直接使用函数指针类型显得冗长而烦琐。类型别名和decltype能让我们简化使用函数指针的代码

  1. typedef bool Func(const string&,const string&);
  2. typedef decltype(lengthCompare)Func2;
  3. //FuncP和FuncP2是指向函数的指针
  4. typedef bool (*FuncP)(const string&,const string&);
  5. typedef decltype(lengthCompare)* FuncP2;

返回指向函数的指针

:::info 和数组类似,虽然不能返回—个函数,但是能返回指向函数类型的指针。然而,我们必须把返回类型写成指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理 :::

  1. using F=int(int*,int);//函数类型
  2. using PF=int(*)(int*,int);//指针类型

小结

  • 函数的三个基本元素决定了函数:返回值、形参、函数名,其中前两个更重要
  • 可以利用引用实现很多功能,比如返回多个值、节省时间开销等等
  • 函数存在作用域
  • 函数名实际上是函数的地址,函数指针可以作为输入值和返回值
  • typedef和decltype以及尾置返回值可以帮助定义复杂的数据结构;