C++ Learning (Next)

第五章 循环和关系表达式

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 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 <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 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 <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) 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 <iostream>
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;
}
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 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 <iostream>
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

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

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 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 <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 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 <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 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 <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 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 用于指针有一些微妙的地方。可以用两种不同的方式将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使得函数能够处理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)
{
    statements
    str++;
}

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 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来访问新建的字符串。

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 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()重置输入,然后还可能需要通过读取不合法的输入来丢弃它们。

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 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]就是一个字符串

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: 244

EXPENSES
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)
{
    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 <iostream>
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)
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 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相同,这将导致编译错误。

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类型。

🚢内联与宏

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++内联函数。

7.2 引用变量

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 <iostream>
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 <iostream>
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 <iostream>
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 <iostream>
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可以避免无意中修改数据的编程错误;
  • 使用const使函数能够处理const和非const实参,否则将只能接受非const数据;
  • 使用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 <iostream>
#include <string>
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、何时使用引用参数

使用引用参数的主要原因有两个:

  • 程序员能够修改调用函数中的数据对象
  • 通过传递引用而不是整个数据对象,可以提高程序的运行速度。

当数据对像较大时(如结构和类对象),第二个原因最重要。这些也是使用指针参数的原因。这是有道理的,因为引用参数实际上是基于指针的代码的另一个接口。

什么时候应使用引用、什么时候应使用指针、什么时候应按值传递呢?

对于使用传递的值而不作为修改的函数。

  • 如果数据对象很小,如内置数据类型或小型结构,则按值传递。
  • 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针。
  • 如果数据对象是较大的结构,则使用const指针或const引用,以提高程序的效率。这样可以节省复制结构所需的时间和空间。
  • 如果数据对象是类对象,则使用const引用。类设计的语义常常要求使用引用,这是C++新增这项特性的主要原因。因此,传递类对象参数的标准方式是按引用传递。

对于修改调用函数中数据的函数:

  • 如果数据对象是内置数据类型,则使用指针。如果看到诸如fixit(&x)这样的代码(其中x是int),则很明显,该函数将修改x。
  • 如果数据对象是数组,则只能使用指针。
  • 如果数据对象是结构,则使用引用或指针。
  • 如果数据对象是类对象,则使用引用。
7.3 默认参数

默认参数值的是当函数调用中省略了实参时自动使用的一个值。例如:如果将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 <iostream>
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])
7.4 函数重载

函数重载指的是可以用多个同名的函数,因此对名称进行了重载。函数重载完成相同的工作,但使用不同的参数列表。

函数重载的关键是函数的参数列表,也称为函数特征标(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

使用一个默认参数的函数要简单些,只需编写一个函数(而不是两个函数),程序也只需为一个函数(而不是两个)请求内存;需要修改函数时,只需修改一个。然而,如果需要使用不同类型的参数,则默认参数便不管用了,在这种情况下,应该使用函数重载。

7.5 函数模板

函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数,其中的泛型可用具体的类型(如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 <typename AnyType>
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 <class AnyType>
void Swap(AnyType &a, AnyType &b)
{
    AnyType temp;
    temp = a;
    a = b;
    b = temp;
}

typename关键字使得参数AnyType表示类型这一点更为明显;然而,有大量代码库是使用关键字class开发的。在这种上下文中,这两个关键字是等价的。

提示:如果需要多个将同一种算法用于不同类型的函数,请使用模板。如果不考虑向后兼容的问题,并愿意键入较长的单词,则声明类型参数时,应使用关键字typename而不使用calss。

程序清单:

#include <iostream>
using namespace std;

// function template prototype
template <typename T>		// 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 <typename T>	// 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 <iostream>
using namespace std;

// function template prototype
template <typename T>		// original template
void Swap(T &a, T &b);

template <typename T>		// 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 <typename T>	
void Swap(T &a, T &b)
{
    T temp;				
    temp = a;
    a = b;
    b = temp;
}

template <typename T>	
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 <class T>			// or template <typename T>	
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标准选择了下面的方法。

  • 对于给定的函数名,可以有非模板函数、模板函数和显式具体化模板函数以及它们的重载版本。
  • 显式具体化的原型和定义应以template<>开头,并通过名称来指出类型。
  • 具体化优先于常规模板,而非模板函数优于具体化和常规模板。

下面是用于交换job结构的非模板函数、模板函数和具体化的原型:

// non template function prototype
void Swap(job &, job &);
    
// template prototype
template <typename T>
void Swap(T &,T &);

// explicit specialization for the job type
template <> void Swap<job>(job &, job &);

如果有多个原型,则编译器在选择原型时,非模板版本优先于显式具体化和模板版本,而显式具体化优先于使用模板生成的版本。

例如:在下面的代码中,第一次调用Swap()时使用通用版本,而第二次调用使用基于job类型的显示具体化版本。

template <class T>
void Swap(T &, T &);
    
//explicit specialization for the job type
template <> void Swap<job>(job &, job &);
int main()
{
    double u,v;
    Swap(u,v);	// use template
    job a, b;
    
    SWap(a,b);	// use void Swap<job>(job &,job &)
} 

Swap<job>中的job是可选的,因为函数的参数类型表明,这是job的一个具体化。因此,该原型也可以这样编写:

template <> void Swap(job &, job &);	// simpler from

4、模板函数的发展

在C++发展的早期,大多数人都没有想到模板函数和模板类会有这么强大而有用。C++98标准做出了相应的修改,并添加了标准模板库。从此以后,模板程序员在不断探索各种可能性,并消除模板的局限性。C++11标准根据这些程序员的反馈做出了相应的修改。

1)是什么类型

在C++98中,编写模板函数时,一个问题是并非总能知道应在声明中使用哪种类型。

template<class T1, class T2>
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<class T1, class T2>
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 <class T1, class T2>
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 <class T1, class T2>
?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<class T1, class T2>
auto gt(T1 x, T2 y)	-> decltype(x + y)
{
    return x + y;
}    

现在,decltype再参数声明后面,因此x和y位于作用域内,可以使用它们。

7.6 总结

C++扩展了C语言的函数功能,通过将inline关键字用于函数定义,并在首次调用该函数前提供其函数定义,可以使得C++编译器将该函数视为内联函数。也就是说,编译器不是让程序跳到独立的代码段,以执行函数,而是用相应的代码替换函数调用,只有在函数很短时才能采用内联方式。

引用变量是一种伪装指针,它允许为变量创建别名(另一个名称)。引用变量主要被用作处理结构和类对象的函数的参数。通常,被声明为特定类型引用的标识符只能指向这种类型的数据;然而,如果一个类(如ofstream)是从另一个类(如ostream)派生出来的,则基类引用可以指向派生类对象。

C++原型可以定义参数的默认值。如果函数调用省略了相应的参数,则程序将使用默认值;如果函数调用提供了参数值,则程序将使用这个值(而不是默认值)。只能在参数列表中从右到左提供默认参数。因此,如果为某个参数提供了默认值,则必须为该参数右边所有的参数提供默认值。

函数的特征标是参数列表。程序员可以定义两个同名函数,只要其特征标不同。这被称为函数多态或函数重载。通常,通过重载函数来为不同的数据类型提供相同的服务。

函数模板自动完成重载函数的过程。只需使用泛型和具体算法来定义函数,编译器将为程序中使用的特定参数类型生成正确的函数定义。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cappuccino-jay

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值