《C++ Primer》读书笔记 第6章:函数

来源:互联网 发布:2017如何开淘宝店铺 编辑:程序博客网 时间:2024/06/05 15:11

6.1 函数基础

我们通过调用运算符来执行函数。调用运算符的形式是一对圆括号。它作用于一个表达式,该表达式是函数或者指向函数的指针。
执行函数的第一步是定义并初始化函数的形参。
return语句用于返回值,并将控制权从被调函数转移回主调函数。
尽管函数调用过程中,实参和形参存在对应关系,但是并没有规定实参的求值顺序,编译器可以以任意可行的顺序对实参求值。要特别注意此点。
函数的形参列表可以为空,但是不能省略。不过为了与c兼容,也可以使用关键字void表示函数没有形参。形参可以没有名字,通常这表示在函数体内不会使用到此形参。但是即使某个形参不被函数使用,也必须为它提供一个实参。
函数的返回值不能是数组或者函数,但是可以是指向函数或者数组的指针(或引用)。

6.1.1 局部对象

形参和函数体内定义的变量统称为局部变量。只存在于块执行期间的对象称为自动对象。对于局部变量对应的自动对象来说,其定义时若没有初始值,则将执行默认初始化,这意味着其将产生未定义的值。
我们可以将局部对象定义为static类型使其称为局部静态对象,这样对象的生命周期将持续到程序终止为止。如果局部静态变量没有默认的初始值,它将执行值初始化,即内置类型的局部静态变量将初始化为零值。

6.1.2 函数声明

函数的名字也必须在使用前进行声明,函数声明不需要有函数体,以分号结束即可。函数声明也称作函数原型。
通常我们将函数的声明放在头文件中,将函数定义放在源文件中。

6.2参数传递

形参初始化的机理与变量初始初始化一致。有传引用调用、传值调用两种形式,而传值调用中又可以传递指针。
通常来说我们应该尽量传递引用来避免对象的拷贝,如果函数无需改变引用形参的值,那么我们最好把形参声明为常引用。而且传递引用为我们在一个函数中返回多个值提供了有效的途径。

const形参和实参

我们知道,顶层const作用于对象本身,底层const作用于当前对象所指示的那个对象。和其他初始化一样,用实参初始化形参是会忽略顶层const,即非const的实参可以用来初始化const的形参类型,所以通常我们应该把形参定义为const类型。
另外,在我们不能仅通过形参是否是const类型来区分两个重载函数

void fcn(const int i);void fcn(int i);        //错误,重复定义了函数fcn,此函数与上面的同名函数并不是重载函数

对于底层const,我们可以使用非const对象初始化一个底层const对象,但是反过来不行。

int i = 42;int &r = i;const int &r2 = 42;void reset(const int &i);void fcn(int &i);reset(r);reset(r2);fcn(r);fcn(r2);        //错误,不能用const int&类型初始化int &类型的形参

在实际编程中药尽量将函数的形参定义为常引用。

数组形参

但我们传递一个数组时,实际上传递的是指向数组首元素的指针。尽管我们不能以值传递的方式传递数组,但是我们可以把形参写成类似数值的形式。

void print(const int[10]);      //这里的数组维度没有任何实际意义,你可以传入一个任意长度的整形数组作为实参void print(const int *);        //与上述函数声明是同一个性质

和其他使用数组的代码一样,以数组为形参的函数也必须确保使用数组时不会发生越界。因为数组十一指针的形式传递给函数的,所以函数一开始并不知道数组的确切尺寸,所以调用者应该为此提供一些额外的信息。常见的处理方法有如下几种:

使用标记指定数组长度

c风格字符串就是使用这种方法:

void print(const char *cp){    if(cp)        while(*cp)        //判断cp是不是一个空字符            cout << *cp++;}

使用标准库规范

第二种技术是传递指向数组首元素和尾后元素的指针

void print(const int *beg, const int *end){    while(beg != end)        cout << *beg++ << endl;}int j[2] = {0, 1};print(beg(j), end(j));          //使用c++11的新标准库函数

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

void print(const int ia[], size_t size);

数组引用形参

c++允许将变量定义为数组的引用,基于同样的道理,形参可以是数组的引用。

void print(int (&arr)[10])      //要特别注意数组的引用形式{    for(auto elem : arr)        cout << elem << endl;}

此种方法要注意的是,数组的维度也是数组类型的一部分,所以这个函数只能传入一个维度为10的int数组作为实参,这无形之中限制了函数的可用性。

传递多维数组

和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针。区别在于多维数组的数组首元素本身就是一个数组,指针就是一个指向数组的指针。此时数组第二维的大小也是数组类型的一部分。

void print(int (*matrix)[10],int rowsize);

要特别注意区分以下两种形式

int *matrix[10];    //表示一个指针数组int (*matrix)[10];  //表示一个指向数组的指针

当然我们也可以使用数组的语法定义函数,此时编译器将自动忽略第一维的大小,但是第二维的大小将视为形参类型的一部分。

void print(int matrix[][10], int rowSize);

含有可变形参的函数

为了编写可以处理不同数量实参的函数,c++ 11提供了两种主要解决方法:如果所有实参类型相同,则可以传递一个名为initializer_list的标准库类型;如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板(以后介绍)。
c++还有一种特殊的形参类型,即省略号“…”,它可以用来传递可变数量的实参,其实这种功能时从c继承来的,一般只用于与c函数交互的接口程序。

initalizer_list

  initalizer_list是一种标准库模板。initalizer_list对象中的元素永远是常量值,我们无法改变initalizer_list对象中元素的值。如果想向initalizer_list形参传递一个值得序列,则必须把序列放在一对花括号之中,且含有initalizer_list形参的函数也可以含有其他形参。因为initalizer_list包含begin.end成员,所以我通常使用范围for循环来处理其中的元素。
  

void error_msg(Errcode e, initalizer_list<string> il){    cout << e.msg() << ":";    for(const auto &elem : il)        cout << elem << " ";    cout << endl;}error_msg(Errcode(42), {"function", "okay"};

省略符形参

省略符形参不会进行类型检查,所以可以传递任意数量不同类型的实参,但是要注意,省略符形参应该仅仅用于c与c++通用的类型,大多数类类型的对象在传递给省略符号形参时都无法正确拷贝。

void foo(...);  

6.3 返回类型和return语句

列表初始化返回值

  c++11新标准规定,函数可以返回花括号包围的值得列表,此处的列表也用来对表示函数返回的临时变量进行初始化。

vector<string> process(){    string expected, actual;    ...    if(expected.empty())        return {};                                else if(expected == actual)        return {"function", "OK"};}

  上述可以返回值序列的主要原因是,一个值序列可以隐式转换为vector对象,上面的返回值语句实际上是发生了一次隐式类型转换。

返回数组指针

  因为指针不能被拷贝,所以函数不能返回数组,但是函数可以返回数组的指针或者引用。

return int (&a)[10];return int (*p)[10];

  由于定义一个数组的指针或者引用比较繁琐,所以可以使用数组别名来简化程序。

typedef int  intArr[10];        //定义intArr为类型别名,要注意此时数组的维度也是类型的一部分using intArr = int[10];

  要想定义一个返回数组指针的函数,应该遵循以下形式

   Type (*functon(parameter_list)[dimension];   int (*func(int i))[10];

  要特别注意理解上述声明形式,func(int i)可以视为是返回值ret,所以上述式子看为int (*ret)[10],表明对返回值进行解引用得到的是一个10维的int数组,所以函数的返回值是一个指向十维int数组的指针。

  在c++11中还有一种简化上述函数声明的方法,就是使用尾置返回类型:

auto func(int i)->int(*)[10];       //注意auto不可以省略

  当然我们也可以使用decltype,

    int add[] = {1,2,3,4,5};    decltype(add) *func(int i);               //需要另外添加一个*声明符

  使用decltype要特别注意的是,decltype并不会把数组类型或者函数类型转化为数组元素指针或者函数指针(这种转化发生在形参初始化以及函数返回值时),所以要想表示数组的指针必须另外加一个*声明符。

6.4函数重载

重载与const形参

  一个拥有顶层const的形参无法与一个没有顶层const的形参区分开来

void lookup(int i);void lookup(const int i);           //无法仅凭顶层const的不同与上边的同名函数重载,将造成重定义错误

  另一方面,如果形参是一个指针或者引用,则可以通过底层const把两个函数区分开来

void lookup(int &i);void lookup(const int &i);      //正确,可以通过底层const来区分形参,此函数与上面的函数构成了重载函数

  我们只能把const对象传递给const形参,而当我们传递非const对象时,将优先调用非const形参的函数版本。

cons_cast和重载

  const_cast在重载函数的情景中最有用。

const string &shortString(const string&s1, const string&s2)         //函数1{    return s1.size() <= s2.size() s1 : s2;     //返回的是一个const对象的引用}string &shortString(string &s1, string &s2)     //函数2{    auto &r = shortString(const_cast < const string& >(s1), const_cast < const string& >(s2));    return const_cast < string & >(r);          //返回一个非const引用} 

  在函数2中r虽然是一个const string&,但是它实际上是绑定在一个非const对象上,所以将其转换为一个非const对象显然是安全的。
  上面两个函数使得其即可以返回const引用,还可以返回非const的引用,而且还使得代码得到了重用(一个函数是用用另一个函数实现的),这多亏了const_cast的使用。

重载与作用域

  如果我们在内层作用域中声明了名字,那么它将隐藏外层作用域中声明的同名实体,所以在不同作用域中无法重载函数。
  在c++语言汇总,名字查找发生在类型检查之前,一旦在内存作用域中找到了名字,则查找将停止,而不会继续查找外层作用域的名字,所以重载函数集必须位于同一作用域中。

6.5特殊用途语言特性

默认实参

  在给定的作用域中一个形参只能被赋予一次默认实参,换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参都必须有默认值。

void lookup(int i1, int i2, int i3, int i4 = 0);    //设置了i4的默认参数void lookup(int i1, int i2, int i3, int i4 = 1);    //错误,重新设置了i4的默认参数,造成编译错误void lookup(int i1, int i2 = 2, int i3, int i4);    //错误,i3未设定默认值时,不可以给i2设定默认值void lookup(int i1, int i2, int i3 = 3, int i4);    //正确

  局部变量不能设定为默认值,除此之外,只要表达式的类型可以转换为形参所需的类型,该表达式就可以作为默认参数,

int i = 80;                                         //i的声明必须在函数之外void lookup(int i1, int i2, int i3, int i4 = i);    //正确

constexpr函数

  constexpr函数是指能够用于常量表达式的函数,它的定义必须遵循以下几条规则:函数的返回值类型及所有形参的类型都必须是字面值类型(但不一定要求是字面值常量),而且函数体中必须有且仅有一条return 语句(要特别注意这最后一点)。
编译器在编译期会把对constexpr函数调用替换为其结果值,为了能在编译过程中随时展开,constexpr函数被隐式的定义为内联函数。

内联函数和constexpr函数通常定义在头文件中。

调试帮助

一些调试用的宏

NDEBUG与assert配合使用,进行断言。定义NDEBUG时,程序将不进行运行时检查,assert断言将没有作用。
还有一些宏如下:

__FILE__    //文件名__LINE__    //行号__TIME_ _   //编译时间__DATE__    //文件编译日期__func__    //函数名 (特别注意在vs中更名为__FUNCTION__)

6.6函数匹配

  在函数确定最佳匹配过程中,实参到类型的转化划分为几个等级,集体排序如下:

  1. 实参类型与形参完全相同
  2. 实参从数组或者函数类型转化为对应的指针类型
  3. 通过const转换实现的匹配
  4. 通过类型提升实现的匹配
  5. 通过算数类型转化实现的匹配
  6. 通过类类型转换实现的匹配

      类型提升:比如char,short类型提升为int类型,float类型提升为double类型。
      算数转化:要注意,所有算数转换的级别都是一样的,比如int至unsigned int并不比int到double具有更高的优先级。

void look(unsigned int i){    cout << "int" << endl;}void look(double d){    cout << "double" << endl;}look(1);            //错误,将造成二义性

6.7函数指针

  当我们把函数名作为一个值使用时,该函数自动转换为指针。此外,我们还可以直接使用指向函数的指针调用该函数,无需提前解引用指针。

void lookup() {}void (* pf)() = nullptr;        //定义了一个函数指针pf = lookup;        //指针赋值pf = &lookup;       //与上面的式子等价pf();               //通过函数指针调用函数(*pf)();            //正确,与上面的调用等价

  在指向不同函数类型的指针间不存在转换规则,但是和往常一样,我们可以为一个函数指针赋一个nullptr。

重载函数的指针

  当我们通过重载函数名为一个函数指针赋值时,指针类型必须与重载函数中某一个函数精确匹配:

    void f(int);    void f(double);    void (*pf)(char) = f;          //错误,无法找到精确匹配的重载函数    void (*pf)(int) = f;           //正确

函数指针形参与返回值

  虽然不能定义函数类型的形参,但是形参可以是指向函数类型的指针,此时实参可以为函数对象,它将会被隐式转换为函数指针。
  另外函数的返回值类型也不可以是函数类型,但是可以是函数指针,函数指针作为返回值时,我们必须把函数的返回值类型写成指针形式,编译器不会吧函数返回类型当做是指针类型处理。

using F = int(int *, int);F f1(int);      //错误,函数类型不可以作为返回值F *f1(int)      //正确,返回值为F*,int(*)(int *, int);int (*f1(int))(int *,int);      //正确,与上述的声明等价

将auto与decltype用于函数指针类型

  可以使用auto将函数的返回值尾置:

auto f1(int)->int(*)(int *,int);        //推断出f1的类型

  使用decltype推导函数类型时要注意,当其作用于函数时,它将返回函数类型而非指针类型,因为我们如果要声明指针类型需要显示的加上*

int add(int *pi, int i);decltype(add) *getFcn(const string &);      //需要显示加上*