#include
#include
using namespace std;
int main()
{
cout << "Enter a word: ";
string word;
cin >> word;
//display letters in reverse order
for(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
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风格字符串库中的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
#include // 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类型。
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';
循环完成的一项最常见、最重要的任务:逐字符地读取来自文件或键盘的文本。例如:可能想要编写一个能够计算输入中的字符数、行数和字数的程序。传统上,C++和C语言一样,也使用while循环来完成这类任务。
cin对象支持3种不同模式的单字符输入,其用户接口各不相同。
1、使用原始的cin进行输入:
#include
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 character
cin >> 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
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) function
while(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
using namespace std;
int main()
{
char ch;
int count = 0;
cin.get(ch); // attempt to read a char
while(cin.fail() == false ) // test for EOF
{
cout << ch; // echo character
++count;
cin.get(ch);
}
cout << endl << count << " characters read\n";
return 0;
}
假设要打印数组所有的内容,可以用一个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),打印完每行后,打印一个换行符。
自定义函数时,需要定义函数、提供函数原型和调用函数。
#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";
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)
{
statements
return value; // value is type cast to type typeName
}
对于有返回值的函数,必须使用返回语句,以便将值返回给调用函数。值本身可以是常量、变量,也可以是表达式,只是其结果的类型必须为typeName类型或可以被转换为typeName。
(例如,如果声明的返回值类型为double,而函数返回一个int表达式,则该int值将被强制转换为double类型)。
C++对于返回值的类型有一定的限制:不能是数组,但可以是其他任何类型:整数、浮点数、指针,甚至可以是结构和对象!(有趣的是,虽然C++函数不能直接返回数组,但可以将数组作为结构或对象组成部分来返回。)
函数原型和函数调用
我们已经熟悉了函数调用,但对函数原型可能不太熟悉,因为它经常隐藏在include文件中。
程序清单:
#include
using namespace std;
void cheers(int); // prototype: no return value
double cube(double x); // prototype: return 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;
}
运行结果:
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
通常,在原型的参数列表中,可以包括变量名,也可以不包括。原型中的变量名相当于占位符,因此不必与函数定义中的变量名相同。
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
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 0
cout << 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
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()的函数那里获得自己的值,而其他变量是从函数中获得自己的值。
int sum_arr(int arr[],int n) // arr = array name , n = size
方括号指出arr是一个数组,而方括号为空则表明,可以将任何长度的数组传递给该函数,但实际情况并非如此:arr实际上并不是数组,而是一个指针!好消息是,在编写函数的其余部分时,可以将arr看作是数组。
程序清单:
演示如同使用数组名那样使用指针的情况。
#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};
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
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 terminate
break;
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
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 terminate
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;
}
运行输出:
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
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 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;
}
运行结果:
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关键字用于指针。第一种方法是让指针指向一个常量对象,这样可以防止使用该指针来修改所指向的值,第二种方法是将指针本身声明为常量,这样可以防止改变指针指向的位置。
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; // valid
const float g_moon = 1.63;
float * pm = &g_moon; // invalid
C++禁止将const的地址赋给非const指针。如果非要这样做,可以使用强制类型转换来突破这种限制。
注意:如果数据类型本身并不是指针,则可以将const数据或非const数据的地址赋给指向const的指针,但只能将非const数据的地址赋给非const指针。
尽可能使用const
将指针参数声明为指向常量数据的指针有两条理由:
如果条件允许,则应将指针形参声明为指向const的指针。
为编写将二维数组作为参数的函数。必须牢记,数组名被视为其地址,因此,相应的形参是一个指针,就像一堆数组一样。
int data[3][4] = {{1,2,3,4},{9,8,7,6},{2,4,6,8}};
int total = sum(data,3);
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]
C-风格字符串由一系列字符组成,以空值字符结尾。将字符串作为参数时意味着传递的是地址,但可以使用const来禁止对字符串参数进行修改。
假设要将字符串作为参数传递给函数,则表示字符串的方式有三种:
上述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
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)
{
statements
str++;
}
str最初指向字符串的第一个字符,因此*str
表示的是第一个字符。例如:第一次调用该函数后,*str
的值将为m——"minimum"的第一个字符。只要字符不为空值字符(\0),*str
就为非零值,因此循环将继续。在每轮循环的结尾处,表达式str++将指针增加一个字节,使之指向字符串的下一个字符。最终,str将指向结尾的空值字符,使得*str
等于0——空值字符的数字编码,从而结束循环。
函数无法返回一个字符串,但是可以返回字符串的地址,这样做效率更高。例如:下面程序定义一个buildstr()的函数,该函数返回一个指针。该函数接受两个参数:一个字符和一个数字。函数使用new创建一个长度与数字参数相等的字符串,然后将每个元素都初始化为该字符,然后,返回指向新字符串的指针。
#include
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 memory
ps = buildstr('+',20); // reuse pointer
cout << 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 string
while (n-- > 0)
pstr[n] = c; // fill rest of string
return 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来访问新建的字符串。
现在将注意力从数组到结构。为结构编写函数比为数组编写函数简单得多。虽然结构变量和数组一样,都可以存储多个数据项,但在涉及到函数时,结构变量的行为更接近于基本的单值变量。也就是说,与数组不同,结构将其数据组合成单个实体或数据对象,该实体被视为一个整体。可以将一个结构赋给另外一个结构,同样,也可以按值传递结构,就像普通变量那样。在这种情况下,函数将使用原始结构的副本。另外,函数也可以返回结构。与数组名(数组第一个元素的地址)不同的是,结构名只是结构的名称,要获得结构的地址,必须使用地址运算符&。在C语言和C++中,都使用符号&来表示地址运算符;另外,C++还使用该运算符来表示引用变量。
程序清单:
#include
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()函数的参数。
介绍个处理空间,而不是时间的案例。具体地说,这个例子将定义两个结构,用于表示两种不同的描述位置的方法,然后开发一个函数,将一种格式转换为另一种格式,并显示结果。
#include
#include
using namespace std;
// structure declarations
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
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()重置输入,然后还可能需要通过读取不合法的输入来丢弃它们。
假设要传递结构的地址而不是整个结构以节省时间和空间,则需要重新编写前面的函数,使用指向结构的指针。重新编写show_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";
}
虽然C- 风格字符串和string对象的用途几乎相同,但与数组相比,string对象与结构的更相似。例如:可以将一个结构赋给另一个结构,也可以将一个对象赋给另一个对象。可以将结构作为完整的实体传递给函数,也可以将对象作为完整的实体进行传递。如果需要使用多个字符串,可以声明一个string对象数组,而不是二维char数组。
程序清单:
#include
#include
using namespace std;
const int SIZE = 5;
void display(const string sa[],int n);
int main()
{
string list[SIZE]; // an array holding 5 string object
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;
}
运行结果:
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]就是一个字符串
在C++中,类对象是基于结构的,因此结构编程方面的有些考虑因素也适用于类。例如:可按值将对象传递给函数,在这种情况下,函数处理的是原始对象的副本。另外,也可以传递指向对象的指针,这让函数能够操作原始对象。
请注意:模板array并非只能存储基本数据类型,它还可以存储类的对象。
#include
#include
#include
using namespace std;
// 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()
{
array expenses;
fill(&expenses);
show(expenses);
return 0;
}
void fill (std::array *pa)
{
for(int i = 0; i < Seasons;i++)
{
cout << "Enter " << Snames[i] << " expenses: ";
cin >> (*pa)[i];
}
}
void show (std::array 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: 244
EXPENSES
Spring: $212
Summer: $255
Fall: $208
Winter: $244
Total Expenses: $919
程序说明:
由于const array 对象Sname是在所有函数之前声明的,因此可后面的任何函数定义中使用它。
cin >> (*pa)[i];
pa是一个指向array*pa
为这种对象,而(*pa)[i]
是该对象的一个元素。由于运算符优先级的影响,其中的括号必不可少。
C++函数有一种有趣的特点——可以调用自己(然而,与C语言不同的是,C++ 不允许main()调用自己),这种功能被称为递归。
如果递归函数调用自己,则被调用的函数也将调用自己,这将无限循环下去,除非代码中包含终止调用链的内容。通常的方法将递归调用放在if语句中。
例如:void类型的递归函数recurs()的代码:
void recurs(argumentlist)
{
statements1
if (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
using namespace std;
void countdown(int n);
int main()
{
countdown(4); // call the recursive function
return 0;
}
void countdown(int n)
{
cout << "Counting down ... " << n << endl;
if (n > 0)
countdown(n - 1); // function calls itself
cout << 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)
在需要将一项工作不断分为两项较小的,类似的工作时,递归非常的有用。
例如:考虑使用这种方法来绘制标尺的情况,标出两端,找到中点并将其标出。然后将同样的操作用于标尺的左半部分和右半部份。如果进一步细分,可将同样的操作用于当前的每一部分。递归方法有时被称为分而治之策略。
程序清单:
#include
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);
}
运行结果:
| |
| | |
| | | | |
| | | | | | | | |
| | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
与数据项相似,函数也有地址。函数的地址是存储其机器语言代码的内存的开始地址。通常,这些地址对用户而言,既不重要,也没有什么用处,但对程序而言,却很有用。例如:可以编写将另一个函数的地址作为参数的函数,这样第一个函数将能够找到第二个函数,并运行它。与直接调用另一个函数相比,这种方法很笨拙,但它允许在不同的时间传递不同函数的地址,这意味着可以在不同的时间使用不同的函数。
假设要设计一个名为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
第一种格式虽然不太好看,但是给出了强有力的提示:代码正在使用函数指针。
程序清单:
#include
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)
函数指针的表示可能非常的恐怖。
下面是一些函数的原型,它们的特征标和返回值类型相同:
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
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 deduction
cout << "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 deduction
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;
}
运行输出:
Using pointers to function:
Address Value
0x61fdc0: 1112.3
0x61fdc0: 1112.3
Using an array of pointers to functions:
Address Value
0x61fdc0: 1112.3
0x61fdc8: 1542.6
0x61fdd0: 2227.9
Using a pointer to a pointers to a function:
Address Value
0x61fdc0: 1112.3
0x61fdc8: 1542.6
0x61fdd0: 2227.9
Using 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相同,这将导致编译错误。
除了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可以减少输入量,在编写代码时不容易犯错,并让程序更容易理解。
函数是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++函数名与函数地址的作用相同。通过将函数指针作为参数,可以传递要调用的函数的名称。
内联函数是C++为提高程序运行速度所做的一项改进。常规函数和内联函数之间的主要区别不在于编写方式,而在于C++编译器如何将它们组合到程序中。要了解内联函数与常规函数的区别,必须深入到程序内部。
编译过程的最终产品是可执行程序:由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机内存中,因此每条指令都有特定的内存地址。计算机随后将逐步执行这些指令。有时,将跳过一些指令,向前或向后跳到特定地址。常规函数调用也使程序跳到另一个地址(函数地址),并在函数结束时返回。
C++内联函数提供了另一种选择,内联函数的编译代码与其他程序代码"内联"起来了。也就是说,编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。如果程序在10个不同的地方调用同一个内联函数,则该程序将包含该函数代码的10个副本。
应该有选择地使用内联函数。如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间将只占整个过程的很小一部分。如果代码执行时间很短,则内联调用就可以节省非内联调用使用的大部分时间。另一方面,由于这个过程相当快,因此尽管节省了该过程的大部分时间,但节省的时间绝对值并不大,除非该函数经常被调用。
要使用这项特性,必须采取下述措施之一:
通常的做法是省略原型,将整个定义(即函数头和所有函数代码)放在本应提供原型的地方。
程序清单:
#include
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类型。
内联与宏
inline工具是C++新增的特性。C语言使用预处理器语句#define来提供宏——内联代码的原始实现。例如:计算平方的宏:
#define SQUARE(X) X*X
这并不是通过传递参数实现的,而是通过文本替换来实现的——X是"参数"的符号标记。
a = SQUARE(5.0); is replaced by a = 5.0 * 5.0;
b = SQUAER(4.5 + 7.5); is replaced by b = 4.5 + 7.5 * 4.5 + 7.5;
d = SQUARE(c++); is replaced by d = c++ * c++;
上述示例只有第一个能正常工作。可以通过使用括号来进行改进:
#define SQUARE(X) ((X)*(X))
但仍然存在这样的问题,即宏不能按值传递。即使使用新的定义,SQUARE(c++)仍将c递增两次,但是上面程序中的内联函数square()能计算c的结果,传递它,以计算其平方值,然后将c递增一次。
这里的目的不是演示如何编写C宏,而是要指出,如果使用C语言的宏执行了类似函数的功能,应考虑将其转换为C++内联函数。
C++新增了一个复合类型:引用变量。引用是已定义的变量的别名(另一个名称)。
例如:如果将twain作为clement变量的引用,则可以交替使用twain和clement来表示该变量。
引用变量的主要用途是用作函数的形参,通过将引用变量用作参数,函数将使用原始数据,而不是其副本。这样除了指针外,引用也可以为函数处理大型结构提供了一种非常方便的途径。同时对于设计类来说,引用也是必不可少的。
1、创建引用变量
C和C++使用&符号来指示变量的地址。C++给&符号赋予了另一个含义,将其用来声明引用。
例如:将rodents 作为 rats变量的别名,可以:
int rats;
int & rodents = rats; // makes rodents an alias for rats
其中,&不是地址运算符,而是类型标识符的一部分。就像声明中的char*
指的是指向char的指针一样,int & 指 的是指向int的引用。上述引用声明允许将rats和rodents互换,它们指向相同的值和内存单元。
#include
using namespace std;
int main()
{
int rats = 101;
int & rodents = rats; // rodents is a reference
cout << "rats = " << rats;
cout << ",rodents = " << rodents << endl;
rodents++;
cout << "rats = " << rats;
cout << ",rodents = " << rodents << endl;
cout << "rats address = " << &rats;
cout << ", rodents address = " << &rodents << endl;
return 0;
}
运行结果:
rats = 101,rodents = 101
rats = 102,rodents = 102
rats address = 0x61fe14, rodents address = 0x61fe14
请注意:下述语句中的&运算符不是地址运算符,而是将rodents的类型声明为 int &
,即指向int变量的引用。
int & rodents = rats;
但下面语句中的&运算符是地址运算符,其中&rodents表示rodents引用的变量的地址。
cout << ", rodents address = " << &rodents << endl;
由此可知,rats和rodents的值和地址都相同。将rodents加1将影响这两个变量。更准确地说,rodents++操作是将一个有两个名称的变量加1.
指针和引用之间是有区别的。例如:可以创建指向rats的引用和指针。
int rats = 101;
int & rodents = rats; // rodents a reference
int * prats = &rats; // prats a pointer
这样,表达式rodents和 *prats
都可以同rats互换,而表达式&rodents和prats都可以同&rats互换。从这一点来说,引用看上去像是伪装表示的指针(其中,*解除引用运算符
被隐式理解 )。实际上,引用还是不同于指针的,除了表示法不同外,还有其它的区别。
例如:必须在声明引用的同时将其初始化。而不能像指针那样,先声明,再赋值。
int rat;
int & rodent;
rodent = rat; // No,you can't do this
注意:必须在声明引用变量时进行初始化。
引用更接近const指针,必须在创建时进行初始化,一旦与某个变量关联起来,就将一直效忠于它。也就是说:
int & rodents = rats;
实际上是下述代码的伪装表示:
int * const pr = & rats;
其中,引用rodents扮演的角色与表达式*pr
相同。
程序清单:
#include
using namespace std;
int main()
{
int rats = 101;
int & rodents = rats;
cout << "rats = " << rats;
cout << ", rodents = " << rodents << endl;
cout << "rats address = " << &rats;
cout << ", rodents address = " << &rodents << endl;
int bunnies = 50;
rodents = bunnies;
cout << "bunnies = " << bunnies;
cout << ", rats = " << rats;
cout << ", rodents = " << rodents << endl;
cout << "bunnies address = " << &bunnies;
cout << ", rodents address = " << &rodents << endl;
return 0;
}
运行结果:
rats = 101, rodents = 101
rats address = 0x61fe14, rodents address = 0x61fe14
bunnies = 50, rats = 50, rodents = 50
bunnies address = 0x61fe10, rodents address = 0x61fe14
最初,rodents引用的是rats,但随后程序试图将rodents作为bunnies的引用。
rodents = bunnies;
乍一看,这种意图暂时是成功的,因为rodents的值从101变成了50.但仔细研究将发现,rats也变成了50,同时rats和rodents的地址相同,而地址与bunnies的地址不同。由于rodents是rats的别名,因此上述赋值语句与下面的语句等效:
rats = bunnies;
也就是说,这意味着:将bunnies变量的值赋给rat变量。简而言之,可以通过初始化声明来设置引用,但不能通过赋值来设置。
2、将引用用作函数参数
引用经常被用作函数参数,使得函数中的变量名成为调用程序中的变量的别名,这种传递参数的方法称为按引用传递。按引用传递允许被调用的函数能够访问调用函数中的变量。C++新增的这项特性是对C语言的超越,C语言只能按值传递。按值传递导致被调用函数使用调用程序的值的拷贝。当然,C语言也允许避开按值传递的限制,采用按指针传递的方式。
解决常见的计算机问题:交换两个变量的值,对使用引用和使用指针做一下比较。
交换函数必须能够修改调用程序中的变量的值。这意味着按值传递变量将不管用,因为函数将交换原始变量的副本的内容,而不是变量本身的内容。但传递引用时,函数将可以使用原始数据。另一种方法是,传递指针来访问原始数据。
程序清单:
#include
using namespace std;
void swapr(int & a, int & b); // a,b are aliases for ints
void swapp(int * p, int * q); // p,q are addresses of ints
void swapv(int a, int b); // a,b are new variables
int main()
{
int wallet1 = 300;
int wallet2 = 350;
cout << "wallet1 = $" << wallet1;
cout << " wallet2 = $" << wallet2 << endl;
cout << "Using references to swap contents:\n";
swapr(wallet1,wallet2); // pass variables
cout << "wallet1 = $" << wallet1;
cout << " wallet2 = $" << wallet2 << endl;
cout << "Using pointers to swap contents again:\n";
swapp(&wallet1,&wallet2); // pass addresses of variables
cout << "wallet1 = $" << wallet1;
cout << " wallet2 = $" << wallet2 << endl;
cout << "Trying to use passing by value:\n";
swapv(wallet1,wallet2); // passing values of variables
cout << "wallet1 = $" << wallet1;
cout << " wallet2 = $" << wallet2 << endl;
return 0;
}
void swapr(int & a,int & b)
{
int temp;
temp = a;
a = b;
b = temp;
}
void swapp(int * p, int * q)
{
int temp;
temp = *p;
*p = *q;
*q = temp;
}
void swapv(int a,int b)
{
int temp;
temp = a;
a = b;
b = temp;
}
运行结果:
wallet1 = $300 wallet2 = $350 << original values
Using references to swap contents:
wallet1 = $350 wallet2 = $300 << values swapped
Using pointers to swap contents again:
wallet1 = $300 wallet2 = $350 << values swapped again
Trying to use passing by value:
wallet1 = $300 wallet2 = $350 << swap failed
引用和指针方法都成功得交换了两个钱包(wallet)中的内容,而按值传递的方法没能完成这项任务。
程序说明:
swapr(wallet1,wallet2); // pass variables
swapp(&wallet1,&wallet2); // pass addresses of variables
swapv(wallet1,wallet2); // passing values of variables
按引用传递和按值传递看起来调用相同,只能通过原型或函数定义才能知道是如何传递的。然而,地址运算符(&)使得按地址传递(swapp())(类型声明int *p
表明,p是一个int指针,因此与p对应的参数应为地址,如&wallet1)。
比较函数swapr()(按引用传递)和swapv()(按值传递)的代码,唯一的外在区别是声明函数参数的方式不同:
void swapr(int & a, int & b);
void swapv(int a, int b);
内在区别是:在swapr()中,变量a和b是wallet1和wallet2的别名,所以交换a和b的值相当于交换wallet1和wallet2的值;但在swapv()中,变量a和b是复制了wallet1和wallet2的值的新变量。因此交换a和b的值并不会影响wallet1和wallet2的值。
比较函数swapr()(传递引用)和swapp()(传递指针),第一个区别是声明函数参数的方式不同:
void swapr(int & a, int & b);
void swapp(int * p, int * q);
另一个区别是指针版本需要在函数使用p和q的整个过程中使用解除引用运算符*
。
应在定义引用变量时对其进行初始化。函数调用使用实参初始化形参,因此函数的引用参数被初始化为函数调用传递的实参。也就是说,下面的函数调用将形参a和b分别初始化为wallet1和wallet2:
swapr(wallet1,wallet2);
3、 引用的属性和特别之处
使用引用参数时,需要了解其一些特点。
程序清单:
#include
using namespace std;
double cube(double a);
double refcube(double &ra);
int main()
{
double x =3.0;
cout << cube(x);
cout << " = cube of " << x << endl;
cout << refcube(x);
cout << " = cube of " << x << endl;
return 0;
}
double cube(double a)
{
a *= a * a;
return a;
}
double refcube(double &ra)
{
ra *= ra * ra;
return ra;
}
运行结果:
27 = cube of 3
27 = cube of 27
refcube()函数修改了main()中的x值,而cube()没有,这提醒了我们为何通常按值传递。变量a位于cube()中,它被初始化为x的值,但修改a并不会影响到x。但由于refcube()使用了引用参数,因此修改ra实际上就是修改了x。如果意图是让函数使用传递给它的信息,而不对这些信息进行修改,同时又想使用引用,则应使用常量引用。
例如:应在函数原型和函数头中使用const:
double refcube(const double &ra);
如果这样做,当编译器发现代码修改了ra的值,将生成错误的消息。
顺便说一句,如果要编写类似于上述示例 的函数(即使用基本数值类型),应采用按值传递的方式,而不要采用按引用传递的方式。
临时变量、引用参数和const
如果实参与引用参数不匹配,C++将生成临时变量。当前,仅当参数为const引用时,C++才允许这样做。
何种情况下,C++将生成临时变量,以及为何对const引用的限制是合理的。
首先,什么时候将创建临时变量呢?如果引用参数是const,则编译器将在下面两种情况下生成临时变量:
左值参数是可以被引用的数据对象。例如:变量、数组元素、结构成员、引用和解除引用的指针都是左值。非左值包括字面常量(用引号括起的字符串除外,它们由地址表示)和包含多项的表达式。在C语言中,左值最初指的是可出现在赋值语句左边的实体,但这是引入关键字const之前的情况,现在,常规变量和const变量都可视为左值,因为可通过地址访问它们。但常规变量属于可修改的左值,而const变量属于不可修改的左值。
注意:如果函数调用的参数不是左值或与相应的const引用参数的类型不匹配,则C++将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数来引用该变量。
应尽可能使用const
将引用参数声明为常量数据的引用的理由有三个:
因此,应尽可能将引用参数声明为const。
C++11 新增了另一种引用:右值引用(rvalue reference)。这种引用可指向右值,是使用&&声明的。
double && rref = std::sqrt(36.00); //not allowed for double &
double j = 15.0;
double && jref = 2.0 * j + 18.5; //not allowed for double &
std::cout << rref << '\n'; // display 6.0
std::cout << jref << '\n'; // display 48.5
新增右值引用的主要目的是:让库设计人员能够提供有些操作的更有效实现。
4、将引用用于结构
引用非常适合用于结构和类(C++的用户定义类型)。确实,引入引用主要是为了用于这些类型的,而不是基本的内置类型。
使用结构引用参数的方式与使用基本变量引用相同,只需要在声明结构参数时使用引用运算符&即可。
struct free_throws
{
string name;
int made;
int attempts;
float percent;
};
则可以这样编写函数原型,在函数中指向该结构的引用作为参数:
void set_pc(free_throws & ft); // use-a reference to a structure
如果不希望函数修改传入的结构,可用const:
void display(const free_throws & ft); //don't allow changes to strcture
程序清单:
#include
#include
using namespace std;
struct free_throws
{
string name;
int made;
int attempts;
float percent;
};
void display(const free_throws & ft);
void set_pc(free_throws & ft);
free_throws & accumulate(free_throws & target,const free_throws & source);
int main()
{
//partial initializations - remaining members set to 0
free_throws one = {"Ifelsa Branch",13,14};
free_throws two = {"Andor knott",10,16};
free_throws three = {"Minnie Max",7,9};
free_throws four = {"Whily Looper",5,9};
free_throws five = {"Long LOng",6,14};
free_throws team = {"Throwgoods",0,0};
// no initialization
free_throws dup;
set_pc(one);
display(one);
accumulate(team,one);
display(team);
// use return value as argument
display(accumulate(team,two));
accumulate(accumulate(team,three),four);
display(team);
// use return value in assignment
dup = accumulate(team,five);
cout << "Dispalying team:\n";
display(team);
cout << "Displaying dup after assignment:\n";
display(dup);
set_pc(four);
// ill - advised assignment
accumulate(dup,five) = four;
cout << "Displaying dup after ill-advised assignment:\n";
display(dup);
return 0;
}
void display(const free_throws & ft)
{
cout << " Name: " << ft.name << '\n';
cout << " Made: " << ft.made << '\t';
cout << "Attempts: " << ft.attempts << '\t';
cout << "Percent: " << ft.percent << '\n';
}
void set_pc(free_throws & ft)
{
if(ft.attempts != 0)
ft.percent = 100.0f * float(ft.made) / float(ft.attempts);
else
ft.percent = 0;
}
free_throws & accumulate(free_throws & target,const free_throws & source)
{
target.attempts += source.attempts;
target.made += source.made;
set_pc(target);
return target;
}
运行结果:
Name: Ifelsa Branch
Made: 13 Attempts: 14 Percent: 92.8571
Name: Throwgoods
Made: 13 Attempts: 14 Percent: 92.8571
Name: Throwgoods
Made: 23 Attempts: 30 Percent: 76.6667
Name: Throwgoods
Made: 35 Attempts: 48 Percent: 72.9167
Dispalying team:
Name: Throwgoods
Made: 41 Attempts: 62 Percent: 66.129
Displaying dup after assignment:
Name: Throwgoods
Made: 41 Attempts: 62 Percent: 66.129
Displaying dup after ill-advised assignment:
Name: Whily Looper
Made: 5 Attempts: 9 Percent: 55.5556
5、将引用用于类对象
将类对象传递给函数时,C++通常的做法是使用引用。例如:可以通过使用引用,让函数将类string、ostream、istream、ofstream和ifstream等类的对象作为参数。
6、对象、继承和引用
ofstream对象可以使用ostream类的方法,这使得文件输入/输出的格式与控制台输入/输出相同。使得能够将特性从一个类传递给另一个类的语言特性被称为继承。简单来说,ostream是基类(因为ofstream是建立在它的基础之上的),而ofstream是派生类(因为它是从ostream派生而来的)。派生类继承了基类的方法,这意味着ofstream对象可以使用基类的特性,如格式化方法precision()和setf()。
继承的另一个特征是,基类引用可以指向派生类对象,而无需进行强制类型转换。这种特征的一个实际结果是,可以定义一个接受基类引用作为参数的函数,调用该函数时,可以将基类对象作为参数,也可以将派生类对象作为参数。例如:参数类型ostream &的函数可以接受ostream对象(如cout)或声明的ofstream对象作为参数。
7、何时使用引用参数
使用引用参数的主要原因有两个:
当数据对像较大时(如结构和类对象),第二个原因最重要。这些也是使用指针参数的原因。这是有道理的,因为引用参数实际上是基于指针的代码的另一个接口。
什么时候应使用引用、什么时候应使用指针、什么时候应按值传递呢?
对于使用传递的值而不作为修改的函数。
对于修改调用函数中数据的函数:
默认参数值的是当函数调用中省略了实参时自动使用的一个值。例如:如果将void wow(int n)设置成n有默认值为1,则函数调用wow()相当于wow(1)。这极大得提高了使用函数的灵活性。假设有一个名为left()的函数,它将字符串和n作为参数,并返回该字符串的前n个字符。更准确地说,该函数返回一个指针,该指针指向原始字符串中被选中的部分组成的字符串。例如:函数调用left(“theory”,3)将创建新字符串"the",并返回一个指向该字符串的指针。现假设第二个参数的默认值被设置为1,则函数调用left(“theory”,3)仍像前面那样工作,3将覆盖默认值。但函数调用left(“theory”)不会出错,它认为第二个参数的值为1,并返回指向字符串"t"的指针。如果程序经常需要抽取一个字符组成的字符串,而偶尔需要抽取较长的字符串,则这种默认值将很有帮助。
默认值的设置必须通过函数原型。由于编译器通过查看原型来了解函数所使用的参数数目,因此函数原型也必须将可能的默认参数告知程序。方法是将值赋给原型中的参数。
char * left (const char * str,int n = 1);
==对于带参数列表的函数,必须从右向左添加默认值。==也就是说,要为某个参数设置默认值,则必须为它右边的所有参数提供默认值:
int harpo(int n,int m = 4,int j = 5); // valid
int chico(int n,int m = 6,int j); // invalid
int groucho(int k = 1, int m = 2,int n = 3); // valid
例如,harpo()原型允许调用该函数时提供1个、2个、3个参数。
beeps = harpo(2); // same as harpo(2,4,5)
beeps = harpo(1,8); // same as harpo(1,8,5)
beeps = harpo(8,7,6); // no default arguments used
==实参按从左到右的顺序依次被赋给相应的形参,而不能跳过任何参数。==因此,下面的调用是不允许的:
beeps = harpo(3,,8); // invalid , doesn't set m to 4
默认参数只是提供了一种便捷的方式。在设计类时,可以通过使用默认参数,减少要定义的析构函数、方法以及方法重载的数量。
程序清单:
#include
using namespace std;
const int ArSize = 80;
char * left(const char * str,int n = 1);
int main()
{
char sample[ArSize];
cout << "Enter a string:\n";
cin.get(sample,ArSize);
char *ps = left(sample,4);
cout << ps << endl;
delete [] ps; // free old string
ps = left(sample);
cout << ps << endl;
delete [] ps; // free new string
return 0;
}
char * left(const char * str,int n)
{
if(n < 0)
n = 0;
char * p = new char[n+1];
int i;
for(i = 0;i < n && str[i];i++)
p[i] = str[i]; // copy characters
while(i <= n)
p[i++] = '\0'; // set rest of string to '\0'
return p;
}
运行结果:
Enter a string:
forthcoming
fort
f
该程序使用new创建一个新的字符串,以存储被选择的字符。
i < n && str[i]
i < n 测试让循环复制了n个字符后终止。测试的第二部分:表达式str[i],是要复制的字符的编码。遇到空值字符(其编码为0)后,循环将结束。这样,while循环将使字符串以空值字符结束,并将余下的空间设置为空值字符。
另一种设置新字符串长度的方法是,将n设置为传递的值和字符串长度中较小的一个。
int len = strlen(str);
n = (n < len) ? n : len; // the lesser of n and len
char * p = new char[n+1];
这将确保new分配的空间不会多于存储字符串所需的空间。
例:当m的值等于n或到达字符串结尾时,下面的值循环都将终止:
int m = 0;
while(m <= n && str[m] != '\0')
m++;
char * p = new char[m+1]; //use m instead of n in rest of code
在str[m]不是空值字符时,表达式str[m] != '\0’的结果为true,否则为false。由于在&&表达式中,非零值被转换为true,而零被转换为false,因此也可以这样编写这个while测试:
while (m <= n && str[m])
函数重载指的是可以用多个同名的函数,因此对名称进行了重载。函数重载完成相同的工作,但使用不同的参数列表。
函数重载的关键是函数的参数列表,也称为函数特征标(function signature)。如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量名是无关紧要的。C++允许定义名称相同的函数,条件是它们的特征标不同。如果参数数目和/或参数类型不同,则特征标也不同。
一些看起来彼此不同的特征标是不能共存的。例如:
double cube(double x);
double cube(double & x);
可能认为在此处使用函数重载,因为它们的特征标看起来不同。然而,从编译器的角度来考虑:
cout << cube(x);
参数x与double x原型和double & x原型都匹配,因此编译器无法确定究竟应使用哪个原型。为避免这种混乱,编译器在检查函数特征标时,将把类型引用和类型本身视为同一种特征标。
匹配函数时,并不区分const和非const变量。
void dribble (char * bits); //overloaded
void dribble (const char * cbits); //overloaded
void dabble (char * bits); //not overloaded
void drivel(const char * bits); //not overloaded
注意:是特征标,而不是函数类型使得可以对函数进行重载。
例如:下面两个声明是互斥的:
long gronk (int n, float m);
double gronk (int n, float m);
//same signatures hence not allowed
因此,C++不允许以这种方式重载gronk()。返回类型可以不同,但特征标也必须不同:
long gronk (int n, float m);
double gronk (float n, float m);
//different signatures hence allowed
何时使用函数重载
虽然函数重载很吸引人,但也不要滥用。仅当函数基本上执行相同的任务,但使用不同的数据时,才应采用函数重载。另外,是否可以通过使用默认参数来实现同样的目的。
例如:可以用两个重载函数来代替面向字符串的left()函数:
char * left(const char * str,unsigned n); // two arguments
char * left(const char * str); // one argument
使用一个默认参数的函数要简单些,只需编写一个函数(而不是两个函数),程序也只需为一个函数(而不是两个)请求内存;需要修改函数时,只需修改一个。然而,如果需要使用不同类型的参数,则默认参数便不管用了,在这种情况下,应该使用函数重载。
函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数,其中的泛型可用具体的类型(如int 或 double)替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。由于模板允许泛型(而不是具体类型)的方式编写程序,因此有时也被称为通用编程。由于类型是用参数表示的,因此模板特性有时也被称为参数化类型(parameterized types)。
例如:定义一个交换两个int值的函数。假设要交换两个double值,则一种方法是复制原来的代码,并用double替换所有的int。如果需要交换两个char 值,可以再次使用同样的技术。进行这种修改将浪费宝贵的时间,且容易出错。如果进行手工修改,则可能会漏掉一个int。如果进行全局查找和替换(如用double替换int)时,可能将:
int x;
short interval;
//转换为:
double x; //intended change of type
short doubleerval; //unintended change of variable name
C++的函数模板功能能自动完成这一过程,可以节省时间,而且更可靠。
函数模板允许以任意类型的方式来定义函数。例如:可以建立一个交换模板:
template
void Swap(AnyType &a, AnyType &b)
{
AnyType temp;
temp = a;
a = b;
b = temp;
}
第一行指出,要建立一个模板,并将类型命名为AnyType。关键字template 和 typename是必需的,除非可以使用关键字class代替typename。另外,必须使用尖括号。类型名可以任意选择(这里是AnyType),只要遵守C++命名规则即可;许多程序员都使用简单的名称,如 T。
模板并不创建任何函数,而只是告诉编译器如何定义函数。需要交换int的函数时,编译器将按模板模式创建这样的函数,并用int代替AnyType。同样,需要交换double函数时,编译器将按模板模式创建这样的函数,并用double代替AnyType。
在标准C++98 添加关键字typename之前,C++使用关键字class来创建模板。也就是说,可以这样编写模板定义:
template
void Swap(AnyType &a, AnyType &b)
{
AnyType temp;
temp = a;
a = b;
b = temp;
}
typename关键字使得参数AnyType表示类型这一点更为明显;然而,有大量代码库是使用关键字class开发的。在这种上下文中,这两个关键字是等价的。
提示:如果需要多个将同一种算法用于不同类型的函数,请使用模板。如果不考虑向后兼容的问题,并愿意键入较长的单词,则声明类型参数时,应使用关键字typename而不使用calss。
程序清单:
#include
using namespace std;
// function template prototype
template // or class T
void Swap(T &a, T &b);
int main()
{
int i =10;
int j = 20;
cout << "i, j = " << i << ", " << j << ".\n";
cout << "Using compiler-generated int swapper:\n";
Swap(i,j); // generates void Swap(int &,int &)
cout << "Now i, j = " << i << ", "<< j <<".\n";
double x = 24.5;
double y = 81.7;
cout << "x, y = " << x<< ", " << y << ".\n";
cout << "Using compiler-generated double swapper:\n";
Swap(x,y);
cout << "Now x, y = " << x<< ", " << y << ".\n";
return 0;
}
//function template definition
template // or calss T
void Swap(T &a, T &b)
{
T temp; // temp a variable of type T
temp = a;
a = b;
b = temp;
}
运行结果:
i, j = 10, 20.
Using compiler-generated int swapper:
Now i, j = 20, 10.
x, y = 24.5, 81.7.
Using compiler-generated double swapper:
Now x, y = 81.7, 24.5.
程序说明:
第一个Swap()函数接受两个int参数,因此编译器生成该函数的int版本。也就是说,用int替换所有的T,生成下面这样的定义:
void Swap(int &a, int &b)
{
int temp;
temp = a;
a = b;
b = temp;
}
程序员看不到这些代码,但编译器确实生成并在程序中使用了它们。第二个Swap()函数接受两个double参数,因此编译器将生成double版本。也就是说,用double替换T,生成下述代码:
void Swap(double &a, double &b)
{
doouble temp;
temp = a;
a = b;
b = temp;
}
注意:函数模板不能缩短可执行程序。对于上述程序清单,最终仍将由两个独立的函数定义,就像以手工方式定义了这些函数一样。最终的代码不包含任何模板,而只包含了为程序生成的实际函数。使用模板的好处是,它使生成多个函数定义更简单、更可靠。
1、重载的模板
需要对多个不同类型使用同一种算法的函数时,可使用模板。然而,并非所有的类型都使用相同的算法,为满足这种需求,可以像重载常规函数定义那样重载模板定义。和常规重载一样,被重载的模板的函数特征标必须不同。
例如:下面程序新增一个交换模板,用于交换两个数组中的元素。原来的模板的特征标为(T &,T &),而新模板的特征标为(T [ ], T [ ],int)。注意,在后一个模板中,最后一个参数的类型为具体类型(int),而不是泛型。并非所有的模板参数都必须是模板参数类型。
程序清单:
#include
using namespace std;
// function template prototype
template // original template
void Swap(T &a, T &b);
template // new template
void Swap(T *a,T *b, int n);
void Show(int a[]);
const int Lim = 8;
int main()
{
int i = 10;
int j = 20;
cout << "i, j = " << i << ", " << j << ".\n";
cout << "Using compiler-generated int swapper:\n";
Swap(i,j); // matches original template
cout << "Now i, j = " << i << ", "<< j <<".\n";
int d1[Lim] = {0,7,0,4,1,7,7,6};
int d2[Lim] = {0,7,2,0,1,9,6,9};
cout << "Original arrays:\n";
Show(d1);
Show(d2);
Swap(d1,d2,Lim); // matches new template
cout << "Swapped arrays:\n";
Show(d1);
Show(d2);
return 0;
}
//function template definition
template
void Swap(T &a, T &b)
{
T temp;
temp = a;
a = b;
b = temp;
}
template
void Swap(T a[],T b[], int n)
{
T temp;
for(int i = 0; i < n;i++)
{
temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}
void Show(int a[])
{
cout << a[0] << a[1] << "/";
cout << a[2] << a[3] << "/";
for(int i = 4;i < Lim;i++)
cout << a[i];
cout << endl;
}
运行结果:
i, j = 10, 20.
Using compiler-generated int swapper:
Now i, j = 20, 10.
Original arrays:
07/04/1776
07/20/1969
Swapped arrays:
07/20/1969
07/04/1776
2、模板的局限性
假设有如下模板函数:
template // or template
void f(T a,T b){ }
通常,代码假定可执行哪些操作。例如:下面的代码假定定义了赋值,但如果T为数组,这种假设将不成立:
a = b
同样,下面的语句假设定义了<,但如果T为结构,该假设便不成立:
if(a > b)
另外,为数组名定义了运算符 > ,但由于数组名为地址,因此它比较的是数组的地址,而这种不是您希望的。
下面的语句假定为类型T定义了乘法运算符,但如果T为数组、指针或结构,这种假设便不成立:
T c = a * b;
总之,编写的模板函数很可能无法处理某些类型。
3、显式具体化
假设定义了如下结构:
struct job
{
char name[40];
double salary;
int floor;
};
另外,假设希望能够交换两个这种结构的内容。原来的模板使用下面的代码来完成交换:
temp = a;
a = b;
b = temp;
由于C++允许将一个结构赋给另一个结构,因此即使T是一个job结构,上述代码也适用。然而,假设只想交换salary和floor成员,而不交换name成员,则需要使用不同的代码,但Swap()的参数将保持不变(两个job结构的引用),因此无法使用模板重载来提供其他的代码。
然而,可以提供一个具体化函数定义,称为显示具体化(explicit specialization)。其中包含所需的代码。当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。
具体化机制随着C++的演变而不断变化。下面介绍C++标准定义的形式。
试验其他具体化方法后,C++98标准选择了下面的方法。
下面是用于交换job结构的非模板函数、模板函数和具体化的原型:
// non template function prototype
void Swap(job &, job &);
// template prototype
template
void Swap(T &,T &);
// explicit specialization for the job type
template <> void Swap(job &, job &);
如果有多个原型,则编译器在选择原型时,非模板版本优先于显式具体化和模板版本,而显式具体化优先于使用模板生成的版本。
例如:在下面的代码中,第一次调用Swap()时使用通用版本,而第二次调用使用基于job类型的显示具体化版本。
template
void Swap(T &, T &);
//explicit specialization for the job type
template <> void Swap(job &, job &);
int main()
{
double u,v;
Swap(u,v); // use template
job a, b;
SWap(a,b); // use void Swap(job &,job &)
}
Swap
中的job是可选的,因为函数的参数类型表明,这是job的一个具体化。因此,该原型也可以这样编写:
template <> void Swap(job &, job &); // simpler from
4、模板函数的发展
在C++发展的早期,大多数人都没有想到模板函数和模板类会有这么强大而有用。C++98标准做出了相应的修改,并添加了标准模板库。从此以后,模板程序员在不断探索各种可能性,并消除模板的局限性。C++11标准根据这些程序员的反馈做出了相应的修改。
1)是什么类型
在C++98中,编写模板函数时,一个问题是并非总能知道应在声明中使用哪种类型。
template
void ft(T1 x, T2 y)
{
?type? xpy = x + y;
}
xpy应为什么类型呢?由于不知道ft()将如何使用,因此无法预先知道这一点。正确的类型可能是T1、T2或其他类型。例如,T1可能是double,而T2可能是int,在这种情况下,两个变量的和将为double类型。T1可能是short,而T2可能是int,在这种情况下,两个变量的和将为int类型。T1还可能是short,而T2可能是char,在这种情况下,加法运算符将导致自动整型提升,因此结果类型为int。另外,结构和类可能重载运算符+,这导致问题更加复杂。因此,在C++98中,没有办法声明xpy的类型。
2)关键字decltype(C++11)
C++11 新增的关键字decltype提供了解决方案。可这样使用该关键字:
int x;
decltype(x) y; // make y the same type as x
给decltype提供的参数可以是表达式,因此在前面的模板函数ft()中,可使用下面的代码:
decltype(x + x) xpy; // make xpy the same type as x + y
xpy = x + y;
或者,将这两条语句合而为一:
decltype(x + x) xpy = x + y;
因此,可以这样修复前面的模板函数ft():
template
void ft(T1 x, T2 y)
{
decltype(x + x) xpy = x + y;
}
为确定类型,编译器必须遍历一个核对表。假设有如下声明:
decltype(expression) var;
则核对表的简化版如下:
第一步:如果expression是一个没有用括号括起的标识符,则var的类型与该标识符的类型相同,包括const等限定符:
double x = 5.5;
double y = 7.9;
double &rx = x;
const double * pd;
decltype(x) w; // w is type double
decltype(rx) u = y; // u is type double &
decltype(pd) v; // v is type const double *
第二步:如果expression是一个函数调用,则var的类型与函数的返回值类型相同:
long indeed(int);
decltype (indeed(3)) m; // m is type int
注意:并不会实际调用函数。编译器通过查看函数的原型来获悉返回类型,而无需实际调用函数。
第三步:如果expression是一个左值,则var为指向其类型的引用。这好像意味着前面的w应为引用类型。因为x是一个左值。但别忘了,这种情况已经在第一步处理过了,要进入第三步,expression不能是未用括号括起的标识符。那么,expression是什么时候将进入第三步呢?一种显而易见的情况是,expression是用括号括起的标识符:
double xx = 4.4;
decltype ((xx)) r2 = xx; // r2 is double &
decltype (xx) w = xx; // w is double (Stage 1 match)
顺便说一句,括号并不会改变表达式的值和左值性。例如:下面两条语句是等效的:
xx = 98.6;
(xx) = 98.6; // () don't affect use of xx
第四步:如果前面的条件都不满足,则var的类型与expression的类型相同:
int j = 3;
int &k = j;
int &n = j;
decltype(j+6) 11; // 11 type int
decltype(100L) i2; // i2 type long
decltype(k+n) i3; // i3 type int
请注意:虽然k和n都是引用,但表达式k+n不是引用;它是两个int的和,因此类型为int。
如果需要多次声明,可结合使用typedef和decltype。
template
void ft(T1 x, T2 y)
{
typedef decltype(x + x) xytype;
xytype xpy = x + y;
xytype arr[10];
xytype & rxy = arr[2]; // rxy a reference
}
3) 另一种函数声明语法(C++11 后置返回类型)
有一个相关的问题是decltype本身无法解决的。请看下面这个不完整的模板函数:
template
?type? gt(T1 x, T2 y)
{
return x + y;
}
同样,无法预先知道将x和y相加得到的类型。好像可以将返回类型设置为decltype(x + y)。但不幸的是,此时还未声明参数x和y,它们不在作用域内(编译器看不到它们,也无法使用它们)。必须在声明参数后使用decltype。为此,C++新增了一种声明和定义函数的语法。
例如:使用内置类型来说明这种语法的工作原理:
double h(int x, float y);
使用新增的语法可编写成这样:
auto h(int x, float y) -> double;
这将返回类型移到了参数声明的后面。->double被称为后置返回类型(trailing return type)。其中auto是一个占位符,表示后置返回类型提供的类型。
也可用于函数定义:
auto h(int x, float y) -> double
{
/* function body */
};
通过结合使用这种语法和decltype,便可给gt()指定返回类型。
template
auto gt(T1 x, T2 y) -> decltype(x + y)
{
return x + y;
}
现在,decltype再参数声明后面,因此x和y位于作用域内,可以使用它们。
C++扩展了C语言的函数功能,通过将inline关键字用于函数定义,并在首次调用该函数前提供其函数定义,可以使得C++编译器将该函数视为内联函数。也就是说,编译器不是让程序跳到独立的代码段,以执行函数,而是用相应的代码替换函数调用,只有在函数很短时才能采用内联方式。
引用变量是一种伪装指针,它允许为变量创建别名(另一个名称)。引用变量主要被用作处理结构和类对象的函数的参数。通常,被声明为特定类型引用的标识符只能指向这种类型的数据;然而,如果一个类(如ofstream)是从另一个类(如ostream)派生出来的,则基类引用可以指向派生类对象。
C++原型可以定义参数的默认值。如果函数调用省略了相应的参数,则程序将使用默认值;如果函数调用提供了参数值,则程序将使用这个值(而不是默认值)。只能在参数列表中从右到左提供默认参数。因此,如果为某个参数提供了默认值,则必须为该参数右边所有的参数提供默认值。
函数的特征标是参数列表。程序员可以定义两个同名函数,只要其特征标不同。这被称为函数多态或函数重载。通常,通过重载函数来为不同的数据类型提供相同的服务。
函数模板自动完成重载函数的过程。只需使用泛型和具体算法来定义函数,编译器将为程序中使用的特定参数类型生成正确的函数定义。