Error[8]: Undefined offset: 280, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

C++ Primer Plus 学习笔记 第 7 章 函数——C++ 的编程模块 复习函数的基本知识

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

库函数是已经定义和编译好的函数,同时可以使用标准库头文件提供原型,因此只需正确调用即可。而自定义函数必须自行处理这 3 个方面——定义、提供原型和调用。

定义函数

可以将函数分成两类:没有返回值的函数和有返回值的函数。
没有返回值的函数被称为void函数,其通用格式如下:

void functionName(parameterList)
{
    statement(s)
    return;         // optional
}

parameterList指定了传递给函数的参数类型和数量,可选返回语句标记了函数的结尾,否则,函数将在右花括号处结束。
有返回值的函数其通用格式如下:

typeName functionName(parameterList)
{
    statements
    return vlaue;         // value is type cast to type typeName
}

对于有返回值的函数,必须使用返回语句,以便将值返回给调用函数。值本身可以是常量、变量,也可以是表达式,只是其结果的类型必须为typeName类型或可以被转换为typeName。函数将最终的值返回给调用函数。C++ 对于返回值的类型有一定的限制:不能是数组,但可以是其他类型——整数、浮点数、指针,甚至可能是结构和对象!(可以将数组作为结构和对象的组成部分返回。)
通常,函数通过将返回值复制到指定的 CPU 寄存器或内存单元中将其返回。随后,调用函数将查看该内存单元。

函数原型和函数调用 为什么需要原型

原型描述了函数到编译器的接口,它将函数返回值的类型(如果有的话)以及参数的类型和数量告诉编译器。
避免使用函数原型的唯一方法是,在首次使用函数之前定义它,但这并不总是可行的。另外,C++ 的编程方格是将main()放在最前面,因为它通常提供了程序的整体结构。

原型的语法

函数原型是一条语句,因此必须以分号结束。获得原型最简单的方法是,复制函数定义中的函数头,并添加分号。
通常,在原型的参数列表中,可以包括变量名,也可以不包括。原型中的变量名相当于占位符,因此不必与函数定义中的变量名相同。
在 C++ 中,原型是必不可少的。参数列表为空和使用关键字void是等效的——意味着函数没有参数。
在 C++ 中,不指定参数列表时应使用省略号,通常,仅当与接受可变参数的 C 函数交互时才需要这样做。

原型的功能

原型确保以下几点:

C++ 自动将传递的值转换为原型中指定的类型,条件是两者都是算术类型。
自动类型转换并不能避免所有可能的错误。
仅当有意义时,原型化才会导致类型转换。例如,原型不会将整型转换为结构或指针。
在编译阶段进行的原型化被称为静态类型检查(static type checking)。静态类型检查可捕获许多在运行阶段非常难以捕获的错误。

函数参数和按值传递

C++ 通常按值传递参数,将数值参数传递给函数,函数将其赋给一个新的变量。
用于接受传递值的变量被称为形参,传递给函数的值被称为实参。C++ 标准使用参数(argument)来表示实参,使用参量(parameter)来表示形参。
在函数中声明的变量(包括参数)是该函数私有的,被称为局部变量,被限制在函数中。也被称为自动变量。

多个参数

函数可以有多个参数,在调用时,用逗号将这些参数分开。
在定义函数时,在函数头中使用逗号分隔的参数声明列表。如果函数的两个参数类型相同,则必须分别指定每个参数的类型,而不能像声明常规变量那样,将声明组合在一起。
原型中的变量名不必与定义中的变量名相同,而且可以省略。然而,提供变量名将使原型更容易理解,尤其是两个参数的类型相同时。这样,变量名可以提醒参量和参数间的相应关系。
形参与其他局部变量的主要区别是,形参从调用函数那里获得自己的值,而其他变量是从函数中获得自己的值。

函数和数组 函数如何使用指针来处理数组

在大多数情况下,C++ 与 C 语言一样,也将数组名视为指针,该规则有一些例外。首先,数组声明使用数组名来标记存储位置;其次,对数组名使用sizeof()将得到整个数组的长度;第三,将地址运算符&用于数组名时,将返回整个数组的地址。
在 C++ 中,当(且仅当)用于函数头或函数原型中,int arr[]int *arr的含义才是相同的。

arr[i] == *(arr + i)    \values in two notations
&arr[i] == arr + i      \addresses in two notations

遍历数组时,使用指针加法和数组下标是等效的。

将数组作为参数意味着什么

将数组作为参数,是将数组的位置(地址)传递给了函数。传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组。这种区别并不违反 C++ 按值传递的方法,传递了一个值,这个值被赋给了一个新的变量,这个值是地址,而不是数组的内容。因此,无法使用sizeof()来获悉原始数组的长度。
数组名与指针对应是一件好事,将数组地址作为参数可以节省复制整个数组的时间和内存。另一方面,使用原始数据增加了破坏数据的风险。

用 const 保护数组

为防止函数无意中修改数组的内容,可在声明形参时使用关键字const。该声明表示,指针指向的的常量数据,不能使用指针修改该数据,但并不意味着原始数组必须是常量。

使用数组区间的函数

处理数组的 C++ 函数,必须将数组中的数据种类、数组的起始位置和数组中元素数量提交给它;舍传统 C/C++ 方法是,将指向数组起始位处的指针作为第一个参数,将数组长度作为第二个参数。
另一种方法是,指定元素区间,可以通过传递两个指针,一个指针标识数组的开头,另一个指针标识数组的结尾。C++ 标准库(STL)将区间方法广义化,使用“超尾”概念来指定区间。也就是说,对于数组而言,标识数组结尾的参数将是指向最后一个元素后面的指针。

指针和 const

可以用两种不同的方式将const关键字用于指针。第一种方式是让指针指向一个常量对象,可以防止使用该指针来修改所指向的值。第二种方法是将指针本身声明为常量,这样可以防止改变指针指向的位置。
可以将常规变量赋给常规指针,也可将常规变量赋给指向const的指针,还可以将const变量赋给指向const的指针,但不能将const的地址赋给常规指针。
只有一层间接关系(如指针指向基本数据类型)时,才可以将非const地址或指针赋给const指针。
注意:如果数据类型本身并不是指针,则可以将const数据或非const数据的地址赋给指向const的指针,但只能将非const数据地址赋给非const指针。

将指针参数声明为指向常量数据的指针有两条理由:

如果条件允许,则应将指针形参声明为指向const的指针。

函数和二维数组

为编写将二维数组作为参数的函数,必须牢记,数组名被视为指针,因此,相应的形参是一个指针,就像一维数组一样。比较难处理的是如何正确地声明指针。例如,假设有如下代码:

int data[3][4] = {{1, 2, 3, 4}, {9, 8, 7, 6}, {2, 4, 6, 8}};
int total = sum(data, 3);

sum()的原型是是什么。
data是一个数组名,该数组有 3 个元素,每个元素本身是一个数组,有 4 个int值组成。因此data的类型是指向由 4 个int组成的数组的指针,因此正确的原型如下:

int sum(int (*ar2)[4], int size);

其中括号是必不可少的,int *ar2[4]将声明一个由 4 个指向int的指针组成的数组,另外,函数参数不能是数组。
还有另外一种格式,这种格式与上述原型的含义完全相同,但可读性更强:

int sum(int ar2[][4], int size);

上述两种原型都指出,ar2是指针而不是数组。同时,指针类型指出,它指向由 4 个int组成的数组,因此,指针类型指定了列数,所以无需将列数作为独立的函数参数进行传递。
由于指针类型指定了列数,因此sum()函数只能接受由 4 列组成的数组。但长度变量指定了行数,因此sum()函数对数组的长度没有限制:

int a[100][4];
int b[6][4];
...
int total1 = sum(a, 100);       \sum all of a
int total2 = sum(b, 6);         \sum all of b
int total3 = sum(a, 10);        \sum first 10 rows of a
int total4 = sum(a+10, 20);     \sum next 20 rows of a

由于参数ar2是指向数组的指针,函数定义是最简单的方法是将ar2看作是一个二维数组的名称。

int sum(int ar2[][4], int size)
{
    int total = 0;
    for (int r = 0; r < size; r++)
        for (int c = 0; c < 4; c++)
            total += ar2[r][c];
    return total;
}

同样,行数被传递给size参数,但无论是参数ar2的声明或是内部循环中,列数都是固定的——4 列。
可以使用数组表示法的原因如下。由于ar2指向数组(它的元素是由 4 个int组成的数组)的第一个元素(元素0),因此表达式ar2+r指向编号为r的元素。因此ar2[r]是编号为r的元素。由于该元素本身就是一个由 4 个int组成的数组,因此ar2[r]是由 4 个int组成的数组的名称。将下标用于数组名将得到一个数组元素,因此ar2[r][c]是由 4 个int组成的数组中的一个元素,是一个int值。必须对指针ar2执行两次解除引用,才能得到数据。最简单的方法是使用方括号两次:ar2[r][c]。然而,如果不考虑难看的话,也可以使用运算符*两次:

ar2 [r][c] == *(*(ar2 + r) + c)     //same thing

ar2                 //pointer to first row of an array of 4 int
ar2 + r             //pointer to row r (an array of 4 int)
*(ar2 + r)          //row r (an array of 4 int, hence the name of an array,
                    //thus a pointer to the first int in the row, i.e., ar2[r]
*(ar2 +r)+ c        //pointer int number c in row r, i.e., ar2[r] + c
*(*(ar2 + r)+ c)    //value of int number c in row r,i.e., ar2[r][c]

sum()的代码在声明参数ar2时,没有使用const,因为这种技术只能用于指向基本类型的指针,而ar2是指向指针的指针。

函数和 C- 风格字符串

C- 风格字符串是以空值字符结尾的字符数组,将字符串作为参数时意味着传递的是地址,可使用const来禁止对字符串参数进行修改。

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

假设要将字符串作为参数传递给函数,则表示字符串的方式有三种:

上述3种选择的类型都是char指针(准确的说明char *),因此可以将其作为字符串处理函数的参数:

char ghost[15] = "galloping";
char * str = "galumphing";
int n1 = strlen(ghost);         \ghost in &ghost[0]
int n2 = strlen(str);           \pointer to char
int n3 = strlen("gamboling");   \address of string

可以说是将字符串作为参数来传递,但实际传递的是字符串第一个字符的地址。这意味着字符串函数原型应将其表示字符串的形参声明为char *类型。
C- 风格字符串与常规char数组之间的一个重要区别是,字符串有内置的结束字符,这意味着不必将字符串长度作为参数传递给函数,而函数可以使用循环依次检查字符串中的每个字符,直到遇至结尾的空值字符为止。

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

函数无法返回一个字符串,但可以返回字符串的地址,这样做的效率更高。

函数和结构

虽然结构变量和数组一样,可以存储多个数据项,但在涉及函数时,结构变量的行为更接近于基本的单值变量。结构将其数据组合成单个实体或数据对象,该实体被视为一个整体。可以将一个结构赋给另一个结构,同样也可以按值传递结构,就像普通变量那样。这种情况下,函数使用原结构的副本。另外,函数也可以返回结构。结构名只是结构的名称,要获得结构的地址,必须使用地址运算符&
使用结构编程时,最直接的方式是像处理基本类型那样来处理结构,即将结构作为参数传递,并在需要时将结构用作返回值使用。按值传递结构有一个缺点,如果结构非常大,则复制结构将增加内存要求,降低系统运行的速度。

传递和返回结构

当结构比较小时,按值传递结构最合理。

传递结构的地址

假设要传递结构的地址而不是整个结构以节省时间和空间,则使用指向结构的指针:

函数和 string 对象

虽然 C- 风格字符串和string对象的用途几乎相同,但与数组相比,string对象与结构更相似。
可以将一个对象赋给另一个对象,可经将对象作为完整的实体时行传递,如果需要多个字符串,可以声明一个string对象数组。

函数与 array 对象

在 C++ 中,类对象是基于结构的,因此结构编程方面的有些考虑因素也适用于类。
可按值将对象传递给函数,函数处理的是原对象的副本。可传递指向对象指针,让函数可以修改原始对象。

递归

C++ 函数可以调用自己(与 C 语言不同的是,C++ 不允许main()调用自己,这种功能被称为递归。

包含一个递归调用的函数

如果递归函数调用自己,则被调用的函数也将调用自己,这将无限循环下去,除非代码中包含终止调用链的内容。通常的方法将递归调用放在if语句中。

void recurs(argumentlist)
{
    statements1
    if (test)
        recurs(arguments)
    statements2
}

test最终将为false,调用链将断开。
只要if语句为true,每个recurs()调用都将执行statements1,然后再调用recurs(),而不会执行statements2。当if语句为false时,当前调用执行statements2。当前调用结束后,程序控制权将返回给调用它的recurs(),而该recurs()将执行其statements2部分,然后结束,并将控制权返回给前一个调用,依此类推。因此,如果recurs()进行了 n 次调用,则statements1部分将按函数调用顺序执行 n 次,然后程序将沿进入的路径返回,statements2部分将以函数调用相反的顺序执行 n 次。

包含多个递归调用的递归

在需要将一项工作不断分为两项较小的、类似的工作时,递归非常有用。但要注意,这样的调用将呈几何级数增长,如果要求的递归层次很多,这种递归方式将是一种糟糕的选择;然而,如果递归层次较少,这将是一种精致而简单的选择。

函数指针

与数据项相似,函数也有地址。函数的地址是存储其机器代码的内存的开始地址。例如,可以编写将另一个函数的地址作为参数的函数。这样第一个函数能够找到第二个函数,并运行它。与直接调用另一个函数相比,这种方法很笨拙,但它允许在不同的时间传递不同函数的地址,这意味着可以在不同的时间使用不同的函数。

指针函数的基础知识

假设要设计一个名为estimate()的函数,估算编写指定行数的代码所需的时间,并希望不同的程序员都将使用该函数。对于所有的用户来说,estimate()中一部分代码都是相同的,但该函数允许每个程序员提供自己的算法来估算时间。为实现这种目标,采用的机制是,将程序员要使用的算法函数的地址传递给estimate()。为此,必须能够完成下面任务:

获取函数的地址

获取函数的地址很简单:只要使用函数名(后面不跟参数)即可。如果think()是一个函数,则think就是该函数的地址。要将函数作为参数传递,必须传递函数名。一定要区分传递的是函数的地址还是函数的返回值:

process(think);     // passes address of think() to process()
thought(think());   // passes return value of think() to thought()

process()调用使得process()函数能够在其内部调整think()函数。thought()调用首先调用think()函数,然后将think()的返回值传递给thought()函数。

声明函数指针

声明指向某种数据类型的指针时,必须指定指针指向的类型。同样,声明指向函数的指针时,也必须指定指针指向的函数类型。这意味着声明应指定函数的返回类型以及函数的特征标(参数列表)。也就是说,声明应像函数原型那样指出有关函数的信息。例如,假设 Pam leCoder 编写了一个估算时间的函数,其原型如下:

double pam(int);    // prototype

则正确的指针类型声明如下:

double (*pf)(int);      // pf points to a function that
                        // takes one int argument and that
                        // returns type double

这与pam()声明类似,这是将pam替换为了(*pf)。由于pam是函数,因此(*pf)也是函数。而如果(*pf)是函数,则pf就是函数指针。
提示:通常,要声明指向特定类型的函数指针,可以首先编写这种函数的原型,然后用(*pf)替换函数名。这样pf就是这类函数的指针。
为提供正确的运算符优先级,必须在声明中使用括号将*pf括起。括号的优先级比*运算符高,因此*pf(int)意味着pf()是一个返回指针的函数,而(*pf)(int)意味着pf是一个指向函数的指针:

double (*pf)(int);      // pf points to a function that returns double
double *pf(int);        // pf() a function that returns a pointer-to-double

正确地声明pf后,便可将相应函数的地址赋给它:

double pam(int);
double (*pf)(int);
pf = pam;           // pf now points to the pam() function

注意:pam()的特征标和返回类型必须与pf相同。
假设要将将要编写的代码行数和估算算法(如pam()函数)的地址传递给estimate(),则其原型将如下:

void estimate(int lines, double (*pf)(int));

上述声明指出,第二个参数是一个函数指针,它指向的函数接受一个int参数,并返回一个double值。要让estimate()使用pam()函数,需要将pam()的地址传递给它:

eatimate(50, pam);      // function call telling estimate() to use pam()

显然,使用函数指针时,比较棘手的是编写原型,而传递地址则非常简单。

使用指针来调用函数

现在进入最后一步,即使用指针来调用被指向的函数。线索来自指针声明。(*pf)扮演的角色与函数名相同,因此使用(*pf)时,只需将它看作函数名即可:

double pam(int);
double (*pf)(int);
pf = pam;               // pf now points to the pam() function
double x = pam(4);      // call pam() using the function name
double y = (*pf)(5)     // call pam() using the pointer pf
double y = pf(5)        // also call pam() using the pointer pf

实际上,C++ 也允许像使用函数名那样使用pf,第一种格式虽然不太好看,但它给出了强有力的提示——代码正在使用函数指针。

历史与逻辑 为何pf(*pf)等价呢?一种学派认为,由于pf是函数指针,而*pf是函数,因此应将(*pf)()用作函数调用。另一种党派认为,由于函数名是指向该函数的指针,指向函数的指针的行为应与函数名相似,因此应将pf()用作函数调用。C++ 进行了折衷——两种方式者是正确的,或者至少是允许的,虽然它们在逻辑上是互相冲突的。

深入探讨函数指针

下面是一些函数的原型,它们的特征标和返回类型相同:

const double * f1(const double ar[], int n);
const double * f2(const double [], int);
const double * f3(const double *, int);

接下来,假设要声明一个指针,它可指向这三个函数之一。假定该指针名为pa,则只需将目标函数原型中的函数名替换为(*pa)

const double (*p1)(const double *, int);

可在声明的同时进行初始化:

const double (*p1)(const double *, int) = f1;

使用C++11 的自动类型推断功能时,代码要简单得多:

auto p2 = f2;       // C++11 automatic type deduction

现在看下面的语句:

cout << (*p1)(av,3) << ": " << *(*p1)(av,3) << endl;
cout << p2(av,3) << ": " << *p2(av,3) << endl;

(*p1)(av,3)p2(av,3)都调用指向的函数(这里是f1()f2()),因此,显示的是这两个函数的返回值,返回值的类型是const double *(即double值的地址),因此前半部分显示的都是一个double值的地址,为查看存储在这些地址的实际值,需要将运算符*应用于这些地址,如表达式*(*p1)(av,3*p2(av,3)所示。
鉴于需要使用三个函数,如果有一个函数指针数组将很方便,这样,就可以使用for循环通过指针依次调用每个函数。如何声明这样的数组呢?显然,这种声明应类似于单个函数指针的声明,但必须在某个地方加上[3],以指出这是一个包含三个函数指针的数组。问题是在什么地方加上[3],答案如下(包含初始化):

const double * (*pa[3])(const double *, int) = {f1, f2, f3};

为什么将[3]放在这个地方呢?pa是一个包含三个元素的数组,而要声明这样的数组,首先要声明pa[3]。该声明的的其他部分指出了数组包含的元素是什么样的。运算符[]的优先级高于*,因此*pa[3]表明pa是一个包含三个指针的数组。上述声明的其他部分指出了每个指针指向的是什么:特征标为const double *, int,且返回类型为const double *的函数。因此,pa是一个包含三个指针的数组,其中每个指针都指向这样的函数,即将const double *, int作为参数,并返回一个const double *
这里能否使用auto呢?不能。自动类型推断只能用于单值初始化,而不能用于初始化列表。但声明pa后,声明同样类型的数组就很简单了:

auto pb = pa;

数组名是指向第一个元素的指针,因此papb都是指向函数指针的指针。
如何使用它们来调用函数呢?pa[i]pb[i]都表示数组中的指针,因此可将任何一种函数调用的表示法用于它们:

const double * px = pa[0](av,3);
const double * py = (*pb[1])(av,3);

要获得指向的double值,可以使用运算符*

double x = *pa[0](av,3);
double y = *(*pb[1])(av,3);

可做的另一件事是创建指向整个数组的指针。由于数组名pa是指向函数指针的指针,因此指向数组的指针将是这样的指针,即它指向指针的指针。由于可使用单个值对其进行初始化,因此可以使用auto

auto pc = &pa;      // C++11 atuomatic type deduction

如果声明该怎么办?显然,这种声明应类似于pa的声明,但由于增加了一层间接,因此需要在某个地方添加一个*。具体地说,如果这个指针名为pd,则需要指出它是一个指针,而不是数组。这意味着声明的核心部分应为(*pd)[3],其中的括号让标识符pd*先结合:

*pd[3]      // an array of 3 pointers
(*pd)[3]    // a pointer to an array of 3 elements

换名话说,pd是一个指针,它指向一个包含三个元素的数组。这些元素是什么呢?由pa的声明的其他部分描述,结果如下:

const double * (*(*pd)[3])(const double *, int) = &pa;

要调用函数,需认识到这样一点:既然pd指向数组,那么*pd就是数组,而(*pd)[i]是数组中的元素,即函数指针。因此,较简单的函数调用是(*pd)[i](av,3),而*(*pd)[i](av,3)是返回的指针指向的值。也可以使用第二种使用指针调用函数的语法:使用(*(*pd)[i])(av,3)来调用函数,而*(*(*pd)[i])(av,3)是指向的double值。
请注意pa(它是数组名,表示地址)和&pa之间的差别。在大多数情况下,pa都是数组第一个元素的地址,即&pa[0]。因此,它是单个指针的地址。但&pa是整个数组(即三个指针块)的地址。从数字上说,pa&pa的值相同,但它们的类型不同。一种差别是,pa+1为数组中下一个元素的地址,而&pa+1为数组pa后面一个数组长度内存块的地址。另一个差别是,要得到第一个元素的值,只需对pa解除一次引用,但需要对&pa解除两次引用:

**&pa == *pa == pa[0]

程序清单7.19 arfupt.cpp

// arfupt.cpp -- an array of function pointers
#include 
// various notations, same signatures
const double * f1(const double ar[], int n);
const double * f2(const double [], int);
const double * f3(const double *, int);
 
int main()
{
    using namespace std;
    double av[3] = {1112.3, 1542.6, 2227.9};
    
    // pointer to a function
    const double * (*p1)(const double *, int) = f1;
    auto p2 = f2;
    // pre-C++ can use the following code instead
    // const double *(*p2)(const double *, int) = f2;
    cout << "Using pointers to functions:\n";
    cout << " Address Value\n";
    cout << (*p1)(av,3) << ": " << *(*p1)(av,3) << endl;
    cout << p2(av,3) << ": " << *p2(av,3) << endl;
    
    // pa an array of pointers
    // auto doesn't work with list initialization
    const double * (*pa[3])(const double *, int) = {f1, f2, f3};
    // but it does work for initializing to a single value
    // pb a pointer to first dlement of pa
    auto pb = pa;
    // pre-C++11 can use the following code instead
    // const double *(**pb)(const double *, int) = pa; 
    cout << "\nUsing an array of pointers to functions:\n";
    cout << " Address Value\n";
    for (int i = 0; i < 3; i++)
        cout << pa[i](av,3) << ": " << *pa[i](av,3) << endl;
    cout << "\nUsing a pointer to a pointer to a function:\n";
    cout << " Address Value\n";
    for (int i = 0; i < 3; i++)
        cout << pb[i](av,3) << ": " << *pb[i](av,3) << endl;
        
    // what about a pointer to an array of function pointers
    cout << "\nUsing pointers to an array of pointers:\n";
    cout << " Address Value\n";
    // easy way to declare pc
    auto pc = &pa;
    // pre-C++11 cna use the following code instead
    // const double * (*(*pc)[3])(const double *, int) = &pa;
    cout << (*pc)[0](av,3) << ": " << *(*pc)[0](av,3) << endl;
    // hard way to declard pd
    const double *(*(*pd)[3])(const double *, int) = &pa;
    // store return value in pdb
    const double * pdb = (*pd)[1](av,3);
    cout << pdb << ": " << *pdb << endl;
    // alternative notation
    cout << (*(*pd)[2])(av,3) << ": " << *(*(*pd)[2])(av,3) << endl;
    return 0;
}

// some rather dull functions

const double * f1(const double * ar, int n)
{
    return ar;
}

const double * f2(const double ar[], int n)
{
    return ar+1;
}

const double * f3(const double ar[], int n)
{
    return ar+2;
}

显示的地址为数组avdouble值的存储位置。

感谢 auto:C++11 的目标之一是让 C++ 更容易使用,从而让程序员将主要精力放在设计而不是细节上。自动类型推断演示了这一点。

auto pc = &pa;                                  // C++11 automatic type deduction
const double * (*(*pc)[3])(const double *, int) = &pa; // C++98, do it yourself

自动类型推断功能表明,编译器的角色发生了改变。在 C++98 中,编译器利用其知识帮助您发现错误,而在 C++11 中,编译器利用其知识帮助您进行正确的声明。
存在一个潜在的缺点。自动类型推断确保变量的类型与赋给它的初值类型一致,但您提供的初值的类型可能不对:

auto pc = *pc;      //oops! used *pa instead of &pa

上述声明导致pc的类型与*pa一致,后面使用它时假定其类型与&pa相同,这将导致编译错误。

使用 typedef 进行简化

auto外,C++ 还提供了其他简化声明的工具。关键字typedef可以创建类型别名:

typedef const real;     // makes real another name for double

这里采用的方法是,将别名当做标识符进行声明,并在开关使用关键字typedef。因此,可将p_fun声明为函数指针类型的别名:

typedef const double * (*p_fun)(const double *, int);   // p_fun now a type name
p_fun p1 = f1;      // p1 points to the f1() function

然后使用这个别名来简化代码:

p_fun pa[3] = {f1, f2, f3};     // pa an array of 3 funtion pointers
p_fun (*pd)[3] = &pa;           // pd points to an array of 3 function pointers

使用typedef可减少输入量,让您编写代码时不容易犯错,并让程序更容易理解。

第 7 章总结

函数是 C++ 的编程模块。要使用函数,必须提供定义和原型,并调用该函数。函数定义是实现函数功能的代码;函数原型描述了函数的接口:传递给函数的值的数目和种类以及函数的返回类型。函数调用使得程序将参数传递给函数,并执行函数的代码。
在默认情况下,C++ 函数按值传递参数。这意味着函数定义中的形参是新的变量,它们被初始化为函数调用所提供的值。因此,C++ 函数通过使用拷贝,保护了原始数据的完整性。
C++ 将数组名参数视为数组第一个元素的地址,从技术上讲,这仍然是按值传递的,因为指针是原始地址的拷贝,但函数将使用指针来访问原始数组的内容。当且仅当声明函数的形参时,下面两个声明才是等价的。

typeName arr [];
typeName * arr;

这两个声明都表明,arr是指向typeName的指针,但在编写函数代码时,可以像使用数组名那样使用arr来访问元素:arr[i]。即使在传递指针时,也可以将形参声明为const指针,来保护原始数据的完整性。由于传递数据的地址时,并不会传输有关数组长度的信息,因此通常将数组长度作为独立的参数来传递。另外,也可传递两个指针(其中一个指向数组开头,另一个指向数组末尾的下一个元素),以指定一个范围,就像 STL 使用的算法一样。
C++ 提供了 3 种表示 C- 风格字体字符串的方法:字符数组、字符串常量和字符串指针。它们的类型都是char *char指针),因此被作为char *类型参数传递给函数。C++ 使用空值字符(string)来结束字符串,因此字符串函数检测空值字符来确定字符串的结尾。
C++ 还提供了string,用于表示字符串。函数可以接受string对象作为参数以及将string对象作为返回值。size()类的方法[+++]可用于判断其存储的字符串的长度。
C++ 处理结构的方式与基本类型完全相同。这意味着可以按值传递结构,并将其用作函数返回类型。
然而,如果结构非常大,则传递结构指针的效率将更高,同时函数能够使用原始数据。这些考虑因素也适用于类对象。
C++ 函数可以是递归的,也就是说,函数代码中可以包括对函数本身的调用。
C++ 函数名与函数地址的作用相同。通过将函数指针作为参数,可以传递要调用的函数的名称。

)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
C++ Primer Plus 学习笔记(第 7 章 函数——C++ 的编程模块)_C_内存溢出

C++ Primer Plus 学习笔记(第 7 章 函数——C++ 的编程模块)

C++ Primer Plus 学习笔记(第 7 章 函数——C++ 的编程模块),第1张

C++ Primer Plus 学习笔记 第 7 章 函数——C++ 的编程模块 复习函数的基本知识

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

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

库函数是已经定义和编译好的函数,同时可以使用标准库头文件提供原型,因此只需正确调用即可。而自定义函数必须自行处理这 3 个方面——定义、提供原型和调用。

定义函数

可以将函数分成两类:没有返回值的函数和有返回值的函数。
没有返回值的函数被称为void函数,其通用格式如下:

void functionName(parameterList)
{
    statement(s)
    return;         // optional
}

parameterList指定了传递给函数的参数类型和数量,可选返回语句标记了函数的结尾,否则,函数将在右花括号处结束。
有返回值的函数其通用格式如下:

typeName functionName(parameterList)
{
    statements
    return vlaue;         // value is type cast to type typeName
}

对于有返回值的函数,必须使用返回语句,以便将值返回给调用函数。值本身可以是常量、变量,也可以是表达式,只是其结果的类型必须为typeName类型或可以被转换为typeName。函数将最终的值返回给调用函数。C++ 对于返回值的类型有一定的限制:不能是数组,但可以是其他类型——整数、浮点数、指针,甚至可能是结构和对象!(可以将数组作为结构和对象的组成部分返回。)
通常,函数通过将返回值复制到指定的 CPU 寄存器或内存单元中将其返回。随后,调用函数将查看该内存单元。

函数原型和函数调用 为什么需要原型

原型描述了函数到编译器的接口,它将函数返回值的类型(如果有的话)以及参数的类型和数量告诉编译器。
避免使用函数原型的唯一方法是,在首次使用函数之前定义它,但这并不总是可行的。另外,C++ 的编程方格是将main()放在最前面,因为它通常提供了程序的整体结构。

原型的语法

函数原型是一条语句,因此必须以分号结束。获得原型最简单的方法是,复制函数定义中的函数头,并添加分号。
通常,在原型的参数列表中,可以包括变量名,也可以不包括。原型中的变量名相当于占位符,因此不必与函数定义中的变量名相同。
在 C++ 中,原型是必不可少的。参数列表为空和使用关键字void是等效的——意味着函数没有参数。
在 C++ 中,不指定参数列表时应使用省略号,通常,仅当与接受可变参数的 C 函数交互时才需要这样做。

原型的功能

原型确保以下几点:

  • 编译器正确处理函数返回值;
  • 编译器检查使用的参数数目是否正确;
  • 编译器检查使用的参数类型是否正确。如不正确,则转换为正确的类型(如果可能的话)。

C++ 自动将传递的值转换为原型中指定的类型,条件是两者都是算术类型。
自动类型转换并不能避免所有可能的错误。
仅当有意义时,原型化才会导致类型转换。例如,原型不会将整型转换为结构或指针。
在编译阶段进行的原型化被称为静态类型检查(static type checking)。静态类型检查可捕获许多在运行阶段非常难以捕获的错误。

函数参数和按值传递

C++ 通常按值传递参数,将数值参数传递给函数,函数将其赋给一个新的变量。
用于接受传递值的变量被称为形参,传递给函数的值被称为实参。C++ 标准使用参数(argument)来表示实参,使用参量(parameter)来表示形参。
在函数中声明的变量(包括参数)是该函数私有的,被称为局部变量,被限制在函数中。也被称为自动变量。

多个参数

函数可以有多个参数,在调用时,用逗号将这些参数分开。
在定义函数时,在函数头中使用逗号分隔的参数声明列表。如果函数的两个参数类型相同,则必须分别指定每个参数的类型,而不能像声明常规变量那样,将声明组合在一起。
原型中的变量名不必与定义中的变量名相同,而且可以省略。然而,提供变量名将使原型更容易理解,尤其是两个参数的类型相同时。这样,变量名可以提醒参量和参数间的相应关系。
形参与其他局部变量的主要区别是,形参从调用函数那里获得自己的值,而其他变量是从函数中获得自己的值。

函数和数组 函数如何使用指针来处理数组

在大多数情况下,C++ 与 C 语言一样,也将数组名视为指针,该规则有一些例外。首先,数组声明使用数组名来标记存储位置;其次,对数组名使用sizeof()将得到整个数组的长度;第三,将地址运算符&用于数组名时,将返回整个数组的地址。
在 C++ 中,当(且仅当)用于函数头或函数原型中,int arr[]int *arr的含义才是相同的。

arr[i] == *(arr + i)    \values in two notations
&arr[i] == arr + i      \addresses in two notations

遍历数组时,使用指针加法和数组下标是等效的。

将数组作为参数意味着什么

将数组作为参数,是将数组的位置(地址)传递给了函数。传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组。这种区别并不违反 C++ 按值传递的方法,传递了一个值,这个值被赋给了一个新的变量,这个值是地址,而不是数组的内容。因此,无法使用sizeof()来获悉原始数组的长度。
数组名与指针对应是一件好事,将数组地址作为参数可以节省复制整个数组的时间和内存。另一方面,使用原始数据增加了破坏数据的风险。

用 const 保护数组

为防止函数无意中修改数组的内容,可在声明形参时使用关键字const。该声明表示,指针指向的的常量数据,不能使用指针修改该数据,但并不意味着原始数组必须是常量。

使用数组区间的函数

处理数组的 C++ 函数,必须将数组中的数据种类、数组的起始位置和数组中元素数量提交给它;舍传统 C/C++ 方法是,将指向数组起始位处的指针作为第一个参数,将数组长度作为第二个参数。
另一种方法是,指定元素区间,可以通过传递两个指针,一个指针标识数组的开头,另一个指针标识数组的结尾。C++ 标准库(STL)将区间方法广义化,使用“超尾”概念来指定区间。也就是说,对于数组而言,标识数组结尾的参数将是指向最后一个元素后面的指针。

指针和 const

可以用两种不同的方式将const关键字用于指针。第一种方式是让指针指向一个常量对象,可以防止使用该指针来修改所指向的值。第二种方法是将指针本身声明为常量,这样可以防止改变指针指向的位置。
可以将常规变量赋给常规指针,也可将常规变量赋给指向const的指针,还可以将const变量赋给指向const的指针,但不能将const的地址赋给常规指针。
只有一层间接关系(如指针指向基本数据类型)时,才可以将非const地址或指针赋给const指针。
注意:如果数据类型本身并不是指针,则可以将const数据或非const数据的地址赋给指向const的指针,但只能将非const数据地址赋给非const指针。

将指针参数声明为指向常量数据的指针有两条理由:

  • 这样可以避免由于无意间修改数据而导致的编程错误;
  • 使用const使用得函数能够处理const和非const实参,否则将只能接受非const数据。

如果条件允许,则应将指针形参声明为指向const的指针。

函数和二维数组

为编写将二维数组作为参数的函数,必须牢记,数组名被视为指针,因此,相应的形参是一个指针,就像一维数组一样。比较难处理的是如何正确地声明指针。例如,假设有如下代码:

int data[3][4] = {{1, 2, 3, 4}, {9, 8, 7, 6}, {2, 4, 6, 8}};
int total = sum(data, 3);

sum()的原型是是什么。
data是一个数组名,该数组有 3 个元素,每个元素本身是一个数组,有 4 个int值组成。因此data的类型是指向由 4 个int组成的数组的指针,因此正确的原型如下:

int sum(int (*ar2)[4], int size);

其中括号是必不可少的,int *ar2[4]将声明一个由 4 个指向int的指针组成的数组,另外,函数参数不能是数组。
还有另外一种格式,这种格式与上述原型的含义完全相同,但可读性更强:

int sum(int ar2[][4], int size);

上述两种原型都指出,ar2是指针而不是数组。同时,指针类型指出,它指向由 4 个int组成的数组,因此,指针类型指定了列数,所以无需将列数作为独立的函数参数进行传递。
由于指针类型指定了列数,因此sum()函数只能接受由 4 列组成的数组。但长度变量指定了行数,因此sum()函数对数组的长度没有限制:

int a[100][4];
int b[6][4];
...
int total1 = sum(a, 100);       \sum all of a
int total2 = sum(b, 6);         \sum all of b
int total3 = sum(a, 10);        \sum first 10 rows of a
int total4 = sum(a+10, 20);     \sum next 20 rows of a

由于参数ar2是指向数组的指针,函数定义是最简单的方法是将ar2看作是一个二维数组的名称。

int sum(int ar2[][4], int size)
{
    int total = 0;
    for (int r = 0; r < size; r++)
        for (int c = 0; c < 4; c++)
            total += ar2[r][c];
    return total;
}

同样,行数被传递给size参数,但无论是参数ar2的声明或是内部循环中,列数都是固定的——4 列。
可以使用数组表示法的原因如下。由于ar2指向数组(它的元素是由 4 个int组成的数组)的第一个元素(元素0),因此表达式ar2+r指向编号为r的元素。因此ar2[r]是编号为r的元素。由于该元素本身就是一个由 4 个int组成的数组,因此ar2[r]是由 4 个int组成的数组的名称。将下标用于数组名将得到一个数组元素,因此ar2[r][c]是由 4 个int组成的数组中的一个元素,是一个int值。必须对指针ar2执行两次解除引用,才能得到数据。最简单的方法是使用方括号两次:ar2[r][c]。然而,如果不考虑难看的话,也可以使用运算符*两次:

ar2 [r][c] == *(*(ar2 + r) + c)     //same thing

ar2                 //pointer to first row of an array of 4 int
ar2 + r             //pointer to row r (an array of 4 int)
*(ar2 + r)          //row r (an array of 4 int, hence the name of an array,
                    //thus a pointer to the first int in the row, i.e., ar2[r]
*(ar2 +r)+ c        //pointer int number c in row r, i.e., ar2[r] + c
*(*(ar2 + r)+ c)    //value of int number c in row r,i.e., ar2[r][c]

sum()的代码在声明参数ar2时,没有使用const,因为这种技术只能用于指向基本类型的指针,而ar2是指向指针的指针。

函数和 C- 风格字符串

C- 风格字符串是以空值字符结尾的字符数组,将字符串作为参数时意味着传递的是地址,可使用const来禁止对字符串参数进行修改。

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

假设要将字符串作为参数传递给函数,则表示字符串的方式有三种:

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

上述3种选择的类型都是char指针(准确的说明char *),因此可以将其作为字符串处理函数的参数:

char ghost[15] = "galloping";
char * str = "galumphing";
int n1 = strlen(ghost);         \ghost in &ghost[0]
int n2 = strlen(str);           \pointer to char
int n3 = strlen("gamboling");   \address of string

可以说是将字符串作为参数来传递,但实际传递的是字符串第一个字符的地址。这意味着字符串函数原型应将其表示字符串的形参声明为char *类型。
C- 风格字符串与常规char数组之间的一个重要区别是,字符串有内置的结束字符,这意味着不必将字符串长度作为参数传递给函数,而函数可以使用循环依次检查字符串中的每个字符,直到遇至结尾的空值字符为止。

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

函数无法返回一个字符串,但可以返回字符串的地址,这样做的效率更高。

函数和结构

虽然结构变量和数组一样,可以存储多个数据项,但在涉及函数时,结构变量的行为更接近于基本的单值变量。结构将其数据组合成单个实体或数据对象,该实体被视为一个整体。可以将一个结构赋给另一个结构,同样也可以按值传递结构,就像普通变量那样。这种情况下,函数使用原结构的副本。另外,函数也可以返回结构。结构名只是结构的名称,要获得结构的地址,必须使用地址运算符&
使用结构编程时,最直接的方式是像处理基本类型那样来处理结构,即将结构作为参数传递,并在需要时将结构用作返回值使用。按值传递结构有一个缺点,如果结构非常大,则复制结构将增加内存要求,降低系统运行的速度。

传递和返回结构

当结构比较小时,按值传递结构最合理。

传递结构的地址

假设要传递结构的地址而不是整个结构以节省时间和空间,则使用指向结构的指针:

  • 将形参声明为指向结构的指针,如函数不应修改结构,可以使用const修饰符;
  • 由于形参是指针而不是结构,因此应使用间接成员运算符->,而不能使用成员运算符.
  • 调用函数时,实参应传递结构的地址,而不应传递结构本身(应使用地址运算符&)。
函数和 string 对象

虽然 C- 风格字符串和string对象的用途几乎相同,但与数组相比,string对象与结构更相似。
可以将一个对象赋给另一个对象,可经将对象作为完整的实体时行传递,如果需要多个字符串,可以声明一个string对象数组。

函数与 array 对象

在 C++ 中,类对象是基于结构的,因此结构编程方面的有些考虑因素也适用于类。
可按值将对象传递给函数,函数处理的是原对象的副本。可传递指向对象指针,让函数可以修改原始对象。

递归

C++ 函数可以调用自己(与 C 语言不同的是,C++ 不允许main()调用自己,这种功能被称为递归。

包含一个递归调用的函数

如果递归函数调用自己,则被调用的函数也将调用自己,这将无限循环下去,除非代码中包含终止调用链的内容。通常的方法将递归调用放在if语句中。

void recurs(argumentlist)
{
    statements1
    if (test)
        recurs(arguments)
    statements2
}

test最终将为false,调用链将断开。
只要if语句为true,每个recurs()调用都将执行statements1,然后再调用recurs(),而不会执行statements2。当if语句为false时,当前调用执行statements2。当前调用结束后,程序控制权将返回给调用它的recurs(),而该recurs()将执行其statements2部分,然后结束,并将控制权返回给前一个调用,依此类推。因此,如果recurs()进行了 n 次调用,则statements1部分将按函数调用顺序执行 n 次,然后程序将沿进入的路径返回,statements2部分将以函数调用相反的顺序执行 n 次。

包含多个递归调用的递归

在需要将一项工作不断分为两项较小的、类似的工作时,递归非常有用。但要注意,这样的调用将呈几何级数增长,如果要求的递归层次很多,这种递归方式将是一种糟糕的选择;然而,如果递归层次较少,这将是一种精致而简单的选择。

函数指针

与数据项相似,函数也有地址。函数的地址是存储其机器代码的内存的开始地址。例如,可以编写将另一个函数的地址作为参数的函数。这样第一个函数能够找到第二个函数,并运行它。与直接调用另一个函数相比,这种方法很笨拙,但它允许在不同的时间传递不同函数的地址,这意味着可以在不同的时间使用不同的函数。

指针函数的基础知识

假设要设计一个名为estimate()的函数,估算编写指定行数的代码所需的时间,并希望不同的程序员都将使用该函数。对于所有的用户来说,estimate()中一部分代码都是相同的,但该函数允许每个程序员提供自己的算法来估算时间。为实现这种目标,采用的机制是,将程序员要使用的算法函数的地址传递给estimate()。为此,必须能够完成下面任务:

  • 获取函数的地址;
  • 声明一个函数指针;
  • 使用函数指针为调用函数。
获取函数的地址

获取函数的地址很简单:只要使用函数名(后面不跟参数)即可。如果think()是一个函数,则think就是该函数的地址。要将函数作为参数传递,必须传递函数名。一定要区分传递的是函数的地址还是函数的返回值:

process(think);     // passes address of think() to process()
thought(think());   // passes return value of think() to thought()

process()调用使得process()函数能够在其内部调整think()函数。thought()调用首先调用think()函数,然后将think()的返回值传递给thought()函数。

声明函数指针

声明指向某种数据类型的指针时,必须指定指针指向的类型。同样,声明指向函数的指针时,也必须指定指针指向的函数类型。这意味着声明应指定函数的返回类型以及函数的特征标(参数列表)。也就是说,声明应像函数原型那样指出有关函数的信息。例如,假设 Pam leCoder 编写了一个估算时间的函数,其原型如下:

double pam(int);    // prototype

则正确的指针类型声明如下:

double (*pf)(int);      // pf points to a function that
                        // takes one int argument and that
                        // returns type double

这与pam()声明类似,这是将pam替换为了(*pf)。由于pam是函数,因此(*pf)也是函数。而如果(*pf)是函数,则pf就是函数指针。
提示:通常,要声明指向特定类型的函数指针,可以首先编写这种函数的原型,然后用(*pf)替换函数名。这样pf就是这类函数的指针。
为提供正确的运算符优先级,必须在声明中使用括号将*pf括起。括号的优先级比*运算符高,因此*pf(int)意味着pf()是一个返回指针的函数,而(*pf)(int)意味着pf是一个指向函数的指针:

double (*pf)(int);      // pf points to a function that returns double
double *pf(int);        // pf() a function that returns a pointer-to-double

正确地声明pf后,便可将相应函数的地址赋给它:

double pam(int);
double (*pf)(int);
pf = pam;           // pf now points to the pam() function

注意:pam()的特征标和返回类型必须与pf相同。
假设要将将要编写的代码行数和估算算法(如pam()函数)的地址传递给estimate(),则其原型将如下:

void estimate(int lines, double (*pf)(int));

上述声明指出,第二个参数是一个函数指针,它指向的函数接受一个int参数,并返回一个double值。要让estimate()使用pam()函数,需要将pam()的地址传递给它:

eatimate(50, pam);      // function call telling estimate() to use pam()

显然,使用函数指针时,比较棘手的是编写原型,而传递地址则非常简单。

使用指针来调用函数

现在进入最后一步,即使用指针来调用被指向的函数。线索来自指针声明。(*pf)扮演的角色与函数名相同,因此使用(*pf)时,只需将它看作函数名即可:

double pam(int);
double (*pf)(int);
pf = pam;               // pf now points to the pam() function
double x = pam(4);      // call pam() using the function name
double y = (*pf)(5)     // call pam() using the pointer pf
double y = pf(5)        // also call pam() using the pointer pf

实际上,C++ 也允许像使用函数名那样使用pf,第一种格式虽然不太好看,但它给出了强有力的提示——代码正在使用函数指针。

历史与逻辑 为何pf(*pf)等价呢?一种学派认为,由于pf是函数指针,而*pf是函数,因此应将(*pf)()用作函数调用。另一种党派认为,由于函数名是指向该函数的指针,指向函数的指针的行为应与函数名相似,因此应将pf()用作函数调用。C++ 进行了折衷——两种方式者是正确的,或者至少是允许的,虽然它们在逻辑上是互相冲突的。

深入探讨函数指针

下面是一些函数的原型,它们的特征标和返回类型相同:

const double * f1(const double ar[], int n);
const double * f2(const double [], int);
const double * f3(const double *, int);

接下来,假设要声明一个指针,它可指向这三个函数之一。假定该指针名为pa,则只需将目标函数原型中的函数名替换为(*pa)

const double (*p1)(const double *, int);

可在声明的同时进行初始化:

const double (*p1)(const double *, int) = f1;

使用C++11 的自动类型推断功能时,代码要简单得多:

auto p2 = f2;       // C++11 automatic type deduction

现在看下面的语句:

cout << (*p1)(av,3) << ": " << *(*p1)(av,3) << endl;
cout << p2(av,3) << ": " << *p2(av,3) << endl;

(*p1)(av,3)p2(av,3)都调用指向的函数(这里是f1()f2()),因此,显示的是这两个函数的返回值,返回值的类型是const double *(即double值的地址),因此前半部分显示的都是一个double值的地址,为查看存储在这些地址的实际值,需要将运算符*应用于这些地址,如表达式*(*p1)(av,3*p2(av,3)所示。
鉴于需要使用三个函数,如果有一个函数指针数组将很方便,这样,就可以使用for循环通过指针依次调用每个函数。如何声明这样的数组呢?显然,这种声明应类似于单个函数指针的声明,但必须在某个地方加上[3],以指出这是一个包含三个函数指针的数组。问题是在什么地方加上[3],答案如下(包含初始化):

const double * (*pa[3])(const double *, int) = {f1, f2, f3};

为什么将[3]放在这个地方呢?pa是一个包含三个元素的数组,而要声明这样的数组,首先要声明pa[3]。该声明的的其他部分指出了数组包含的元素是什么样的。运算符[]的优先级高于*,因此*pa[3]表明pa是一个包含三个指针的数组。上述声明的其他部分指出了每个指针指向的是什么:特征标为const double *, int,且返回类型为const double *的函数。因此,pa是一个包含三个指针的数组,其中每个指针都指向这样的函数,即将const double *, int作为参数,并返回一个const double *
这里能否使用auto呢?不能。自动类型推断只能用于单值初始化,而不能用于初始化列表。但声明pa后,声明同样类型的数组就很简单了:

auto pb = pa;

数组名是指向第一个元素的指针,因此papb都是指向函数指针的指针。
如何使用它们来调用函数呢?pa[i]pb[i]都表示数组中的指针,因此可将任何一种函数调用的表示法用于它们:

const double * px = pa[0](av,3);
const double * py = (*pb[1])(av,3);

要获得指向的double值,可以使用运算符*

double x = *pa[0](av,3);
double y = *(*pb[1])(av,3);

可做的另一件事是创建指向整个数组的指针。由于数组名pa是指向函数指针的指针,因此指向数组的指针将是这样的指针,即它指向指针的指针。由于可使用单个值对其进行初始化,因此可以使用auto

auto pc = &pa;      // C++11 atuomatic type deduction

如果声明该怎么办?显然,这种声明应类似于pa的声明,但由于增加了一层间接,因此需要在某个地方添加一个*。具体地说,如果这个指针名为pd,则需要指出它是一个指针,而不是数组。这意味着声明的核心部分应为(*pd)[3],其中的括号让标识符pd*先结合:

*pd[3]      // an array of 3 pointers
(*pd)[3]    // a pointer to an array of 3 elements

换名话说,pd是一个指针,它指向一个包含三个元素的数组。这些元素是什么呢?由pa的声明的其他部分描述,结果如下:

const double * (*(*pd)[3])(const double *, int) = &pa;

要调用函数,需认识到这样一点:既然pd指向数组,那么*pd就是数组,而(*pd)[i]是数组中的元素,即函数指针。因此,较简单的函数调用是(*pd)[i](av,3),而*(*pd)[i](av,3)是返回的指针指向的值。也可以使用第二种使用指针调用函数的语法:使用(*(*pd)[i])(av,3)来调用函数,而*(*(*pd)[i])(av,3)是指向的double值。
请注意pa(它是数组名,表示地址)和&pa之间的差别。在大多数情况下,pa都是数组第一个元素的地址,即&pa[0]。因此,它是单个指针的地址。但&pa是整个数组(即三个指针块)的地址。从数字上说,pa&pa的值相同,但它们的类型不同。一种差别是,pa+1为数组中下一个元素的地址,而&pa+1为数组pa后面一个数组长度内存块的地址。另一个差别是,要得到第一个元素的值,只需对pa解除一次引用,但需要对&pa解除两次引用:

**&pa == *pa == pa[0]

程序清单7.19 arfupt.cpp

// arfupt.cpp -- an array of function pointers
#include 
// various notations, same signatures
const double * f1(const double ar[], int n);
const double * f2(const double [], int);
const double * f3(const double *, int);
 
int main()
{
    using namespace std;
    double av[3] = {1112.3, 1542.6, 2227.9};
    
    // pointer to a function
    const double * (*p1)(const double *, int) = f1;
    auto p2 = f2;
    // pre-C++ can use the following code instead
    // const double *(*p2)(const double *, int) = f2;
    cout << "Using pointers to functions:\n";
    cout << " Address Value\n";
    cout << (*p1)(av,3) << ": " << *(*p1)(av,3) << endl;
    cout << p2(av,3) << ": " << *p2(av,3) << endl;
    
    // pa an array of pointers
    // auto doesn't work with list initialization
    const double * (*pa[3])(const double *, int) = {f1, f2, f3};
    // but it does work for initializing to a single value
    // pb a pointer to first dlement of pa
    auto pb = pa;
    // pre-C++11 can use the following code instead
    // const double *(**pb)(const double *, int) = pa; 
    cout << "\nUsing an array of pointers to functions:\n";
    cout << " Address Value\n";
    for (int i = 0; i < 3; i++)
        cout << pa[i](av,3) << ": " << *pa[i](av,3) << endl;
    cout << "\nUsing a pointer to a pointer to a function:\n";
    cout << " Address Value\n";
    for (int i = 0; i < 3; i++)
        cout << pb[i](av,3) << ": " << *pb[i](av,3) << endl;
        
    // what about a pointer to an array of function pointers
    cout << "\nUsing pointers to an array of pointers:\n";
    cout << " Address Value\n";
    // easy way to declare pc
    auto pc = &pa;
    // pre-C++11 cna use the following code instead
    // const double * (*(*pc)[3])(const double *, int) = &pa;
    cout << (*pc)[0](av,3) << ": " << *(*pc)[0](av,3) << endl;
    // hard way to declard pd
    const double *(*(*pd)[3])(const double *, int) = &pa;
    // store return value in pdb
    const double * pdb = (*pd)[1](av,3);
    cout << pdb << ": " << *pdb << endl;
    // alternative notation
    cout << (*(*pd)[2])(av,3) << ": " << *(*(*pd)[2])(av,3) << endl;
    return 0;
}

// some rather dull functions

const double * f1(const double * ar, int n)
{
    return ar;
}

const double * f2(const double ar[], int n)
{
    return ar+1;
}

const double * f3(const double ar[], int n)
{
    return ar+2;
}

显示的地址为数组avdouble值的存储位置。

感谢 auto:C++11 的目标之一是让 C++ 更容易使用,从而让程序员将主要精力放在设计而不是细节上。自动类型推断演示了这一点。

auto pc = &pa;                                  // C++11 automatic type deduction
const double * (*(*pc)[3])(const double *, int) = &pa; // C++98, do it yourself

自动类型推断功能表明,编译器的角色发生了改变。在 C++98 中,编译器利用其知识帮助您发现错误,而在 C++11 中,编译器利用其知识帮助您进行正确的声明。
存在一个潜在的缺点。自动类型推断确保变量的类型与赋给它的初值类型一致,但您提供的初值的类型可能不对:

auto pc = *pc;      //oops! used *pa instead of &pa

上述声明导致pc的类型与*pa一致,后面使用它时假定其类型与&pa相同,这将导致编译错误。

使用 typedef 进行简化

auto外,C++ 还提供了其他简化声明的工具。关键字typedef可以创建类型别名:

typedef const real;     // makes real another name for double

这里采用的方法是,将别名当做标识符进行声明,并在开关使用关键字typedef。因此,可将p_fun声明为函数指针类型的别名:

typedef const double * (*p_fun)(const double *, int);   // p_fun now a type name
p_fun p1 = f1;      // p1 points to the f1() function

然后使用这个别名来简化代码:

p_fun pa[3] = {f1, f2, f3};     // pa an array of 3 funtion pointers
p_fun (*pd)[3] = &pa;           // pd points to an array of 3 function pointers

使用typedef可减少输入量,让您编写代码时不容易犯错,并让程序更容易理解。

第 7 章总结

函数是 C++ 的编程模块。要使用函数,必须提供定义和原型,并调用该函数。函数定义是实现函数功能的代码;函数原型描述了函数的接口:传递给函数的值的数目和种类以及函数的返回类型。函数调用使得程序将参数传递给函数,并执行函数的代码。
在默认情况下,C++ 函数按值传递参数。这意味着函数定义中的形参是新的变量,它们被初始化为函数调用所提供的值。因此,C++ 函数通过使用拷贝,保护了原始数据的完整性。
C++ 将数组名参数视为数组第一个元素的地址,从技术上讲,这仍然是按值传递的,因为指针是原始地址的拷贝,但函数将使用指针来访问原始数组的内容。当且仅当声明函数的形参时,下面两个声明才是等价的。

typeName arr [];
typeName * arr;

这两个声明都表明,arr是指向typeName的指针,但在编写函数代码时,可以像使用数组名那样使用arr来访问元素:arr[i]。即使在传递指针时,也可以将形参声明为const指针,来保护原始数据的完整性。由于传递数据的地址时,并不会传输有关数组长度的信息,因此通常将数组长度作为独立的参数来传递。另外,也可传递两个指针(其中一个指向数组开头,另一个指向数组末尾的下一个元素),以指定一个范围,就像 STL 使用的算法一样。
C++ 提供了 3 种表示 C- 风格字体字符串的方法:字符数组、字符串常量和字符串指针。它们的类型都是char *char指针),因此被作为char *类型参数传递给函数。C++ 使用空值字符(string)来结束字符串,因此字符串函数检测空值字符来确定字符串的结尾。
C++ 还提供了string,用于表示字符串。函数可以接受string对象作为参数以及将string对象作为返回值。size()类的方法可用于判断其存储的字符串的长度。
C++ 处理结构的方式与基本类型完全相同。这意味着可以按值传递结构,并将其用作函数返回类型。
然而,如果结构非常大,则传递结构指针的效率将更高,同时函数能够使用原始数据。这些考虑因素也适用于类对象。
C++ 函数可以是递归的,也就是说,函数代码中可以包括对函数本身的调用。
C++ 函数名与函数地址的作用相同。通过将函数指针作为参数,可以传递要调用的函数的名称。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/1324255.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-12
下一篇 2022-06-12

发表评论

登录后才能评论

评论列表(0条)

保存