一、函数的基本知识
要使用C++函数,需要:
- 提供函数原型;
- 提供函数定义;
- 调用函数;
#include <iostream>
using namespace std;
void simple ();
int main()
{
cout << "main() will call the simple() function: \n";
simple ();
cout << "main() is finished with the simple() function.\n";
return 0;
}
void simple()
{
cout << "I'm but a simple function.\n";
}
1.定义函数
可以将函数分成两类:没有返回值的函数和有返回值的函数。没有返回值的函数被称为void函数,其通用格式如下:
void functionName(parameterList)
{
statement (s)
return;//optional}
其中,parameterList 指定了传递给函数的参数类型和数量。可选的返回语句标记了函数的结尾;否则,函数将在右花括号处结束。通常,可以用void函数来执行某种操作。
有返回值的函数将生成一个值,并将它返回给调用函数。这种函数的类型被声明为返回值的类型,其通用格式如下:
typeName functionName(parameterList)
{
statements
return value;}
对于有返回值的函数,必须使用返回语句,以便将值返回给调用函数。值本身可以是常量、变量,也可以是表达式,只是其结果的类型必须为typeName类型或可以被转换为typeName。然后,函数将最终的值返回给调用函数。C++对于返回值的类型有一定的限制:不能是数组,但可以是其他任何类型——整数、浮点数、指针,甚至可以是结构和对象!虽然C++函数不能直接返回数组,但可以将数组作为结构、对象或使用指针来返回。
通常,函数通过将返回值复制到指定的CPU寄存器或内存单元中来将其返回。随后,调用程序将查看该内存单元。返回函数和调用函数必须就该内存单元中存储的数据的类型达成一致。函数原型将返回值类型告知调用程序,而函数定义命令被调用函数应返回什么类型的数据。
函数在执行返回语句后结束。如果函数包含多条返回语句,则函数在执行遇到的第一条返回语句后结束。有返回值的函数向调用程序返回一个值,然后调用程序可以将其赋给变量、显示或将其用于别的用途。
#include <iostream>
using namespace std;
void cheers(int);
double cube(double x); // prototype: returns a double
int main()
{
cheers(5);
cout << "Give me a number: ";
double side;
cin >> side;
double volume = cube(side);
cout << "A " << side << "-foot cube has a volume of ";
cout << volume << " cubic feet.\n";
cheers(cube(2));
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;
}
2.函数原型与调用
(1)为什么需要原型
原型描述了函数到编译器的接口,也就是说,它将函数返回值的类型以及参数的类型和数量告诉编译器。
- 首先,原型告诉编译器,它将接受一个什么类型的参数。如果程序没有提供这样的参数,原型将让编译器捕获这种错误。
- 其次,函数完成计算后,将把返回值放置在指定的位置,可能是CPU寄存器,也可能是内存中。然后调用函数将从这个位置取得返回值。
- 由于原型指出了返回类型,因此编译器知道应检索多少个字节以及如何解释它们。如果没有这些信息,编译器将只能进行猜测,而编译器不会。
为何编译器需要原型,难道它就不能在文件中进一步查找,以了解函数是如何定义的吗?
- 这种方法的一个问题是效率不高。编译器在搜索文件的剩余部分时将必须停止对main()的编译。
- 一个更严重的问题是,函数甚至可能并不在文件中。C++允许将一个程序放在多个文件中,单独编译这些文件,然后再将它们组合起来。在这种情况下,编译器在编译main()时,可能无权访问函数代码。
- 如果函数位于库中,情况也将如此。避免使用函数原型的唯一方法是,在首次使用函数之前定义它,但这并不总是可行的。
- 另外,C++的编程风格是将main()放在最前面,因为它通常提供了程序的整体结构。
(2)原型的语法
函数原型是一条语句,因此必须以分号结束。获得原型最简单的方法是,复制函数定义中的函数头,并添加分号。然而,函数原型不要求提供变量名,有类型列表就足够了。通常,在原型的参数列表中,可以包括变量名,也可以不包括。原型中的变量名相当于占位符,因此不必与函数定义中的变量名相同。
C++原型与ANSI 原型的区别:
- ANSIC借鉴了C++中的原型,但这两种语言还是有区别的。其中最重要的区别是,为与基本C兼容,ANSIC中的原型是可选的,但在C++中,原型是必不可少的。
- 在C++中,括号为空与在括号中使用关键字void是等效的,意味着函数没有参数。在ANSI C中,括号为空意味着不指出参数,这意味着将在后面定义参数列表。在C++中,不指定参数列表时应使用省略号。通常,仅当与接受可变参数的C函数(如printf())交互时才需要这样做。
(3)原型的功能
原型确保以下几点:
- 编译器正确处理函数返回值;
- 编译器检查使用的参数数目是否正确;
- 编译器检查使用的参数类型是否正确。如果不正确,则转换为正确的类型(如果可能的话)。
例如,当参数数目不对时,假设
double x=cube();//cube原型需要一个参数
如果没有函数原型,某些C编译器将允许它通过。由于对于ANSI C来说,原型是可选的。当函数被调用时,它将找到cube()调用存放值的位置,并使用这里的值。这正是ANSI C从C++借鉴原型之前,C语言的工作方式。但在C++中,原型不是可选的,因此可以确保不会发生这类错误。
例如。当参数类型不对时,在C语言中,这将造成奇怪的错误。例如,如果函数需要一个int值(假设占16位),而程序员传递了一个double值(假设占64位),则函数将只检查64位中的前16位,并试图将它们解释为一个int值。但C++自动将传递的值转换为原型中指定的类型,条件是两者都是算术类型。
通常,原型自动将被传递的参数强制转换为期望的类型。但函数重载时可能导致二义性,因此不允许某些自动强制类型转换。
自动类型转换并不能避免所有可能的错误。例如,如果将8.33E27传递给期望一个int值的函数,则这样大的值将不能被正确转换为int值。当较大的类型被自动转换为较小的类型时,有些编译器将发出警告,指出这可能会丢失数据。
仅当有意义时,原型化才会导致类型转换。例如,原型不会将整数转换为结构或指针。在编译阶段进行的原型化被称为静态类型检查。可以看出,静态类型检查可捕获许多在运行阶段非常难以捕获的错误。
二、函数参数和按值传递
C++通常按值传递参数,这意味着将数值参数传递给函数,而后者将其赋给一个新的变量,即使用一个新的副本,不会影响原值。
用于接收传递值的变量被称为形参,传递给函数的值被称为实参。出于简化的目的,C++标准使用参数(argument)来表示实参,使用参量(parameter)来表示形参,因此按值参数传递将参数赋给参量。
在函数中声明的变量(包括参数)是该函数私有的。在函数被调用时,计算机将为这些变量分配内存;在函数结束时,计算机将释放这些变量使用的内存。这样的变量被称为局部变量,因为它们被限制在函数中。这样做有助于确保数据的完整性。这还意味着,如果在main()中声明了一个名为x的变量,同时在另一个函数中也声明了一个名为x的变量,则它们将是两个完全不同的、毫无关系的变量。这样的变量也被称为自动变量,因为它们是在程序执行过程中自动被分配和释放的。
1.多个参数
函数可以有多个参数。在调用函数时,只需使用逗号将这些参数分开即可。同样,在定义函数时,也在函数头中使用由逗号分隔的参数声明列表。如果函数的两个参数的类型相同,则必须分别指定每个参数的类型。
#include <iostream>
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(char c, int n)
{
while (n-- > 0)
cout << c;
}
2.另外一个接受两个参数的函数
#include <iostream>
using namespace std;
// Note: some implementations require double instead of 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(total, choices);
cout << " of winning. \n";
cout << "Next two numbers (q to quit): ";
}
cout << "bye\n";
return 0;
}
long double probability(unsigned numbers, unsigned picks)
{
long double result = 1.0;
long double n;
unsigned p;
for (n = numbers, p = picks; p > 0; n--, p--)
result = result * n / p;
return result;
}
循环不是首先将所有的分子项相乘,而是首先将1.0与第一个分子项相乘,然后除以第一个分母项。然后下一轮循环乘以第二个分子项,并除以第二个分母项。这样得到的乘积将比先进行乘法运算得到的小,因子越多,中间值的差别就越大。当数字非常大时,这种交替进行乘除运算的策略可以防止中间结果超出最大的浮点数。
形参与其他局部变量的主要区别是,形参从调用它的函数那里获得自己的值,而其他变量是从函数中获得自己的值。
三、函数和数组
#include <iostream>
using namespace std;
const int ArSize = 8;
int sum_arr(int arr[], int n);
int main()
{
int cookies[ArSize] = {1, 2, 4, 8, 16, 32, 64, 128};
int sum = sum_arr(cookies, ArSize);
cout << "Total cookies eaten: " << sum << "\n";
return 0;
}
int sum_arr(int arr[], int n)
{
int total = 0;
for (int i = 0; i < n; i++)
total = total + arr[i];
return total;
}
1.函数如何使用指针来处理数组
在大多数情况下,C++和C语言一样,也将数组名视为指针,C++将数组名解释为其第一个元素的地址。
该规则有一些例外。
- 首先,数组声明使用数组名来标记存储位置;
- 其次,对数组名使用sizeof将得到整个数组的长度(以字节为单位);
- 第三,将地址运算符&用于数组名时,将返回整个数组的地址。
程序执行下面的函数调用:
int sum = sum_arr(cookies, ArSize);其中,cookies是数组名,而根据C++规则,cookies是其第一个元素的地址,因此函数传递的是地址。由于数组的元素的类型为int,因此cookies的类型必须是int指针,即int*。这表明,正确的函数头应该是这样的:
int sum_arr(int * arr, int n) // arr = array name, n = size其中用int*arr替换了 int arr[]。这证明这两个函数头都是正确的,因为在C++中,当且仅当用于函数头或函数原型中,int*arr和int arr[]的含义才是相同的。它们都意味着arr是一个int指针。然而,数组表示法(int arr[])提醒用户,arr不仅指向int,还指向int数组的第一个int。当指针指向数组的第一个元素时,可以使用数组表示法;而当指针指向一个独立的值时,使用指针表示法。在其他的上下文中,int*arr和int arr[]的含义并不相同。例如,不能在函数体中使用int tip[]来声明指针。
由于arr是指针,arr[i]==*(arr+i),&arr[i]==arr+i。将指针(包括数组名)加1,实际上是加上了一个与指针指向的类型的长度(以字节为单位)相等的值。对于遍历数组而言,使用指针加法和数组下标时等效的。
2.将数组作为参数意味着什么
sum_arr()函数将cookies 的地址赋给指针变量arr,将ArSize 赋给int变量n。这意味着,程序实际上并没有将数组内容传递给函数,而是将数组的位置(地址)、包含的元素种类(类型)以及元素数目(n变量)提交给函数。有了这些信息后,函数便可以使用原来的数组。传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组。实际上,这种区别并不违反C++按值传递的方法,sum_arr()函数仍传递了一个值,这个值被赋给一个新变量,但这个值是一个地址,而不是数组的内容。
数组名与指针对应的好处和坏处?
- 将数组地址作为参数可以节省复制整个数组所需的时间和内存。如果数组很大,则使用拷贝的系统开销将非常大;程序不仅需要更多的计算机内存,还需要花费时间来复制大块的数据。
- 另一方面,使用原始数据增加了破坏数据的风险。在经典的C语言中,这确实是一个问题;但ANSI C和C++中的const限定符提供了解决这种问题的办法。
#include <iostream>
using namespace std;
const int ArSize = 8;
int sum_arr(int arr[], int n);
int main()
{
int cookies[ArSize] = {1, 2, 4, 8, 16, 32, 64, 128};
cout << cookies << " = array address, ";
cout << sizeof cookies << " = sizeof cookies\n";
int sum = sum_arr(cookies, ArSize);
cout << "Total cookies eaten: " << sum << endl;
sum = sum_arr(cookies, 3);
cout << "First three eaters ate " << sum << " cookies.\n";
sum = sum_arr(cookies + 4, 4);
cout << "Last four eaters ate " << sum << "cookies. \n";
return 0;
}
int sum_arr(int arr[], int n)
{
int total = 0;
cout << arr << " = arr, ";
cout << sizeof arr << " = sizeof arr\n";
for (int i = 0; i < n; i++)
total = total + arr[i];
return total;
}
首先,cookies和arr指向同一企地址。但sizeof cookies的值为32,而sizeof arr为8。这是由于sizeof cookies是整个数组的长度,而sizeof arr只是指针变量的长度。这也是必须显式传递数组
长度,而不能在 sum_arr()中使用sizeof arr的原因,指针本身并没有指出数组的长度。
3.更多数组函数示例
(1)填充数组
由于接受数组名参数的函数访问的是原始数组,而不是其副本,因此可以通过调用该函数将值赋给数组元素。该函数的一个参数是要填充的数组的名称,将数组长度作为第二个参数传递,另外,用户也可能希望在数组被填满之前停止读取数据,因此需要在函数中建立这种特性。由于用户输入的元素数目可能少于数组的长度,因此函数应返回实际输入的元素数目。
该函数的原型可以写做:
int fill_array(double ar[], int limit);该函数接受两个参数,一个是数组名,另一个指定了要读取的最大元素数;该函数返回实际读取的元素数。
可以使用循环连续地将值读入到数组中,但如何提早结束循环呢?
一种方法是,使用一个特殊值来指出输入结束。如果限定所有的属性都不为负,则可以使用负数来指出输入结束。另外,该函数应对错误输入作出反应,如停止输入等。例如:
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)
{
cin.clear();
while (cin.get() != '\n')
continue;
cout << "Bad input; input process terminated.\n";
break;
}
else if (temp < 0)
break;
ar[i] = temp;
}
return i;
}
(2)显示数组及用const保护数组
创建显示数组内容的函数,只需将数组名和填充的元素数目传递给函数,然后该函数使用循环来显示每个元素。然而,还有另一个问题,需要确保显示函数不修改原始数组。除非函数的目的就是修改传递给它的数据,否则应避免发生这种情况。使用普通参数时,这种保护将自动实现,这是由于C++按值传递数据,而且函数使用数据的副本。然而,接受数组名的函数将使用原始数据,这正是fill_array()函数能够完成其工作的原因。为防止函数无意中修改数组的内容,可在声明形参时使用关键字const:
void show_array(const double ar[], int n) ;
该声明表明,指针ar指向的是常量数据。这意味着不能使用ar修改该数据,也就是说,可以使用像ar[0]这样的值,但不能修改。这并不是意味着原始数组必须是常量,而只是意味着不能在show_array()函数中使用ar来修改这些数据。因此,show_array()将数组视为只读数据。假设无意间在 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[], int n)
{
for (int i = 0; i < n; i++)
ar[i] *= r;
}
(4)将上述部分组合起来
根据数据的存储方式(数组)和使用方式(3个函数)定义了数据的类型,因此可以将它们组合成一个程序。由于已经建立了所有的数组处理工具,因此main()的编程工作非常简单。该程序检查用户输入的是否是数字,如果不是,则要求用户这样做。余下的大部分编程工作只是让main()调用前面开发的函数。
#include <iostream>
using namespace std;
const int Max = 5;
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);
int main()
{
double properties[Max];
int size = fill_array(properties, Max);
show_array(properties, size);
if (size > 0)
{
cout << "Enter revaluation factor: ";
double factor;
while (!(cin >> factor))
{
cin.clear();
while (cin.get() != '\n')
continue;
cout << "Bad input; Please enter a number: ";
}
revalue(factor, properties, size);
show_array(properties, size);
}
cout << "Done.\n";
return 0;
}
int fill_array(double ar[], int limit)
{
double temp;
int i;
for (i = 0; i < limit; i++)
{
cout << "Enter value #" << (i + 1) << ": ";
cin >> temp;
if (!cin)
{
cin.clear();
while (cin.get() != '\n')
continue;
cout << "Bad input; input process terminated.\n";
break;
}
else if (temp < 0)
break;
ar[i] = temp;
}
return i;
}
void show_array(const double ar[], int n)
{
for (int i = 0; i < n; i++)
{
cout << "Property #" << (i + 1) << ": $";
cout << ar[i] << endl;
}
}
void revalue(double r, double ar[], int n)
{
for (int i = 0; i < n; i++)
ar[i] *= r;
}
我们首先考虑的是通过数据类型和设计适当的函数来处理数据,然后将这些函数组合成一个程序。有时也称为自下而上的程序设计(bottom-up programming),因为设计过程从组件到整体进行。这种方法非常适合于OOP——它首先强调的是数据表示和操纵。而传统的过程性编程倾向于从上而下的程序设计(top-down programming),首先指定模块化设计方案,然后再研究细节。这两种方法都很有用,最终的产品都是模块化程序。
4.使用数组区间的函数
对于处理数组的C++函数,必须将数组中的数据种类、数组的起始位置和数组中元素数量提交给它;传统的C/C++方法是,将指向数组起始处的指针作为一个参数,将数组长度作为第二个参数(指针指出数组的位置和数据类型),这样便给函数提供了找到所有数据所需的信息。
还有另一种给函数提供所需信息的方法,即指定元素区间(range),这可以通过传递两个指来完成:一个指针标识数组的开头,另一个指针标识数组的尾部。例如,C++标准模板库(STL)将区间方法广义化了。STL方法使用“超尾”概念来指定区间。也就是说,对于数组而言,标识数组结尾的参数将是指向最后一个元素后面的指针。
例如,假设有这样的声明:double elbuod[20};
则指针elboud和elboud+20定义了区间。首先,数组名elboub指向第一个元素。表达式elboud+19指向最后一个元素(即elboud[19]),因此,elboud+20指向数组结尾后面的一个位置,将区间传递给函数将告诉函数应处理哪些元素。
#include <iostream>
using namespace std;
const int ArSize = 8;
int sum_arr(const int *begin, const int *end);
int main()
{
int cookies[ArSize] = {1, 2, 4, 8, 16, 32, 64, 128};
int sum = sum_arr(cookies, cookies + ArSize);
cout << "Total cookies eaten: " << sum << endl;
sum = sum_arr(cookies, cookies + 3);
cout << "First three eaters ate " << sum << " cookies.\n";
sum = sum_arr(cookies + 4 , cookies +8);
cout << "Last four eaters ate " << sum << " cookies. \n";
return 0;
}
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;
}
for (pt = begin; pt != end; pt++)
total = total + *pt;它将pt设置为指向要处理的第一个元素(begin指向的元素)的指针,并将*pt(元素的值)加入到total中。然后,循环通过递增操作来更新pt,使之指向下一个元素。只要pt不等于end,这一过程就将继续下去。当pt等于end时,它将指向区间中最后一个元素后面的一个位置,此时循环将结束。
指针cookies+ArSize指向最后一个元素后面的一个位置(数组有ArSize个元素,因此cookies[ArSize-1]是最后一个元素,其地址为cookies+ArSize-1)。因此,区间[cookies,cookies+ArSize]指定的是整个数组。同样,cookies,cookies+3指定了前3个元素,依此类推。
根据指针减法规则,在sum_arr()中,表达式end-begin是一个整数值,等于数组的元素数目。必须按正确的顺序传递指针,因为这里的代码假定begin在前面,end在后面。
5.指针和const
可以用两种不同的方式将 const 关键字用于指针。第一种方法是让指针指向一个常量对象,这样可以防止使用该指针来修改所指向的值,第二种方法是将指针本身声明为常量,这样可以防止改变指针指向的位置。
(1)指向常量的指针
声明一个指向常量的指针pt:
int age = 16;
const int * pt = &age;该声明指出,pt指向一个const int,因此不能使用pt来修改这个值。换句话来说,*pt的值为const,不能被修改。即像*pt+=1;是不被允许的。
pt的声明并不意味着它指向的值实际E就是一个常量,而只是意味着对pt而言,这个值是常量。例如,pt指向age,而age 不是const。可以直接通过age变量来修改age的值,但不能使用pt指针来修改它。
*pt=20;//是错误的
age=20;//是有效的
以前我们将常规变量的地址赋给常规指针,而这里将常规变量的地址赋给指向const的指针。而还有另外两种搭配,将const变量的地址赋给指向const的指针、将const变量的地址赋给常规指针。
其中第一种操作是有效的,
const int a=10;
const int * pa=&a;//是有效的
int* pa=&a;//是错误的
对于第一种情况来说,既不能使用变量名来修改值,也不能使用指针来修改。C++禁止第二种情况的原因——如果将const变量的地址赋给pa,则可以使用pa来修改变量的值,这使得变量的const状态产生矛盾,因此C++禁止将const的地址赋给非const指针。如果非要这样做,可以使用强制类型转换来突破这种限制。
如果将指针指向指针,则情况将更复杂。假如涉及的是一级间接关系,则将非 const 指针赋给const指针是可以的:
int a=1;//可用来修改值
int* pa=&a;//可用来修改值
const int* cpa=pa;//*cpa=2;是错误操作
然而,进入两级间接关系时,与一级间接关系一样将const和非const混合的指针赋值方式将不再安全。如果允许这样做,则可以编写这样的代码:、
const int ** pp2;//声明了一个指向指向const int的指针的指针。这意味着pp2可以指向一个指向const int的指针,但它本身不是一个指向const int的指针。
int *p1;
const int n = 13;
pp2 = &p1; //假设它可行(这行代码试图将一个非常量指针p1
的地址赋给一个指向常量指针的指针pp2
。这是不允许的,因为p1
是非const的,而pp2
是const的。在C++中,你不能将非const指针的地址赋给const指针,因为这可能会导致通过const指针修改数据,违反了const的语义。)
*pp2 = &n;// 这行代码是合法的,因为你正在将一个指针的地址(&n
)赋给另一个指针。这里&n
是获取n
的地址,n
是一个编译时常量,所以它的地址可以赋给任何类型的指针,包括指向const int的指针。这行代码实际上是创建了一个指向n
的指针,并将这个指针存储在pp2
指向的位置。
*p1 = 10; //有效上述代码将非const地址(&p1)赋给了const指针(pp2),因此可以使用p1来修改const数据,则发生矛盾。因此,仅当只有一层间接关系(如指针指向基本数据类型)时,才可以将非const地址或指针赋给const指针。
如果数据类型本身并不是指针,则可以将const数据或非const数据的地址赋给指向const的指针,但只能将非const数据的地址赋给非const指针。
假设有一个由const数据组成的数组:
const int months[12] = {31,28,31,30,3F,30, 31, 31,30,31,30,31};则禁止将常量数组的地址赋给非常量指针将意味着不能将数组名作为参数传递给使用非常量形参的函数:
int sum(int arr[], int n); // should have been const int arr []
int j = sum(months,12);
上述函数调用试图将const 指针(months)赋给非const 指针(arr),编译器将禁止这种函数调用。
将指针参数声明为指向常量数据的指针的原因为:
- 这样可以避免由于无意间修改数据而导致的编程错误;
- 使用const使得函数能够处理const和非 const实参,否则将只能接受非 const数据。
如果条件允许,则应将指针形参声明为指向const的指针。
int age = 39;
const int * pt = &age;第二个声明中的const只能防止修改pt指向的值,而不能防止修改pt的值。也就是说,可以将一个新地址赋给pt:
int.sage = 80;
pt = &sage;
但仍然不能使用pt来修改它指向的值。
(2)指针为常量
这种使用const的方式使得无法修改指针的值:
int a = 1;
const int * pca = &a;
int * const cpa = &a;
在最后一个声明中,关键字const的位置与上述不同。这种声明格式使得cpa只能指向a,但允许使用cpa来修改a的值。中间的声明不许使用pca来修改a的值,但允许将pca指向另一个位置。简而言之,cpa和*pca都是const,而*cpa和pca不是。
还可以声明指向const 对象的 const 指针:
double d = 2.03;
const double * const pd = &d;
其中,pd只能指向d,而pd不能用来修改d的值。简而言之,pd和*pd都是const。
通常,将指针作为函数参数来传递时,可以使用指向const的指针来保护数据。
void show_array(const double ar[], int n);
在该声明中使用const意味着 show_array()不能修改传递给它的数组中的值。只要只有一层间接关系,就可以使用这种方法。这里的数组元素是基本类型,但如果它们是指针或指向指针的指针,则不能使用 const。
四、函数和二维数组
为编写将二维数组作为参数的函数,数组名被视为其地址。因此,相应的形参是一个指针,就像一维数组一样,比较难处理的是如何正确地声明指针。
假设data是一个数组名,该数组有3个元素。第一个元素本身是一个数组,由4个int值组成。因此data的类型是指向由4个int组成的数组的指针,因此原型可以声明为:
int sum(int (*ar2) [4],int size);其中的括号是必不可少的,函数参数不能是数组。
还有另外一种格式,这种格式与上述原型的含义完全相同,但可读性更强:
int sum(int ar2 [] [4], int size);上述两个原型都指出,ar2是指针而不是数组。指针类型指出,它指向由4个int组成的数组。因此,指针类型指定了列数,不需要将列数作为独立的函数参数进行传递。
由于指针类型指定了列数,因此sum()函数只能接受由4列组成的数组。但长度变量指定了行数,因此sum()对数组的行数没有限制。使用ar2的方法就是将它作为二维数组的名称使用即可。
可以使用数组表示法的原因为:
由于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)
sum()的代码在声明参数ar2时。没有使用const,因为这种方法只能用于指向基本类型的指针,而ar2是指向指针的指针。
五、函数和C-风格字符串
1.将C-风格字符串作为参数的函数
假设要将字符串作为参数传递给函数,则表示字符串的方式有三种
- char数组;
- 用引号括起的字符串常量(也称字符串字面值);
- 被设置为字符串的地址的char指针。
但上述3种选择的类型都是char指针(准确地说是char*),因此可以将其作为字符串处理函数的参数。可以说是将字符串作为参数来传递,但实际传递的是字符串第一个字符的地址。这意味着字符串函数原型应将其表示字符串的形参声明为char * 类型。
C-风格字符串与常规char数组之间的一个重要区别是,字符串有内置的结束字符。这意味着不必将字符串长度作为参数传递给函数,而函数可以使用循环依次检查字符串中的每个字符,直到遇到结尾的空值字符为止。
#include <iostream>
using namespace std;
unsigned int c_in_str(const char *str, char ch);
int main()
{
char mmm[15] = "minimum";
char *wail = "ululate";
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;
}
unsigned int c_in_str(const char *str, char ch)
{
unsigned int count = 0;
while (*str)
{
if (*str == ch)
count++;
str ++;
}
return count;
}
2.返回C-风格字符串的函数
#include <iostream>
using namespace std;
char *buildstr(char c, int n);
int main()
{
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('+', 5);
cout << ps << "-DONE-" << ps << endl;
delete[] ps;
return 0;
}
char *buildstr(char c, int n)
{
char *pstr = new char[n + 1];
pstr[n] = '\0';
while (n-- > 0)
pstr[n] = c;
return pstr;
}
程序之所以从后向前,而不是从前向后填充字符串,是为了避免使用额外的变量。注意,变量pstr的作用域为 buildstr函数内,因此该函数结束时,pstr(而不是字符串)使用的内存将被释放。但由于函数返回了pstr的值,因此程序仍可以通过main()中的指针ps来访问新建的字符串。当该字符串不再需要时,程序使用derete释放该字符串占用的内存。然后,将ps指向为下一个字符串分配的内存块,然后释放它们。这种让函数返回一个指针,该指针指向new分配的内存的设计的缺点是,程序员必须记住使用delete。
六、函数和结构体
在涉及到函数时,结构体变量的行为更接近于基本的单值变量,为结构体编写函数比为数组编写函数要简单得多。也就是说,与数组不同,结构体将其数据组合成单个实体或数据对象,该实体被视为一个整体。可以将一个结构体赋给另外一个结构体,同样,也可以按值传递结构体,就像普通变量那样。在这种情况下,函数将使用原始结构体的副本。另外,函数也可以返回结构体。与数组名就是数组第一个元素的地址不同的是,结构体名只是结构体的名称,要获得结构的地址,必须使用地址运算符&。
使用结构体编程时,最直接的方式是像处理基本类型那样来处理结构体,将结构体作为参数传递,并在需要时将结构体用作返回值使用。然而,按值传递结构有一个缺点。如果结构体非常大,则复制结构体将增加内存要求,降低系统运行的速度。出于这些原因(同时由于最初C语言不允许按值传递结构体),更倾向于传递结构的地址,然后使用指针来访问结构的内容。C++还提供了引用传递的方式。
1.传递和返回结构体
当结构体比较小时,按值传递结构体最合理。
#include <iostream>
using namespace std;
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()
{
travel_time day1 = {5, 45};
travel_time day2 = {4, 55};
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;
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)
{
cout << t.hours << " hours, "
<< t.mins << " minutes\n";
}
2.另一种示例
#include <iostream>
#include <cmath>
using namespace std;
struct polar
{
double distance;
double angle;
};
struct rect
{
double x;
double y;
};
polar rect_to_polar(rect xypos);
void show_polar(polar dapos);
int main()
{
rect rplace;
polar pplace;
cout << "Enter the x and y values: ";
while (cin >> rplace.x >> rplace.y)
{
pplace = rect_to_polar(rplace);
show_polar(pplace);
cout << "Next two numbers (q to quit) : ";
}
cout << "Done.\n";
return 0;
}
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;
}
void show_polar(polar dapos)
{
const double Rad_to_deg = 57.29577951;
cout << "distance = " << dapos.distance;
cout << ", angle = " << dapos.angle * Rad_to_deg;
cout << " degrees\n";
}
3.传递结构体的地址
假设要传递结构体的地址而不是整个结构以节省时间和空间,则需要重新编写前面的函数,使用指向结构体的指针。首先来看一看如何重新编写show_polar()函数。需要修改三个地方:
- 调用函数时,将结构的地址而不是结构本身传递给它;
- 将形参声明为指向polar的指针,即polar*类型。由于函数不应该修改结构,因此可以使用const修饰符;
- 由于形参是指针而不是结构,因此应间接成员运算符(->),而不是成员运算符(句点)。
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";}
#include <iostream>
#include <cmath>
using namespace std;
struct polar
{
double distance;
double angle;
};
struct rect
{
double x;
double y;
};
void rect_to_polar(const rect *pxy, polar *pda);
void show_polar(const polar *pda);
int main()
{
rect rplace;
polar pplace;
cout << "Enter the x and y values: ";
while (cin >> rplace.x >> rplace.y)
{
rect_to_polar(&rplace, &pplace);
show_polar(&pplace);
cout << "Next two numbers (q to quit) : ";
}
cout << "Done.\n";
return 0;
}
void show_polar(const polar *pda)
{
const double Rad_to_deg = 57.29577951;
cout << "distance = " << pda->distance;
cout << ", angle = " << pda->angle * Rad_to_deg;
cout << " degrees\n";
}
void rect_to_polar(const rect *pxy, polar *pda)
{
pda->distance =
sqrt(pxy->x * pxy->x + pxy->y * pxy->y);
pda->angle = atan2(pxy->y, pxy->x);
}
七、函数和string对象
虽然C-风格字符串和string对象的用途几乎相同,但与数组相比,string对象与结构的更相似。例如,可以将一个结构体赋给另一个结构体,也可以将一个string赋给另一个string。可以将结构体作为完整的实体传递给函数,也可以将string作为完整的实体进行传递。如果需要多个字符串,可以声明一个string 对象数组,而不是二维char数组。
#include <iostream>
#include <string>
using namespace std;
const int SIZE = 5;
void display(const string sa[], int n);
int main()
{
string list[SIZE];
cout << "Enter your " << SIZE << " favorite astronomical 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;
}
八、函数和array对象
在C++中,类对象是基于结构体的,因此结构体编程方面的有些考虑因素也适用于类。例如,可按值将对象传递给函数,在这种情况下,函数处理的是原始对象的副本。另外,也可传递指向对象的指针,这让函数能够操作原始对象。
#include <iostream>
#include <array>
#include <string>
using namespace std;
const int Seasons = 4;
const array<string, Seasons> Snames =
{"Spring", "Summer", "Fall", "Winter"};
void fill(array<double, Seasons> *pa);
void show(array<double, Seasons> da);
int main()
{
array<double, Seasons> expenses;
fill(&expenses);
show(expenses);
return 0;
}
void fill(array<double, Seasons> *pa)
{
using namespace std;
for (int i = 0; i < Seasons; i++)
{
cout << "Enter " << Snames[i] << " expenses: ";
cin >> (*pa)[i];
}
}
void show(array<double, Seasons> 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;
}
九、递归
C++函数可以调用自己,这种功能被称为递归。
1.包含一个递归调用的递归
如果递归函数调用自己,则被调用的函数也将调用自己,这将无限循环下去,除非代码中包含终止调用链的内容。通常的方法将递归调用放在if语句中。
void recurs(argumentlist)
{
statements1
if (test)
recurs (arguments)
statements2}
test最终将为false,调用链将断开。只要if语句为true,每个recurs()调用都将执行 statements 1,然后再调用recurs(),而不会执行statements 2。当if语句为false时,当前调用将执行statements2。当前调用结束后,程序控制权将返回给调用它的recurs(),而该recurs()将执行其stataments2部分,然后结束,并将控制权返回给前一个调用,依此类推。因此,如果recurs()进行了5次递归调用,则第一个 statements1部分将按函数调用的顺序执行5次,然后statements2部分将以与函数调用相反的顺序执行5次。进入5层递归后,程序将沿进入的路径返回。
#include <iostream>
using namespace std;
void countdown(int n);
int main()
{
countdown(4);
return 0;
}
void countdown(int n)
{
cout << "Counting down ... " << n << endl;
if (n > 0)
{
countdown(n - 1);
}
cout << n << ": Kaboom! \n";
}
注意,每个递归调用都创建自己的一套变量,因此当程序到达第5次调用时,将有5个独立的n变量,其中每个变量的值都不同。为验证这一点,可以修改程序,使之显示n的地址和值:
#include <iostream>
using namespace std;
void countdown(int n);
int main()
{
countdown(4);
return 0;
}
void countdown(int n)
{
cout << "Counting down ... " << n <<" (n at " << &n << ") " << endl;
if (n > 0)
{
countdown(n - 1);
}
cout << n << ": Kaboom! "<<" (n at " << &n << ") " << endl;
}
另外,注意到在Counting down阶段和Kaboom阶段的相同层级,n的地址相同。
2.包含多个递归调用的递归
在需要将一项工作不断分为两项较小的、类似的工作时,递归非常有用。例如,考虑使用这种方法来绘制标尺。标出两端,找到中点并将其标出。然后将同样的操作用于标尺的左半部分和右半部分。如果要进一步细分,可将同样的操作用于当前的每一部分。递归方法有时被称为分而治之策略。
接下来的程序使用递归函数 subdivide()演示了这种方法,该函数使用一个字符串,该字符串除两端为 | 字符外,其他全部为空格。main函数使用循环调用 subdivide()函数6次,每次将递归层编号加1,并打印得到的字符串。这样,每行输出表示一层递归。
#include <iostream>
using namespace std;
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] = '|';
cout << ruler << endl;
for (i = 1; i <= Divs; i++)
{
subdivide(ruler, min, max, i);
cout << ruler << endl;
for (int j = 1; j < Len - 2; j++)
ruler[j] = ' ';
}
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);
}
在程序中,subdivide()函数使用变量level来控制递归层。函数调用自身时,将把level减1,当level为0时,该函数将不再调用自己。注意,subdivide()调用自己两次,一次针对左半部分,另一次针对右半部分。最初的中点被用作一次调用的右端点和另一次调用的左端点。调用次数将呈几何级数增长,即调用一次导致两个调用,然后导致4个调用,再导致8个调用,依此类推。这就是6层调用能够填充64个元素的原因。这将不断导致函数调用数(以及存储的变量数)翻倍,因此如果要求的递归层次很多,这种递归方式将是一种糟糕的选择;然而,如果递归层次较少,这将是一种精致而简单的选择。
十、函数指针
与数据项相似,函数也有地址。函数的地址是存储其机器语言代码的内存的开始地址。通常,这些地址对用户而言,既不重要,也没有什么用处,但对程序而言,却很有用。例如,可以编写将另一个函数的地址作为参数的函数。这样第一个函数将能够找到第二个函数,并运行它。与直接调用另一个函数相比,这种方法很笨拙,但它允许在不同的时间传递不同函数的地址,这意味着可以在不同的时间使用不同的函数。
1.函数指针的基础知识
假设要设计一个名为estimate()的函数,估算编写指定行数的代码所需的时间,并且希望不同的程序员都将使用该函数。对于所有的用户来说,estimate()中一部分代码都是相同的,但该函数允许每个程序员提供自己的算法来估算时间。为实现这种目标,采用的机制是,将程序员要使用的算法函数的地址传递给estimate()。为此,必须能够完成下面的工作:
- 获取函数的地址;
- 声明一个函数指针;
- 使用函数指针来调用函数。
(1)获取函数地址
获取函数的地址只要使用函数名(后面不跟参数)即可。也就是说,如果think()是一个函数,则think 就是该函数的地址。要将函数作为参数进行传递,必须传递函数名。一定要区分传递的是函数的
地址还是函数的返回值:
process (think)://函数地址
thought (think () );//函数返回值
process()调用使得process()函数能够在其内部调用think()函数。thought()调用首先调用think()函数,然后将think()的返回值传递给thought()函数。
(2)声明函数指针
声明指向某种数据类型的指针时,必须指定指针指向的类型。同样,声明指向函数的指针时,也必须指定指针指向的函数类型。这意味着声明应指定函数的返回类型以及函数的特征标(参数列表)。也就是说,声明应像函数原型那样指出有关函数的信息。
例如,假设我编写了一个估算时间的函数,其原型如下:
double pam(int);
则正确的指针类型声明如下:
double (*pf) (int);//pf指向一个参数是一个int的函数,且返回值为double
这与pam()声明类似,这是将pam替换为了(*pf)。由于pam是函数,因此(*pf)也是函数。而如果(*pf)是函数,则pf就是函数指针。通常,要声明指向特定类型的函数的指针,可以首先编写这种函数的原型,然后用(*pf)替换函数名。这样pf就是这类函数的指针。为提供正确的运算符优先级,必须在声明中使用括号将*pf括起。括号的优先级比*运算符高,因此*pf(int)意味着pf()是一个返回指针的函数,而(*pf)(int)意味着pf是一个指向函数的指针:
double (*pf) (int); // pf指向一个返回double的函数
double *pf(int);// pf () 是一个返回double指针的函数正确地声明pf后,便可以将相应函数的地址赋给它:
double pam(int);
double (*pf) (int);
pf = pam;
注意,pam()的特征标和返回类型必须与pf相同。如果不相同,编译器将拒绝这种赋值:
double ned(double) ;
int ted(int);
double (*pf) {int);
pf = ned;//无效
pf = ted;//无效现在看一下前面提到的estimate()函数。假设要将将要编写的代码行数和估算算法的地址传递给它,则其原型将如下:
void estimate(int lines, double (*pf) (int));
(3)使用指针来调用函数
现在进入最后一步,即使用指针来调用被指向的函数。(*pf)扮演的角色与函数名相同,因此使用(*pf)时,只需将它看作函数名即可:
double pam(int);
double (*pf) (int);
pf = pam;
double x = pam(4);
double y=(*pf)(5);实际上,C++也允许像使用函数名那样使用pf:
double y = pf (5);第一种格式虽然不太好看,但它给出了强有力的提示——代码正在使用函数指针。
为何pf和(*pf)等价呢?一种学派认为,由于pf是函数指针,而*pf是函数,因此应将(*pf)()用作函数调用。另一种学派认为,由于函数名是指向该函数的指针,指向函数的指针的行为应与函数名相似,因此应将pf()用作函数调用使用。C++认为这2种方式都是正确的,或者至少是允许的,虽然它们在逻辑上是互相冲突的。
2.函数指针示例
#include <iostream>
using namespace std;
double betsy(int);
double pam(int);
void estimate(int lines, double (*pf)(int));
int main()
{
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))
{
cout << lines << " lines will take ";
cout << (*pf)(lines) << " hour(s)\n";
}
3.深入探讨函数指针
下面通过一个示例演示使用函数指针时面临的一些挑战。首先,下面是一些函数的原型,它们的特征标和返回类型相同:
const double * fl(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 * ar。
接下来,假设要声明一个指针,它可指向这三个函数之一。假定该指针名为pa,则只需将目标函数原型中的函数名替换为(*pa):
const double * (+p1) (const double *, int);
可在声明的同时进行初始化:
const double * (*p1):(const double *, int)=f1;使用C++11的自动类型推断功能时,
auto p2 = f2;
现在来看下面的语句:
cout .<< (*p1}(av, 3) << ": " <<*(*pl) (av,3) << endl;
cout << p2 (av,3) << ": " << *p2 (av, 3) << endl;根据前面介绍的知识可知,(*p1)(av,3)和p2(av,3)都调用指向的函数并将av和3作为参数。因此,显示的是这两个函数的返回值。返回值的类型为const double*(即double值的地址),因此在每条cout语句中,前半部分显示的都是一个double值的地址。为查看存储在这些地址处的实际值,需要将运算符*应用于这些地址,如表达式*(*p1)(av,3)和*p2(av,3)所示。
鉴于需要使用三个函数,如果有一个函数指针数组将很方便。这样,将可使用“for 循环通过指针依次调用每个函数。显然,这种声明应类似于单个函数指针的声明,但必须在某个地方加上[3],以指出这是一个包含三个函数指针的数组。问题是在什么地方加上[3]:
const double * (*pa[3]) (const double * int) = {f1,f2, f3};pa是一个包含三个元素的数组,而要声明这样的数组,首先需要使用pa[3]。该声明的其他部分指出了数组包含的元素是什么样的。运算符[]的优先级高于*,因此*pa[3]表明pa是一个包含三个指针的数组。上述声明的其他部分指出子每个指针指向的是什么:特征标为const double * int,且返回类型为 const double*的函数。因此,pa是一个包含三个指针的数组,其中每个指针都指向这样的函数,即将const double *和int 作为参数,并返回三个const double*。
这里不能使用auto。自动类型推断只能用于单值初始化一而不能用于初始化列表。但声明数组pa后声明同样类型的数组就很简单了:
auto pb = pa;pa[i]和pb[i]都表示数组中的指针,因此可将任何一种函数调用表示法用于它们:
const double * px = pa[0](av,3);
const double * py = (*pb[1])(av,3);
可做的另一件事是创建指向整个数组的指针。由于数组名pa是指向函数指针的指针,因此指向数组的指针将是这样的指针,即它指向指针的指针。由于可使用单个值对其进行初始化,因此可使用auto:
auto pc = &pa;
如果想要自己声明,显然,这种声明应类似于pa的声明,但由于增加了一层间接,因此需要在某个地方添加一个*。具体地说,如果这个指针名为pd,则需要指出它是一个指针,而不是数组。这意味着声明的核心部分应为(*pd)[3],其中的括号让标识符pd与*先结合。
换句话说,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]
#include <iostream>
using namespace std;
const double *f1(const double ar[], int n);
const double *f2(const double[], int);
const double *f3(const double ar[], int n);
int main()
{
double av[3] = {1112.3, 1542.6, 2227.9};
const double *(*p1)(const double *, int) = f1;
auto p2 = 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;
const double *(*pa[3])(const double *, int) = {f1, f2, f3};
auto pb = 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;
cout << "\nUsing pointers to an array of pointers: \n";
cout << " Address Value\n";
auto pc = &pa;
cout << (*pc)[0](av, 3) << "? " << *(*pc)[0](av, 3) << endl;
const double *(*(*pd)[3])(const double *, int) = &pa;
const double *pdb = (*pd)[1](av, 3);
cout << pdb << ": " << *pdb << endl;
cout << (*(*pd)[2])(av, 3) << ": " << *(*(*pd)[2])(av, 3) << endl;
return 0;
}
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值的存储位置。这个示例可能看起来比较深奥,但指向函数指针数组的指针并不少见。实际上,类的虚方法实现通常都采用了这种技术。所幸的是,这些细节由编译器处理。
C++11的目标之一是让C++更容易使用,从而让程序员将主要精力放在设计而不是细节上。自动类型推断功能表明,编译器的角色发生了改变。在C++98中,编译器利用其知识帮助您发现错误,而在C++11中,编译器利用其知识帮助您进行正确的声明。
4.使用typedef进行简化
除auto外,C++还提供了其他简化声明的工具。关键字typedef能够创建类型别名:
typedef double real;//real变成了double的别名
这里采用的方法是,将别名当做标识符进行声明,并在开头使用关键字typedef。因此,可将p_fun声明为程序中使用的函数指针类型的别名:
typedef const double *(*p_fun) (const double *, int); // p_fun 现在是一个类型名
p_fun p1 = f1; // p1指向f1函数
然后使用这个别名来简化代码,使用typedef可减少输入量,编写代码时不容易犯错,并让程序更容易理解。