第6章 函数

第6章 函数

6.1 函数基础

编写函数

举例:编写一个求数的阶乘的程序。n的阶乘是从1n所有数字的乘积,例如5的阶乘是1201*2*3*4*5 = 120

// 用 while 循环
int fact (int val)
{
   
    int ret = 1;        // 局部变量,用于保存计算结果
    while (val > 1)
        ret *= val--;   // 把 ret 和 val 的乘积赋给 ret,然后将 val 减 1
    return ret;         // 返回结果
}
// 普通 for 循环
int fact (int val)
{
   
    if (val < 0)
        return -1;
    int ret = 1;
    // 从 1 连乘到 val
    for (int i = 1; i != val + 1; ++i)
        ret *= i;
    return ret;
}

调用函数

int main()
{
   
    int j = fact(5);    // j 等于 120,即 fact(5) 的结果
    cout << "5! is " << j << endl;
    return 0;
}

函数的调用完成两项工作:
一、用实参初始化函数对应的形参;
二、将控制权转移给被调用函数,此时主调函数(calling function)的执行被暂时中断,被调函数(called function)开始执行。

return语句完成的两项工作:
一、返回return语句中的值(如果有的话);
二、将控制权从被调函数转移回主调函数。

函数的形参列表

函数的形参列表可以为空,要想顶一个不带形参的函数,最常用的办法是书写一个空的形参列表。不够为了与C语言兼容,也可以使用关键字void表示函数没有形参:

void f1() {
    /* ...*/ }      // 隐式地定义空形参列表
void f2(void) {
    /* ...*/}   // 显式地定义空形参列表

每个形参都是含有一个声明符的声明,即使两个形参的类型一样,也必须把两个类型都写出来:

int f3(int v1, v2) {
    /* ...*/ }     // 错误
int f4(int v1, int v2) {
    /* ...*/ } // 正确

函数返回类型

一种特殊的返回类型是void,它表示函数不返回任何值。

函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。

6.1.1 局部对象

在C++语言中,名字有作用域,对象有生命周期(lifetime)。

函数体是一个语句块,块构成一个新的作用域,形参和函数体内部定义的变量称为局部变量(local variable)。

自动对象

我们把只存在于块执行期间的对象称为自动对象(automatic object)。

形参是一种自动对象。函数开始时为形参申请存储空间,因为形参定义在函数题作用域之内,所以一旦函数终止,形参也就被销毁。

局部静态对象

有些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间。可以将局部变量定义成static类型从而获得这样的对象。

局部静态对象(local static object)在程序执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。

// 这段程序将输出从 1 到 10(包括 10 在内)的数组
size_t count_calls()
{
   
    static size_t ctr = 0;  // 调用结束后,这个值仍然有效
    return ++ctr;
}
int main()
{
   
    for (size_t i = 0; i != 10; ++i)
        cout << count_calls() << endl;
    return 0;
}

/* Output:
1
2
3
4
5
6
7
8
9
10
*/

在控制流第一次经过ctr的定义之前,ctr被创建并初始化为0。每次调用将ctr1并返回新值。每次执行count_calls函数时,变量ctr的值都已经存在并且等于函数上一次推出时ctr的值。因此,第二次调用时ctr的值是1,第三次调用时ctr的值是2,以此类推。

练习6.7:编写一个函数,当它第一次被调用时返回 0,以后每次被调用返回值加 1。

/* 练习6.7:编写一个函数,当它第一次被调用时返回 0,以后每次被调用返回值加 1。*/

#include <iostream>
using namespace std;

// 这段程序将输出从 1 到 10(包括 10 在内)的数组
unsigned myCnt()
{
   
    // static size_t ctr = 0;  // 调用结束后,这个值仍然有效
    // return ++ctr;
    static unsigned iCnt = -1;  // iCnt 是静态局部变量
    ++iCnt;
    return iCnt; 
}

int main()
{
   
    cout << "Please enter any char and press enter to continue." << endl;
    char ch;
    while (cin >> ch)
    {
   
        cout << "The function myCnt() has been called: " << myCnt() << " times." << endl;
    }
    return 0;
}
/* Output:
Please enter any char and press enter to continue.
s  
The function myCnt() has been called: 0 times.
1
The function myCnt() has been called: 1 times.
t
The function myCnt() has been called: 2 times.
-
The function myCnt() has been called: 3 times.

^Z
*/

6.1.2 函数声明

函数声明无须函数体,定义需要函数体。

函数声明也称作函数原型(function prototype)。

在头文件中进行函数声明

函数应该在头文件中声明而在源文件中定义。

练习6.8:编写一个名为Chapter6.h的头文件,令其包含6.1节练习(第184页)中的函数声明。

#ifndef CHAPTER6_H_INCLUDED
#define CHAPTER6_H_INCLUDED

int fact(int );
double myABS(double );
double myABS(double );

#endif // CHAPTER6_H_INCLUDED

补充知识漏洞:

#ifndef#endif 要一起使用,如果丢失#endif,可能会报错。

c语言中,对同一个变量或者函数进行多次声明是不会报错的。所以如果h文件里只是进行了声明工作,即使不使用# ifndef宏定义,多个c文件包含同一个h文件也不会报错。

但是在c++语言中,#ifdef的作用域只是在单个文件中。所以如果h文件里定义了全局变量,即使采用#ifdef宏定义,多个c文件包含同一个h文件还是会出现全局变量重定义的错误。

使用#ifndef可以避免下面这种错误:如果在h文件中定义了全局变量,一个c文件包含同一个h文件多次,如果不加#ifndef宏定义,会出现变量重复定义的错误;如果加了#ifndef,则不会出现这种错误。

6.1.3 分离式编译 (separate compilation)

分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译。

编译和链接多个源文件

不熟悉,待整理 P187

6.2 参数传递

当形参是引用类型时,我们说它对应的实参被引用传递(passed by reference)或者函数被传引用调用(called by reference)。和其他引用一样,引用形参也是它绑定的对象的别名;也就是说,引用形参是它对应的实参的别名。

当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递(passed by value)或者函数被传值调用(called by value)。

练习6.13:假设 T 是某种类型的名字,说明以下两个函数声明的区别:一个是 void f(T),另一个是 void f(&T)。

/* 练习6.13:假设 T 是某种类型的名字,说明以下两个函数声明的区别:
一个是 void f(T),另一个是 void f(&T)。*/

#include <iostream>
using namespace std;

void a(int);    // 传值参数
void b(int &);  // 传引用参数

int main()
{
   
    int s = 0, t = 10;
    a(s);
    cout << s << endl;
    b(t);
    cout << t << endl;
    return 0;
}

void a(int i)
{
   
    ++i;
    cout << i << endl;
}

void b(int &j)
{
   
    ++j;
    cout << j << endl;
}

/*Output:
1
0
11
11
*/

6.2.1 传值参数

指针形参

指针的行为和其他非引用类型一样。当执行指针拷贝操作时,

int n = 0, i = 42;
int *p = &n, *q = &i;   // p 指向 n;q 指向 i
*p = 42;    // n 的值改变;p 不变
p = q;      // p 现在指向了 i;但是 i 和 n 的值都不变

指针形参的行为与之类似:

// 该函数接受一个指针,然后将指针所指的位置为 0
void reset(int *ip)
{
   
    *ip = 0;    // 改变指针 ip 所指对象的值
    ip = 0;     // 只改变了 ip 的局部拷贝,实参未被改变
}

调用reset函数之后,实参所指的对象被置为 0,但是实参本身并没有改变:

int i = 42;
reset(&i);                      // 改变 i 的值而非 i 的地址
cout << "i = " << i << endl;    // 输出 i = 0

熟悉C的程序员常常使用指针类型的形参访问函数外部的对象。
在C++语言中,建议使用引用类型的形参替代指针。

练习6.10:编写一个函数,使用指针形参交换两个整数的值。在代码中调用该函数并输出交换后的结果,以此验证函数的正确性。

/* 练习6.10:编写一个函数,使用指针形参交换两个整数的值。
在代码中调用该函数并输出交换后的结果,以此验证函数的正确性。*/

#include <iostream>
using namespace std;
// 在函数体内部通过解引用操作改变指针所指的内容
void mySWAP(int *p, int *q)
{
   
    int tmp = *p;   // tmp 是一个整数
    *p = *q;
    *q = tmp;
}

int main()
{
   
    int a = 5, b = 10;
    int *r = &a, *s = &b;
    cout << "Before the exchange: a = " << a << ", b = " << b << endl;
    mySWAP(r, s);
    cout << "After the exchange: a = " << a << ", b = " << b << endl;
    return 0;
}
/*
Before the exchange: a = 5, b = 10
After the exchange: a = 10, b = 5
*/

练习6.12:改写6.2.1节中练习6.10(第 188 页)的程序,使用引用而非指针交换两个整数的值。

/* 练习6.12:改写6.2.1节中练习6.10(第 188 页)的程序,使用引用而非指针交换两个整数的值。
你觉得哪种方法更易于使用呢?为什么?*/

#include <iostream>
using namespace std;

void mySWAP(int &i, int &j)
{
   
    int tmp = i;
    i = j;
    j = tmp;
}

int main()
{
   
    int a = 5, b = 10;
    cout << "Before the exchange: a = " << a << ", b = " << b << endl;
    mySWAP(a, b);
    cout << "After the exchange: a = " << a << ", b = " << b << endl;
    return 0;
}

/*
Before the exchange: a = 5, b = 10
After the exchange: a = 10, b = 5
*/

// 与使用指针相比,使用引用交换变量的内容从形式上看更简单一些,并且无须额外声明指针变量,也避免了拷贝指针的值。

6.2.2 传引用参数

// 对于引用的操作实际上是作用在所引的对象上
int n = 0, i = 42;
int &r = n; // r 绑定了 n (即 r 是 n 的另一个名字)
r = 42;     // 现在 n 的值是 42
r = i;      // 现在 n 的值和 i 相同
i = r;      // i 的值和 n 相同
使用引用避免拷贝

拷贝大的类类我选哪个队吸纳过或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。

举例:string对象非常长,所以应该尽量避免直接拷贝它们,这时使用引用形参是比较明智的选择。又因为比较长度无须改变string对象的内容,所以把形参定义成对常量的引用:

// 比较两个 string 对象的长度
bool isShorter(const string &s1, const string &s2)
{
   
    return s1.size() < s2.size();
}

注意:如果函数无须改变应用型参的值,最好将其声明为常量引用。

与值传递相比,引用传递的优势主要体现在三个方面:

一是可以直接操作引用形参所引的对象;
二是使用引用形参可以避免拷贝大的类类型对象或容器类型对象;
三是使用引用形参可以帮助我们从函数中返回多个值

当函数的目的是交换两个参数的内容时应该使用引用类型的形参;
当参数是string对象时,为了避免拷贝很长的字符串,应该使用引用类型。

在其他情况下可以使用值传递的方式,而无须使用引用传递,例如求整数的绝对值或者阶乘的程序。

使用引用形参返回额外信息

举例:定义一个名为find_char的函数,它返回在string对象中某个指定字符第一次出现的位置。同时,我们也希望函数能返回该字符出现的总次数。该如何定义函数使得它能够既返回位置也返回出现次数呢?

一种方法是定义一个新的数据类型,让它包含位置和数量两个成员。
还有另一种更简单的方法,我们可以给函数传入一个额外的引用实参,令其保存字符出现的次数:

// 返回 s 中 c 第一次出现的位置索引
// 引用形参 occurs 负责统计 c 出现的总次数
string::size_type find_char(const string &s, char c, string::size_type &occurs)
{
   
    auto ret = s.size();        // 第一次出现的位置(如果有的话)
    occurs = 0;                 // 设置表示出现次数的形参的值
    for (decltype(ret) i = 0; i != s.size(); ++i) {
   
        if (s[i] == c) {
   
            if (ret == s.size())
                ret = i;        // 记录 c 第一次出现的位置
            ++occurs;           // 将出现的次数加 1
        }
    }
    return ret;                 // 出现次数通过 occurs 隐式地返回
}

// 调用 find_char 函数
auto index = find_char(s, 'o', ctr);

练习6.15:说明 find_char 函数中的三个形参为什么是现在的类型,特别说明为什么 s 是常量引用而 occurs 是普通引用?为什么 s 和 occurs 是引用类型而 c 不是?如果令 s 是普通引用会发生什么情况?如果令 occurs 是常量引用会发生什么情况?

find_char函数的三个参数的类型设定与该函数的处理逻辑密切相关,原因分别如下:

  • 对于待查找的字符串 s 来说,为了避免拷贝长字符串,使用引用类型;同时我们只执行查找操作,无须改变字符串的内容,所以将其声明为常量引用。
  • 对于带查找的字符 c 来说,它的类型是 char,只占1个字节,拷贝的代价很低,而且我们无须操作实参在内存中实际存储的内容,只把它的值拷贝给形参即可,所以不需要使用引用类型。
  • 对于字符出现的次数 occurs 来说,因为需要把函数内对实参值的更改反映在函数外部,所以必须将其定义成引用类型;但是不能把它定义成常量引用,否则就不能改变所引的内容了。

6.2.3 const形参和实参

当形参是const时,顶层const作用于对象本身:

const int ci = 42;  // 不能改变 ci,const 是顶层的
int i = ci;         // 正确:当拷贝 ci 时,忽略了它的顶层 const
int *const p = &i;  // const 是顶层的,不能给 p 赋值
*p = 0;             // 正确:通过 p 改变对象的内容是允许的,现在 i 变成了 0
指针或引用形参与const

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

int i = 42;
const int *cp = &i; // 正确:但是 cp 不能改变 i
const int &r = i;   // 正确:但是 r 不能改变 i
const int &r2 = 42; // 正确
int *p = cp;        // 错误:p 的类型和 cp 的类型不匹配
int &r3 = r;        // 错误:r3 的类型和 r 的类型不匹配
int &r4 = 42;       // 错误:不能用字面值初始化一个非常量引用
// reset() 函数
void reset(int &i)  // i 是传给 reset 函数的对象的另一个名字
{
   
    i = 0;          // 改变了 i 所引对象的值
}

int i = 0;
const<
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值