第五章 循环和关系表达式

5.1 for循环

使用for循环访问字符串
#include <iostream>
#include <string>
using namespace std;
int main()
{cout << "Enter a word: ";string word;cin >> word;//display letters in reverse orderfor(int i = word.size() - 1;i >= 0;i--)  //长度减1是去掉末尾的\0  cout << word[i];cout << "\nByte.\n";return 0;
}

运行结果:

Enter a word: animal
lamina
Byte.
前缀格式和后缀格式:

i++是后缀递增,

++i是前缀递增。

for(n = lim; n > 0; --n)     // 前缀
......
for(n = lim; n > 0; n--)        // 后缀
......

从逻辑上说,在上述两种情形下,使用前缀格式和后缀格式没有任何区别。表达式的值未被使用,因此只存在副作用。在上述的例子中,使用这些运算符的表达式为完整表达式,因此将n加1和n减1的副作用将在程序进入下一步之前完成,前缀格式和后缀格式的最终效果相同。

然而,虽然选择使用前缀格式还是后缀格式对程序的行为没有影响,但执行速度可能有细微的差别。对于内置类型的当代的编译器而言,没有什么问题。对于类而言,前缀版本的效率比后缀版本的高。

总之,对于内置类型,采用哪种格式不会有差别;但对于用户定义的类型,如果有用户定义的递增和递减运算符,则前缀格式的效率更高

递增/递减运算符和指针

可以将递增运算符用于指针和基本变量。将递增运算符用于指针时,将把指针的值增加其指向的数据类型占用的字节数,这种规则适用于对指针递增和递减:

double arr[5] = {21.1,32.8,23.4,45.2,3.4};
double *pt  = arr; // pt points to arr[0] i.e to 21.1
++pt;         // pt points to arr[1] i.e to 32.8

可以结合使用这些运算符和*运算符来修改指针指向的值。前缀递增/递减和解除引用运算符的优先级相同,以从右到左的方式结合。后缀递增和后缀递减的优先级相同,但比前缀运算符的优先级高,这两个运算符以从左到右的方式结合。

double x = *++pt; // increment pointer,take the value; arr[2],or 23.4

前缀运算符的从右到左的结合规则意味着*++pt的含义:先将++应用于pt,然后将*应用于被递增后的pt。

++*pt;     // increment the pointed to value; i.e.,change 23.4 to 24.4

意味着先取pt指向的值,然后将这个值加1。在这种情况下,pt仍然指向arr[2]。

(*pt)++;   // increment pointed-to value

圆括号指出,首先对指针解除引用,得到24.4。然后,运算符++将这个值递增到25.4,pt仍然指向arr[2]。

x = *pt++;    // dereference original location, then increment pointer
//先解引用,再将指针pt自增

后缀运算符++的优先级更高,这意味着将运算符用于pt,而不是*pt,因此对指针递增。

然后后缀运算符意味着将对原来的地址(&arr[2])而不是递增后的新地址解除引用,因此*pt++的值为arr[2],即25.4,但该语句执行完毕后,pt的值将为arr[3]的地址。

程序清单:

#include <iostream>
using namespace std;
int main(){cout << "Please enter five values:\n";double number;double sum = 0.0;for(int i = 1; i <= 5;i++){cout << "Value " << i << ":";cin >> number;sum += number;}cout << "The sum to " << sum << endl;cout << "and average to " << sum/5 << ".\n";return 0;
}

运行结果:

Please enter five values:
Value 1:1942
Value 2:1948
Value 3:1957
Value 4:1974
Value 5:1980
The sum to 9801
and average to 1960.2.
C风格字符串的比较

数组名是数组的地址,同样,用引号括起来的字符串常量也是其地址。

C风格字符串库中的strcmp()函数来比较,该函数接受两个字符串地址作为参数,这意味着参数可以是指针、字符串常量或字符数组名。如果两个字符串相同,该函数将返回零;如果第一个字符串按字母顺序排在第二个字符串之前,则strcmp()将返回一个负值;如果第一个字符串按字母顺序排在第二个字符串之后,则strcpm()将返回一个正数值。

在有些语言(如BASIC和标准Pascal)中,存储在不同长度的数组中的字符串彼此不相等。但在C风格字符串是通过结尾的空值字符定义的,而不是由其所在数组的长度定义的。这意味着两个字符串即使被存储在长度不同的数组中,也可能是相同的。

char big[80] = "Daffy";           // 5 letters plus \0
char little[6] = "Daffy";        // 5 letters plus \0

虽然不能用关系运算符比较字符串,但却可以用它们来比较字符,因为字符实际上是整型。

因此下面的代码可以用来显示字母表中的字符,至少对于ASCII字符集和Unicode字符集来说是有效的:

for(ch = 'a';ch <= 'z';ch++)cout << ch;

程序清单:
例如:在for循环的测试条件中使用了strcmp()。该程序显示一个单词,修改其字母,然后再次显示这个单词,这样循环往复,查到strcmp()确定该单词与字符串"matc"相同为止。

#include <iostream>
#include <cstring>        // prototype for strcmp()
using namespace std;
int main()
{char word[5] = "?ate";      // or string word = "?ate";for(char ch = 'a';strcmp(word,"mate");ch++){cout << word << endl;word[0] = ch;}cout << "After loop end,word is " << word << endl;return 0;
}

运行结果:

?ate
aate
bate
cate
date
eate
fate
gate
hate
iate
jate
kate
late
After loop end,word is mate

程序说明:

strcmp()判断出两个字符串不相同,测试就继续进行,最显而易见的测试是这样的:

strcmp(word,"mate") = 0;

如果字符串不相等,则该语句的值为1(true)。如果字符串相等,则语句的值为0(false)。

类型别名

C++为类型建立别名的方式有两种。一种是使用预处理器:

#define BYTE char;   // preprocessor replaces BYTE with char

这样,预处理器将在编译程序时用char替换所有的BYTE,从而使BYTE成为char的别名。

第二种方法是使用C++(和C)的关键字typedef来创建别名。

例如:要将byte作为char的别名,可以这样做:

typedef char byte;       // make byte an alias for char

通用格式:

typedef typeName aliasName;

换句话说,如果要将aliasName作为某种类型的别名,可以声明aliasName,如同将aliasName声明为这种类型的变量那样,然后在声明的前面加上关键字typedef。

例如:要让byte_pointer成为char指针的别名,可将byte_pointer声明为char指针,然后在前面加上typedef:

typedef char *  byte_pointer;    // pointer to char type

相比于#define,使用typedef是一种更佳的选择,有时候,也是唯一的选择。

注意:typedef不会创建新类型,而只是为已有的类型建立一个新名称。如果将word作为int的别名,则cout将把word类型的值视为int类型。

5.2 基于范围的for循环(C++11)

C++11 新增了一种循环:基于范围(range-based)的for循环。

这简化了一种常见的循环任务:对于数组(或容器类,如vector和array)的每个元素执行相同的操作。

double prices[5] = {4.99,10.99,6.87,7.99,8.49};
for(double x : prices)cout << x << std::endl;

其中,x最初表示数组prices的第一个元素。显示第一个元素后,不断执行循环,而x依次表示数组的其他元素。因此,上述代码显示全部5个元素,每个元素占据一行。总之,该循环显示数组中的每个值。

要修改数组的元素,需要使用不同的循环变量语法:

for(double &x : prices)x = x + 0.80;       // 20% of sale

符号&表明x是一个引用变量。这种声明让接下来的代码能够修改数组的内容。

还可以结合使用基于范围的for循环和初始化列表:

for(int x:{3,5,2,8,6})cout << x << " ";
cout << '\n';

5.3 循环和文本输入

循环完成的一项最常见、最重要的任务:逐字符地读取来自文件或键盘的文本。例如:可能想要编写一个能够计算输入中的字符数、行数和字数的程序。传统上,C++和C语言一样,也使用while循环来完成这类任务。

cin对象支持3种不同模式的单字符输入,其用户接口各不相同。

1、使用原始的cin进行输入:

#include <iostream>
using namespace std;
int main()
{char ch;int count = 0;cout << "Enter characters;enter # to quit:\n";cin >> ch;while(ch != '#')   // test the character{cout << ch;     // echo the character++count;     // count the charactercin >> ch;      // get the next character}cout << endl << count << " characters read\n";return 0;
}

运行结果:

Enter characters;enter # to quit:
see ken run#really fast
seekenrun
9 characters read

注意:cIn在读取char值时,将忽略空格和换行符。因此输入中的空格没有被回显,也没有被包括在计数内。

更为复杂的是,发送给cin的输入被缓冲。这意味着只有在用户按下回车键后,他输入的内容才会被发送给程序。这就是在运行该程序时, 在#后面输入字符的原因。按下回车键后,整个字符序列将被发送给程序,但程序在遇到#字符后将结束对输入的处理。

2、使用cin.get(char)进行补救

通常,逐个字符读取输入的程序需要检查每个字符,包括空格、制表符和换行符。cin所属的istream类中包含一个能够满足这种要求的成员函数。具体地说,成员函数cin.get(ch)读取输入中的下一个字符(即使它是空格),并将其赋给变量ch。

程序清单:

#include <iostream>
using namespace std;
int main()
{char ch;int count = 0;cout << "Enter characters;enter # to quit:\n";cin.get(ch);      // use the cin.get(ch) functionwhile(ch != '#')  {cout << ch;      ++count;      cin.get(ch);        }cout << endl << count << " characters read\n";return 0;
}

运行结果:

Enter characters;enter # to quit:
Did you use a #2 pencil?
Did you use a
14 characters read

现在,该程序回显了每个字符,并将全部字符计算在内,其中包括空格。输入仍被缓冲,因此输入的字符个数仍可能比最终到达程序的要多。

3、使用哪一个cin.get()

char name[ArSize];
...
cout << "Enter your name:\n";
cin.get(name,ArSize).get();

最后一行相当于两个连续的函数调用:

cin.get(name,ArSize);
cin.get();

cin.get()的一个版本接受两个参数:数组名(字符串(char * 类型)的地址)和ArSize(int类型的整数)。其中,数组名是第一个元素的地址,因此字符数组名的类型为char *

另一种用法:只接受一个char参数:

char ch;
cin.get(ch);

在C++中可以这样使用,因此该语言支持被称为函数重载的OOP特性。

函数重载:允许创建多个同名函数,条件是它们的参数列表不同。

例如:如果在C++中使用cin.get(name,ArSize),则编译器将找到char *和int作为参数的cin.get()版本。如果使用cin.get(ch),则编译器将使用接受一个char参数的版本。如果没有提供参数,则编译器将使用不接受任何参数的cin.get()版本。

函数重载允许对多个相关的函数使用相同的名称,这些函数以不同方式或针对不同类型执行相同的基本任务。

4、文件尾条件

如果输入来自于文件,则可以使用一种功能更强大的技术:检测文件尾(EOF)。

很多操作系统都允许通过键盘来模拟文件尾条件。在Unix中,可以在行首按下Ctrl+D来实现;在Windows命令提示符模式下,可以在任意位置按Ctrl+Z和Enter。用于PC的Microsoft Visual C++、Borland C++ 5.5 和 GNU C++ 都能够识别行首的Ctrl + Z,但用户必须随后按下回车键。

很多PC编程环境都将Ctrl + Z视为模拟的EOF。

程序清单:

#include <iostream>
using namespace std;
int main()
{char ch;int count = 0;cin.get(ch);        // attempt to read a charwhile(cin.fail() == false )      // test for EOF{cout << ch;       // echo character++count;cin.get(ch); }cout << endl << count << " characters read\n";return 0;
}

5.4 嵌套循环和二维数组

假设要打印数组所有的内容,可以用一个for循环来改变行,用另一个被嵌套的for循环来改变列:

for (int row = 0; row < 4; row++)
{for(int col = 0; col < 5; ++col)cout << maxtemps[row][col] << "\t";cout << endl;
}

在每个值之后打印一个制表符(使用C++转义字符表示时为\t),打印完每行后,打印一个换行符。

第六章 函数:C++的编程模块

6.1 函数的基本知识

自定义函数时,需要定义函数、提供函数原型和调用函数。

#include <iostream>
using namespace std;
void simple();          // function prototypeint main()
{cout << "main() will call the simple() function:\n";simple();          // function callcout << "main() is finished with the simple() function.\n";return 0;
}// function definition
void simple()
{cout << "I'm but a simple function.\n";
}

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

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

其中,parameterList指定了传递给函数的参数类型和数量。

void函数相当于Pascal中的过程、FORTRAN中的字程序和现代BASIC中的子程序。

通常,可以用void函数来执行某种操作。例如,将Cheers!打印指定次数(n)的函数:

void cheers(int n)
{for (int i = 0;i < n ;i++)std::cout << "Cheers! ";std::cout << std::endl;
}

有返回值的函数将生成一个值,并将它返回给调用函数。这种函数的类型被声明为返回值的类型。其通用格式:

typeName functionName(parameterList)
{statementsreturn value;        // value is type cast to type typeName
}

对于有返回值的函数,必须使用返回语句,以便将值返回给调用函数。值本身可以是常量、变量,也可以是表达式,只是其结果的类型必须为typeName类型或可以被转换为typeName。

(例如,如果声明的返回值类型为double,而函数返回一个int表达式,则该int值将被强制转换为double类型)。

C++对于返回值的类型有一定的限制:不能是数组,但可以是其他任何类型:整数、浮点数、指针,甚至可以是结构和对象!(有趣的是,虽然C++函数不能直接返回数组,但可以将数组作为结构或对象组成部分来返回。)

函数原型和函数调用

我们已经熟悉了函数调用,但对函数原型可能不太熟悉,因为它经常隐藏在include文件中。

程序清单:

#include <iostream>
using namespace std;
void cheers(int);       // prototype: no return value
double cube(double x);  // prototype: return a double
int main()
{cheers(5);         // function callcout << "Give me a number: ";double side;cin >> side;double volume = cube(side);     // function callcout << "A " << side << "-foot cube has a volume of ";cout << volume << " cubic feet.\n";cheers(cube(2));       // prototype protection at workreturn 0;
}void cheers(int n)
{for(int i = 0; i < n; i++)cout << "Cheers! ";cout << endl;
}
double cube(double x)
{return x*x*x;
}

运行结果:

Cheers! Cheers! Cheers! Cheers! Cheers!
Give me a number: 5
A 5-foot cube has a volume of 125 cubic feet.
Cheers! Cheers! Cheers! Cheers! Cheers! Cheers! Cheers! Cheers!

为什么需要原型:

原型描述了函数到编译器的接口,也就是说,它将函数返回值的类型以及参数的类型和数量告诉编译器。

double volume = cube(side);

首先,原型告诉编译器,cube()有一个double参数。如果程序没有提供这样的参数,原型将让编译器能够捕获这种错误。其次,cube()函数完成计算后,将把返回值放置在指定的位置——可能是CPU寄存器,也可能是内存中。然后调用函数将从这个位置取得返回值。由于原型指出了cube()的类型为double,因此编译器知道应检索多少个字节以及如何解释它们。

原型的语法:

函数原型是一条语句,因此必须以分号结束。获得原型最简单的方法是,复制函数定义的函数头,并添加分号。

double cube(double x);   // add ; to header to get prototype

然而,函数原型不要求提供变量名,有类型列表就足够了。对于cheer()的原型,该程序只提供了参数类型:

void cheers(int);        // okay to drop variable names in prototype

通常,在原型的参数列表中,可以包括变量名,也可以不包括。原型中的变量名相当于占位符,因此不必与函数定义中的变量名相同。

6.2 函数参数和按值传递

C++通常按值传递参数,这意味着将数值参数传递给函数,而后者将其赋值给一个新的变量。

double volume = cube(side);

cube()的函数头:

double cube(double x)

用于接受传递值的变量被称为形参,传递给函数的值被称为实参。出于简化的目的,C++标准使用参数(argument)来表示实参,使用参量(parameter)来表示形参,因此参数传递将参量赋给参数。

在函数中声明的变量(包括参数)是该函数私有的。在函数被调用时,计算机将为这些变量分配内存;在函数结束时,计算机将释放这些变量使用的内存。这样的变量被称为局部变量。

多个参数

函数可以有多个参数,在调用函数时,只需要使用逗号将这些参数分开即可。

n_chars('R',25);

在定义函数时,也在函数头中使用由逗号分隔的参数声明列表:

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);   // prototype,style 1

和一个参数的情况一样,原型中的变量名不必与定义中的变量名相同,而且可以省略:

void n_chars(char,int);   // prototype,style 2

然而,提供变量名将使原型更容易理解,尤其是两个参数的类型相同时。这样,变量名可以提醒参量和参数间的对应关系:

double melon_density(double weight,double volume);

程序清单:

#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')                   // q to quit{cout << "Enter an integer:";cin >> times;n_chars(ch,times);cout << "\nEnter another character or press the""q-key to quit: ";cin >> ch;}cout << "The value of times is " << times << ".\n";cout << "Byte\n";return 0;
}void n_chars(char c, int n)        // display  c  n  times
{while(n-- > 0)              // continue until n reachers 0cout << c;
}

运行结果:

Enter a character: W
Enter an integer:50
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
Enter another character or press theq-key to quit: a
Enter an integer:20
aaaaaaaaaaaaaaaaaaaa
Enter another character or press theq-key to quit: q
The value of times is 20.
Byte
另外一个接受两个参数的函数

程序清单:

#include <iostream>
using namespace std;
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;
}

运行结果:

Enter the total number of choices on the game card and
the number of picks allowed:
49 6
You have one chance in 1.39838e+007 of winning.
Next two numbers(q to quit): 51 6
You have one chance in 1.80095e+007 of winning.
Next two numbers(q to quit): 38 6
You have one chance in 2.76068e+006 of winning.
Next two numbers(q to quit): q
bye

程序说明:
首先是形参(number 和 picks),这是在左括号前面的函数头声明的;其次是其他局部变量(result、n和p),它们是在将函数定义括起的括号内声明的。形参与其他局部变量的主要区别是,形参从调用probability()的函数那里获得自己的值,而其他变量是从函数中获得自己的值。

6.3 函数和数组

int sum_arr(int arr[],int n) // arr = array name , n = size

方括号指出arr是一个数组,而方括号为空则表明,可以将任何长度的数组传递给该函数,但实际情况并非如此:arr实际上并不是数组,而是一个指针!好消息是,在编写函数的其余部分时,可以将arr看作是数组。

程序清单:
演示如同使用数组名那样使用指针的情况。

#include <iostream>
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};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;
}

运行结果:

Total cookies eaten: 255

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

在大多数情况下,C++和C语言一样,也将数组名视为指针。C++将数组名解释为其第一个元素的地址。

cookies == &cookies[0];        // array name is address of first element

该规则有一些例外。首先,数组声明使用数组名来标记存储位置;其次,对数组名使用sizeof将得到整个 数组的长度(以字节为单位);第三,将地址运算符&用于数组名时,将返回整个数组的地址。例如:&cookies将返回一个32字节内存块的地址。

int sum = sum_arr(cookies,ArSize);

其中,cookies是数组名,而根据C++规则,cookies是其第一个元素的地址,因此函数传递的是地址。由于数组的元素的类型是int,因此cookies的类型必须是int指针,即int*。这表明,正确的函数头应该是:

int sum = sum_arr(int * arr, int n)

其中用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是指针还是数组名,表达式arr[3]都指的是数组的第4个元素。就目前而言,下面两个是恒等式,将不会有任何的坏处:

arr[i] == *(arr + i)      // values in two notations
&arr[i] == arr + i           // addresses int two notation

记住,将指针(包括数组名)加1,实际上是加上了一个与指针指向的类型的长度(以字节为单位)相等的值。对于遍历数组而言,使用指针加法和数组下标时等效的。

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

实际上并没有将数组的内容传递给函数,而是将数组的位置(地址)、包含的元素种类(类型)以及元素数目(n变量)提交给函数。有了这些信息后,函数便可以使用原来的数组。传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组。

         // arr告知数组地址
int sum_arr(int arr[],int n)// arr[]与*arr相同,指出arr是指针

数组名和指针对应是件好事。将数组地址作为参数可以节省复制整个数组所需的时间和内存。如果数组很大,则使用拷贝的系统开销将非常大;程序不仅需要更多的计算机内存,还需要花费时间来复制大块的数据。另一方面,使用原始的数据增加了破坏数据的风险。

程序清单:

#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 << "\n";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;
}

运行结果:

0x61fdf0 = array address, 32 = sizeof cookies
0x61fdf0 = arr, 8 = sizeof arr
Total cookies eaten: 255
0x61fdf0 = arr, 8 = sizeof arr
First three eaters ate 7 cookies.
0x61fe00 = arr, 8 = sizeof arr
Last four eaters ate 240 cookies.

注意:地址值和数组的长度随系统而异。

为将数组类型和元素数量告诉数组处理函数,请通过两个不同的参数来传递:

void fillArray(int arr[], int size);     // prototype

而不要试图使用方括号表示法来传递数组长度:

void fillArray(int arr[size]);       // No bad prototype

更多数组函数示例:
1、填充数组

由于接受数组名参数的函数访问的是一个原始数组,而不是其副本,因此可以通过调用该函数将值赋给数组元素。

可以使用循环连续地将数值读入到数组中,但如何提早结束循环呢?一种方法是,使用一个特殊值来指定输入结束。由于所有的属性都不为负,因此可以使用负数来指出输入结束。另外,函数应对错误输入作反应,如停止输入等。

int fillArray(double ar[], int limit)
{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 terminatebreak;ar[i] = temp;}return i;
}

注意,代码中包含了对用户的提示。如果用户输入的是非负值,则这个值将被赋给数组,否则循环结束。如果用户输入的都是有效值,则循环将在读取最大数目的值后结束。循环完成的最后一项工作是将i加1,因此循环结束后,i将比最后一个数组索引大1,即等于填充的元素数目。然后返回这个值。

2、显示数组及用const保护数组

创建显示数组内容的函数很简单,只需将数组名和填充的元素数目传递给函数,然后该函数使用循环来显示每个元素。然而,还有个很重要的问题,确保显示函数不修改原始数组。除非函数的目的就是修改传递给它的数据,否则应避免发生这种情况。使用普通参数时,这种保护将自动实现,这是由于C++按值传递数据,而且函数使用数据的副本。然而,接受数组名的函数将使用原始数据 ,为防止函数无意中修改数组的内容,可以声明形参时使用关键字const

void show_array(const double arr[], int n);

该声明表明,指针arr指向的是常量数据,这意味着不能使用arr修改该数据,也就是说,可以使用像arr[0这样的值,但不能修改。注意:这并不是意味着原始数组必须是常量,而只是意味着不能在show_array()函数中使用arr来修改这些数据。因此,show_array()将数组视为只读数据。

3、修改数组

例如:将每个元素与同一个重新评估因子相乘。需要给函数传递3个参数:因子、数组和元素数目。

void revalue(double r,double arr[],int n)
{for (int i = 0; i < n; i++)arr[i] *= r;
}

4、将上述代码组合起来

#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))    // bad input{cin.clear();while (cin.get() != '\n')continue;cout << "Bad input;input process terminated.\n";break;}revalue(factor,properties,size);show_array(properties,size);}cout << "Done.\n";cin.get();cin.get();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)    // bad input{cin.clear();while (cin.get() != '\n')continue;cout << "Bad input;input process terminated.\n";break;}else if(temp < 0)       // signal to terminatebreak;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;
}

运行输出:

Enter value #1:100000
Enter value #2:80000
Enter value #3:222000
Enter value #4:240000
Enter value #5:118000
Property # 1: $100000
Property # 2: $80000
Property # 3: $222000
Property # 4: $240000
Property # 5: $118000
Enter revaluation factor: 0.8
Property # 1: $80000
Property # 2: $64000
Property # 3: $177600
Property # 4: $192000
Property # 5: $94400
Done.
使用数组区间的函数

一种给函数提供所需信息的方法:指定元素区间(range)。可以通过传递两个指针来完成:一个指针标识数组的开头,另一种指针标识数组的结尾。例如:C++标准模板库,将区间方法广义化了。STL方法使用“超尾”概念来指定区间。也就是说,对于数组而言,标识数组结尾的参数将指向最后一个元素后面的指针。

double elboud[20];

则指针elboud和elboud + 20 定义了区间。数组名elboud指向第一个元素,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);       // first 3 elementscout << "First three eaters ate " << sum << " cookies.\n";sum = sum_arr(cookies + 4,cookies + 8);       // last 4 elementscout << "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;
}

运行结果:

Total cookies eaten: 255
First three eaters ate 7 cookies.
Last four eaters ate 240 cookies.

指针cookies + ArSize 指向最后一个元素后面的一个位置(数组有ArSize个元素,因此cookies[ArSize - 1] 是最后一个元素,其地址为cookies + ArSize - 1)。因此,区间[cookies, cookies + ArSize]指向的是整个数组。

注意:根据指针减法规则,在sum_arr()中,表达式end - begin 是一个整数值,等于数组的元素数目。

指针和const

将const 用于指针有一些微妙的地方。可以用两种不同的方式将const关键字用于指针。第一种方法是让指针指向一个常量对象,这样可以防止使用该指针来修改所指向的值,第二种方法是将指针本身声明为常量,这样可以防止改变指针指向的位置。

int age = 39;
const int * pt = &age;

该声明指出,pt指向一个const int,因此不能使用pt来修改这个值。换句话来说,*pt的值为const,不能被修改:

*pt += 1;      // invalid
cin >> *pt;       // invalid

pt的声明并不意味着它指向的值实际上就是一个常量,而只是意味着对pt而言,这个 值是常量。例如:pt指向age,而age不是const。可以直接通过age变量来修改age的值,但不能使用pt指针来修改它:

*pt = 20;       // invalid
age = 20;      // valid

以前将常规变量的地址赋给常规指针,现在若将常规的地址赋给指向const指针。出现两种可能:将const变量的地址赋给指向const的指针、将const的地址赋给常规指针。第一种是可行的,但是第二种是不可行的。

const float g_earth  = 9.80;
const float * pe  = &g_earch;  // validconst float g_moon = 1.63;
float * pm = &g_moon;      // invalid

C++禁止将const的地址赋给非const指针。如果非要这样做,可以使用强制类型转换来突破这种限制。

注意:如果数据类型本身并不是指针,则可以将const数据或非const数据的地址赋给指向const的指针,但只能将非const数据的地址赋给非const指针。

尽可能使用const

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

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

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

6.4 函数和二维数组

为编写将二维数组作为参数的函数。必须牢记,数组名被视为其地址,因此,相应的形参是一个指针,就像一堆数组一样。

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

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组成的数组。因此,指针类型指定了列数,这就是没有将列数作为独立的函数参数进行传递的原因。

由于参数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指向数组的第一个元素,因此表达式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)
ar2                  // pointer to first row of an array of 4 int
ar2 + r                // pointer to row r (an array of 4 int )
*(ar2 + r) // row r (an array of 4 int ) ,hence the name of an array// thus a pointer to the first int in the row ,i.e.,ar2[r]
*(ar2 + r) + c        // pointer int number c in row r,i.e.,ar2[r] + c
*(*(ar2 + r) + c)     // value of int number c in row r,i.e. ar2[r][c]

6.5 函数和C-风格字符串

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

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

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

  • 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数组只是数组,而不是字符串)。这意味着不必将字符串长度作为

参数传递给函数,而函数可以使用循环依次检查字符串中的每个字符,直到遇到结尾的空值字符为止。

#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)           // quit when * str is '\0'{if (*str == ch)count++;str++;                // move pointer to next char}return count;
}

运行结果:

3 m characters in minimum
2 u characters in ululate

c_in_str()函数不应修改原始字符串,因此它在声明形参str时使用了限定符const。这样,如果错误地址函数修改了字符串的内容,编译器将捕获这种错误。

处理字符串中字符的标准方式

while(*str)
{statementsstr++;
}

str最初指向字符串的第一个字符,因此*str表示的是第一个字符。例如:第一次调用该函数后,*str的值将为m——"minimum"的第一个字符。只要字符不为空值字符(\0),*str就为非零值,因此循环将继续。在每轮循环的结尾处,表达式str++将指针增加一个字节,使之指向字符串的下一个字符。最终,str将指向结尾的空值字符,使得*str等于0——空值字符的数字编码,从而结束循环。

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

函数无法返回一个字符串,但是可以返回字符串的地址,这样做效率更高。例如:下面程序定义一个buildstr()的函数,该函数返回一个指针。该函数接受两个参数:一个字符和一个数字。函数使用new创建一个长度与数字参数相等的字符串,然后将每个元素都初始化为该字符,然后,返回指向新字符串的指针。

#include <iostream>
using namespace std;
char * buildstr(char c, int n);     // prototype
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;                            // free memoryps = buildstr('+',20);                    // reuse pointercout << ps << " DONE " << ps << endl;delete [] ps;return 0;
}char * buildstr(char c, int n)
{//函数使用new创建一个长度与数字参数相等的字符串char * pstr = new char[n + 1]; pstr[n] = '\0';                          // terminate stringwhile (n-- > 0)pstr[n] = c;                      // fill rest of stringreturn pstr;
}

运行结果:

Enter a character: V
Enter an integer: 46
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
++++++++++++++++++++ DONE ++++++++++++++++++++

程序说明:要创建包含n个字符的字符串,需要能够存储n+1个字符的空间,以便能够存储空值字符。该函数请求分配n+1个字节的内存来存储该字符串,并将最后一个字符设置为空值字符,然后从后向前对数组进行填充。

下面的循环将循环n次,直到n减少为0,这将填充n个元素:

while(n-- > 0)ptsr[n] = c;

在最后一轮循环开始时,n的值为1。由于n是先使用这个值,然后将其递减,因此while循环测试条件将对1和0进行比较,发现测试为true,循环继续。测试后,函数将n减为0,因此pstr[0]是最后一个被设置为c的元素。之所以从后向前(而不是从前向后)填充字符串,是为了避免使用额外的变量。

从前向后填充代码:

int i = 0;
while (i < n)pstr[i++] = c;

注意:变量pstr的作用域为buildstr函数内,因此该函数结束时,pstr(而不是字符串)使用的内存将被释放。但由于函数返回了pstr的值,因此程序仍然可以通过main()中的指针ps来访问新建的字符串。

6.6 函数和结构

现在将注意力从数组到结构。为结构编写函数比为数组编写函数简单得多。虽然结构变量和数组一样,都可以存储多个数据项,但在涉及到函数时,结构变量的行为更接近于基本的单值变量。也就是说,与数组不同,结构将其数据组合成单个实体或数据对象,该实体被视为一个整体。可以将一个结构赋给另外一个结构,同样,也可以按值传递结构,就像普通变量那样。在这种情况下,函数将使用原始结构的副本。另外,函数也可以返回结构。与数组名(数组第一个元素的地址)不同的是,结构名只是结构的名称,要获得结构的地址,必须使用地址运算符&。在C语言和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";
}

运行输出:

Two-day total: 10 hours, 40 minutes
Three-day total: 15 hours, 12 minutes

其中,travel_time就像是一个标准的类型名,可被用来声明变量、函数的返回值和函数的参数类型。由于total和t1变量是travel_time结构,因此可以对它们使用句点成员运算符。由于sum()函数返回travel_time结构,因此可以将其用作show_time()函数的参数。

2、另一个处理结构的函数示例

介绍个处理空间,而不是时间的案例。具体地说,这个例子将定义两个结构,用于表示两种不同的描述位置的方法,然后开发一个函数,将一种格式转换为另一种格式,并显示结果。

#include <iostream>
#include <cmath>
using namespace std;// structure declarations
struct polar
{double distance;   // distance from origindouble angle;        // direction from origin
};
struct rect
{double x;      // horizontal distance from origindouble y;     // vertical distance from origin
};// prototypes
polar rect_to_polar(rect xypos);
void show_polar(polar dapos);int main()
{rect rplace;rect 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;
}// convert rectangular to plar 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;
}// show polar coordinates,converting angle to degree
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";
}

程序说明:
该程序如何使用cin来控制while循环:

while(cin >> rplace.x >> rplace.y)

cin是istream类的一个对象。抽取运算符(>>)被设计成使得cin >> rplace.x也是一个istream对象。类运算符是使用函数实现的,使用cin >> rplace.x时,程序将调用一个函数,该函数返回一个istream值。整个while循环的测试表达式的最终结果为cin,而cin被用于测试表达式中时,将根据输入是否成功,被转换为bool值true或者false。在该程序中,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()重置输入,然后还可能需要通过读取不合法的输入来丢弃它们。

3、传递结构的地址

假设要传递结构的地址而不是整个结构以节省时间和空间,则需要重新编写前面的函数,使用指向结构的指针。重新编写show_polar()函数,需要修改三个地方:

  • 调用函数时,将结构的地址(&pplace)而不是结构本身(pplace)传递给它;
  • 将形参声明为指向polar的指针,即polar* 类型。由于函数不应该修改结构,因此使用const修饰符;
  • 由于形参是指针而不是结构,因此应使用间接运算符(->),而不是成员运算符(句点)。
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";
}

6.7 函数和string对象

虽然C- 风格字符串和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];     // an array holding 5 string objectcout << "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;
}

运行结果:

Enter your 5 favorite astronomical sights:
1:Orion Nebula
2:M13
3:Saturn
4:Jupiter
5:Moon
Your list :
1: Orion Nebula
2: M13
3: Saturn
4: Jupiter
5: Moon

如果需要string数组,只需要使用通常的数组声明格式即可

string list[SIZE];       // an array holding 5 string object

这样,数组list的每个元素都是一个string对象,可以如此使用:

getline(cin,list[i]);

getline()函数可读取整行,包括前导和嵌入的空格,并将其存储在字符串对象中。

同样,形参sa是一个指向string对象的指针,因此sa[i]是一个string对象,可以这样使用:

cout << i + 1 << ": " << sa[i] << endl;

string与char的区别

string 是定义一个字符串,存储的是一段如“abcd”的数据,而且最后还有一个结束符’\0’;

char 是定义一个字符,存储一个字符,占一个字节。

char数组可以表示字符串,比如:char[10]就是一个字符串

6.8 函数与array对象

在C++中,类对象是基于结构的,因此结构编程方面的有些考虑因素也适用于类。例如:可按值将对象传递给函数,在这种情况下,函数处理的是原始对象的副本。另外,也可以传递指向对象的指针,这让函数能够操作原始对象。

请注意:模板array并非只能存储基本数据类型,它还可以存储类的对象。

#include <iostream>
#include <array>
#include <string>
using namespace std;// constant data
const int Seasons = 4;
const std::array<std::string,Seasons> Snames = {"Spring","Summer","Fall","Winter"};// function to modify array object
void fill(std::array<double,Seasons> *pa);//function that uses array object without modifying it
void show(std::array<double,Seasons> da);int main()
{array<double,Seasons> expenses;fill(&expenses);show(expenses);return 0;
}void fill (std::array<double,Seasons> *pa)
{for(int i = 0; i < Seasons;i++){cout << "Enter " << Snames[i] << " expenses: ";cin >> (*pa)[i];}
}void show (std::array<double,Seasons> da)
{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;
}

运行结果:

Enter Spring expenses: 212
Enter Summer expenses: 255
Enter Fall expenses: 208
Enter Winter expenses: 244EXPENSES
Spring: $212
Summer: $255
Fall: $208
Winter: $244
Total Expenses: $919

程序说明:
由于const array 对象Sname是在所有函数之前声明的,因此可后面的任何函数定义中使用它。

cin >> (*pa)[i];

pa是一个指向array<double,4>对象的指针,因此*pa为这种对象,而(*pa)[i]是该对象的一个元素。由于运算符优先级的影响,其中的括号必不可少。

6.9 递归

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

1、包含一个递归调用的递归

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

例如:void类型的递归函数recurs()的代码:

void recurs(argumentlist)
{statements1if (test)recurs(arguments)statements2
}

test最终将为false,调用链将断开。只要if语句为true,每个recurs()调用都将执行statements1,然后再调用recurs(),而不会执行statements2。当if语句为false时,当前调用将执行statements2。当前调用结束后,程序控制权将返回给调用它的recurs(),而该recurs()将执行其statements2部分,然后结束,并将控制权返回给前一个调用,依次类推。

因此,如果recurs()进行了5次递归调用,则第一个statements1部分将按函数调用的顺序执行5次,然后statements2部分将以与函数调用相反的顺序执行5次。进入5层递归后,程序将沿进入的路径返回。

#include <iostream>
using namespace std;
void countdown(int n);
int main()
{countdown(4);          // call the recursive functionreturn 0;
}void countdown(int n)
{cout << "Counting down ... " << n << endl;if (n > 0)countdown(n - 1);           // function calls itselfcout << n << ":Kaboom!\n";
}

运行结果:

Counting down ... 4
Counting down ... 3
Counting down ... 2
Counting down ... 1
Counting down ... 0
0:Kaboom!
1:Kaboom!
2:Kaboom!
3:Kaboom!
4:Kaboom!

注意:每个递归调用都创建自己的一套变量,因此当程序到达第5次调用时,将有5个独立的n变量,其中每个变量的值都不同。

为验证这一点,可以修改程序,使之显示n的地址和值:

cout << "Counting down ... " << n << " (n at " << &n << ")" << endl;cout << n << ": Kaboom!" << "          (n at " << &n << ")" << endl;

运行结果:

Counting down ... 4 (n at 0x61fe00)
Counting down ... 3 (n at 0x61fdd0)
Counting down ... 2 (n at 0x61fda0)
Counting down ... 1 (n at 0x61fd70)
Counting down ... 0 (n at 0x61fd40)
0: Kaboom!          (n at 0x61fd40)
1: Kaboom!          (n at 0x61fd70)
2: Kaboom!          (n at 0x61fda0)
3: Kaboom!          (n at 0x61fdd0)
4: Kaboom!          (n at 0x61fe00)
2、包含多个递归调用的递归

在需要将一项工作不断分为两项较小的,类似的工作时,递归非常的有用。

例如:考虑使用这种方法来绘制标尺的情况,标出两端,找到中点并将其标出。然后将同样的操作用于标尺的左半部分和右半部份。如果进一步细分,可将同样的操作用于当前的每一部分。递归方法有时被称为分而治之策略。

程序清单:

#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] = ' ';                // 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);
}

运行结果:

|                                                               |
|                               |                               |
|               |               |               |               |
|       |       |       |       |       |       |       |       |
|   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||

6.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)声明函数指针

声明指向某种数据类型的指针时,必须指定指针指向的类型。同样,声明指向函数的指针时,也必须指定指针指向的函数类型。这意味着声明应指定函数的返回类型以及函数的特征标(参数列表)。也就是说,声明应像函数原型那样指出有关函数的信息。

例如:假设编写一个估算时间的函数,其原型:

double pam(int);     // prototype

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

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

这与pam()声明类似,这是将pam替换为(*pf)。由于pam是函数,因此(*pf)也是函数。而如果(*pf)是函数,则pf就是函数指针。

提示:通常,要声明指向特定类型的函数指针,可以首先编写这种函数的原型,然后用(*pf)替换函数名。这样pf就是这类的函数的指针。

为提供正确的运算符优先级,必须在声明中使用括号将*pf括起。括号的优先级比*运算符高,因此*pf(int)意味着pf()是一个返回指针的函数,而(*pf)(int)意味着pf是一个指向函数的指针。

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

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

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

注意:pam()的特征标和返回类型必须与pf相同。如果不相同,编译器将拒绝这种赋值:

double ned(double);
int ted(int);
double (*pf)(int);
pf = ned;              // invalid -- mismatched signature
pf = ted;              // invalid -- mismatched return types

假设要将编写的代码行数和估算算法(如pam()函数)的地址传递给它,则原型如下:

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

第二个参数是函数指针,它指向的函数接受一个int参数,并返回一个double值。

要让estimate()使用pam()函数,需要将pam()的地址传递给它:

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

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

3) 使用指针来调用函数

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

double pam(int);
double (*pf)(int);
pf = pam;
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

第一种格式虽然不太好看,但是给出了强有力的提示:代码正在使用函数指针。

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";
}

运行结果:

How many lines of code do you need? 30
Here's Betsy's estimate:
30 lines will take 1.5 hour(s)
Here's Pam's estimate:
30 lines will take 1.26 hour(s)
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。

程序清单:

#include <iostream>
using namespace std;
const double * f1(const double ar[], int n);
const double * f2(const double [], int);
const double * f3(const double *, int);int main()
{double av[3] = {1112.3, 1542.6, 2227.9};const double *(*p1)(const double *,int) = f1;auto p2 = p1;      // C++ automatic type deductioncout << "Using pointers to function:\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 pointers 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;               // C++11 automatic type deductionconst 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;
}

运行输出:

Using pointers to function:Address Value
0x61fdc0: 1112.3
0x61fdc0: 1112.3Using an array of pointers to functions:Address Value
0x61fdc0: 1112.3
0x61fdc8: 1542.6
0x61fdd0: 2227.9Using a pointer to a pointers to a function:Address Value
0x61fdc0: 1112.3
0x61fdc8: 1542.6
0x61fdd0: 2227.9Using pointers to an array of pointers:Address Value
0x61fdc8: 1542.6
0x61fdd0: 2227.9

感谢 auto

C++11 的目标之一是让C++更容易使用,从而让程序员将主要精力放在设计上而不是细节上。

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

自动类型推动功能表明,编译器的角色发生了改变。在C++98中,编译器利用其知识帮助您发现错误,而在C++11中,编译器利用其知识帮助您进行正确的声明。

存在一个潜在的缺点,自动类型推断确保变量的类型与赋给它的初值的类型一致,但您提供的初值可能不对:

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

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

4、 使用typedef进行简化

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

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

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

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

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

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

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

6.11 总结

函数是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++函数名与函数地址的作用相同。通过将函数指针作为参数,可以传递要调用的函数的名称。

第七章 函数探幽

7.1 C++内联函数

内联函数是C++为提高程序运行速度所做的一项改进。常规函数和内联函数之间的主要区别不在于编写方式,而在于C++编译器如何将它们组合到程序中。要了解内联函数与常规函数的区别,必须深入到程序内部。

编译过程的最终产品是可执行程序:由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机内存中,因此每条指令都有特定的内存地址。计算机随后将逐步执行这些指令。有时,将跳过一些指令,向前或向后跳到特定地址。常规函数调用也使程序跳到另一个地址(函数地址),并在函数结束时返回。

C++内联函数提供了另一种选择,内联函数的编译代码与其他程序代码"内联"起来了。也就是说,编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。如果程序在10个不同的地方调用同一个内联函数,则该程序将包含该函数代码的10个副本。

应该有选择地使用内联函数。如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间将只占整个过程的很小一部分。如果代码执行时间很短,则内联调用就可以节省非内联调用使用的大部分时间。另一方面,由于这个过程相当快,因此尽管节省了该过程的大部分时间,但节省的时间绝对值并不大,除非该函数经常被调用。

要使用这项特性,必须采取下述措施之一:

  • 在函数声明前加上关键字inline
  • 在函数定义前加上关键字inline

通常的做法是省略原型,将整个定义(即函数头和所有函数代码)放在本应提供原型的地方。

程序清单:

#include <iostream>
using namespace std;
inline double square(double x){return x*x;}
int main()
{double a,b;double c = 13.0;a = square(5.0);b = square(4.5 + 7.5);cout << "a = " << a << ",b = " << b << "\n";cout << "c = " << c;cout << ",c square = " << square(c++) << "\n";cout << "Now c = " << c << "\n";return 0;
}

运行结果:

a = 25,b = 144
c = 13,c square = 169
Now c = 14

输出表明,内联函数和常规函数一样,也是按值来传递参数的。如果参数为表达式,则函数将传递表达式的值。这使得C++的内联功能远远胜过C-语言的宏定义。

尽管程序没有提供独立的原型,但C++原型特性仍在起作用。这是因为在函数首次使用前出现的整个函数定义充当了原型。这意味着可以给square()传递int或者long值,将值传递给函数前,程序自动将这个值强制转换为double类型。

C++ Learning (Next)相关推荐

  1. ObjecT4:On-line multiple instance learning (MIL)学习

    原文链接:http://blog.csdn.net/ikerpeng/article/details/19235391 用到论文,直接看翻译. 文章:Robust object tracking wi ...

  2. 训练softmax分类器实例_CS224N NLP with Deep Learning(四):Window分类器与神经网络

    Softmax分类器 我们来回顾一下机器学习中的分类问题.首先定义一些符号,假设我们有训练集 ,其中 为输入, 为标签,共包括 个样本: 表示第 个样本,是一个 维的向量: 表示第 个样本的标签,它的 ...

  3. 【强化学习】Playing Atari with Deep Reinforcement Learning (2013)

    Playing Atari with Deep Reinforcement Learning (2013) 这篇文章提出了第一个可以直接用强化学习成功学习控制policies的深度学习模型. 输入是r ...

  4. 转【面向代码】学习 Deep Learning(二)Deep Belief Nets(DBNs)

    [面向代码]学习 Deep Learning(二)Deep Belief Nets(DBNs) http://blog.csdn.net/dark_scope/article/details/9447 ...

  5. CloudSim4.0 Learning(1)

    CloudSim4.0 Learning(1) 文章目录 *CloudSim4.0 Learning(1)* *Example1* *1 CloudSim初始化* *1.1 CloudSim.init ...

  6. 2018-12 文献阅读:A Survey on Transfer Learning(2009)

    文献阅读:A Survey on Transfer Learning(2009) 2018-12 文章的作者是Sinno Jialin pan 与 Qiang Yang 关键词:迁移学习(transf ...

  7. 强化学习 Reinforcement Learning(三)——是时候用 PARL 框架玩会儿 DOOM 了!!!(下)

    强化学习 Reinforcement Learning(三)-- 是时候用 PARL 框架玩会儿 DOOM 了!!!(下) 本文目录 强化学习 Reinforcement Learning(三)-- ...

  8. A Transformer-based Framework for Multivariate Time Series Representation Learning(KDD2022)

    A Transformer-based Framework for Multivariate Time Series Representation Learning(KDD2022) 提出了一种基于t ...

  9. Life-Long Learning(LLL)终身学习的理解

    Life-Long Learning(LLL) 看了李宏毅老师的Life-Long Learning视频,以及粗略阅读了A continual learning survey:Defying forg ...

  10. 2018-12 文献阅读:Boosting for Transfer Learning(2007)--迁移学习

    2018-12 文献阅读:Boosting for Transfer Learning(2007) 2018-12 文章的作者Wenyuan Dai&Qiang yang&Gui_Ro ...

最新文章

  1. 剑指offer java版(一)
  2. Java 结构体之 JavaStruct 使用教程一 初识 JavaStruct
  3. phoenixframework自动化测试平台webUI代码示例
  4. c 打印二叉树_树、二叉树、二叉查找树(二叉搜索树)
  5. 数据挖掘的技术都有哪些?
  6. 按自己的思想写商业计划
  7. 安装CommonAPI
  8. 邓俊辉 数据结构 第二章 向量 笔记
  9. 地震 位错模型 matlab,地球物理学论文范文参考 地球物理学毕业论文范文[精选]...
  10. Python 爬虫---初窥门径
  11. Linux 磁盘动态扩容 PVM(转载)
  12. Pose Estimation 入门理解
  13. c++获取系统时间实例2
  14. 微信7.0.10正式版来了!朋友圈斗图彻底关闭了!
  15. java万物之源——集合框架
  16. 行列式、逆矩阵、列空间和零空间(3Blue1Brown学习笔记)
  17. 西门子PLC协议-S7COMM
  18. matlab模拟土壤水分入渗,基于HYDRUS-2D的负压灌溉土壤水分入渗数值模拟
  19. VMware 创建 启动指定虚拟机 的 快捷方式
  20. 微信小程序项目《天使童装》

热门文章

  1. codeforces 918 D MADMAX 记忆化搜索
  2. Instruments之相关介绍(一)
  3. 算法大赛神器:集成学习方法关键点介绍
  4. python调用photoshop_Python和Photoshop
  5. 电脑usb蓝牙的使用
  6. 外接USB蓝牙设置无法启动
  7. Origin图选择性粘贴到word出现问题,提示‘word出现问题’解决方法
  8. r720换固态硬盘后如何重装系统_联想拯救者r720笔记本NVME接口M.2固态硬盘怎么安装win7系统...
  9. Unity5.6 VideoPlayer用法
  10. Python爬虫 | 2008-2018年各省专利统计数据