乐趣在于发现。仔细研究,读者将在函数中找到乐趣。C++自带了一个包含函数的大型库(标准ANSI库加上多个C++类),但真正的编程乐趣在于编写自己的函数;另一方面,要提高编程的效率,可以深入的学习STL和BOOST C++提供的功能。本章和第8章介绍如何定义函数、给函数传递信息以及从函数那里获得信息,本章首先复习函数是如何工作的,然后着重介绍如何使用函数来处理数组、字符串和结构,最后介绍递归和函数指针。如何读者熟悉C语言,将发现本的很多内容是熟悉的。然而,不要因此掉以轻心,产生错误认识。在函数方面,C++在C语言的基础上新增了一些功能,这将在第8章介绍。现在,把注意力放在基础知识上。
来复习下介绍过的有关函数的知识。要使用C++函数,必须完成如下工作:
提供函数定义;
提供函数原型;
调用函数。
库函数是已经定义和编译好的函数,同时可以使用标准库头文件提供其原型,因此只需正确的地调用这种函数即可。本书前面的示例已经多次这样做了。例如,标准C库中有一个 strlen() 函数,可用来确定字符串的长度。相关的标准头文件 csting 包含了 strlen() 和其他一些归纳法字符串相关的函数的原型。这些预备工作使程序员能够在程序中随意使用 strlen() 函数。
然而,创建自己的函数时,必须自行处理这3个方面--定义、提供原型和调用。程序清单7.1用一个简短的示例演示了这3个步骤。
// calling.cpp -- defining, prototyping, and calling a function
#include
using namespace std;
void simple(); // function prototype
int main()
{
cout << "main() will call the simple() function:\n";
simple(); // function call
cout << "main() is finished with the simple() function.\n";
// cin.get();
return 0;
}
// function definition
void simple()
{
cout << "I'm but a simple function.\n";
}
下面是该程序的输出:
执行函数 simple() 时,将暂停执行 main() 中的代码;等函数 simple() 执行完毕后,继续执行 main() 中的代码。
7.11 定义函数
可以将函数分为两类:没有返回值的函数和有返回值的函数。没有返回值的函数称为 void 函数,其通常格式如下:
void functionName(parameterLise)
{
statement(s)
return; // optional <可选择的>
}
其中,parameterList 指定了传递给函数的参数类型和数量,本章后面将更详细地介绍该列表。可选的返回语句标记了函数的结尾;否则,函数将在右花括号处结束。void 函数相当于 pascal 中的过程、FORTRAN 中的子程序和现代 BASIC 中的子程序过程。通常,可以用 void 函数来执行某种操作。例如,将 Chessrs!打印指定次数(n)的函数如下:
void chessre(int n)
{
for (int i = 0; i < n; i++)
cout << "Chessre! " << endl;
}
参数列表 int n 意味着调用函数 Chessre() 时,应将一个 int 类型的值作为参数传递给它。
有返回值的函数将生成一个值,并将它返回给调用函数。换句话来说,如果函数返回 9.0 的平方根(sqrt( 9.0),则该函数调用的值为 3.0。这种函数的类型被声明为返回值类型,其通用格式如下:
typeName functionName(parameterList)
{
statements
return value; // value is type cast to type typeName
}
对于有返回值的函数,必须使用返回语句,以便将值返回给调用函数。值本身可以是常量、变量,也可以是表达式,只是其结果的类型必须为 typeName 类型或可以转换为 typeName (例如,如果声明的返回类型为 double,而函数返回一个 int 类型表达式,则该 int 类型值将被强制转换为 double 类型)。然后,函数将最终的值返回给调用函数。C++对于返回值的类型有一定的限制:不能是数组,但可以是其他任何类型--整数、浮点数、指针,甚至可以是结构和对象!(有趣的是,虽然C++函数不能值返回数组,但可以将数组作为结构或对象组成部分来返回。)
作为一名程序员,并不需要知道函数是如何返回值的,但是对这个问题有所了解将有助于澄清概念。(另外,还有助于与朋友和家人交换意见。)通常,函数通过将返回值复制到指定的CPU寄存器或内存单元中来将其返回。随后,调用程序将棵看该内存单元。返回函数和调用函数必须就该内存单元中存储的数据的类型达到一致。函数原型将返回类型告知调用程序,而函数定义命令被调用函数返回什么类型的数据(参见图7.1)。在原型中提供与定义中相同的信息似乎有些多余,但这样做确实有道理。要让信差从办公室的办公桌上取走一些物品,则向信差和办公中的同事交代自己的意图,将提高信差顺利完成这项工作的概率。
函数在执行返回语句后结束。如果函数包含多条返回语句(例如,它们位于不同的 if else 选项中),则函数在执行遇到的第一条返回语句后结束。例如,在下面的例子中,else 并不是必须的,但可帮助马虎的读者理解程序员的意图:
int bigger(int a, int b)
{
if (a > b)
return a; // if a > b, function terminates here
else
return b; // otherwise, function terminates here
}
如果函数包含多条返回语句,通常认为它会令人迷惑,有些编译器将针对这一点发出警告。然而,这里的代码很简单,很容易理解。
有返回值的函数与 Pascal、FORTRAN 和 BASIC 中的函数相似,它们向调用程序返回一值,然后调用程序可以将其赋给变量、显示或将其用于别的用途。下面是一个简单的例子,函数返回 double 值的立方:
double cube(double x) // x times x timesx < cube: 立方体; 立方形; 立方形的东西(尤指食物); 立方; 三次幂;>
{
return x * x * x; // a type double value
}
例如,函数调用 cube(1, 2) [书中将1.2写成了1,2,怀疑这个是不是盗版的]将返回 1.728 。请注意,上述返回语句使用了一个表达式,函数将计算该表达式的值(这里为 1.728),并将其返回。
至此,读者已熟悉了函数调用,但对函数原型可能不太熟悉,因为它经常隐藏在 include 文件中。程序清单 7.2 在一个程序中使用了函数 cheer() 和 cube() 。请留意其中的函数原型。
程序清单 7.2 protos.cpp
// protos.cpp -- using prototypes and function calls
#include
using namespace std;
void cheers(int); // prototype: no return value
double cube(double x); // prototype: returns a double
int main()
{
cheers(5); // function call
cout << "Give me a number: ";
double side;
cin >> side;
double volume = cube(side); // function call
cout << "A " << side << "-foot cube has a volume of ";
cout << volume << " cubic feet.\n";
cheers(cube(2)); // prototype protection at work
return 0;
}
void cheers(int n)
{
for (int i = 0; i < n; i++)
cout << "Cheers! ";
cout << endl;
}
double cube(double x)
{
return x * x *x;
}
下面是该程序运行情况:
简单补充一下:为何最后会打印出 8 个 cheers!;由于 main() 函数最后一句为 cheers(cube(2));这里会执行括号内的函数cube(2),而调用double cube(double x),此函数,传递的参数为 2;即 double cube(double 2),进入函数体,即是 return 2 * 2 * 2 = 8,而后再执行cheers(cube(2))=cheers(8),因此会打印出 8 个 cheers!
正如前面指出的,读者应将重点放在原型上。那么,应了解有关原型的哪些内容呢?首先,需要知道C+++要求提供原型的原因。其次,由于C++要求提供原型,因此还应知道正确的语法。最后应当感谢原型所做的一切。下面依次介绍这几点,将程序清单7.2作为讨论的基础。
1. 为什么需要原型
原型描述了函数到编译器的接口,也就是说,它将函数返回值的类型(如果有的话)以及参数的类型和数量告诉编译器。例如,原型将如何影响程序清单7.2中下述函数调用:
double volume = cube(side);
首先,原型告诉编译器,cube()有一个 double 参数。如果程序没有提供这样的参数,原型就让编译器能够捕获这种错误。其次,cobe()函数完成计算后,将把返回值放置在指定的位置--可能是CPU寄存器,也可能是内存中。然后调用函数(这里为main())将从这个位置取得返回值。由于原型指出了cube()类型为 double,因此编译器知道应检索多少个字节以及如何解释它们。如果没有这些信息,编译器只能进行猜测。而编译器是不会这样做的。
读者可能还会问,为何编译器需要原型,难道它就不能在文件中进一步查找,以了解函数是如何定义的吗?这种方法的一个总是是效率不高。编译器在搜索文件的剩余部分时将必须停止对 main()的编译。一个更严重的总是是,函数甚至可能并不在文件中。C++允许将一个程序放在多个文件中,单独编译这些文件,然后再将它们组合起来。在这种情况下,编译器在编译 main()时,可能无权访问函数代码。如何函数位于库中,情况也是如此。避免使用函数原型的唯一方法是,在首次使用函数之前定义它,但这并不总是可行的。另外C++的编程风格是将 main()放在最前面,因为它通常提供了程序的整体结构。
2. 原型的语法
函数原型是一条语句,因此必须以分号结束。获得原型最简单的方法是,复制函数定义中的函数头,并添加分号。对于 cube(),程序清单7.2中的程序正是这样做的:
double cube(double x); // add ; to header to get prototype
然而,函数原型不要求提供变量名,有类型列表就足够了。对于 cheer()的原型,该程序只提供了参数类型:
void cheers(int); // okay to drop variable names in prototype <可以在原型中删除变量名>
通常,在原型的参数列表中,可以包括变量名,也可以不包括。原型中的变量名相当于点位符,因此不必与函数定义中的变量名相同。
ANSI C 借鉴了C++中的原型,但这两种语言还是有区别的。其中最重要的区别是,与基本C兼容,ANSI C 中的原型是可选的,但在C++中,原型是必不可少的。例如,下面的函数声明:
void say_hi();
在C++中,括号为空与在括号中使用关键字 void 是等效的--意味着函数没有参数。在 ANSI C 中,括号为空意味着不指出参数--这意味着将在后面定义参数列表。在C++中,不指定参数列应该使用省略号:
void sya_bye(...); // C++ abdication of responsibility
通常,仅当与接受可变参数C函数(如printf())交互时才需要这么做。
3. 原型的功能
原型可以帮助编译器完成许多的工作;但它对程序有什么帮助呢?它们可以极大地降低程序出错的几率。具体来说,原型确保以下几点:
编译器正确处理函数返回值;
编译器检查使用的参数数目是否正确;
编译器检查使用的参数类型是否正确。如果不正确,则转换为正确的类型(如果可能的话)。
前面已经讨论了如何正确处理返回值。下面来看一看参数数目不对时将发生的情况。例如,假设进行了如下调用:
double z = cube();
如果没有函数原型,编译器将通过,当函数被调用时,它将找到 cube() 调用存放值的位置,并使用这里的值。这正是ANSIC从C++借鉴原型之前,C 语言的工作方式。由于对于ANSI C 来说,原型是可选的,因此有些C 语言程序正是这样工作的。但在C++中,原型不是可选的,因此可以确保不会发生这类错误。
接下来,假设提供了一个参数,但其类型不正确。在 C 语言中,这将造成奇怪的错误。例如。如果函数需要一个 int 值 (假设占16位),而程序员传递了一个 double 值(假设占64位),则函数将只检查64位中的前16位,并试图将它们解释为一个 int 值 。但C++彼动将传递的值转换为原型中指定的类型,条件是两者都是算术类型。例如,程序清单 7.2 将能够应付下述语句中两次出现类型不匹配的情况:
cheers(cube(2));
首先,程序将 int 的值 2 传递给 cube(),而后者期望的是 double 类型。编译器注意到,cube()原型指定了一个 double 类型参数,因此将 2 转换为 2.0------一个 double 类型的值。接下来,cube()返回一个 double 值(8.0),这个值被用作 cheer()的参数。编译器将再一次检查原型,并发现cheer()要求一个 int 参数,因此它将返回值转换为整数 8.通常,原型自动将被传递的参数强制转换为期望的类型。(但第 8 章将介绍的函数重载可能导致二义性,因此不允许某些自动强制类型转换。)
自动类型转换并不能避免所有可能的错误。例如,如果将 8.33E27传递给期望一个 int 值的函数,则这样大的值将不能被正确转换为 int 值。当较大的类型被自己转换为较小的类型时,有些编译器将发出警告。指出这可能会丢失数据。
仅当有意义时,原型化才会导致类型转换。例如,原型不会将整数转换为结构或。
在编译阶段进行的原型化被称为静态类型检查(static type checking)。可以看出。静态类型检查可捕获许多在运行阶段非常难以捕获的错误。
下面详细介绍一下函数参数。C++通常按值传递参数,这意味着将数值参数传递给函数,而后者将其赋给一个新的变量。例如,程序清单 7.2 包含下面的函数调用:
double volume = cube(side);
其中,side 是一个变量,在前面的程序运行中,其值为 5。cube()的函数头如下:
double cube(double x)
被调用时,该函数将创建一个新的名为 x 的 double 变量 ,并将其初始化为 5。这样,cube()执行的操作将不会影响 main()中的数据,因为 cube()使用的是 side 的副本,而不是原来的数据。稍后将个实现这种保护的例子。用于接收传递值的变量被称为形参;传递给函数的值被称为实参。出于简化的目的,C++标准使用参数(argument)来表示实参,使用参量(parameter)来表示形参,因此参数传递将参量赋给参数(参见图 7.2)。
在函数中声明的变量(包括参数)是该函数私有的,在函数被调用时,计算机将为这些变量分配内存;在函数结束时,计算机将释放这些变量使用的内存(有些C++文献将分配和释放内存称为创建和毁坏变量,这样似乎更激动人心)。这样的变量被称为局部变量,因为它们被限制在函数中。前面提到过,这样做有助于确保数据的完整性。这还意味着,如果在 main()中声明了一个名为 x 的变量,同时在另一个函数中也声明了一个名为 x 的变量,则它们将是两个完全不同的、毫无关系的变量,这与加利福尼亚州的 Albany与纽约的 Albany 是两个完全不同的地方是一样的道理(参见图 7.3)。这样的变量也被称为自动变量,因为它们是在程序执行过程中自动被分配和释放的。
7.2.1 多个参数
函数可以有多个参数。在调用函数时,只需使用逗号将这些参数分开即可:
n_chars('R', 25);
上述函数调用将两个参数传递给函数 n_chars(),我们将稍后定义该函数。
同样,在定义函数时,也在函数头中使用由逗号分隔的参数声明列表:
void n_chars(char c, int n) // two arguments
该函数头指出,函数 n_char() 接受一个 char 参数和一个 int 参数。传递给函数的值被赋给参数 c 和 n。如果函数的两个参数的类型相同,则必须分别指定每个参数的类型,而不能像声明常规变量那样,将声明组合在一起:
void fifi(float a, float b) // declare each variable separately
void fufu(float a, b) // NOT acceptable
(抽烟的又来了,办公室空间小,开着空调,还抽烟,眼睛熏得痛,开门开空调.........)
和其它函数一样,只需要添加分号就可以得到该函数的原型:
void n_chars(char c, int n); // ptototype ,style 1
和一个参数情况一样,原型中的变量名不必与定义中的变量名相机,而且可以省略:
void n_chars(char, int); // prototype, style 2
然而,提供变量名将使原型更容易理解,尤其是两个参数的类型相同时。这样没哟量名可以提醒参量和参数间的对应关系:
double melon_density(double weight,double volume);
昨天下午突然的肚子痛,去医院检查花了500+,验血、验尿,还有CT,抽血的一小妹妹可能是实习的,扎完左边手没抽到血,又换到右边,针头扭来扭去,那叫一个痛,最后把手还扎乌了,表下出血了。开药吃却花了4元不到,医生说如果吃了三天的药还痛,就直接来医院开刀,阑尾炎
谁知道TMD傍晚吃了四颗药后疼痛就减了好多,今天到现在差不多已经感觉不到痛了,真TMD杯具,昨天没有写代码了,今天继续。
程序 7.3 演示了一个接受两个参数的函数,它还表明,在函数中修改形参的值不会影响调用程序中的数据。
程序清单 7.3 twoarg.cpp
// twoarg.cpp -- a function with 2 arguments
#include
using namespace std;
void n_chars(char, int);
int main()
{
int times;
char ch;
cout << "Enter a character: ";
cin >> ch;
while (ch != 'q')
{
cout << "Enter an integer: ";
cin >> times;
n_chars(ch, times); // function with two arguments
cout << "\nEnter another character or press the q-key to quit: "
cin >> ch;
}
cout << "The value of times is " << times << ".\n";
cout << "Bye\n";
return 0;
}
void n_chars(charc, int n) // displays c n times
{
while (n-- > 0)
cout << c;
}
程序运行结果:
程序说明
程序清单7.3 中的 main() 函数使用一个 while 循环提供重复输入(并让读者温习使用循环的技巧),它使用 cin >> ch,而不是 cin.get(ch) 或 ch = cin.get() 来读取一个字符。这样做是有原因的,在前面读过,这两个 cin.get() 函数读取所有的输入字符,包括空格和换行符,而 cin >> 跳过空格和换行符。当用户对程序提示作出响应时,那也得在每行的最后按 Enter 键,以生成换行符。但当输入下一个字符为数字时,cin.get() 将读取后面的换行符,可以通过编程来避开这种麻烦,但比较简便的方法是像该程序那样使用 cin。(这里消化消化...多看几遍)
n_char() 函数接受两个参数:一个是字符 c,另一个是整数 n。然后,它使用循环来显示该字符,显示次数为 n:
while (n-- > 0)
cout << c;
程序通过将 n 变量递减来计数,其中 n 是参数列表的形参,main()中的 times 变量的值被赋给该变量。然后,while 循环将 n 递减到 0,但前面的运行情况表明,修改 n 的值对 times 没有影响。即使在函数 main()中使用名称 n 而不是 times,在函数 n_chars() 中修改 n 的值时,也不会影响函数 main()中 n 的值。
7.2.2 另外一个接受两个参数的函数
下面创建另一个功能更强大的函数,它执行执行重要的计算任务。另外,该函数将演示局部变量的用法,而不是形参的用法。
目前美国许多州都采用某种纸牌游戏的形式来发行彩票,让参与者从卡片中选择一定数目的选项。例如,从 51个数字中选取 6 个。随后,彩票管理者将随机抽取 6 个数。如果参与者选择的数字与这 6 个完全相同,将赢得大约几百万美元的奖金。我们的函数将计算中将的几率。(是的,能够成功预测获奖号码的函数将更有用,但虽然 C++ 的功能非常强大,目前还不具备超自然能力。)
首先,需要一个公式。假设必须从 51 个数字中选取 6 个,而获奖的机率为 1/R,则 R 的计算公式如下:
选择 6 个数时,分母为前 6 个整数的乘积或 6 的阶乘。分子也是 6 个连续整数的乘积,从 51 开始,依次减1.推而广之,如果从 numbers 个数中选取 picks 个数,而分母是 picks 的阶乘,分子为 numbers 开始向前的 picks 个整数的乘积。可以用 for 循环进行计算:
long double result = 1.0;
for (n = numbers, p = picks; p > 0; n--, p--)
resule = result *n /p;
循环不是首先将所有的分子项相乘,而首先将 1.0 与第一个分子项相乘,然后除以第一个分母项。然后下一轮循环乘以第二个分子项,并除以第二个分母项。这样得到的乘积将比先进行乘法运算得到的小。例如,对于 (10 * 9) / ( 2 * 1) 和 (10 / 2)*(9 / 1),前者将计算 90 / 2,得到 45,后者将计算 5 * 9,得到 45。这两种方法得到的结果相同,但前者的中间值 90 大于后者 5,因子越多,中间值的差别就越大,当数字非常大时,这种交替进行乘除运算的策略可以防止中间结果超出最大的浮点数。
程序清单 7.4 在 probability() 函数中使用了这个公式,由于选择的数目和总数都为正,因此该程序将这些变量声明为 unsigned.int类型(简称 unsigned)。将若干整数相乘可以得到相当大的结果,因此 lotto.cpp 将该函数的返回值声明为 long double 类型。另外,如果使用整形,则像 49 / 6 这样的运算将出现舍入误差。
注意:有些 C++ 实现不运行 long double 类型,如果是这样,可以使用 double 类型。
程序清单 7.4 lotto.cpp
// lotto.cpp -- probability of winning < 获胜概率 >
#include
using namespace std;
// NOte: some implementations require double instead of long double
// 注意:有些实现需要double而不是long double
long double probability(unsigned numbers, unsigned picks);
int main()
{
double total, choices;
cout << "Enter the total number of choices on the game card and\n"
" the number of picks allowed: \n";
while ((cin >> total >> choices) && choices <= total)
{
cout << "You have one chance in ";
cout << probability(botal, choices); // compute the odds
cout << " of winning.\n";
cout << "Next two numbers (q to quie): ";
}
cout << "Bye\n";
return 0;
}
// the following function calculates the probability of picking picks
// numbers correctly from numbers choices
long double probabiliey(unsigned numbers, unsigned picks)
{
long double result = 1.0; // here come some local veriables
long double n;
unsigned p;
for (n = numbers, p = picks; p > 0; n--, p--)
result = result * n / p;
return result;
}
程序运行结果:
先说下昨天写代码的情况,写完代码后在编译器里运行,提示错误不能编译,大概是double __cdecl probability(unsigned int,unsigned int),因为之前代码已经说得很清楚,我的编译器不支持 long double 已经将 long double改成了 double ,还提示错误,难道是编译器不支持 unsigned 么,于是将 unsigned 改成__int16 或 __int32,错误依旧,这是神马情况,于是在网上查,vc6 unsigned之类的关键字,结果还真有人提到vc6 不支持unsigned之类的提问,不过都没有解决我这个问题,后来我把整段的错误提示复制到搜索栏 ”unresolved external symbol "double __cdecl probability(unsigned int,unsigned int)" (?probability@@YANII@Z)“,一搜,于是有回答说出现,如果出现”unresolved external symbol“这个,首先查下拼写错误,于是我再一看,竟然把函数”double probability(unsigned numbers, unsigned picks)“写成了”double probabiliey(unsigned numbers, unsigned picks)“,真是浪费时间。这个是代码写少还是没有仔细看原代码来着。。
程序说明
程序清单 7.4 中的 probability()函数演示了可以在函数中使用的两种局部变量。首先是形参(number 和 picks),这是在左括号前面的函数头中声明的;其次是其他的局部变量(result、n 和 p),它们是在将函数定义括起的括号内声明的。形参和其他局部变量的主要区别是,形参从调用 probability()的函数那里获得自己的值,而其他变量是从函数中获得自己的值。
对于调用的那个函数,刚开始写代码的时候一直没有看懂,为何,那么长的一个公式竟然可以用一个简简单单的 result = result * n / p;就可以解决的,写完代码我就回宿舍了,在路上边走边想,于是想到了之前提到的那个 R 的计算公式为:
在这里可以分解为:R = (51 / 6) * (50 / 5 ) * (49 / 4) * (48 / 3) * (47 / 2) * (46 / 1),这种操作,然后用一个循环,循环条件为分母!=0,然后分母分子每次循环的时候自减1,就是代码中写到的n--,c--;于是就有了这种操作了。这里还忘了还有一个 return result;这段代码还没弄清楚什么意思,因为之前要么用的 return 0,要么是 void 函数根本不用 return 返回了,这里突然来一个return result,我就有点懵了,这还是得用F10或F11大法来解释了,
一开始用F10一步步看,到调用函数的时候换F11进入函数体,按F11直接进入了下面函数体
已经可以看到,第一轮循环后result = 49 / 6 = 8.1666666666666661
中间的循环就不看了,下面是最后一轮循环的图片
这里程序运行到了return result 代码了,继续按 F10 函数调用结束,回到 main()上,
然后可以看到。。
有点明白了,probability(total, choices);调用函数的作用就是先传递两个参数然后计算出 result 的值,并打印出 result 的值,说通俗点就是 result = probability(total.choices);应该是这样了,试下改成 return 0 或注释掉这一行或 return 1;试下
不能注释,此函数不是 void ,现在弄清楚了,调用的函数就是计算 result 的值并打印出来。
到目前为止,本书的函数示例都很简单,参数和返回值的类型都是基本类型。但是,函数是处理更复杂的类型(如数组和结构)的关键。下面来看看如何将数组和函数结合在一起。(这印刷得语句不通...)
假设使用一个数组来记录家庭野餐中每人吃了多少甜饼(每个数组索引都对应一个人,元素值对应于这个人所吃甜饼数量)。现在想知道总数。这很容易,只需使用循环将所有数组元素累积起来即可。将数组元素累加是一项非常常见的任务,因此 设计一个完成这项工作的函数很有意义。这样就不必在每次计算数组总和时都要编写新的循环了。
考虑函数接口所涉及的内容。由于函数计算总数,因此应返回答案。如果不分吃甜饼,则可以让函数的返回类型为 int 。另外,函数需要知道要对哪个数组传递数组长度。这里唯一的新内容是,需要将一个形参声明为数组名。下面来看一看函数头及其其他部分:
int sum_arr(int arr[ ], int n) // arr = array name, n = size
这看起来似乎合理。方括号指出 arr 是一个数组,而方括号为空则表明,可以将任何长度的数组传递给该函数。但实际情况并非如此:arr 实际上并不是数组,而是一个指针!好消息是,在编写函数的其余部分时,可以将 arr 看作是数组。首先,通过一个示例验证这种方法可行,然后看看它为什么可行。
程序清单 7.5 演示如同使用数组名那样使用指针的情况。程序将数组初始化为某些值,并会用 sum_arr()函数计算总数。注意到 sum_arr()函数使用 arr 时,就像是使用数组名一样。
程序清单 7.5 arrfun1.cpp (先到上面还有个return result没弄清楚,我先去上面..)
// arrfun1.cpp -- functions with an array argument
#include
using namespace std;
const int ArSize = 8;
int sum_arr(int arr[], int n); // prototype
int main()
{
int cookies[ ArSize ] = {1, 2, 4, 8, 16, 32, 64, 128}
// sone systems require preceding int with static to
// enable array initialization
int sum = sum_arr(cookies, ArSize);
cout << "Total cookies eaten: " << sum << "\n";
return 0;
}
// return the sum of an integer array
int sum_arr(int arr[], int n)
{
int total = 0;
for (int i = 0; i < n; i++)
total = total + arr[ i ];
return total; // 注意这里又不是 return 0; 根据上次分析这个函数是计算 toral 的值
}
7.3.1 函数如何使用指针来处理数组
在大多数情况下,C++ 和 C 语言一样,也将数组名视为指针。第 4 章介绍过,C++ 将数组名解释为其第一个元素地址:
cookies == &cookies[ 0 ] // array name is address of first enement < 数组名是第一个元素的地址 >
该规则有一些例外。首先,数组声明使用数组来标记存储位置;其次,对数组名使用 sizeof 将得到整数个数组的长度(以字节为单位);第三,正如第 4 章指出的,将地址运算符 & 用于数组时,将返回整个数组的地址,例如 &cookies 将返回一个32 字节内存块的地址(如果 int 长为 4 字节)。
程序清单 7.5 执行下面的函数调用:
int sum = sum_arr(cookies, ArSize);
其中,cookies 是数组名,而根据 C++ 规则,cookies 是其第一个元素的地址,因此函数传递的是地址。由于数组的元素类型为 int,因此 cookies 的类型必须是 int 类型的指针,即 int*。这表明,正确的函数头应该是这样的:
int sum_arr(int *arr, int n) // arr = arr name, n = Size;
其中用 int * arr 替换了 int arr[ ]。这证明这两个函数头都是正确的,因为在 C++ 中,当(且仅当)用于函数头或函数原型中,int * arr 和 int arr[ ] 的含义才是相同的。它们都意味着 arr 是一个 int 指针。然则,数组表示法(int arr[ ])提醒用户,arr 不仅指向 int,还指向 int 数组的第一个 int。当指针指向数组的第一个元素时,本书使用数组表示法dmj当指针指向一个独立的值时,使用指针表示法(int * arr)。别忘了,在其他的上下文中,int * arr 和 int arr[ ] 的含义并不相同。例如,不能在函数体中使用 int tip [ ]来声明指针。
鉴于变量 arr 实际上就是一个指针,函数的其余部分是合理的。第 4 章在介绍动态数组时指出过,同数组名或指针一样,也可以用方括号数组表示法来访问数组元素。无论 arr 是指针还是数组名。表示 arr [ 3] 都指出是数组的第 4 个元素。就目前而言,提醒读者记得下面两个恒等式,将不会有任何坏处:
搞不懂中英文版这里都写错了么,弄得我一头雾水,书上写法如下:
arr [ i ] = *(ar + i) // value in two notations < 两个符号的值 >
&arr [ i ] = ar + i // addresses in two notations < 两个符号中的地址 >
在网上查了下,好多的写法都是
arr [ i ] = *(arr + i) // value in two notations < 两个符号的值 >
&arr [ i ] = arr + i // addresses in two notations < 两个符号中的地址 >
第二种写法明显还有点靠谱点,左边和左右都有相同的数组名和下标,上面的写法是个什么鬼,英文版的都是上面写法,数组名都不一样,光下标相同,等式两边会相同?我就信了你的邪,还是我没有理解透?
记住,将指针(包括数组名)加1,实际上是加上了一个与指针指向的类型的长度(以字节为单位)相等的值。对于遍历数组而言,使用指针加法和数组下村时等效的。
7.3.2 将数组作为参数意味着什么
我们来看一看程序清单 7.5 暗示了什么。函数调用 sum_arr(coolies, ArSize)将cookies 数组第一个元素的地址和数组中的元素数目传递给 sum_arr()函数。sun_arr()函数将 cookies 的地址赋给指针变量 arr,将 ArSize 赋给 int 变量 n。这意味着,程序清单 7.5 实际上并没有将数组内容传递给函数,而是将数组的位置(地址)、包含的元素种类(类型)以及元素数目(n 变量)提交给函数(参见图 7.4)。有了这些信息后,函数便可以使用原来的数组。传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组。实际上,这种区别并不违反C++按值传递的方法,sum_arr()函数仍传递了一个值,这个值被赋给一个新变量,但这个值是一个地址,而不是数组的内容。
指针名与指针对应是好事么?确实是一件好事。将数组地址作为参数可以节省复制整个数组所需的时间内存。如果数组很大,则使用拷贝的系统开销灰常大;程序不似需要更多的计算机内存,还需要花费时间来复制大块的数据。另一方面,使用原始数据破坏数据的风险。在经典的C让不让中,这确实是一个问题,但ANIS C 和 C++ 中的 const 限定符提供了解决这种问题的办法。稍后将介绍一个这样的示例,但先来修改程序清单 7.5,以演示数组函数是如何动作的。程序清单 7.6 青蛙,cookies 和 arr 的值相同。它还演示了指针概念如何使 sum_arr 函数比以前更通用。该程序使用限定符 std::而不是编译指令 using 来提供对 cout 和 endl的访问权。
程序清单 7.6 arrfun2.cpp
// arrfun2.cpp -- functions with an array argument
#include
const int ArSize = 8;
int sum_arr(int arr[], int n);
//use std:: instead of using directive < 而不是使用指令 >
int main()
{
int cookies[ ArSize ] = {1, 2, 4, 8, 16, 32, 64, 128};
// some systems require preceding int with static to
// enable array initialization
std :: cout << cookies << " = array address, ";
// some sytems require a type cast: unsigned (cookies)
std :: cout << sizeof cookies << " = sizeof cookies\n";
int sum = sum_arr(cookies, ArSize);
std :: cout << "Total cookies eaten: " << sum << std :: endl;
// 计算数组中前三个元素的和
sum = sum_arr(cookies, 3); // a lie
std :: cout << "First three eaters ate " << sum << " cookies.\n";
// 计算数组中第 5、6、7、8 四元素的和
sum = sum_arr(cookies + 4, 4); // another lie
std :: cout << "Last four eaters ate " << sum << " cookies.\n";
return 0;
}
// return the sum of an intrger array
int sum_arr(int arr[], int n)
{
int total = 0;
std :: cout << arr << " = arr, ";
// some systems require a type cast: unsigned (arr)
std :: cout << sizeof arr << " = sizeof arr\n";
for (int i = 0; i < n; i++)
total = total + arr[ i ];
return total;
}
程序运行情况为:
注意,地址值和数组长度随系统而异。另外,有些 C++ 实现以十进制而不是十六进制格式显示地址,还有些编译器以十六进制显示地址时,会加上前缀 0x。
程序说明
程序清单 7.6 说明了数组函数的一些有趣的地方。首先,cookies 和 arr 指向同一个地址。但 sizeof cookies 的值为 32,而sizeof arr 为4,这是由于 sizeof cookies 是整个数组的长度,而 sizeof arr 只是指针变量的长度(上述程序运行结果是从一个使用 4字节地址的系统中获得的)。顺便说一句,这也是必须显示传递数组长度,而不能在 sum_arr()中使用 sizeof arr 的原因;指针本身并没有指出数组的长度。
办公室的网是三天两头的出问题,真是醉了……
由于 sum_arr()只能通过第二个参数获知数组中的元素数量,因此可以对函数“说谎”。例如,程序第二次使用该函数时,这样的调用它:
sum = sum_arr(cookies, 3);
通过告诉函数 cookies 有3个元素,可以让它计算前3个元素的总和。
还可以提供假的数组起始位置:
sum = sum_arr(cookies + 4, 4);
由于 cookies 是第一个元素的地址,因此 cookies + 4 是第 5 个元素的地址。这条语句将计算数组第 5、6、7、8个元素的总和。请注意输出中第三次函数调用选择将不同于前两个调用的地址赋给 arr 的。可以使用 &cookies[ 4 ],而不是 cookies + 4 作为参数;它们的含义是相同的。
注意:为将数组类型和元素数量告诉数组处理函数,请通过两个不同的参数来传递它们:
void fillArray(int arr[ ], int size); // prototype
而不要试图使用方括号表示法来传递数组长度:
void fillArray(int arr[ size ]); // NO-- bad prototype < 不行,坏的函数原型 >
7.3.3 更多数组函数示例
选择使用数组来表示数据时,实际上是在进行一次设计方面的决策。但设计决策不仅仅是确定数据的存储方式,还涉及到如何使用数据。程序员常会发现,编写特定的函数来处理特定的数据操作是有好处的(这里讲的好处指的是程序的可靠性更高、修改和高度更为方便)。另外,构思程序时将存储属性与操作结合起来,便是朝 OOP 思想迈进了重要的一步;以后将证明这是很有好处的。
来看一个简单的安全。假设要使用一个数组来记录房地产的价值。在这种情况下,程序员必须确定要使用哪种类型,当然,double 的取值范围比 int 和 long 大,并且提供了足够多的有效位数来精确地表示这些值。接下来必须决定数组元素的数目。(对于使用 new 创建动态数组来说,可以稍后再决定,但我们希望全事情简单一点)。如果房地产数目不超过 5 个,则可以使用一个包含 5 个元素的 double 数组。
现在,考虑要对房地产数组执行的操作。两个基本的操作分别是,将值读入到数组中和显示数组的内容。我们再添加另一个操作:重新评估每种房地产的值。为简单起见,假设所有的房地产都以相同的比率增加或者减少。(别忘了,这是一本关于 C++的书,而不是关于房地产管理的书)接下来,为每项操作编写一个函数,然后编写相应的代码。下面首先介绍这些步骤,然后将其用于一个完整的示例中。
1. 填充数组
由于接受数组名参数的函数是原始数组,而不是其副本,因此可以通过调用该函数将值赋给数组元素。该函数的一个参数是要填充的数组的名称。通常,程序可以管理多个人的投资,因此需要多个数组,因此不能在函数中设置数组长度,而要将数组长度作为第二个参数传递,就像前一个示例那样,另外,用户也可能希望在数组被填满之前停止读取数据,因此需要在函数中建立这种特性。由于用户输入的元素数目可能少于数组的长度,因此函数应返回实际输入的元素数目。因此,该函数的原型如下:
int fill_array(double ar[ ], int limit);
该函数接受两个参数,一个是数组名,另一个指定了要读取的最大元素数;该函数返回实际读取的元素数。例如,如果使用该函数来处理一个包含 5 个元素的数组,则将 5 作为第二个参数。如果只输入 3 个值,则该函数将返回3。
可以使用循环连续地将值读入到数组中,但如何提早结束循环呢?一种方法是,使用一个特殊值来指出输入结束,由于所有的属性都不为负,因此可以使用负数来指出输入结束。另外,该函数应该对错误输入作为反应,如停止输入等,这样,该函数的代码如下所款:
int fill_array(double ar [ ], int limit)
{
using namespace std;
double temp;
int i;
for (i = 0; i < limit; i++)
{
cout <<"Enter value#“ << (i + 1) << ": ";
cin >> temp;
if (!cin) // bad input
{
cin.clear();
while (cin.get() != '\n')
continue;
cout << "Bad input; input process terminated.\n";
break;
}
else if (temp < 0) // signal to terminate < 终止信号 >
break;
ar[ i ] = temp;
}
return i;
}
注意,代码中包含了对用户的提示。如果用户输入的是非负值,则这个值将赋给数组,否则循环结束。如果用户输入的都是有效值,则循环将在读取最大数目的值后结束,循环完成的最后一项工作是将 i 加 1,因此循环结束后,不将比最后一个数组索引大 1,即等于填充的元素数目。然后函数返回这个值。
2. 显示数组及用 const 保护数组
创建显示数组内容的函数很简单。只需将数组名和填充的元素数目传递给函数,然后该函数使用循环来显示每个元素。然而,还有另一个问题----确保显示函数不修改原始数组,除非函数的目的就是修改传递给它的数据,否则应避免发生这种情况。使用普通参数时,这种保护将自动实现,这是由于 C++按值传递数据,而且函数使用数据的副本。然而,接受数组名的函数将使用原始数据,这正是fill_array()函数能够完成基工作的原有。为防止函数无意中修改数组的内容,可在声明形参时使用关键字 const (参见第 3 章):
void show_array(const double ar[ ],int n);
该声明表明,指针 ar 指向的是常量数据。这意味着不能使用 ar 修改该数据,也就是说,可以使用像 ar[ 0 ]这样的值,但不能修改。注意,这并不是意味着原始数组必须是常量,而只是意味着不能在 show_array() 函数中使用 ar 来修改这些数据。因此,show_array() 将数组视为只读数据。假设无意间在 show_array() 函数中执行了如下的操作,从而违反了这种限制:
ar[ 0 ] += 10; // 将数组中第一个元素的值加上 10,再赋给数组中第一个元素
编译器将禁止这种操作。例如,Borland C++ 将给出一条如下所示的错误消息:
Cannot modify a const object in funtion show_array(const double *, int)
其它编译器可能用其它措词来表示其不满。
这条消息提醒用户,C++ 将声明 const double ar[ ] 解释为 const double *ar。因此,该声明实际上是说,ar 指向的是一个常量值。稍后将详细讨论这个问题。下面是 show_array()函数的代码:
void show_array(const double ar[ ], int n)
{
using namespace std;
for (int i = 0; i < n; i++)
{
cout <<"Property#" << (i + 1) << ": $";
cout << ar [ i ] << endl;
}
}
3. 修改数组
在这个例子中,对数组进行的第三项操作是将每个元素与同一个重新评估因子相乘。需要给函数传递 3 个参数:因子、数组和元素数目。该函数不需要返回值,因此其代码如下:
void revalue(double r, double ar[ ], inr n)
{
for (int i = 0; i < n; i++)
ar[ i ] *= r;
}
由于这个函数将修改数组的值,因此在声明 ar 时,不能使用 const。
4. 将上述代码组合起来
至此,根据数组的存储方式(数组)和使用方式(3 个函数)定义了数据的类型,因此可以将它们组合成一个程序。由于已经建立了所有的数组处理工具,因此 main()的编程工作非常简单。该程序检查用户输入的是否是数字,如果不是,则要求用户这么做。余下的大部分编程工作只是让 main()调用前面开发的函数。程序清单 7.7 列出了最终的代码,它将编译指令 using 放在那些需要 iostream 工具的函数中。
程序清单 7.7 arrfun3.cpp
// arrfun3.cpp -- array functions and const
#include
const int Max = 5;
// function prototypes
int fill_array(double ar[], int limit);
void show_array(const double ar[], int n); // don't change data
void revalue(double r, double ar[], int n); // revalue:对…再估价;对… 重新评价;调整(货币)价值
int main()
{
using namespace std;
double properties[ Max ]; // properties:所有物; 财产; 财物; 不动产; 房地产;
int size = fill_array(properties, Max); // 1, 读入输入的数值存储到数组中
show_array(properties, size); // 2, 显示刚才输入的数组
if (size > 0)
{
cout << "Enter revaluation factor: ";
double factor;
while (!(cin >> factor)) // bad input
{
cin.clear();
while (cin.get() != '\n')
continue;
cout << "Bad input; Please enter a number: ";
}
revalue (factor, properties, size); // 3, 重新评估价值
show_array(properties, size); // 4, 再次显示评估后的数组
}
cout << "Dont.\n";
//cin.get(); // 这里两行代码可用可不用,没感觉有很大的作用
//cin.get();
return 0;
}
int fill_array(double ar[], int limit)
{
using namespace std;
double temp;
int i;
for (i = 0; i < limit; i++)
{
cout << "Enter value #" << (i + 1) << ": ";
cin >> temp;
if (!cin) // bad input
{
cin.clear();
while (cin.get() != '\n')
continue;
cout << "Bad input; input process terminated.\n";
break;
}
else if (temp < 0) // signal to terminate
break;
ar[ i ] = temp;
}
return i;
}
// the following function can use, but not alter, < 以下函数可以使用,但不能更改 >
// the array whose address is ar < 地址为ar的数组 >
void show_array(const double ar[], int n)
{
using namespace std;
for (int i = 0; i < n; i++)
{
cout << "Property #" << (i + 1) << ": $";
cout << ar[ i ] << endl;
}
}
// multiplies each element of ar[] by r < 将ar[]的每个元素乘以r >
void revalue(double r, double ar[], int n)
{
for (int i = 0; i < n; i++)
ar[ i ] *= r;
}
这是程序运行的结果:我并不知道为何程序要用上两个cin.get();
刚开始有一段代码没弄清楚,就是
int size = fill_array(properties, Max);
show_array(properties,size);
程序一开始已经声明了 const int Max = 5,那么第一句,int size = fill_array(properties, Max) 应该等于 5,即 size = 5;那么第二句应该可以写成 show_array(properties, Max)了吧,于是我把代码改成了Max,运行代码,当输入 5 个 int 类型的值时,程序没有问题,当我输入少于 5 个 int 类型的值时,问题出来了,程序仍然会打印出 5 个值,但有效的却是我输入的那几个,其余的是随系统生成了,看来这里必须得用 size。
函数 fill_array()指出,当用户输入 5 项房地产值或负值后,将结束输入。第一次运行演示了输入 5 项房地产值的情况,第二次运行演示了输入负值的情况。
5. 程序说明
前面已经讨论了与该示例相关的重要编程细节,因此这里回顾一下整个过程。首先考虑的是通过数据类型和设计适当的函数来处理数据,然后将这些函数组合成一个程序。有时也称为自下而上的程序设计(bottom-up protramming),因此设计过程从组件到整体进行。这种方法非常适合于OPP----它首先强调的是数据表示和操纵。而传统的过程性编程倾向于从上而下的程序设计(top-down programming),首先指定模块化设计方案,然后再研究细节。这两种方法都很有用,最终的产品都是模块化程序。
6. 数组处理函数的常用编写方式
假设要编写一个处理 double 数组的函数。如果该函数要修改数组,其原型可能类似于下面这样:
void f_modify(double ar[ ], int n);
如果函数不修改数组,其原型可能类似于下面这样:
void _f_no_change(const double ar[ ], int n);
当然,在函数原型中可以省略变量名,也可将返回类型指定为类型。这里的要点是,ar 实际上是一个指针,指向传入数组的第一个元素,另外,由于通过参数传递了元素数目,这两个函数都可使用任何长度的数组,只要数组的类型为 double:
double rewards[ 1000 ];
double faults[ 50 ];
..........
f_modify(rewards, 1000);
f_modify(faults, 50);
这种做法是通过传递两个数字(数组地址和元素数)实现的。正如看到的,函数缺少一些有关原始数组的知识;例如,它不能使用 size 来获悉原始数组的长度,而必须依赖于程序员传入正确的元素数。
7.3.4 使用数组区间的函数
正如看到的,对于处理数组的 C++ 函数,必须将数组中的数据种类、数组的起始位置和数组中元素数量提交给它;传统的 C++ 方法是,将 指向数组起始处的指针作为一个参数,将数组长度作为第二个参数(指针指出数组的位置和数据类型),这样便给函数提供了找到所有数据所需的信息。
还有另一种给函数提供所需信息的方法,即指定元素区间(range),这可以通过传递两个指针来完成:一个指针标识数组的开头,另一个指针标识数组的尾部。例如,C++ 标准模板库(STL,将在第 16 章介绍)将区间方法广义化了。STL 方法使用“超尾”概念来指定区间。也就是说,对于数组而言,标识数组结尾的参数将指向最一个元素后面的指针。例如,假设有这样的声明:
double elbuod[ 20 ];
则指针 elbuod (这书上又是什么原因,竟然把 elboud,写成 elbuob,还好我有英文版,这又是一个错误,改之)和 elbuod + 20 定义了区间。首先,数组名 elbuod 指向第一个元素。表达式 elbuod + 19 指向最后一个元素(即 elbuod[ 19 ]),因此,elbuod + 20 指向数组结尾后的一个位置。将区间传递给函数将告诉函数应处理哪些元素。程序清单 7.8 对程序清单 7.6 作了修改,使用两个指针来指向区间。
程序清单 7.8 arrrun4.cpp
// arrfun4.cpp -- functions with an array range
#include
const int ArSize = 8;
int sum_arr(const int * begin, const int * end);
int main()
{
using namespace std;
int cookies [ ArSize ] = {1, 2, 4, 8, 16, 32, 64, 128};
// some systems require preceding int with static to
// enable array initialization
int sum = sum_arr(cookies, cookies + ArSize);
cout << "Total cookies eaten: " << sum << endl;
sum = sum_arr(cookies, cookies + 3); // first 3 elements
cout << "First three eaters ate " << sum << " cookies.\n";
sum = sum_arr(cookies + 4, cookies + 8); // last 4 elements
cout << "Last four eaters ate " << sum << " cookies.\n";
return 0;
}
// return the sum of an integer array
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;
}
下面是程序的输出结果:
程序说明
注意程序清单 7.8 中 sum_array()函数中的 for 循环:
for (pt = begin; pt != end; pt++)
total = total + * pt;
它将 pt 设置为指向要处理的第一个元素(begin 指向的元素)的指针,并将 *pt(元素的值)加入到 total中。然后,循环通过递增操作来更新 pt,使之指向下一个元素。只要 pt 不等于 end,这一过程就将继续下去。当 pt 等于 end 时,它将指向区间中最后一个元素后面的一个位置,此时循环将结束。
其次,注意不同的函数调用是如何指定数组中不同的区间的:
int sum = sum_arr(cookies, cookies + ArSize);
......
sum = sum_arr(cookies, cookies + 3);
......
sum = sum_arr(cookies + 4, cookies + 8);
指针 cookies + ArSize 指向最后一个元素后面的一个位置(数组有ArSize 个元素,因此 cookies [ ArSize -1 ] 是最后一个元素,其地址为 cookies + ArSize -1)。因此区间[cookies, cookies + ArSize]指定的是整个数组,同样,cookies, cookies + 3 指定了前 3 个元素,依此类推。
请注意,根据指向减法规则,在 sum_arr()中,表达式 end - begin 是一个整数值,等于数组中元素数目。
另外,必须按正确的顺序传递指针,因为这里的代码假定 begin 在前面,end 在后面。
7.3.5 指针和 const
将 const 用于指针有一些很微妙的地方(指针看起来总是很微妙),我们来详细探讨一下。可以用两种不同的方式将 const 关键字用于指针。第一种方法是让指针指向一个常量对象,这样可以防止使用该指针来修改所指向的值,第二种方法是将指针本身声明为常量,这样可以防止改变指针指向的位置,下面来看看细节。
首先,声明一个指向常量的指针 pt;
int age = 39;
const int * pt = &age;
该声明指出,pt 指向一个 const int(这里为 39),因此不能使用 pt 来修改这个值。换句话来说,*pt 的值为 const,不能被修改:
* pt += 1; // INVALID because pt points to a const int
cin >> *pt; // INVALID for the same rason
现在来看一个微妙的问题。pt 的声明并不意味着它指向的值实际上就是一个常量,而只是意味着对 pt 而言,这个值是常量。例如,pt 指向 age,而 age 不是 const。可以直接通过 age 变量来修改 age的值,但不能使用 pt 指针来修改它:
* pt = 20; // INVALID because pt points to a const int
age = 20; // VALID because age is not declared to be const
以前我们将常规变量的地址赋给常规指针,而这里将常规变量的地址赋给指向 const 的指针。因此还有两种可能:将 const 变量的地址赋给指向 const 的指针、将 const 的地址赋给常规指针。这两种操作都可行么?第一种方法可行,但第二种方法不可行:
const float g_earth = 9.80;
const float * pe = &g_earth; // VALID
const float g_moon = 1.63;
float * pm = &g_moon; // INVALID
对于第一种情况来说,既不能使用 g_earth 来修改值 9.80,也不能使用 pe 来修改。C++ 禁止第二种情况的原因很简单----如果 g_moon 的地址赋给 pm,则可以使用 pm 来修改 g_moon 的值,这使得g_moon 的 const 状态很荒谬,因此 C++ 禁止将 const 的地址赋给非 const 指针。如果读者非要这么做,可以使用强制类型转换来突破这种限制,详情参阅第 15 章中对运算符 const_cast 的讨论。
如果将指针指向指针,则情况将更复杂。前面讲过,假如涉及的是一级间接关系,则将非 const 指针赋给 const 指针是可以的:
int age = 39; // age++ is a valid operation
int * pd = &age; // *pd = 41 is a valid operation
const int * pt = pd; // * pt = 42 is an invalid operation
然而,进入两级间接关系时,与一级间接关系一样将 const 和非 const 混合的指针赋值方式将不再安全。如果允许这么做,则可以编写这样的代码:
const int ** pp2;
int * pl;
const int n = 13;
pp2 = &pl; // not allowed, but suppose it were
*pp2 = &n; // valid, both const, but sets pl to point at n
* pl = 10; // valid, but changes const n
上述代码将非 const 地址(&pl)赋给了 const 指针(pp2),因此可以使用 pl来修改 const数据。因此,仅当只有一层间接关系(如指针指向基本数据)时,才可以将非 const 地址或指针赋给 construction 指针。
注意:如果数据类型本身并不是指针,则可以将 const 数据或非 const 数据的地址赋给指向 const 的指针,但只能将非 const 数据的地址赋给非 const 指针。
假设有一个由 const 数据组成的数组:
const int mouths[ 12 ] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
则禁止将常量数组的地址赋给非常量指针将意味着不能将数组名作为参数传递给使用非常量形参的函数:
int sum(int arr[ ], int n); // should have been const int arr[ ]
......
int j = sum(mouths, 12): // not allowed
上述函数调用试图将 const 指针(mouths)同级非 const指针(arr),编译器将禁止这种函数调用。
将指针参数声明为指向常量数据的指针有两条理由:
这样可以避免由于无意间修改数据而导致编程错误;
使用 const 使得函数能够处理 const 和非 const 实参,否则将只能接受非 const 数据。
如果条件允许,则应将指针形参声明为指向 const 的指针。
为了说明另一个微妙之处,请看下面的声明:
int age = 39;
const int * pt = &age;
第二个声明中的 const 只能防止修改 pt 指向的值(这里为39),而不能防止修改 pt 的值。也就是说,可以将一个新地址赋给 pt:
int sage = 80;
pt = &sage; // okay to point to another location
但仍然不能使用 pt 来修改它指向的值(现在为80)。
第二种使用 const 的方式使用无法修改指针的值:
int sloth = 3;
const int * ps = &sloth; // a point to const int
int * const finger = &sloth; // a const point to int
在最后一个声明中,关键字 const 的值与以前不同。这种声明格式使用 finger 只能指向 sloth,但允许使用 finger 来修改solth的值,中间的声明不允许使用 ps 来修改 sloth 的值,但允许将 ps 指向另一个位置。简而言之,finger 和 *ps 都是 const,而 *finger 和 ps 不是(参见图 7.5)。
如果愿意,还可以声明指向 const 对象的 const 指针:
double trouble = 2.0E30;
const double * const stick = &trouble;
其中,stick 只能指向 trouble,而 stick 不能用来修改 trouble 的值。简而言之,stick 和 * stick 都是 const。
通常,将指针作为函数参数来传送时,可以使用指向const的指针来保护数据。例如,程序清单 7.5 中的 show_array()的原型:
void show_array(const double ar[ ], int n);
在该声明中使用 const 意味着 show_array() 不能修改传递给它的数组中的值,只要只有一层间接关系,就可以使用这种技术。例如,这里的数组元素是基本类型,但如果它们是指针或指向指针的指针,则不能使用 const。
(这上面说到的东西太多,一时还没办法接受,放到手机里先,时不时的看下)。201909042130
为编写将二维数组作为参数的函数,必须牢记,数组名视为其地址,因此,相应的形参是一个指针,就像一维数组一样,比较难处理的是如何正确地声明指针。例如,假设有下面的代码:
int data[ 3 ][ 4 ] = {{1, 2, 3, 4}, {9, 8, 7, 6}, {2, 4, 6, 8}};
int total = sum(data, 3):
则 sum()的原型是什么样的呢?函数为何将行数 (3) 作为参数,而不(又印掉一个字么?)将列数(4)作为参数?
data 是一个数组名,该数组有 3 个元素。第一个元素本身是一个数组,有 4 个 int 值组成。因此 data 的类型是指向由 4 个 int 组成的数组的指针,因此正确的原型如下:
int sum(int (* ar2)[ 4 ], int size);
其中的括号是必不可少的,因为下面的声明将声明一个由 4 个指向 int 的指针组成的数组,而不是由一个指向 4 个 int 组成的数组的指针;另外,函数参数不能是数组:
int *ar2[ 4 ]
还有另外一种格式,这种格式与上述原型的含义完全相同,但可读性更强:
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 的声明或是内部 for 循环中,列数都是固定的-- 4列。
可以使用数组表示法的原因如下,由于 ar2 指向的数组(它的元素是由 4 个 int 组成的数组)的第一个元素(元素 0),因此表达式 ar2 + r 指向编号为 r 的元素,因此 ar2[ r ] 是编号为 r 的元素。由于该元素本身就是一个由 4 个 int 组成的数组,因此 ar2[r] 是由 4 个int 组成的数组的名称。将下标用于数组名将得到一个数组元素,因此 ar[ r ][ c ]是一个由 4 个 int 组成的数组中的一个元素,是一个 int 值。必须对指针 ar2 执行两次解除引用,才能得到数据。最简单的有一个是使用方括号两次:ar2[ r ][ c ]。然而,如果不考虑难看的话,也可以使用运算符 * 两次:
ar2[ r ][ c ] == *(*(ar2 + r) + c) // 等效
为理解这一点,读者可以从内向外解析各个子表达式的含义:
ar2 // pointer to first row of an array of 4 int < ar2 是指向数组(4 个 int 组成的数组)第一个元素的指针 >
ar2 + r // pointer to row r (an array of 4 int) < 指向编号为 r 的元素 >
*(ar2 + r) // row r(an array of 4 int, hence the name of an array, thus a pointer 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;< 指向r行c列的指针,即 ar2[ r ] + c >
*(*(ar2 + r) + c // value of int number c in row r,i.e., ar2[ r ] [ c ];< 指向r行c列地址的值,即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 is &ghost[ 0 ]
int n2 = strlen(str); // pointer to char
int n3 = strlen("gamboling"); // address of string
可以说是将字符串作为参数来传递,但实际传递的是字符串的第一个字符的地址。这意味着字符串函数原型应将其表示字符串的形参声明为 char *类型。
C-风格字符串与常规 char 数组之间的一个重要区别是,字符串有内置的结束字符(即包含字符,但不以空值字符结尾的 char 数组只是数组,而不是字符串)。这意味着不必将字符串长度作为参数传递给函数,而函数可以使用循环依次检查字符串中的每个字符,直到遇到结尾的空值字符为止。程序清单 7.9演示 了这种方法,使用一个函数来计算特定的字符在字符串中出现的次数。由于该程序不需要处理负数,因此它将计数变量的类型声明为 unsigned int。
// strgfun.cpp -- functions with a string argument
#include
unsigned int ms = c_in_str(mmm, 'm');
unsigned int us = c_in_str(wail, 'u');
cout << ms << " m characters in " << mmm << endl;
cout << us << " u characters in " << wail << endl;
return 0;
}
// this function counts the number of ch characters
// in the string str
unsigned int c_in_str(const char * str, char ch)
{
unsigned int count = 0;
while(*str) // quit when *str is '\0'
{
if (*str == ch)
count++;
str++; // move point to next char
}
return count;
}
下面是该程序的运行结果:
程序说明
由于程序清单 7.9 中的 c_int_str()函数不应修改原始字符串,因此它在声明形参 str 时使用了限定符 const。这样,如果错误地址函数修改了字符串的内容,编译器将捕获这种错误。当然,可以在函数头中使用数组表示法,而不是 str:
unsigned int c_in_str(const char str[ ], char ch) // 也是 OK 的
然而,使用指针表示法提醒读者注意,参数不一定必须是数组名,也可以是其他形式的指针。
该函数本身演示了处理字符串中字符的标准方式:
while (*str)
{
// statements
str++;
}
str 最初指向字符串的第一个字符,因此 *str 表示的是第一个字符。例如,第一次调用该函数后,*str 的值为 m----“minimum”的第一个字符。只要字符不为空值字符(\0),*str 就为非零值,因此循环将继续。在每轮循环结尾处,表达式 str++将指针增加一个字节,使之指向字符串中的下一个字符。最终,str 将指向结尾的空值字符,使得 *str 等于 0 ---- 空值字符的数字编码。从而结束循环。用F10+F11大法如下: 此时 mmm 和 wail的值为
可以 看到此时字符串 str 的值为 "minimum",而 *str 指向字符串的第一个字符 'm',
继续 F10
可 以看到第一次 str++后 字符串的值变成了 “inimun”
此时可以看到 字符串 str 的值为“inimum”,指针*str 指向原字符串“minimum”的第二个字符“i” ,即此时字符串 str 的第一个字符,然后第二次 str++后,
依此就是我们所看到的结果了。
7.5.2 返回C-风格字符串的函数
现在,假设要编写一个返回字符串的函数。虽然函数无法返回一个字符串,但可以返回字符串的地址,这样做的效率更高。例如,程序清单 7.10 定义了一个名为bulidstr()的函数,该函数返回一个指针。该函数接受两个参数:一个字符和一个数字。函数使用 new 创建一个长度与数字参数相等的字符串,然后将每个元素都初始化为该字符。然后,返回指向新字符串的指针。
程序清单 7.10 strgback.cpp
// strgback.cpp -- a function that returns a pointer to char
#include
char * buildstr(char c, int n); // rototype
int main()
{
using namespace std;
int times;
char ch;
cout << "Enter a character: ";
cin >> ch;
cout << "Enter an integer: ";
cin >> times;
char * ps = buildstr(ch, times);
cout << ps << endl;
delete [] ps;
ps = buildstr('+', 20); // reuse pointer
cout << ps << "-DONE-" << ps << endl;
delete [] ps;
return 0;
}
// builds string made of n c characters
char * buildstr(char c, int n)
{
char * pstr = new char[ n + 1 ];
pstr[ n ] = '\0'; // terminate string
while (n-- > 0)
pstr[ n ] = c; // fill rest of string
return pstr;
}
程序运行结果:
说实话,这个程序是完全看不懂,又是指针,又是传参,调用函数倒是比较简单,搞得点信心看下去莫得......
程序说明
要创建包含 n 个字符的字符串,需要能够存储 n + 1 个字符的空间,以便能够存储空值字符。因此,程序清单 7.10 中的函数请求分配 n + 1 个字节的内存来存储该字符串,并将最后一个字节设置为空值字符,然后从后向前对数组进行填充。在程序清单 7.10 中,下面的循环将循环 n 次,直到 n 减少到 0,这将填充 n 个元素:
while (n-- > 0)
pstr[ n ] = c;
在最后一轮循环开始时,n 的值为 1。由于 n--意味着先使用这个值,然后将其递减,因此 while 循环测试条件 将对1 和 0 进行比较,发现测试为 true ,循环继续。测试后,函数将 n 减为 0,因此 pstr[ 0 ]是最后一个被设置为 c 的元素。之所以从后向前(而不是从前向后)填充字符串,是为了避免使用额外的变量。从前向后填充的代码将与下面类似:
int i = 0;
while (i <n)
pstr[ i++ ]
注意,变量 pstr 的作用域为 buildstr 函数内,因此该 函数结束时,pstr(而不是字符串)使用的内存将被释放。但由于函数返回了pstr 的值,因此程序仍可以通过 main()中的指针 ps 来访问新建的字符串。
当该字符串不再需要时,程序清单 7.10 中的程序使用 delete 释放该字符串占用的内存。然后将 ps 指向为下一个字符串分配的内存块,然后释放它们。这种设计(记函数返回一个指针,该指针指向 new 分配的内存)的缺点是,程序员必须记住使用 delete。在第 12 章中,读者将知道 C++ 类如何使用构造函数和析构函数负责为你处理这些细节。
现在将注意力从数组转到结构。为结构编写函数比为数组编写函数要简单得多。虽然结构变量和数组一样,都可以存储多个数据项,但在涉及到函数时,结构变量的行为更接近于基本的单值变量。也就是说,与数组不同,结构将其数据组合成单个实体或数据对象,该实体被视为一个整体。前面讲过,可以将一个结构赋给另一个结构,同样,也可以按值传递结构,就像普通变量那样,在这种情况下,函数将使用原始结构的副本。另外,函数也可以返回结构。与数组名就是数组第一个元素的地址不同的是,结构名只是结构的名称,要获得结构的地址,必须使用取地址运算符&。在 C 语言和 C++ 中,都使用符号 & 来表示地址运算符;另外,C++还使用该运算符来表示引用变量,这将在第 8 章讨论。
使用结构编程时,最直接的方式是像处理基本类型那样来处理结构; 也就是说,将结构作为参数传递,并在需要时将结构用作返回值使用。然而,按值传递结构有一个缺点。如果结构非常大,则复制结构将增加内存要求,降低系统运行的速度。出于这些原因(同时由于最初 C 语言不允许按值传递结构),许多 C 程序倾向于传递结构的地址,然后使用指针来访问结构的内容。C++提供了第三种选择----按引用传递(将在第 8 章介绍)。下面介绍其他两种传递方式。首先介绍传递的返回整个结构。
当结构比较小时,按值传递结构最合理,下面来看两个使用这种技术的示例。第一个例子处理行程时间,有些地图指出。从Thunder Falls 到 Bingo 城需要 3 小时 50 分钟,而从 Bingo 城到 Gotesquo 需要 1 小时 25分钟。对于这种时间,可以使用结构来表示----一个成员表示小时值,另一个成员表示分钟值。将两个时间加起来需要一些技巧,因此可能需要将分钟转换为小时,例如,前面 列出的两个时间的总和为 4 小时 75 分钟,应将它转换为 5 小时 15 分钟。下面开发用于表示时间值的结构,然后再开发一个函数,它接受两个这样的结构为参数,并返回表示参数的和的结构。
定义结构的工作很简单:
struct travel_time
{
int hours;
int mins;
}; // 注意这里的分号表忘了
接下来,看一下返回两个这种结构的总和的 sum()函数的原型。返回值的类型应为 travel_time,两个参数也应为这种类型。因此,原型应如下所示:
travel_time sum(travel_time t1, travel_time t2);
要将两个时间相加,应首先将分钟成员相加,然后通过整数除法(除数为 60)得到小时的值,通过求模运算符(%)得到剩余的分钟数,程序清单 7.11 在 sum()函数中使用了这种计算方式,并使用 show_time()函数显示 travel_time 结构的内容。
程序清单 7.11 travel.cpp
// travel.cpp -- using structures with runctions
#include
struct travel_time
{
int hours;
int mins;
};
const int Mins_per_hr = 60;
travel_time sum(travel_time t1, travel_time t2);
void show_time(travel_time t);
int main()
{
using namespace std;
travel_time day1 = {5, 45}; // 5 hours, 45 minutes
travel_time day2 = {4, 55}; // 4 hours, 55 minutes
travel_time trip = sum(day1, day2);
cout << "Two - day total: ";
show_time(trip);
travel_time day3 = {4, 32};
cout << "Three - day total: ";
show_time(sum(trip, day3));
return 0;
}
travel_time sum(travel_time t1, travel_time t2)
{
travel_time total; // 记得用 int total 声明时一定初始化此 total的值先为0,这里却有点不一样,不用先初始化??
total.mins = (t1.mins + t2.mins) % Mins_per_hr;
total.hours = t1.hours + t2.hours + (t1.mins + t2.mins) / Mins_per_hr;
return total;
}
void show_time(travel_time t)
{
using namespace std;
cout << t.hours << " hours, " << t.mins << " minites\n";
}
看下面的说明,看得头都大了,讲得也是一知半解,其中一句,“sum()函数返回 tralel_time结构”,可是上面代码明明 sum()函数返回的是 total,为何说是返回 travel_time结构? 难道这两个是一个意思?不知道能不能坚持下去,慢慢想下
已经两天没写代码了,昨天晚上加班转固18年大客户到晚上11点半,今天又没希望写代码了。。
其中,tralel_time 就像是一个标准的类型名,可被用来声明变量、函数的返回类型和函数的参数类型。由于 total 和 t1 变量是 travel_time 结构,因此可以对它们使用句点成员运算符。由于 sum()函数返回 travel_time结构(就这一句,上同明明返回的是 tralel_time结构中两元素的和,为何这里却说是返回 travel_time 结构??),因此可以将其用作 show_time()函数的参数。由于默认情况下,C++ 函数按值传递参数,因此函数调用 show_time(sum(trip, day3)) 将执行函数调用 sum(trip, day3),以获得其返回值。然后,show_time()调用 sum()的返回值(而不是函数自身)传递给 show_time()。下面是该程序的输出:
我想了下 show_time(sum(trip,day3))先调用 sum(trip,day3),又由于 trip 是返回 sum(day1,day2),即 day1 和 day2 的和,因此这里 sum(trip,day3)应该是返回 trip 和 day3的和,也就是 day1、day2 和 day3 的和,然后把这三天的和作为参数传递给 show_time(),计算出真正的 time。
7.6.2 另一个处理函数示例
前面介绍的有关函数和 C++ 结构的大部分知识都用于 C++ 类中,因此有必要介绍另一个示例。这次要处理的是空间,而不是时间。具体地说,这个例子将定义两个结构,用于表示两种不同的描述位置的方法,然后开发一个函数,将一种格式转换为另一种格式,并显示结果,这个例子用到的数学知识比前一个要多,但并不需要像学习数学那样学习 C++。
假设要描述屏幕上某点的位置,或地图上某点相对于原点的位置,则一种方法是指出该点相对于原点的水平偏移量和垂直偏移量。传统上数学家使用 x 表示水平偏移量,使用 y 表示垂直偏移量(参见图 7.6 )。x 和 y 一起构成了直角坐标(rectangular coordinates)。可以定义由两个坐标组成的表示位置:
struct rect
{
double x; // horizontal distance frome origin < 与原点的水平距离 >
double y; // vertical distance frome origin < 与原点的垂直距离 >
};
另一种描述点位置的方法是,指出它偏离原点的距离和方向(例如,东偏北 40 度)。传统上,数学家从正水平轴开始按逆时针方向度量角度(参见图 7.7 )。距离和角度一起构成了极坐标(polar coordinates)。可以定义另一个表示这种位置:
struct polar
{
double distance; // distance from origin < 与原点的距离 >
double angle; // direction from origin < 原点方向 >
};
下面来创建一个显示 polar 结构的内容的函数。C++ 库(从 C 语言借鉴而来)中的数学函数假设角度的单位为弧度,因此应以弧度为单位来测量角度。但为了便于显示,我们将弧度值转换为角度值。这意味着需要将弧度值乘以 180/π ----约为 57.29577951。该函数的代码如下:
// show polar coordinates, converting angle to degrees
void show_polar(polar dapos) // 形参的类型为 polar。
{
using namespace std;
const double Red_to_deg = 57.29577591;
cout <<"distance = " << dapos,distance;
cout << ", angle = " << dapos.angle * Red_to_dag;
cout << " degrees\n";
}
请注意,形参的类型为 polar。将一个 polar 结构传递给该函数时,该结构的内容将被复制到 dapos 结构中,函数随后将使用该拷贝完成工作。由于 dapos 是一个结构,因此该函数使用成员运算符句点(参见第 4 章)来标识结构成员。
接下来,让我试着再前进一步,编写一个将直角坐标转换为极坐的函数。该函数接受一个 rect 参数,将返回一个 polar 结构。这需要使用数学库中的函数,因此程序必须包含头文件 cmath (在较旧的系统中为 math.h)。另外,在有些系统中,还必须命令编码器载入数学库(参见第 1 章)。可以根据毕达哥拉斯定理,使用水平和垂直坐标来计算距离:
distance = squt(x * x + y * y)
数学库中的 atan2()函数可根据 x 和 y 的值计算角度:
angle = atan2(y, x)
还有一个 atan()函数,但它不能区别 180度内和之外的角度。在数学函数中,这种不确定性与在生存手册中一样不受人欢迎。
有了这些公式后,便可以这样编写该函数:
// convert rectangular to polar coordinates
polar rect_to_polar(rect xypos)
{
polar answer;
answer.distance = sqrt (xypos.x *xypos.x + xypos.y * xypos.y);
answer.angle = atan2(xypos.y, xypos.x);
return answer; // returns a polar structure < 这里又出现返回一个 polar结构,明明是一个结构里元素的运算 >
}
编写好函数后,程序的其他部分编写起来就非常简单了。程序清单 7.12 列出了程序的代码。
中秋的晚上一个人在黄冈,这几天晚上天天加班,都抽不出时间来写代码了……
// strctrun.cpp -- functions with a structure argument
#include
#include
// structure declarations
struct polar
{
double distance; // distance from origin
double angle; // direction from origin
};
struct rect
{
double x; // borizontal distance from origin
double y; // vertical distance from origin
};
// prototypes
polar rect_to_polar(rect xypos);
void show_polar(polar dapos);
int main()
{
using namespace std;
rect rplace;
polar pplace;
cout << "Enter the x and y values: ";
while (cin >> rplace.x >> rplace.y) // slick use of cin
{
pplace = rect_to_polar(rplace);
show_polar(pplace);
cout << "Next two numbers (q to quit): ";
}
cout << "Done.\n";
return 0;
}
// convert rectangular to polar coordinates
polar rect_to_polar(rect xypos)
{
using namespace std;
polar answer;
answer.distance = sqrt (xypos.x * xypos.x + xypos.y * xypos.y);
answer.angle = atan2(xypos.y, xypos.x);
return answer; // returns a polar structure
}
// show polar coordinates, converting angle to degrees
void show_polar (polar dapos)
{
using namespace std;
const double Rad_to_deg = 57.29577951;
cout << "distance = " << dapos.distance;
cout << ", angle = " << dapos.angle * Rad_to_deg;
cout << " degrees\n";
}
程序说明
程序清单 7.12 中的两个函数已经在前面讨论过了,因此下面复习下该程序如何使用 cin 来控制 while 循环:
while (cin >> rplace.x >> rplace.y)
前面讲过,cin 是 istream 类的一个对象。抽取运算符(>>)被设计成使得 cin >> rplace.x 也是一个 istream 对象。类运算符是使用函数实现的。使用 cin >> rpace.x 时,程序将调用一个函数,该函数返回一个 istream 值。将抽取运算符用于 cin >> rplace.x 对象(就像 cin >> rplace.x >> rplace.y 一样),也将获得一个 istream 对象。因此,整个 while 循环的测试表达式最终结果为 cin ,而 cin 被用于测试表达式中时,将根据输入是否成功,被转换为 bool 值 true 或 false。例如,在程序清单 7.12 中的循环中,cin 期望用户输入两个数字,如果用户输入了 q (前面的输出示例就是这样做的,其实只要输入非数字即可),cin >> 将知道 q 不是数字,从而将 q 留在输入队列中,并返回一个将被转换为 false(又有一个输入错误,看下英文版是不是也输入错了)的值,导致循环结束。
请将这种读取数字的方法与下面更为简单的方法进行比较:
for (int i = 0; i < limit; i++)
{
cout << "Enter value #" <<(i + 1) << ": ";
cin >> temp;
if (temp < 0)
break;
ar[ i ] = temp;
}
要提早结束该循环,可以输入一个负值。这将输入限制为非负值。这种限制符合某些程序的需要,但通常需要一种不会将某些值排除在外的、终止循环的方式。将 cin >> 用作测试条件消除了这种限制,因为它接受任何有效的数字输入。在需要使用循环来输入数字时,别忘了考虑使用这种方式。另外请记住,非数字输入将设置一个错误条件,禁止进一步读取输入。如果程序在输入循环后还需要进行输入,则必须使用 cin.clear()重置输入,然后还可能需要通过读取不合法的输入来丢弃它们。程序清单 7.7 演示了这些技术。
题外语:表格 excel 也支持上例中的 atan2() 函数,不过有点不一样的就是,atan2()里面的两个参数,如果要得到程序清单 7.12 中的 angle = 53.1301,则 atan2(xypos.x, xypos.y)要这样传参,否则 angle = 36.8699。
7.6.3 传递结构的地址
假设要传递结构的地址而不是整个结构以节省时间和空间,则需要重新编写前面的函数,使用指向结构的指针。首先来看一看如何重新编写 show_polar()函数。需要修改三个地方:
调用函数时,将结构的址(&pplace)而不是结构本身(pplace)传递给它;
将形参声明为指向 polar 的指针,即 polar *类型。由于函数不应该修改结构,因此使用了 const 修饰符;
由于形参是指针而不是结构,因此应该间接成员运算符(->),而不是成员运算符(句点)。
完成上述修改后,该函数如下所示:
// show polar coordinates,converting angle to degrees
void show_polar (const polar * pda)
{
using namespace std;
const double Red_to_deg = 57.29577951;
cout << "distance = " << pda -> distacne;
cout << ", angle = " << pda -> angle * Rad_to_deg;
cout << " degrees\n";
}
接下来对 rec_to_polar 进行修改。由于原来的 rect_to_polar 函数返回一个结构,因此修改工作更复杂些。为了充分利用指针的效率,应使用指针,而不是返回值。为此,需要将两个指针传递给该函数。第一个指针指向要转换的结构,第二个指针指向存储转换结果的结构。函数不返回一个新的结构,而是修改调用函数中已有的结构。因此,虽然第一个参数是 const 指针,但第二个参数却不是。也可以像修改函数 show_polar()修改这个函数。程序清单 7.13列出了修改后的程序。
程序清单 7.13 strctptr.cpp
// strctptr.cpp -- functions with pointer to structure arguments
#include
#include
// structure templates
struct polar
{
double distance; // distance from origin
double angle; // direction from origin
};
struct rect
{
double x; // horizontal distance from origin
double y; // vertical distance from origin
};
// prototypes
void rect_to_polar (const rect * pxy, polar * pda);
void show_polar (const polar * pda);
int main()
{
using namespace std;
rect rplace;
polar pplace;
cout << "Enter the x and y value: ";
while (cin >> rplace.x >> rplace.y)
{
rect_to_polar(&rplace, &pplace); // pass addresses
show_polar(&pplace);
cout << "Next two numbers (q to quit): ";
}
cout << "Done.\n";
return 0;
}
// show polar coordinates, converting angle to degrees
void show_polar (const polar * pda)
{
using namespace std;
const double Rad_to_deg = 57.29577951;
cout << "distance = " << pda -> distance;
cout << ", angle = " << pda -> angle * Rad_to_deg;
cout << " degrees\n";
}
// convert rectangular to polar coordinates
void rect_to_polar (const rect * pxy, polar * pda)
{
using namespace std;
pda -> distance = sqrt(pxy -> x * pxy -> x + pxy -> y * pxy -> y);
pda -> angle = atan2(pxy -> y, pxy -> x);
}
从用户的角度来说,程序清单 7.13 的行为与程序清单 7.12 相同。它们之间的差别在于,程序清单 7.12 使用的是结构的副本,而程序清单 7.13 使用的是指针,让函数能够对原始结构进行操作。下面是程序运行情况:终于写这么长的代码(对于我来说)一次成功了,没有出现语法错误
我了个去,都快一周没有学习了,这回家天天晚上加班,这也是醉了.....不说了,继续
虽然 C-风格字符串和 string 对象的用途几乎相同,但与数组相比,string 对象与结构的更相似。例如,可以将一个结构赋给另一个结构,也可以将一个对象赋给另一个对象。可以将结构作为完整的实体传递给函数,也可以将对象作为完整的实体进行传递。如果需要多个字符串,可以声明一个 string 对象数组,而不是二维 char 数组。
程序清单 7.14 提供了一个小型示例,它声明了一个 string 对象数组,并将该数组传递给一个函数以显示其内容。
程序清单 7.14 topfive.cpp
// topfive.cpp -- handling an array of string objects
#include
#include
using namespace std;
const int SIZE 5;
void display(const string sa[], int n);
int main()
{
string lise[ SIZE ]; // an array holeing 5 string object
cout << "Enter your " << SIZE << " favorite adtronomical sights:\n";
for (int i = 0; i < SIZE; i++)
{
cout << i + 1 << ": ";
getline(cin.list[ i ];
}
cout << " Your list:\n";
display(list, SIZE);
return 0;
}
void display(const string sa[], int n)
{
for (int i = 0; i < n; i++)
cout << i + 1 << ": " << sa[ i ] << endl;
}
对于该示例,需要指出的一点是,除函数 getling()外,该程序像对待内置类型(如 int)一样对待 string 对象。如果需要 string 数组,只需使用通常的数组声明格式即可:
string list[ SIZE ]; // an array holeing 5 string object
这样,数组 list 的每个元素都是一个 string 对象,可能像下面这样使用它:
getling(cin, list[ i ]);
同样,形参 sa 是一个指向 string 对象的指针,因此 sa[ i ] 就一个 string 对象,可以像下面这样使用它:
cout << i + 1 << ": " << sa[ i ] << endl;
在 C++中,类对象是基于结构的,因此结构编程方面的有些考虑因素也适用于类。例如,可按值将对象传递给函数,在这种情况下,函数处理的是原始对象的副本。另外,也可传递指向对的指针,这让函数能够操作原始对象。下面来看一个使用 C++模板类 array 的例子。
假设您要使用一个array 对象来存储一年四个季度的开支:
std::array
本书前面说过,要使用 array 类,需要包含头文件 array,而名称 array 位于名称空间 std 中。如果函数来显示 expenses 的内容,可按值传递 expenses:
show(expenses);
但如果函数需要修改对象 expenses,则需将该对象的地址传递给函数(下一章将讨论另一种方法-----使得引用):
这与程序清单 7.13 处理结构时使用的方法相同。
如何声明这两个函数呢?expenses的类型为 array
void show(std::array
void fill(std::array
这些考虑因素是这个示例程序的核心。该程序还包含其它一些功能。首先,它用符号常量替换了 4:
const int Seasons = 4;
其次,它使用了一个 const array 对象,该对你包含 4 个 string 对象,用于表示几个季度:
const std::array
请注意,模板 array 并非只能存储基本数据类型,它还可存储类对象。程序清单 7.15 列出了该程序的完整代码。
程序清单 7.15 arrobj.cpp
// arrobj.cpp -- functions with array objects (C++11)
#include
#include
#include
// constant data
const int Seasons = 4;
const std::array Snames = {"Spring", "Summer", "Fall", "Winter"};
// function to modify array object
void fill(std:: array * pa);
// function that uses array object without modifying it
void show(std:: array da;
int main()
{
std:: array expenses;
fill(&expenses);
show(expenses);
return 0;
}
void fill(std :: array *pa)
{
using namespace std;
for (int i = 0; i < Seasons; i++)
{
cout << "Enter " << Snames[ i ] << " expenses: ";
cin >> (* pa) [ i ];
}
}
void show(std :: array da)
{
using namespace std;
double total = 0.0;
cout << "\nEXPENSES\N";
for (int i = 0; i < Seasons; i++)
{
cout << Snames[ i ] << ": $" << da[ i ] << endl;
total += da[ i ];
}
cout << "Total Expenses: $" << total << endl;
}
程序说明
由于 const array 对象 Snames 是在所有函数之前声明的,因此(这句肯定又是掉了字了)可以在后面的任何函数定义中使用它。与 const Seasons 一样,Snames 也有整个源代码文件共享。这个程序没有使用编译指令 using.因此必须使用 std::限定array 和 string。为简化程序,并将重点放在函数可如何使用对象上,函数 fill()没有检查输入是否有效。
函数 fill()和 show()都有缺点。函数 show()存在的问题是,expenses 存储了四个 double 值,而创建一个新对象将将 expenses 的值复制到其中的效率太低,如果修改该程序,使其处理每月甚至每日的开支,这种情况将更严重。
函数 fill()使用指针来直接处理原始对象,这避免了上述效率低下的问题,但代价是代码看起来更复杂:
fill(&expenses); // don't forget the &
......
cin >> ( * pa) [ i ];
在最后一条语句中,pa 是一个指向 array
使用第 8 章 将讨论的引用可解决效率和表示法两方面的问题。
如果递归函数调用自己,则被调用的函数也将调用自己,这将无限循环下支,除非代码中包含终止调用链的内容。通常的方法将递归调用放在 if 语句中。例如,void 类型的递归 recurs()的代码如下:
void recurs(argumentlist)
{
staements1
if (test)
recurs(arguments)
staements2
}
test最终将为 false,调用链将断开。
递归调用将导致一系列有趣的事件。只要 if 语句为 true,每个 recurs()调用将执行 statements1,然后再调用recurs(),而不针执行statements2。当 if 语句为 false 时,当前调用将执行 statements2。当前调用结束后,程序控制权将返回给调用它的recurs(),而该recurs()将执行其stataments2部分,然后结束,并将控制权返回给前一个调用,依此类推。因此,如果recurs()进行了5次递归调用,则第一个 statements1 部分将按函数调用 的顺序执行 5 次,然后 statements2 部分将以与函数调用相反的顺序执行 5 次。进入 5 层递归后,程序将沿进入的路径返回。程序清单7.16 演示了这种行为。
// recur.cpp -- using recursion
#include
void countdown(int n);
int main()
{
countdown(4); // call the recursive function
return 0;
}
void countdown(int n)
{
using namespace std;
cout << "Counting down ..." << n << endl;
if (n > 0)
countdown(n - 1); // function calls itself
cout << n << ": Kabomm!\n";
}
下面是该程序的输出:
注意,每个递归调用都创建自己的一套变量,因此当程序到达第 5 次调用时,将有 5 个独立的 n 变量,其中每个变量的值都不同。为验证这一点,我们可以修改程序清单 7.16,使之显示 n 的地址和值:
cont << "Counting down ... " << n << " (n at " << &n << ")" << endl;
...
cout << n << ": Kaboom!" << " (n at " << &n << ")" << endl;
经过上述修改后,程序输出类似下面:
注意,在一个内存单元(内存地址为0012FE30),存储的 n 值为 4;在另一个内存单元(内存地址为0012FED8),存储 n 的值为 3;等等。另外,注意到在 Counting down 阶段和 Kaboom 阶段的相同层级,n 的地址相同。
7.92 包含多个递归调用的递归
在需要将一项工作不断分为两项较小的、类似的工作时,递归非常有用。例如,请考虑使用这种方法来绘制标尺的情况。标出两端,找到中点并将其标出。然后将同样的操作用于标尺的左半部分和右半部分。如果要进一点细分,可将同样的操作用于当前的每一部分。递归方法有时被称为分而治之策略(divide-and-conquer strategy)。程序 7.17 使用递归函数 subdivide()演示了这种方法,该函数使用一个字符串,该字符串除两端为 | 字符外,其他全部为空格。main 函数使用循环调用 subdivede()函数 6 次,每次递归层编号加 1,并打印得到的字符串。这样,每行输出表示一层递归。该程序使用限定符 std::而不是编译指令 using,以提醒读者还可以采取这种方式。
程序清单 7.17 ruler.cpp
// ruler.cpp -- using recursion to subdivide a ruler
#include
const int Len = 66;
const int Divs = 6;
void subdivide(char ar[], int low, int high, int level);
int main()
{
char ruler[ Len ];
int i;
for (i = 1; i < Len - 2; i++)
ruler[ i ] = ' ';
ruler[ Len - 1 ] = '\0';
int max = Len - 2;
int min = 0;
ruler[ min ] = ruler[ max ] = '|';
std :: cout << ruler << std :: endl;
for (i = 1; i <= Divs; i++)
{
subdivide(ruler, min, max, i);
std :: cout << ruler << std :: endl;
for (int j = 1; j <= Len - 2; j++)
ruler[ j ] = ' '; // reset to blank ruler
}
return 0;
}
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.17 中,subdivide()函数使用变量 level 来控制递归层。函数调用自身时将把 level 减 1,当 level 为 0 时,该函数将不再调用自己。注意,subdivide()调用自己两次,一次针对左半部分,另一次针对右半部分。最初的中点被用作一次调用的右端点和别一次调用的左端点。请注意,调用次数呈几何级数增长。也就是说,调用一次导致两个调用,然后导致 4 个调用,再导致 8 个调用,依此类推。这就是 6 层调用能够填充 64个元素的原因(2^6=64)。这将不断导致函数调用函数(以及存储的变量数)翻倍,因此如果要求的递归层次很多,这种递归方式将是一种糟糕的选择;然而,如果递归屋次较少,这将是一种精致而简单的选择。(题外语:说实话这个例子是完全摸不着头脑,尽管是F10+F11大法,仍然是一懵B!,先过吧)
如果未提到函数指针,则对 C 或 C++ 函数的讨论将是不完整的。我们将大致介绍一下这个主题,将完整的介绍留给更高级的图书。(重启,打字都有些卡...)
与数据项相似,函数也有地址。函数的地址是存储其机器语言代码的内存的开始地址。通常,这些地址对用户而言,既不重要,也没有什么用处,但对程序而言,却很有用。例如,可以编写将另一个函数的地址作为参数的函数。这样第一个函数能够找到第二个函数,并运行它。与直接调用另一个函数相比,这种方法很笨拙,但它允许在不面的时间传递不同的函数地址,这意味着可以在不同的时间使用不同的函数。
7.10.1 函数指针的基础知识
首先通过一个例子来阐释这一过程。假设要设计一个名为 estimate()的函数,估算编写指定行数的代码所需的时间,并且希望不同的程序员都将使用该函数。对于所有的用户来说,estimate()中一部分代码都是相同的,但该函数允许每个程序员提供自己的算法来估算时间。为实现这种目标,采用的机制是,将程序员要使用的算法函数的地址传递给 estimate()。为此,必须能够完成下面的工作:
获取函数的地址;
声明一个函数指针;
使用函数指针来调用函数。
1. 获取函数的地址
获取函数的地址很简单:只要使用函数名(后面不跟参数)即可。也就是说,如果 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()函数。
2. 声明函数指针
声明指向某种数据类型的指针时,必须指定指针指向的类型。同样,声明指向函数的指针时,也必须指定指针指向的函数类型。这意味着声明指定函数的返回类型以及函数的特征(参数列表)。也就是说,声明应像函数原型那样指出有关函数的信息。例如,假设 Pam leCoder编写了一个估算时间的函数,其原型如下:
double pan(int); // prototype
则正确的指针类型声明如下:
double (*pf)(int); // pf points to a function that takes
// ont int argument and that
// returns type double
这与 pan()声明类似,这是将 pan 替换为了 (*pf)。由于 pan 是函数,因此(*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 ponter-to-double
正确地声明 pf 后,便可以将相应函数的地址赋给它:
double pan(int);
double (*pf)(int);
pf = pam; // pf now points to the pam() function
注意,pam()特征标和返回类型必须与 pf 相同。如果不相同,编译器将拒绝这种赋值:
double ned(double);
int ted(int);
double(*pf)(int);
pf = ned; // invalid -- mismatched signature < 无效 -- 特征标不匹配 >
pf = ted; // invalid -- mismatched return types < 无效 -- 返回类型不匹配 >
现在回过头来看一下前面提到的 estimate()函数。假设要将将要编写的代码行数和估算算法(如 pam()函数)的地址传递给它,则其原型将如下:
void estimate(int lines, double(*pf)(int));
上述声明指出,第二个参数是一个函数指针,它指向的函数接受一个 int 参数,并返回一个 double 值。要让 estimate()使用 pam()函数,需要将 pam()的地址传递给它:
extimate(50, pam); // function call telling estimate() to use pam()
显然,使用函数指针时,比较棘手的是编写原型,而传递地址则灰常简单。
(节日很无聊,不知道干嘛,还是写写代码吧...)
3. 使用指针来调用函数
现在进入最后一步,即使用指针来调用被指向的函数。线索来自指针声明。前面讲过,(*pf)扮演的角色与函数名相同,因此使用(*pf)时,只需将它看作函数即可:
double pam(int);
double (*pf)(int);
pf = pam; // pf now points to thd pam() function
double x = pam(4); // call pam() using the function name
double y = (*pf)(5);// call pam() using the pointer pf
实际上,C++ 也允许像使用函数名那样使用 pf:
double y = pf(5); // also call pam() using the pointer pf
第一种格式虽然不太好看,但它给出了强有力的提示 -- 代码正在使用函数指针。
历史与逻辑
真是灰常棒的语法!为何 pf 和 (*pf)等价呢?一种学派认为,由于 pf 是函数指针,而 *pf 是函数,因此应将(*pf)()用作函数调用。另一种学派认为,由于函数名是指向该函数的指针,指向函数的指针的行为应与函数名相似,因此应将 pf()用作函数调用使用。C++进行了折衷 --这 2 种方式都是正确的,或者至少是允许的,虽然它们在逻辑上是互相冲突的。在认为这种折衷粗糙之前,应该想到,容忍逻辑上无法自圆其说的观点正是人类思维活动的特别。
7.10.2 函数指针示例
程序清单 7.18 演示了如何使用函数指针。它两次调用 estimate()函数,一次传递 betsy()函数的地址,另一次传递 pam()函数的地址。在第一种情况下,estimate()使用 betsy()计算所需的小时数;在第二种情况下,estimate()使用 pam()进行计算。这种设计有助于今后的程序开发。当 Ralph 为估算时间而开发自己的算法时,将不需要重新编写 estimate()。相反,他只需提供自己的 Ralph()函数,并确保该函数的特征标和返回类型正确即可。当然,重新编写 estimate()也并不是一件灰常困难的工作,但同样的原则也适用于更复杂的代码。另外,函数指针方式使得 Ralph 能够修改 estimate()的行为,虽然他接触不到 estimate()的源代码。
程序清单 7.18 fun_ptr.cpp
// fun_ptr.cpp -- pointers to functions
#include
double betsy(int);
double pam(int);
// second argument is pointer to a type double function that
// takes a type int argument
void estimate(int lines, double (*pf)(int));
int main()
{
using namespace std;
int code;
cout << "How many lines of code do you need? ";
cin >> code;
cout << "Here's Betsy's estimate:\n";
estimate(code, betsy);
cout << "Here's Pam's estimate:\n";
estimate(code, pam);
return 0;
}
double betsy(int lns)
{
return 0.05 * lns;
}
double pam(int lns)
{
return 0.03 * lns + 0.0004 * lns * lns;
}
void estimate(int lines, double (*pf)(int))
{
using namespace std;
cout << lines << " lines will take ";
cout << (*pf)(lines) << " hour(s)\n";
}
7.10.3 深入探讨函数指针
函数指针的表示可能非常恐怖。下面通过一个示例演示使用函数指针时面临的一些挑战。首先,下面是一些函数的原型,它们的特征标和返回类型相同:
const double * f1(const double ar[ ], int n);
const double * f2(const double [ ], int);
const double * f3(const double *, int);
这些函数的特征标看似不同,但实际上相同。首先,前面说过,在函数原型中,参数列表 const double ar[ ] 与 const double * ar 的含义完全相同。其次,在函数原型中,可以省略标识符。因此,const double ar [ ]可简化为 const double [ ],而 const double * ar 可简化为 const double *。因此,上述所有函数特征标的含义都相同。另一方面,函数定义必须提供标识符,因此需要使用 const double ar [ ]或 const double * ar。
接下来,假设要声明一个指针。它可指向这三个函数之一。假定该指针名为 pa,则只需将目标函数原型中的函数名替换为(*pa):
const double * (*p1)(const double *, int);
可在声明的同时进行初始化:
const double * (*p1)(const double *, int) = f1;
使用 C++ 的自动类型推断功能时,代码要简单得多:
auto p2 = f2; // C++ automatic type deduction
现在来看下面的语句:
cont << (*p1)(av, 3) << ": " << *(*p1)(av, 3) << endl;
cout << p2(av, 3) << ": " << *p2(av, 3) << endl;
根据前面介绍的知识可知,(*p1)(av,3)和 p2(av, 3)都调用指向的函数(这里为f1()和 f2()),交将 av 和 3 作为参数。因此,显示的是这两个函数的返回值。返回值的类型为 const double *(即 double 值的地址),因此在每条 cont 语句中,前半部分显示的都是一个 double 值的地址。为查看存储在这些地址处的实际值,需要将运算符 * 应用于这些地址,如表达式 *(*p1)(av,3)和 *p2(av,3)所示。
鉴于需要使用三个函数,如果有一个函数指针数组将很方便。这样,将可使用 for 循环通过指针依次调用每个函数,以指出这是一个包含三个函数指针的数组。问题是在什么地方加上 [ 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++ automatic type deduction
显示这种声明应类似于 pa 的声明,但由于增加了一层间接,因此需要在某个地方添加一个 *。具体地说,如果这个指针名为 pd,则需要指出它是一个指针,而不是数组。这意味着声明的核心部分应为(*pd)[ 3 ],其中括号让标识符 pd 与 * 先结合 :
*pd [ 3 ] // an array of 3 pointers < 由三个指针组成的数组 >
(*pd)[ 3 ] // a pointer to an array of 3 elements < 指向由3个元素组成的数组的指针 >
换句话来说,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后面一个 12 字节 内存块的地址(这里假设地址为 4 字节)。另一个差别是,要得到第一个元素的值,只需要对 pa 解除一次引用,但需要对 &pa 解除两次引用:
**&pa == *pa ==pa[ 0 ]
程序清单 7.19 使用了这里讨论的知识。出于演示的目的,函数 f1()等都非常简单。下如注释指出的,这个程序演示了 auto 的 C++98 替代品。
程序清单 7.19 arfupt.cpp
// arfupt.cpp -- an array of runction 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; // C++11 automatic type deduction
//pre-C++11 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 element of pa
auto pb = pa;
// pre - C++11 can use the following code instead
cout << "\nUsing an array of pointers to runctions:\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 << " Addess 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 can 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 declare 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;
// cin.get();
return 0;
}
// sone 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;
}
抄代码都抄了20+分钟,基本上是完全抄代码的,完全的看不懂是什么意思,记得上次抄代码的时候说那次是第一次写这么长的代码,和这个比起来真是没得比,而且这个难度相当的大,什么时候我自己也能写这样的代码啊?? 这代码提示要 C++11,看来要使用 VS2010了,看这次抄这么一大段代码有没有抄错...错误还是有几处,一是有一行少个“)”,二是把 pdb 写成 pda,三是少写一个“*”,调整代码后输出结果如下:
显示的地址为数组 av 中 double 值的存储位置。
这个示例可能看起来比较深奥,但指向函数指针数组的指针并不少见。实际上,类的虚方法实现通常都采用了这种技术(参见第 13 章)。所幸的是,这些细节由编译器处理。
C++11 的目标之一是让 C++ 更容易使用,从而让程序员将主要精力放在设计而不是细节上。程序清单 7.19 演示了这一点:
auto pc = &pa;
const double *(*(*pd)[ 3 ])(const double *, int) = &pa;
自动类型推断功能表明, 编译器的角色发生了改变,在 C++98 中,编译器利用其知识帮助您发现错误,而在 c++11 中,编译器利用其知识帮助您进行正确的声明。
存在一个潜在的缺点。自动类型推断确保变量的类型与赋给它的初值的类型一致,但您提供的初值的类型可能 不对:
auto pc = *pa; // oops! used *pa instead of &pa
上述声明导致 pc 的类型与 *pa 一致,在程序清单 7.19中,后面使用它时假定其类型与 &pa 相同,这将导致编译错误。
除 auto 外,C++ 还提供了其化简化声明的工具。您可能还记得,第 5 章说过,关键字 typedef 让您能够创建类型别名:
eypedef double real; // makes real anohter name for double
这里采用的方法是,将别名当做标识符进行声明,并在开关使用关键字 typedef。因此,可将 p_fun 声明为程序清单 7.19 使用的函数指针类型的别名:
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 function ponters
p_fun (*pd)[ 3 ] = &pa; // pd points to an array of 3 function pointers
使用 typedef 可减少输入量,让您编写代码时不容易犯错,并让程序更容易理解。
C 语言提供的 typedef 关键字,可以使用它来为类型取一个新名字,注意红字的字,是 为类型 取一个新名字,那么看清楚了后,这样就比较通俗易懂了。
下面实例为 单字节类型 定义了一个新名字 BYTE:
typedef unsigned char BYTE;
在这个类型定义之后,标识符 BYTE 可作为类型名 unsigned char的缩写,例如:
BYTE b1, b2; // 即等效于 unsigned b1, b2;
typedef 常给结构体类型命名
#include
#include
using namespace std;
typedef struct Book
{
char * name;
int id;
} Book;
int main()
{
Book book; // Book 命名了我们的类型
book.name = "我的C++教程";
book.id = 12345;
cout << "书标题: " << book.name << endl;
cout << "书ID: " << book.id << endl;
return 0;
}
示例中的代码 Book book;根本看不出来效果,用关键字 typedef 用不用作用都一样,如下,于是我把重新定义的名字换了一个,于是我们就能看得出来效果了。
函数是 C++ 的编程模块。要使用函数,必须提供定义和原型,并调用该函数。函数定义是实现函数功能的代码;函数原型描述了函数的接口;传递给函数的值的数目和种类以及函数的返回类型。函数调用使得程序将参数传递给函数,并执行函数的代码。
在默认情况下,C++ 函数按值传递参数。这意味着函数定义中的形参是新的变量,它们被初始化为函数调用所提供的值。因此,C++ 函数通过使用拷贝,保护了原始数据的完整性。
C++ 将数组名参数视为数组第一个元素的地址。从技术上讲,这仍然是按值传递的,因为指针是原始地址的拷贝,但函数将使用指针来访问原始数组的内容。当且仅当声明函数的形参时,下面两个声明才是等价的:
typeName arr[ ];
typeName * arr;
这两个声明都表明,arr 是指向 typeName 的指针,但在编写函数代码时,可以像使用数组名那个使用 arr 来访问元素: arr[ i ]。即使在传递指针时,也可以将形参声明为 const 指针,来保护原始数据的完整性。由于传递数据的地址时,并不会传输有关数组长度的信息,因此通常将数组长度作为独立的参数来传递。另外,也可传递两个指针(其中一个指向数组开头,另一个指向数组末尾的下一个元素),以指定一个范围,就像 STL 使用的算法一样。
C++ 提供了 3 种表示 C- 风格字符串的方法:字符数组、字符串常量和字符串指针。它们的类型都是 char*(char指针),因此被作为 char* 类型参数传递给函数。C++使用空值字符(\0)来结束字符串,因此字符串函数检测空值字符来确定字符串的结尾。
C++ 还提供了 string 类,用于表示字符串。函数可以接受 string 对你作为参数以及将 string 对你作为返回值。string 类的方法 size()可用于判断其存储的字符串的长度。
C++ 处理结构的方式与基本类型完全相同,这意味着可以按值传递结构,并将其用作函数返回类型。然而,如果结构非常大,则传递结构指针的效率将更高,同时函数能够使用原始数据。这些考虑因素也适用于类对像。
C++ 函数可以是递归的,也就是说,函数代码中可以扬对函数本身的调用。
C++函数名与函数地址的作用相同(看下图)。通过将函数指针作为参数,可以传递要调用的函数的名称。