第7章 函数-C++的编程模块

说明

看《C++ Primer Plus》时整理的学习笔记,部分内容完全摘抄自《C++ Primer Plus》(第6版)中文版,Stephen Prata 著,张海龙 袁国忠译,人民邮电出版社。只做学习记录用途。

本章介绍 C++ 中编程模块函数的基础用法。

7.1 函数的基本知识

要使用 C++ 函数,必须完成以下工作:

  • 提供函数原型;
  • 提供函数定义;
  • 调用函数。

7.1.1 函数原型

函数原型描述了函数到编译器的接口,它将函数返回值的类型以及参数的类型和数量告诉编译器。一般情况下 C++ 程序在首次使用函数之前都需要提供函数原型,避免使用函数原型的唯一方法是:在首次使用该函数前提供它的定义,例如将函数定义写在 main() 函数前面。函数原型是一条语句,常包含于 include 文件中,此时通过 #include 编译指令即可提供函数原型。

//方式一:包含头文件
#include <cmath>

//方式二:函数原型(省略参数名)
double sqrt(double);

//方式三:函数原型(参数名等价于占位符)
double sqrt(double x);

C++ 函数原型与 ANSI C 函数原型是有区别的:ANSI C 函数原型不需指出参数的类型和数量,例如原型 void say_hi(); 能与对应的无参函数匹配,也能与对应的有参函数匹配,对函数参数没有做限制;而在 C++ 中,原型 void say_hi(); 与原型 void say_hi(void); 是等效的,它们都只能与对应的无参函数匹配。在 C++ 中,不指定参数列表时应使用省略号:

//不指定参数列表(C++语法)
void say_hi(...);

//不指定参数列表(ANSI C语法)
void say_hi();

函数原型有以下几个作用:

  • 使编译器正确处理函数返回值
  • 使编译器检查使用的参数数目是否正确;
  • 使编译器检查使用的参数类型是否正确,若不正确,则强制转换为正确的类型(可能丢失数据),条件是转换前后的类型都是算术类型,且没有函数重载所出现的二义性。

在编译阶段进行的原型化被称为静态类型检查,可以捕获许多在运行阶段非常难以捕获的错误。

7.1.2 函数定义

函数分成两类:没有返回值的函数和有返回值的函数,没有返回值的函数被称为 void 函数,其通用格式如下。其中,parameterList 指定了传递给函数的参数类型和变量,语句 return; 是对 void 函数而言是可选的,也可以使用 return; 提前结束函数。

//没有返回值的函数
void functionName(parameterList)
{
    statement(s);
    return;
}

有返回值的函数将生成一个值,并将它返回给调用函数,其通用格式如下。其中 value 的类型必须或者可以被强制转换为 typeName,C++ 对返回值的类型有限制:不能是数组,但可以是其他任何类型(可以将数组作为结构或者对象组成部分来返回)。

//有返回值的函数
typeName functionName(parameterList)
{
    statement(s);
    return value;
}

7.1.3 函数调用

函数调用比较简单,一个例子如下:

//提供std::cout<<函数原型
#include <iostream>

//提供say_hi()函数原型
void say_hi();

//在main()中调用
int main()
{
    //调用say_hi()函数
    say_hi();
    
    return 0;
}

//提供say_hi()函数定义
void say_hi()
{
    std::cout << "Hello World.\n";
}

执行函数 say_hi() 时,将暂停执行 main() 中的代码,等 say_hi() 执行完毕后,再继续执行 main() 中的代码。对于有返回值的函数,函数通过将返回值复制到指定的 CPU 寄存器或内存单元中来将其返回;随后,调用程序将查看该内存单元。被调用函数和调用函数必须就该内存单元中存储的数据类型达成一致,函数原型将返回值类型告知调用函数,而函数定义命令被调用函数应返回什么类型的数据。

7.2 函数参数和按值传递

函数可以有多个参数,默认情况下,C++ 将按值传递函数的参数。函数参数以及在函数中声明的变量都是该函数私有的,通常情况下,在函数被调用时,计算机将为这些变量分配内存,在函数结束时,计算机将释放这些变量使用的内存。用于接收传递值的变量被称为形参,给函数传递值的变量被称为实参;C++ 标准使用参数(argument)来表示实参,使用参量(parameter)来表示形参。

//提供函数原型
#include <iostream>
double square(double);

//函数调用
int main()
{
    double argument = 2.0;
    std::cout << square(argument);
    
    return 0;
}

//提供函数定义
double square(double parameter)
{
    return parameter * parameter;
}

如上例所示,main() 函数中的变量 argument 为实参,square() 函数中的变量 parameter 为形参。当 square() 函数被调用时,它将创建名为 parameter 的变量,并将其初始化为 argument 的值,初始化赋值必须符合 C++ 语法规则,例如不可将 const int * 实参传递给 int * 形参,但可将 int * 实参传递给 const int * 形参。因此,square() 函数使用的是 argument 的副本,而不是原来的数据,按值传递的方式不会更改原来的数据

7.3 函数和一维数组

7.3.1 const 和一级指针

可用三种不同的方式将 const 关键字用于一级指针,如下所示:

//方式一:指向常量数据的指针
const int * ptc;
int const * ptc;

//方式二:指针本身为常量,需在声明时初始化
int x = 55;
int * const cpt = &x;

//方式三:指向常量数据且本身也为常量的指针,需在声明时初始化
int x = 55;
const int * const cptc = &x;
int const * const cptc = &x;

Microsoft Visual Studio 中连续多个 const 会被编译器解释成一个,即 const const const const intconst int 等效,除此之外,const int constMicrosoft Visual Studio 中也与 const int 等效。

以上三种类型指针的特性如下:

  • 类型为 const int * 的指针 ptc 表示 *ptc 为常量,不能用该指针修改所指对象的值,但可修改其所指向的地址(指针自身的值),可将 intconst int数据的地址、int *const int * 类型的指针、以及 int * constconst int * const 类型的指针赋给 ptc(接受数据或指针修饰为 const 或非 const)。
  • 类型为 int * const 的指针 cpt 表示 cpt 为常量,能用该指针修改所指对象的值,但不可修改其所指向的地址(指针自身的值),只能将 int 数据的地址 、int * 类型的指针、以及 int * const 类型的指针赋给 cpt(只接受数据修饰为非 const),且必须在声明时初始化。
  • 类型为 const int * const 的指针 cptc 表示 *cptccptc 都为常量,不能用该指针修改所指对象的值,也不可修改其所指向的地址(指针自身的值),和 const int * 类型的指针一样,可将 intconst int数据的地址、int *const int * 类型的指针、以及 int * constconst int * const 类型的指针赋给 cptc(接受数据或指针修饰为 const 或非 const),且必须在声明时初始化。

对于类型为 int * 的常规指针,有以下特性:

  • 类型为 int * 的指针 pt 表示 *ptpt 都不为常量,能用该指针修改所指对象的值,也能修改其所指向的地址(指针自身的值),和 int * const 类型的指针一样,只能将 int 数据的地址 、int * 类型的指针、以及 int * const 类型的指针赋给 pt(只接受数据修饰为非 const)。

7.3.2 将一维数组作为函数参数

可使用以下方式将数组作为函数参数,它的函数原型如下。当且仅当用于函数头函数原型中时,int * arrint arr[] 的含义才是相同的,但数组表示法 int arr[] 只能将初始指针指向数组的第一个元素,指针表示法 int * arr 则可以将初始指针指向数组的任一个位置。

//函数原型一:可更改数组数据,以下两种方式等效
int sum_arr(int arr[], int arrSize);
int sum_arr(int * arr, int arrSize);

定义函数 sum_arr() 时,可用数组表示法 arr[i] 或指针表示法 *(arr+i) 访问数组元素。

//sum_arr()函数定义
int sum_arr(int arr[], int arrSize)
{
    int total = 0;
    for (int i = 0; i < n; i++)
        total = total + arr[i];
    return total;
}

在使用函数 sum_arr() 时,将一维数组名作为实参传入即可,这并不违反 C++ 按值传递的规则,只不过此时传递的值是一个地址,而不是数组的内容,但访问时是访问的原始数组,并非其副本。需要说明的一点时,对形参使用 sizeof() 并不会获得整个数组的内存量大小,而是相应指针变量的内存量大小,以 32 位系统为例,sizeof(arr)=4

//初始化数组
const int ArSize = 8;
int cookies[ArSize] = {1,2,4,8,16,32,64,128};

//使用函数sum_arr()求总和
int sum = sum_arr(cookies, ArSize);

//使用函数sum_arr()求前3个元素和
int sum = sum_arr(cookies, 3);

//使用函数sum_arr()求后4个元素和
int sum = sum_arr(cookies + 4, 4);

由于形参的类型为 int * 非常量指针,因此可在函数体内对数组数据进行更改,也就是对原始数组数据进行更改。若要确保函数不修改原始数组,可使用关键字 const 进行保护,如下所示,此时形参的类型为 const int *

//函数原型二:不可更改数组数据,以下四种方式等效
int sum_arr(const int arr[], int arrSize);
int sum_arr(int const arr[], int arrSize);
int sum_arr(const int * arr, int arrSize);
int sum_arr(int const * arr, int arrSize);

7.3.3 使用数组区间的函数

另一种传递数组信息的方法是,指定元素区间(range),这可以通过传递两个指针来完成:一个指针标识数组的开头,另一个指针标识数组的尾部,C++ 标准模板库 STL 就使用了“超尾”概念来指定区间。上面 sum_arr() 函数原型的另一种写法如下:

//函数原型三:不可更改数组数据,指定元素区间
int sum_arr(const int * begin, const int * end);

对应的函数定义如下:

int sum_arr(const int * begin, const int * end)
{
    const int * pt;
    int total = 0;
    
    for (pt = begin; pt != end; pt++)
        total = total + *pt;
    return total;
}

对应的函数调用如下:

//初始化数组
const int ArSize = 8;
int cookies[ArSize] = {1,2,4,8,16,32,64,128};

//使用函数sum_arr()求总和
int sum = sum_arr(cookies, cookies + ArSize);

//使用函数sum_arr()求前3个元素和
int sum = sum_arr(cookies, cookies + 3);

//使用函数sum_arr()求后4个元素和
int sum = sum_arr(cookies + 4, cookies + 8);

7.4 函数和二维数组

7.4.1 二维数组和二级指针

以下面的程序为例,和一维数组类似,C++ 将二维数组名解释为其第一个元素的地址,而二维数组的第一个元素为一维数组,因此,二维数组名 array2d&array2d[0] 等效,它们的类型都为 short (*)[5];对数组名应用地址运算符时,得到的是整个数组的地址,它的类型为 short (*)[5][5],假设 short 宽 2 字节,系统为 32 位,数组首地址为0x00BCF8FC,例子中几种表示的区别为:

  • 数组名 array2d&array2d[0] 等效,类型都为 short (*)[5],存储的是一个 10 字节内存块的地址,它们指向的对象是包含 5 个元素的 short 数组,但在运用 sizeof() 时,这两者会有区别,sizeof(array2d)=50sizeof(&array2d[0])=4
  • 表示 &array2d 的类型为 short (*)[5][5],存储的是一个 50 字节内存块的地址,它指向的对象是 5 行 5 列的二维 short 数组。
  • 表示 &array2d[0][0]array2d[0] 等效,类型都为 short *,存储的是一个 2 字节内存块的地址,它指向的对象是 short 类型数据,但在运用 sizeof() 时,这两者会有区别,sizeof(&array2d[0][0])=4sizeof(array2d[0])=10
  • 类型 short **,存储的是一个 4 字节内存块的地址,它指向的对象是 short* 类型数据。
//声明并初始化数组
short array2d[5][5] = {{5,2,8,4,1},
                       {2,2,4,6,8},
                       {1,5,8,9,4},
                       {5,7,6,2,5},
                       {7,6,5,8,1}};

//声明并初始化指针一:以下几种为等效表示
short (*ptra)[5] = array2d;     //方式一:值为0x00BCF8FC
short (*ptra)[5] = &array2d[0]; //方式二:值为0x00BCF8FC

//声明并初始化指针二
short (*ptrb)[5][5] = &array2d; //值为0x00BCF8FC

//声明并初始化指针三:以下几种为等效表示
short *ptrc = &array2d[0][0];   //方式一:值为0x00BCF8FC
short *ptrc = array2d[0];       //方式二:值为0x00BCF8FC

//声明并初始化指针四:以下几种为等效表示
short *ptrTmp[5] = {array2d[0],array2d[1],array2d[2],array2d[3],array2d[4]};
short** ptrd = ptrTmp;          //方式一:值为0x00BCF8A4
short** ptrd = new short*[5]{
    array2d[0],
    array2d[1],
    array2d[2],
    array2d[3],
    array2d[4]};                //方式二:值为0x01156470,需配合使用delete[]释放内存
short** ptrd = new short*[5]();
ptrd[0] = array2d[0];
ptrd[1] = array2d[1];
ptrd[2] = array2d[2];
ptrd[3] = array2d[3];
ptrd[4] = array2d[4];           //方式三:值为0x01046AE0,需配合使用delete[]释放内存

//访问数组第3行第4列的元素
cout << array2d[2][3];    //结果为9
cout << *(array2d[2]+3);  //结果为9
cout << *(*(array2d+2)+3);//结果为9

cout << ptra[2][3];     //结果为9
cout << *(ptra[2]+3);   //结果为9
cout << *(*(ptra+2)+3); //结果为9

cout << (*ptrb)[2][3];  //结果为9
cout << *((*ptrb)[2]+3);//结果为9
cout << *(*(*ptrb+2)+3);//结果为9

cout << ptrc[2*5+3];    //结果为9
cout << *(ptrc+2*5+3);  //结果为9

cout << ptrd[2][3];     //结果为9
cout << *(ptrd[2]+3);   //结果为9
cout << *(*(ptrd+2)+3); //结果为9

//应用指针算术时单位1表示的字节数
cout << int(array2d+1)-int(array2d);//结果为10
cout << int(ptra+1)-int(ptra);      //结果为10
cout << int(ptrb+1)-int(ptrb);      //结果为50
cout << int(ptrc+1)-int(ptrc);      //结果为2
cout << int(ptrd+1)-int(ptrd);      //结果为4

//应用sizeof()获得内存量大小
cout << sizeof(array2d);//结果为50
cout << sizeof(ptra);   //结果为4
cout << sizeof(ptrb);   //结果为4
cout << sizeof(ptrc);   //结果为4
cout << sizeof(ptrd);   //结果为4

7.4.2 const 和二级指针

可用七种不同的方式将 const 关键字用于二级指针,如下所示:

//方式一:所指一级指针指向的数据为常量
const int ** pptc;
int const ** pptc;

//方式二:所指一级指针为常量
int *const* pcpt;

//方式三:二级指针本身为常量,需在声明时初始化
int x = 55;
int * pt = &x;
int ** const cppt = &pt;

//方式四:二级指针本身为常量,所指一级指针也为常量,所指一级指针指向的数据也为常量,需在声明时初始化
int x = 55;
const int * pt = &x;
const int *const* const cpcptc = &pt;

//方式五:二级指针本身为常量,所指一级指针也为常量,需在声明时初始化
int x = 55;
int * pt = &x;
int *const* const cpcpt = &pt;

//方式六:二级指针本身为常量,所指一级指针指向的数据也为常量,需在声明时初始化
int x = 55;
const int * pt = &x;
const int ** const cpptc = &pt;

//方式七:所指一级指针也为常量,所指一级指针指向的数据也为常量
int x = 55;
const int * pt = &x;
const int *const* pcptc = &pt;

以上七种类型指针的特性如下:

  • 类型为 const int ** 的指针 pptc 表示 **pptc 为常量,不能用该指针修改所指一级指针指向的数据的值,但可修改其所指一级指针的值,也可修改其所指向的地址(指针自身的值),只能将 const int * 类型的一级指针地址、const int **const int ** const 类型的二级指针值赋给 pptc
  • 类型为 int * const * 的指针 pcpt 表示 *pcpt 为常量,能用该指针修改所指一级指针指向的数据的值,不可修改其所指一级指针的值,但可修改其所指向的地址(指针自身的值),只能将 int *int * const 类型的一级指针地址、int **int ** constint * const *int * const * const 类型的二级指针值赋给 pcpt
  • 类型为 int ** const 的指针 cppt 表示 cppt 为常量,能用该指针修改所指一级指针指向的数据的值,也可修改其所指一级指针的值,但不可修改其所指向的地址(指针自身的值),只能将 int * 类型的一级指针地址、int **int ** const 类型的二级指针值赋给 cppt,且必须在声明时初始化。
  • 类型为 const int *const* const 的指针 cpcptc 表示 **cpcptc*cpcptccpcptc 都为常量,不能用该指针修改所指一级指针指向的数据的值,不可修改其所指一级指针的值,也不可修改其所指向的地址(指针自身的值),能将 int *int * constconst int *const int * const类型的一级指针地址、const int **const int ** constint **int ** constint * const *int * const * constconst int * const *const int *const* const 类型的二级指针值赋给 cpcptc,且必须在声明时初始化。
  • 类型为 int *const* const 的指针 cpcpt 表示 *cpcptcpcpt 都为常量,能用该指针修改所指一级指针指向的数据的值,不可修改其所指一级指针的值,也不可修改其所指向的地址(指针自身的值),能将 int *int * const 类型的一级指针地址、int **int ** constint * const *int * const * const 类型的二级指针值赋给 cpcpt,且必须在声明时初始化。
  • 类型为 const int ** const 的指针 cpptc 表示 **cpptccpptc 都为常量,不能用该指针修改所指一级指针指向的数据的值,可修改其所指一级指针的值,但不可修改其所指向的地址(指针自身的值),只能将 const int * 类型的一级指针地址、const int **const int ** const 类型的二级指针值赋给 cpptc,且必须在声明时初始化。
  • 类型为 const int *const* 的指针 pcptc 表示 **pcptc*pcptc 都为常量,不能用该指针修改所指一级指针指向的数据的值,也不可修改其所指一级指针的值,但可修改其所指向的地址(指针自身的值),能将 int *int * constconst int *const int * const类型的一级指针地址、const int **const int ** constint **int ** constint * const *int * const * constconst int * const *const int *const* const 类型的二级指针值赋给 pcptc

对于类型为 int ** 的常规指针,有以下特性:

  • 类型为 int ** 的指针 ppt 表示 **ppt*pptppt 都不为常量,能用该指针修改所指一级指针指向的数据的值,也可修改其所指一级指针的值,也可修改其所指向的地址(指针自身的值),只能将 int * 类型的一级指针地址赋给 ppt

7.4.3 将二维数组作为函数参数

//函数原型一
int sum_arr2d(int ** arr2d, int rowSize, int columnSize);

若函数原型按如上声明,且二维数组由 newmalloc 所分配,例如 int ** ARR2d = new int *[rowSize];...,则可直接将二维数组名 ARR2d 传递给函数形参 arr2d。对于不是由 newmalloc 所分配的二维数组,做如下转换后,可将指针 ptrd 传递给函数形参 arr2d,还有其他转换方式,可参考前面内容。

//声明并初始化数组
int array2d[5][5] = {{5,2,8,4,1},
                     {2,2,4,6,8},
                     {1,5,8,9,4},
                     {5,7,6,2,5},
                     {7,6,5,8,1}};

//转换
int *ptrTmp[5] = {array2d[0],array2d[1],array2d[2],array2d[3],array2d[4]};
int** ptrd = ptrTmp;

//函数调用
int sum = sum_arr2d(ptrd, 5, 5);

若二维数组的列数固定,比如需处理的二维数组列数都固定为 5,则可用以下方式声明函数原型,这时函数实参的类型不能为 int **,例如不接受将 int ** ARR2d = new int *[rowSize];... 方式所得的 ARR2d 直接传递给函数形参 arr2d

//函数原型二,以下两种格式等效
int sum_arr2d(int (*arr2d)[5], int rowSize);
int sum_arr2d(int arr2d[][5], int rowSize);

依据函数原型二所定义的函数可直接接受常规二维数组名,但其列数必须为 5,否则计算结果可能与预期不符:

//声明并初始化数组
int array2d[5][5] = {{5,2,8,4,1},
                     {2,2,4,6,8},
                     {1,5,8,9,4},
                     {5,7,6,2,5},
                     {7,6,5,8,1}};

//函数调用
int sum = sum_arr2d(array2d, 5);

由于由 newmalloc 所分配的二维数组,只能保证同一行的内存连续,而行与行之间的内存不一定连续(即第 1 行行尾不一定与第 2 行行首元素在内存上相邻),因此无法将 int ** 转换为 int (*)[5],然后将其传递给类型为 int (*)[5] 的形参 arr2d。见如下例子,使用 new 分配出来的二维数组相邻行之间的地址差并不等于每行占用的内存量宽度,但常规二维数组相邻行之间的地址差等于每行占用的内存量宽度。

//声明并初始化数组(new动态分配)
int ** array2dnew = new int *[5];
array2dnew[0] = new int[5]{5,2,8,4,1};
array2dnew[1] = new int[5]{2,2,4,6,8};
array2dnew[2] = new int[5]{1,5,8,9,4};
array2dnew[3] = new int[5]{5,7,6,2,5};
array2dnew[4] = new int[5]{7,6,5,8,1};

//显示地址
cout << array2dnew[0];	//0x00F569B0
cout << array2dnew[1];	//0x00F563D8
cout << array2dnew[2];	//0x00F56418
cout << array2dnew[3];	//0x00F56240
cout << array2dnew[4];	//0x00F56280

//声明并初始化数组(常规二维数组)
int array2d[5][5] = {{5,2,8,4,1},
                     {2,2,4,6,8},
                     {1,5,8,9,4},
                     {5,7,6,2,5},
                     {7,6,5,8,1}};

//显示地址
cout << array2d[0];	//0x008FF8F8
cout << array2d[1];	//0x008FF90C
cout << array2d[2];	//0x008FF920
cout << array2d[3];	//0x008FF934
cout << array2d[4];	//0x008FF948

7.5 函数和 C - 风格字符串

7.5.1 将 C - 风格字符串作为函数参数

将 C - 风格字符串作为函数参数时,函数原型可按以下方式声明:

//方式一:可修改原始字符串
int cstrlen(char * cstr);
int cstrlen(char cstr[]);

//方式二:不可修改原始字符串
int cstrlen(const char * cstr);
int cstrlen(const char cstr[]);
int cstrlen(char const * cstr);
int cstrlen(char const cstr[]);

由于 C - 风格字符串以空值字符 \0 结尾,因此可在函数体内以此来判断字符串是否结束。调用这类函数时,可用以下三种方式传递 C - 风格字符串:

  • char 数组。
  • 用引号括起来的字符串常量(也称字符串字面值)。
  • 被设置为字符串的地址的 char 指针。

7.5.2 返回 C - 风格字符串的函数

若要返回 C - 风格字符串,可返回字符串的地址,函数原型可按以下方式声明:

//返回C-风格字符串
char * buildcstr(char c, int n);

这个函数使用 new 创建一个长度为 n 的字符串,然后将每个字符都初始化为 c,函数定义如下:

char * buildcstr(char c, int n)
{
    char * pstr = new char[n+1];
    pstr[n] = '\0';
    while(n-- > 0)
        pstr[n] = c;
    return pstr;
}

由于函数内部使用了 new[],因此在调用该函数后,必须记住使用 delete[] 释放内存。

7.6 函数和 string 对象

将 C - 风格字符串作为函数参数或将其返回时,由于涉及到了指针操作,可能还会有动态内存分配,这对程序员来说不是很方便,增加了犯错的可能性,因此可使用 string 对象来替代 C - 风格字符串,C++ 像对待内置类型(如 int)一样对待 string 对象。

7.7 函数和结构

7.7.1 按值传递和返回结构

将结构作为函数参数或返回值返回时,默认方式是按值传递,就像普通变量那样,函数将使用原始结构的副本。若结构非常大,则复制结构将增加内存要求,降低系统运行速度,此时可传递结构的地址或按引用传递结构,将在后面介绍。

//定义结构
struct rect
{
    double x;
    double y;
};

//函数原型:按值传递与返回结构
rect convertRect(rect xypos);

7.7.2 传递结构的地址

假设要传递结构的地址而不是整个结构以节省时间和空间,可按如下方式声明函数原型:

//函数原型一:可修改原始结构数据
void convertRect(rect* xypos);

//函数原型二:不可修改原始结构数据
rect* convertRect(const rect* xypos);

返回 rect * 时,可返回形参结构的地址,也可返回新 new 出来的结构地址,但不可返回函数体内临时结构的地址(临时结构在函数执行完后会被回收)。

7.8 函数和 array 对象

可按值将对象传递给函数,此时函数处理的是原始对象的副本;也可传递指向对象的指针,这让函数能够操作原始对象。以 array 对象为例,可声明以下函数原型:

//函数原型一:指针传递,可修改原始数据
void fill(std::array<double, 4> * pa);

//函数原型二:指针传递,不可修改原始数据
void fill(const std::array<double, 4> * pa);

//函数原型三:按值传递,无法修改原始数据
void fill(std::array<double, 4> da);

若要在函数内访问 array 对象元素,访问方式如下:

//函数原型一时的访问方式
cout << (*pa)[i];

//函数原型二时的访问方式
cout << (*pa)[i];

//函数原型三时的访问方式
cout << da[i];

7.9 函数递归

C++ 函数可以自己调用自己,但不允许 main() 调用自己,这种功能被称为递归

7.9.1 包含一个递归调用的递归

如果递归函数调用自己,则被调用的函数也将调用自己,应在代码中包含终止调用链的内容,通常的方法是使用 if 语句继续递归或终止递归:

//包含一个递归调用的递归
void recurs(argumentlist)
{
    statements1;
    if (test-expression)
        recurs(arguments);
    statements2;
}

每个递归调用都创建自己的一套变量,它们彼此独立,互不影响。

7.9.2 包含多个递归调用的递归

在需要将一项工作不断分为两项较小的、类似的工作时,递归非常有用,这有时被称为分而治之策略(divide-and-conquer strategy)。以下为书中的例子:

//包含多个递归调用的递归
void subdivide(char ar[], int low, int high, int level)
{
    if (level == 0)
        return;
    int mid = (high + low) / 2;
    ar[mid] = '|';
    subdivide(ar, low, mid, level - 1);
    subdivide(ar, mid, high, level - 1);
}

若递归层次很多,选择递归将是一种糟糕的选择,不仅时间和空间消耗较大,还可能会造成调用栈溢出。若递归层次较少,选择递归将是一种精致而简单的选择。

7.10 函数指针

函数的地址是存储其机器语言代码的内存的开始地址。可以编写将另一个函数的地址作为参数的函数,它允许在不同的时间传递不同函数的地址,这意味着可以在不同的时间使用不同的函数。

7.10.1 函数指针类型

声明指向函数的指针时,必须指定函数的返回类型以及函数的特征标(参数列表),可以首先编写这种函数的原型,然后用 (*pf) 替换函数名,这样 pf 就是这类函数的指针。以下面的程序为例,要获取函数的地址,只需使用函数名即可(后面不跟参数),这与数组地址有几分相似,函数指针 pf 的类型是 double (*)(int),由于 pf 是指向 pam() 函数的指针,因此 (*pf) 是函数,使用函数指针调用函数时,C++ 将 pf(*pf) 看作是等价的(虽然前者是函数指针,后者是函数),将 pf() 用作函数调用与将 (*pf)() 用作函数调用,效果一样。

//函数原型
double pam(int);

//声明对应的函数指针
double (*pf)(int);

//赋值,也可在声明时进行
pf = pam;

//使用函数指针调用函数,以下几种方式等效
double x = pam(4);   //方式一
double x = (*pf)(4); //方式二
double x = pf(4);    //方式三

//输出函数地址
cout << pam;  //值为0x001F1384
cout << pf;   //值为0x001F1384

对函数指针进行赋值时,对应函数的特征标和返回类型必须与 pf 相同,如果不相同,编译器将拒绝这种赋值。例如函数指针类型 const double * (*)(const double *, int) 与下面几种函数匹配,它们的特征标看似不同,但实际上相同,还可使用 auto 关键字自动推断函数指针的类型。

//函数原型
const double * f1(const double ar[], int n);
const double * f2(const double * ar, int n);
const double * f3(const double [], int);
const double * f4(const double *, int);

//声明函数指针
const double * (*pf)(const double *, int);

//赋值
pf = f1;
pf = f2;
pf = f3;
pf = f4;

//可使用自动类型推断
auto pff = f1;

假设要设计一个名为 estimate() 的函数,用于估算编写指定行数的代码所需的时间,而且允许每个程序员提供自己的算法来估算时间,此时可将函数地址作为该函数的输入参数,其函数原型可使用如下声明,需保证每个程序员提供的函数特征标和返回类型都一致且与 double (*)(int) 匹配。

//将函数指针作为函数参数
void estimate(int lines, double (*pf)(int));

7.10.2 函数指针数组

指向函数指针数组的指针通常用于类的虚方法实现中,细节通常由编译器自动处理。如下程序所示,函数指针数组大体性质与一维数组相似,其中需要注意的是:

  • 运算符 ()[] 的优先级比 * 要高,因此需在合适的地方使用括号 () 提高 * 的优先级。
  • 无法将指针算术运用于函数名,即出现 fb+1arrpf[i]+1 时编译器会报错。
  • 无法将 sizeof() 运用于函数名 fb ,但可用于 arrpf[i],即出现 sizeof(fb) 时编译器会报错,但 sizeof(arrpf[i]) 则不会。
//函数原型
double fa(int);
double fb(int);
double fc(int);
double fd(int);

//声明并初始化函数指针数组
double (*arrpf[4])(int) = {fa,fb,fc,fd};

//声明并初始化指向函数指针数组第一个元素的指针,以下三种方式对arrpfb等效
double (**arrpfb)(int) = arrpf;     //方式一
double (**arrpfb)(int) = &arrpf[0]; //方式二
auto arrpfb = &arrpf[0];            //方式三

//声明并初始化指向整个函数指针数组的指针,以下两种方式对arrpfc等效
double (*(*arrpfc)[4])(int) = &arrpf; //方式一
auto arrpfc = &arrpf;                 //方式二

//调用函数fb,以下几种方式等效
int x = 5;
double y = fb(x);

double y = arrpf[1](x);
double y = (*arrpf[1])(x);
double y = (*(arrpf+1))(x);
double y = (**(arrpf+1))(x);

double y = arrpfb[1](x);
double y = (*arrpfb[1])(x);
double y = (*(arrpfb+1))(x);
double y = (**(arrpfb+1))(x);

double y = (*arrpfc)[1](x);
double y = (*(*arrpfc)[1])(x);
double y = (*(*arrpfc+1))(x);
double y = (**(*arrpfc+1))(x);

//应用指针算术时单位1表示的字节数(32系统)
cout << int(arrpf+1)-int(arrpf);        //结果为4
cout << int(&arrpf[0]+1)-int(&arrpf[0]);//结果为4
cout << int(&arrpf+1)-int(&arrpf);      //结果为16

cout << int(arrpfb+1)-int(arrpfb);      //结果为4
cout << int(arrpfc+1)-int(arrpfc);      //结果为16

//应用sizeof()获得内存量大小(32系统)
cout << sizeof(arrpf);    //结果为16
cout << sizeof(&arrpf[0]);//结果为4
cout << sizeof(&arrpf);   //结果为4
cout << sizeof(arrpf[0]); //结果为4

cout << sizeof(arrpfb);   //结果为4
cout << sizeof(arrpfc);   //结果为4

7.10.3 使用 typedef 进行简化

可将 typedef 关键字用于函数指针类型,简化程序的编写,如下所示,此时 p_fun 就是函数指针类型 double (*)(int) 的别名,上述数组以及指针声明可采用以下等效形式。

//指定函数指针别名
typedef double (*p_fun)(int);

//声明并初始化函数指针数组,与前面等效
p_fun arrpf[4] = {fa,fb,fc,fd};

//声明并初始化指向函数指针数组第一个元素的指针,与前面等效
p_fun* arrpfb = &arrpf[0]; 

//声明并初始化指向整个函数指针数组的指针,与前面等效
p_fun (*arrpfc)[4] = &arrpf;
posted @ 2022-09-14 07:53  木三百川  阅读(252)  评论(0编辑  收藏  举报