要使用 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
。该声明表示,指针指向的的常量数据,不能使用指针修改该数据,但并不意味着原始数组必须是常量。
处理数组的 C++ 函数,必须将数组中的数据种类、数组的起始位置和数组中元素数量提交给它;舍传统 C/C++ 方法是,将指向数组起始位处的指针作为第一个参数,将数组长度作为第二个参数。
另一种方法是,指定元素区间,可以通过传递两个指针,一个指针标识数组的开头,另一个指针标识数组的结尾。C++ 标准库(STL)将区间方法广义化,使用“超尾”概念来指定区间。也就是说,对于数组而言,标识数组结尾的参数将是指向最后一个元素后面的指针。
可以用两种不同的方式将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- 风格字符串是以空值字符结尾的字符数组,将字符串作为参数时意味着传递的是地址,可使用const
来禁止对字符串参数进行修改。
假设要将字符串作为参数传递给函数,则表示字符串的方式有三种:
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
数组之间的一个重要区别是,字符串有内置的结束字符,这意味着不必将字符串长度作为参数传递给函数,而函数可以使用循环依次检查字符串中的每个字符,直到遇至结尾的空值字符为止。
函数无法返回一个字符串,但可以返回字符串的地址,这样做的效率更高。
函数和结构虽然结构变量和数组一样,可以存储多个数据项,但在涉及函数时,结构变量的行为更接近于基本的单值变量。结构将其数据组合成单个实体或数据对象,该实体被视为一个整体。可以将一个结构赋给另一个结构,同样也可以按值传递结构,就像普通变量那样。这种情况下,函数使用原结构的副本。另外,函数也可以返回结构。结构名只是结构的名称,要获得结构的地址,必须使用地址运算符&
。
使用结构编程时,最直接的方式是像处理基本类型那样来处理结构,即将结构作为参数传递,并在需要时将结构用作返回值使用。按值传递结构有一个缺点,如果结构非常大,则复制结构将增加内存要求,降低系统运行的速度。
当结构比较小时,按值传递结构最合理。
传递结构的地址假设要传递结构的地址而不是整个结构以节省时间和空间,则使用指向结构的指针:
- 将形参声明为指向结构的指针,如函数不应修改结构,可以使用
const
修饰符; - 由于形参是指针而不是结构,因此应使用间接成员运算符
->
,而不能使用成员运算符.
; - 调用函数时,实参应传递结构的地址,而不应传递结构本身(应使用地址运算符
&
)。
虽然 C- 风格字符串和string
对象的用途几乎相同,但与数组相比,string
对象与结构更相似。
可以将一个对象赋给另一个对象,可经将对象作为完整的实体时行传递,如果需要多个字符串,可以声明一个string
对象数组。
在 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;
数组名是指向第一个元素的指针,因此pa
和pb
都是指向函数指针的指针。
如何使用它们来调用函数呢?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;
}
显示的地址为数组av
中double
值的存储位置。
感谢 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
相同,这将导致编译错误。
除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
可减少输入量,让您编写代码时不容易犯错,并让程序更容易理解。
函数是 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++ 函数名与函数地址的作用相同。通过将函数指针作为参数,可以传递要调用的函数的名称。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)