C++ Learning (Next)

C++ Learning (Next),第1张

第五章 循环和关系表达式 5.1 for循环 使用for循环访问字符串
#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风格字符串的比较

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


C风格字符串库中的strcmp()函数来比较,该函数接受两个字符串地址作为参数,这意味着参数可以是指针、字符串常量或字符数组名。


如果两个字符串相同,该函数将返回零;如果第一个字符串按字母顺序排在第二个字符串之前,则strcmp()将返回一个负值;如果第一个字符串按字母顺序排在第二个字符串之后,则strcpm()将返回一个正数值。


在有些语言(如BASIC和标准Pascal)中,存储在不同长度的数组中的字符串彼此不相等。


但在C风格字符串是通过结尾的空值字符定义的,而不是由其所在数组的长度定义的。


这意味着两个字符串即使被存储在长度不同的数组中,也可能是相同的。


char big[80] = "Daffy";			// 5 letters plus for(ch = 'a';ch <= 'z';ch++)
    cout << ch;

char little[6] = "Daffy";		// 5 letters plus ?ate
aate
bate
cate
date
eate
fate
gate
hate
iate
jate
kate
late
After loop end,word is mate

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


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

strcmp(word,"mate") = 0;	

程序清单:
例如:在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;
}

运行结果:

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

程序说明:

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

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

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


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


类型别名

C++为类型建立别名的方式有两种。


一种是使用预处理器:

typedef typeName aliasName;

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


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


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

typedef char *  byte_pointer;	// pointer to char type

通用格式:

typedef不会创建新类型,而只是为已有的类型建立一个新名称

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


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

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

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


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


如果将word作为int的别名,则cout将把word类型的值视为int类型。


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

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


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


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

其中,x最初表示数组prices的第一个元素。


显示第一个元素后,不断执行循环,而x依次表示数组的其他元素。


因此,上述代码显示全部5个元素,每个元素占据一行。


总之,该循环显示数组中的每个值。


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

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

符号&表明x是一个引用变量。


这种声明让接下来的代码能够修改数组的内容。


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

Enter characters;enter # to quit:
Did you use a #2 pencil?
Did you use a
14 characters read
5.3 循环和文本输入

循环完成的一项最常见、最重要的任务:逐字符地读取来自文件或键盘的文本。


例如:可能想要编写一个能够计算输入中的字符数、行数和字数的程序。


传统上,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;
}

运行结果:

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

注意: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;
}

运行结果:

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

现在,该程序回显了每个字符,并将全部字符计算在内,其中包括空格。


输入仍被缓冲,因此输入的字符个数仍可能比最终到达程序的要多。


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

char *

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

char *

cin.get()的一个版本接受两个参数:数组名(字符串(char ch; cin.get(ch); 类型)的地址)和ArSize(int类型的整数)。


其中,数组名是第一个元素的地址,因此字符数组名的类型为char *


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

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

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


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


例如:如果在C++中使用cin.get(name,ArSize),则编译器将找到void functionName(parameterList) { statement(s) return; // optional } 和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;
}
5.4 嵌套循环和二维数组

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

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

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


第六章 函数:C++的编程模块 6.1 函数的基本知识

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


#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函数。


其通用格式:

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

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


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


通常,可以用void函数来执行某种 *** 作。


例如,将Cheers!打印指定次数(n)的函数:

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

对于有返回值的函数,必须使用返回语句,以便将值返回给调用函数。


值本身可以是常量、变量,也可以是表达式,只是其结果的类型必须为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;
}

运行结果:

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

为什么需要原型:

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


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

首先,原型告诉编译器,cube()有一个double参数。


如果程序没有提供这样的参数,原型将让编译器能够捕获这种错误。


其次,cube()函数完成计算后,将把返回值放置在指定的位置——可能是CPU寄存器,也可能是内存中。


然后调用函数将从这个位置取得返回值。


由于原型指出了cube()的类型为double,因此编译器知道应检索多少个字节以及如何解释它们。


原型的语法:

函数原型是一条语句,因此必须以分号结束。


获得原型最简单的方法是,复制函数定义的函数头,并添加分号。


double volume = cube(side);

然而,函数原型不要求提供变量名,有类型列表就足够了。


对于cheer()的原型,该程序只提供了参数类型:

double cube(double x)

通常,在原型的参数列表中,可以包括变量名,也可以不包括。


原型中的变量名相当于占位符,因此不必与函数定义中的变量名相同。


6.2 函数参数和按值传递

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


用于接受传递值的变量被称为形参,传递给函数的值被称为实参

cube()的函数头:

n_chars('R',25);

void n_chars(char c,int n); // two arguments


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


在函数中声明的变量(包括参数)是该函数私有的。


在函数被调用时,计算机将为这些变量分配内存;在函数结束时,计算机将释放这些变量使用的内存。


这样的变量被称为局部变量。


多个参数

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


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

该函数头指出,函数n_char接受一个char参数和一个int参数。


传递给函数的值被赋给参数c和n。


如果函数的两个参数的类型相同,则必须分别指定每个参数的类型,而不能像声明常规变量那样,将声明组合在一起。


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

和其他函数一样,只需要添加分号就可以得到该函数的原型:

double melon_density(double weight,double volume);

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

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

然而,提供变量名将使原型更容易理解,尤其是两个参数的类型相同时。


这样,变量名可以提醒参量和参数间的对应关系:

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

程序清单:

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

运行结果:

int sum_arr(int arr[],int n)	// arr = array name , n = size
另外一个接受两个参数的函数

程序清单:

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

运行结果:

Total cookies eaten: 255

程序说明:
首先是形参(number 和 picks),这是在左括号前面的函数头声明的;其次是其他局部变量(result、n和p),它们是在将函数定义括起的括号内声明的。


形参与其他局部变量的主要区别是,形参从调用probability()的函数那里获得自己的值,而其他变量是从函数中获得自己的值。


6.3 函数和数组
C++将数组名解释为其第一个元素的地址

方括号指出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;
}

运行结果:

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

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

在大多数情况下,C++和C语言一样,也将数组名视为指针。


int sum = sum_arr(cookies,ArSize);


int*

该规则有一些例外。


首先,数组声明使用数组名来标记存储位置;其次,对数组名使用sizeof将得到整个 数组的长度(以字节为单位);第三,将地址运算符&用于数组名时,将返回整个数组的地址。


例如:&cookies将返回一个32字节内存块的地址。


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

其中,cookies是数组名,而根据C++规则,cookies是其第一个元素的地址,因此函数传递的是地址。


由于数组的元素的类型是int,因此cookies的类型必须是int指针,即int * arr


这表明,正确的函数头应该是:

int * arr

其中用int * arr替换了int arr[ ]。


这证明这两个函数头都是正确的,因为在C++中,当(且仅当)用于函数头或函数原型中,arr[i] == *(arr + i) // values in two notations &arr[i] == arr + i // addresses int two notation 和int arr[ ]的含义才是相同的。


它们都意味着arr是一个int指针。


然而,数组表示法(int arr[ ])提醒用户,arr不仅指向int,还指向int数组的第一个int。


当指针指向数组的第一个元素时,使用数组表示法;而当指针指向一个独立的值时,使用指针表示法。


注意:在其他上下文中, // arr告知数组地址 int sum_arr(int arr[],int n) // arr[]与*arr相同,指出arr是指针 和int arr[ ]的含义并不相同。


例如:不能在函数体中使用int tip[ ]来声明指针。


鉴于变量arr实际上就是一个指针,函数的其余部分是合理的。


同数组名和指针一样,也可以用方括号表示法来访问数组元素。


无论arr是指针还是数组名,表达式arr[3]都指的是数组的第4个元素。


就目前而言,下面两个是恒等式,将不会有任何的坏处:

将数组地址作为参数可以节省复制整个数组所需的时间和内存

记住,将指针(包括数组名)加1,实际上是加上了一个与指针指向的类型的长度(以字节为单位)相等的值。


对于遍历数组而言,使用指针加法和数组下标时等效的。


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

实际上并没有将数组的内容传递给函数,而是将数组的位置(地址)、包含的元素种类(类型)以及元素数目(n变量)提交给函数。


有了这些信息后,函数便可以使用原来的数组。


传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组。


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


如果数组很大,则使用拷贝的系统开销将非常大;程序不仅需要更多的计算机内存,还需要花费时间来复制大块的数据。


另一方面,使用原始的数据增加了破坏数据的风险。


程序清单:

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

运行结果:

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

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


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

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

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

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

更多数组函数示例:
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。


Enter value #1:100000
Enter value #2:80000
Enter value #3:222000
Enter value #4:240000
Enter value #5:118000
Property # 1: 0000
Property # 2: 000
Property # 3: 2000
Property # 4: 0000
Property # 5: 8000
Enter revaluation factor: 0.8
Property # 1: 000
Property # 2: 000
Property # 3: 7600
Property # 4: 2000
Property # 5: 400
Done.

该声明表明,指针arr指向的是常量数据,这意味着不能使用arr修改该数据,也就是说,可以使用像arr[0这样的值,但不能修改。


注意:这并不是意味着原始数组必须是常量,而只是意味着不能在show_array()函数中使用arr来修改这些数据。


因此,show_array()将数组视为只读数据。


3、修改数组

例如:将每个元素与同一个重新评估因子相乘。


需要给函数传递3个参数:因子、数组和元素数目。


double elboud[20];

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

运行输出:

Total cookies eaten: 255
First three eaters ate 7 cookies.
Last four eaters ate 240 cookies.
使用数组区间的函数

一种给函数提供所需信息的方法:指定元素区间(range)。


可以通过传递两个指针来完成:一个指针标识数组的开头,另一种指针标识数组的结尾。


例如:C++标准模板库,将区间方法广义化了。


STL方法使用“超尾”概念来指定区间。


也就是说,对于数组而言,标识数组结尾的参数将指向最后一个元素后面的指针。


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

则指针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;
}

运行结果:

*pt

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


因此,区间[cookies, cookies + ArSize]指向的是整个数组。


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


指针和const

将const 用于指针有一些微妙的地方。


可以用两种不同的方式将const关键字用于指针。


第一种方法是让指针指向一个常量对象,这样可以防止使用该指针来修改所指向的值,第二种方法是将指针本身声明为常量,这样可以防止改变指针指向的位置。


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

该声明指出,pt指向一个const int,因此不能使用pt来修改这个值。


换句话来说,const float g_earth = 9.80; const float * pe = &g_earch; // valid const float g_moon = 1.63; float * pm = &g_moon; // invalid 的值为const,不能被修改:

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

pt的声明并不意味着它指向的值实际上就是一个常量,而只是意味着对pt而言,这个 值是常量。


例如:pt指向age,而age不是const。


可以直接通过age变量来修改age的值,但不能使用pt指针来修改它:

  • 这样可以避免由于无意间修改数据而导致的编程错误;
  • 以前将常规变量的地址赋给常规指针,现在若将常规的地址赋给指向const指针。


    出现两种可能:将const变量的地址赋给指向const的指针、将const的地址赋给常规指针。


    第一种是可行的,但是第二种是不可行的。


    int sum(int (*ar2)[4],int size);
    

    C++禁止将const的地址赋给非const指针。


    如果非要这样做,可以使用强制类型转换来突破这种限制。


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


    尽可能使用const

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

      int *ar2[4];
    • 使用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 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;
    }
    

    还有另外一种格式,这种格式与上述原型的含义完全相同,但可读性更强。


    ar2[r][c]

    上述两个原型都指出,ar2是指针而不是数组。


    还需要注意的是,指针类型指出,它指向由4个int组成的数组。


    因此,指针类型指定了列数,这就是没有将列数作为独立的函数参数进行传递的原因。


    由于参数ar2是指向数组的指针,那么如何在函数定义中使用它,最简单的方法就是将ar2看作是一个二维数组的名称。


    ar2[r][c]

    同样,行数被传递给size参数,但无论是参数ar2的声明或是内部for循环中,列数都是固定的:4列。


    可以使用数组表示法的原因:由于ar2指向数组的第一个元素,因此表达式ar2+r指向编号为r的元素。


    因此ar2[r]是编号为r的元素。


    由于该元素本身就是一个由4个int组成的数组,因此ar2[r]是由4个int组成的数组的名称。


    将下标用于数组名将得到一个数组元素,因此ar2[r][c] == *(*(ar2 + r) + c) 是由4个int组成的数组中的一个元素,是一个int值。


    必须对指针ar2执行两次解除引用,才能得到数据。


    最简单的方法是使用方括号两次: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]


    或者也可以使用运算符* 两次。


  • char 数组;
  • 用引号括起的字符串常量(也称字符串字面值);
  • 6.5 函数和C-风格字符串

    C-风格字符串由一系列字符组成,以空值字符结尾。


    将字符串作为参数时意味着传递的是地址,但可以使用const来禁止对字符串参数进行修改。


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

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

    • 被设置为字符串的地址的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

    上述3种选择的类型都是char指针(准确地说是char *),因此可以将其作为字符串处理函数的参数:

    3 m characters in minimum
    2 u characters in ululate
    

    可以说是将字符串作为参数来传递,但实际传递的是字符第一个字符的地址。


    这意味着字符串函数原型应将其表示字符串的形参声明为while(*str) { statements str++; } 类型。


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

    运行结果:
     
    
    *str


    c_in_str()函数不应修改原始字符串,因此它在声明形参str时使用了限定符const。



    这样,如果错误地址函数修改了字符串的内容,编译器将捕获这种错误。


    处理字符串中字符的标准方式:
     
    
    *str


    str最初指向字符串的第一个字符,因此*str表示的是第一个字符。



    例如:第一次调用该函数后,*str的值将为m——"minimum"的第一个字符。



    只要字符不为空值字符(\0),函数无法返回一个字符串,但是可以返回字符串的地址就为非零值,因此循环将继续。



    在每轮循环的结尾处,表达式str++将指针增加一个字节,使之指向字符串的下一个字符。



    最终,str将指向结尾的空值字符,使得Enter a character: V Enter an integer: 46 VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV ++++++++++++++++++++ DONE ++++++++++++++++++++ 等于0——空值字符的数字编码,从而结束循环。


    返回C-风格字符串的函数
    要创建包含n个字符的字符串,需要能够存储n+1个字符的空间,以便能够存储空值字符,这样做效率更高。



    例如:下面程序定义一个buildstr()的函数,该函数返回一个指针。



    该函数接受两个参数:一个字符和一个数字。



    函数使用new创建一个长度与数字参数相等的字符串,然后将每个元素都初始化为该字符,然后,返回指向新字符串的指针。


     
    #include << "Enter a character: ";
        cin >
    using namespace std;
    char * buildstr(char c, int n);		// prototype
    int main()
    {
        
        int times;
        char ch;
        
        cout << "Enter an integer: ";
        cin >> ch;
        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] = '';							// terminate string
        while (n-- >> times;
        char *ps = buildstr(ch,times);
        cout 
    0) pstr[n] = c; // fill rest of string return pstr; }

    运行结果:

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

    程序说明:符号&来表示地址运算符


    该函数请求分配n+1个字节的内存来存储该字符串,并将最后一个字符设置为空值字符,然后从后向前对数组进行填充。

    下面的循环将循环n次,直到n减少为0,这将填充n个元素: 
    
    while(n-- > 0) ptsr[n] = c;


    在最后一轮循环开始时,n的值为1。


    由于n是先使用这个值,然后将其递减,因此while循环测试条件将对1和0进行比较,发现测试为true,循环继续。


    测试后,函数将n减为0,因此pstr[0]是最后一个被设置为c的元素。


    之所以从后向前(而不是从前向后)填充字符串,是为了避免使用额外的变量。

    从前向后填充代码:

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

    注意:变量pstr的作用域为buildstr函数内,因此该函数结束时,pstr(而不是字符串)使用的内存将被释放。


    但由于函数返回了pstr的值,因此程序仍然可以通过main()中的指针ps来访问新建的字符串。


    6.6 函数和结构

    现在将注意力从数组到结构。


    为结构编写函数比为数组编写函数简单得多。


    虽然结构变量和数组一样,都可以存储多个数据项,但在涉及到函数时,结构变量的行为更接近于基本的单值变量。


    也就是说,与数组不同,结构将其数据组合成单个实体或数据对象,该实体被视为一个整体。


    可以将一个结构赋给另外一个结构,同样,也可以按值传递结构,就像普通变量那样。


    在这种情况下,函数将使用原始结构的副本。


    另外,函数也可以返回结构。


    与数组名(数组第一个元素的地址)不同的是,结构名只是结构的名称,要获得结构的地址,必须使用地址运算符&。


    在C语言和C++中,都使用如果程序在输入循环后还需要进行输入,则必须使用cin.clear()重置输入;另外,C++还使用该运算符来表示引用变量。

    1、传递和返回结构:
    程序清单: 
    << "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";
    }
    #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

    运行输出:

  • 调用函数时,将结构的地址(&pplace)而不是结构本身(pplace)传递给它;

  • 其中,travel_time就像是一个标准的类型名,可被用来声明变量、函数的返回值和函数的参数类型。


    由于total和t1变量是travel_time结构,因此可以对它们使用句点成员运算符。


    由于sum()函数返回travel_time结构,因此可以将其用作show_time()函数的参数。


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

    介绍个处理空间,而不是时间的案例。


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

     
    #include << "Enter the x and y values: ";
        while (cin >
    #include << "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";
    }
    
    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 
    > rplace.x >> rplace.y) { pplace = rect_to_polar(rplace); show_polar(pplace); cout


    程序说明:
     该程序如何使用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的值,导致循环结束。

     
    < limit; i++)
    {
        cout << "Enter value #" << (i+1)<<": ";
        cin >< 0)
            break;
        ar[i] = temp;
    }
    for(int i = 0; i 
    > temp; if(temp


    要提早结束该循环,可以输入一个负值。


    将cin>>用作测试条件消除了这种限制,因为它接受任何有效的数字输入。


    在需要使用循环来输入数字时,可以采用这种方式。


    polar*,然后还可能需要通过读取不合法的输入来丢弃它们。


    3、传递结构的地址

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

      重新编写show_polar()函数,需要修改三个地方:

    • 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

      将形参声明为指向polar的指针,即string list[SIZE]; // an array holding 5 string object 类型。

    • 由于函数不应该修改结构,因此使用const修饰符;

      由于形参是指针而不是结构,因此应使用间接运算符(->),而不是成员运算符(句点)。

     
    << "distance = " << pda-><< ", angle = " << pda->void show_polar (const polar * pda)
    {
        const double Rad_to_deg = 57.29577951;
        
        cout  << " degrees\n";
    }
    distance;
        cout 
    angle * Rad_to_deg; cout


    6.7 函数和string对象

    虽然C- 风格字符串和string对象的用途几乎相同,但与数组相比,string对象与结构的更相似。


    例如:可以将一个结构赋给另一个结构,也可以将一个对象赋给另一个对象。


    可以将结构作为完整的实体传递给函数,也可以将对象作为完整的实体进行传递。


    如果需要使用多个字符串,可以声明一个string对象数组,而不是二维char数组。

    程序清单: 
    #include << "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;
    }
    
    #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

    运行结果:

    getline(cin,list[i]);

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

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

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

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

    也可以传递指向对象的指针,这让函数能够 *** 作原始对象

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

    Enter Spring expenses: 212 Enter Summer expenses: 255 Enter Fall expenses: 208 Enter Winter expenses: 244 EXPENSES Spring: 2 Summer: 5 Fall: 8 Winter: 4 Total Expenses: 9

    string与char的区别:

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

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

    char数组可以表示字符串,比如:char[10]就是一个字符串
    6.8 函数与array对象

    在C++中,类对象是基于结构的,因此结构编程方面的有些考虑因素也适用于类。


    例如:可按值将对象传递给函数,在这种情况下,函数处理的是原始对象的副本。


    另外,*pa


    请注意:模板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< Seasons;i++)
        {
            cout << "Enter " << Snames[i] << " expenses: ";
            cin > expenses;
        fill(&expenses);
        show(expenses);
        return 0;
    }
    
    void fill (std::array *pa)
    {
        for(int i = 0; i << "\nEXPENSES\n";
        for(int i = 0; i < Seasons;i++)
        {
            cout << Snames[i] << ": $" << da[i] << endl;
            total += da[i];
        }
        cout << "Total Expenses: $" << total << endl;
    }
    > (*pa)[i];
        }
    }
    
    void show (std::array
    da) { double total = 0.0; cout

    运行结果:

    (*pa)[i]

    程序说明:

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

     
    
    cin >> (*pa)[i];


    pa是一个指向array

    对象的指针,因此void recurs(argumentlist) { statements1 if (test) recurs(arguments) statements2 } 为这种对象,而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! 是该对象的一个元素。


    由于运算符优先级的影响,其中的括号必不可少。


    6.9 递归

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


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

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


    通常的方法将递归调用放在if语句中。

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

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

    test最终将为false,调用链将断开。


    只要if语句为true,每个recurs()调用都将执行statements1,然后再调用recurs(),而不会执行statements2。


    当if语句为false时,当前调用将执行statements2。


    当前调用结束后,程序控制权将返回给调用它的recurs(),而该recurs()将执行其statements2部分,然后结束,并将控制权返回给前一个调用,依次类推。


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


    进入5层递归后,程序将沿进入的路径返回。

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

    运行结果:

    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)

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

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

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

    运行结果:

    函数的地址是存储其机器语言代码的内存的开始地址
    2、包含多个递归调用的递归

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


    例如:考虑使用这种方法来绘制标尺的情况,标出两端,找到中点并将其标出。


    然后将同样的 *** 作用于标尺的左半部分和右半部份。


    如果进一步细分,可将同样的 *** 作用于当前的每一部分。


    递归方法有时被称为分而治之策略。

    程序清单: 
    < Len - 2; i++)
            ruler[i] = ' ';
        ruler[Len - 1] = '
    '; 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); } #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

     
    
    运行结果:


  • 获取函数的地址
  • 6.10 函数指针
    与数据项相似,函数也有地址。



  • 声明一个函数指针


  • 通常,这些地址对用户而言,既不重要,也没有什么用处,但对程序而言,却很有用。



    例如:可以编写将另一个函数的地址作为参数的函数,这样第一个函数将能够找到第二个函数,并运行它。



    与直接调用另一个函数相比,这种方法很笨拙,但它允许在不同的时间传递不同函数的地址,这意味着可以在不同的时间使用不同的函数。


    1、函数指针的基础知识
    假设要设计一个名为estimate()的函数,估算编写指定行数的代码所需要的时间,并且希望不同的程序员都将使用该函数。



    对于所有的用户来说,estimate()中一部分代码都是相同的,但该函数允许每个程序员提供自己的算法来估算时间。



    为实现这种目标,采用的机制是,将程序员使用的算法函数的地址传递给estimate()。

      为此,需要完成:

    • 使用函数指针来调用函数
    • process(think); // passes address of think() to process() thought(think()); // passes return value of think() to thought() double pam(int); // prototype


      1)获取函数的地址


      获取函数的地址很简单:只要使用函数名即可。



      也就是说,如果think()是一个函数,则think就是该函数的地址。



      要将函数作为参数进行传递,必须传递函数名。



      一定要区分传递的是函数的地址还是函数的返回值。


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


      process()调用使得process()函数能够在其内部调用think()函数。



      thought()调用首先调用think()函数,然后将think()的返回值传递给thought()函数。


      2)声明函数指针


      声明指向某种数据类型的指针时,必须指定指针指向的类型。



      同样,声明指向函数的指针时,也必须指定指针指向的函数类型。



      这意味着声明应指定函数的返回类型以及函数的特征标(参数列表)。



      也就是说,声明应像函数原型那样指出有关函数的信息。

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

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


      *pf


      这与pam()声明类似,这是将pam替换为(*pf)。



      由于pam是函数,因此(*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 double pam(int); double (*pf)(int); pf = pam; // pf now points to the pam() function

      double ned(double); int ted(int); double (*pf)(int); pf = ned; // invalid -- mismatched signature pf = ted; // invalid -- mismatched return types
       
      
      正确地声明pf后,便可以将相应函数的地址赋给它:


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


      注意:pam()的特征标和返回类型必须与pf相同。

      如果不相同,编译器将拒绝这种赋值:

      estimate(50,pam); // function call telling estimate() to use pam()
       
      
      假设要将编写的代码行数和估算算法(如pam()函数)的地址传递给它,则原型如下:


      (*pf)


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

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


      (*pf)


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


      3) 使用指针来调用函数


      现在进入最后一步,即使用指针来调用被指向的函数。



      线索来自指针声明。

      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
      扮演的角色与函数名相同,因此使用double y = pf(5);  // also call pam() using the pointer pf
      时,只需将它看作函数名即可:

      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)
       
      
      实际上,C++也允许像使用函数名那样使用pf:


      const double * f1(const double ar[], int n); const double * f2(const double [], int); const double * f3(const double *, int);


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

       
      2、函数指针示例 
      程序清单: 
      << "How many lines of code do you need? ";
          cin ><< "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";
      }
      #include 
      using namespace std; double betsy(int); double pam(int); void estimate(int lines, double (*pf)(int)); int main() { int code; cout

      > code; cout

       
      
      运行结果:


      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

      3、 深入探讨函数指针
      函数指针的表示可能非常的恐怖。

       
      
      下面是一些函数的原型,它们的特征标和返回值类型相同:


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


      三者是等价的。



      但是函数定义必须提供标识符,因此需要使用const double ar[] 或者const double * ar。

       
      程序清单: 
      << "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;
      }
      
      #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

       
      
      运行输出:

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


      感谢 auto


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


      *pa


      自动类型推动功能表明,编译器的角色发生了改变。



      在C++98中,编译器利用其知识帮助您发现错误,而在C++11中,编译器利用其知识帮助您进行正确的声明。

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


      关键字typedef能够创建类型的别名


      该声明导致pc的类型与typedef double real; // makes real another name for double 一致。



      后面使用它时假定其类型与&pa相同,这将导致编译错误。


      4、 使用typedef进行简化
      除了auto外,C++还提供了其他简化声明的工具。

      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。

      因此,可将p_fun声明为使用的函数指针类型的别名:

      typeName arr[]; typeName * arr;
       
      
      然后使用这个别名来简化代码:


      char*


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


      6.11 总结
      函数是C++的编程模块。



      要使用函数,必须提供定义和原型,并调用该函数。



      函数定义是实现函数的功能的代码;函数原型描述了函数的接口:传递给函数的值的数目和种类以及函数的返回类型。



      函数调用使得程序将参数传递给函数,并执行函数的代码。



      在默认情况下,C++函数按值传递参数。



      这意味着函数定义中的形参是新的变量,它们被初始化为函数调用所提供的值。



      因此,C++函数通过使用拷贝,保护了原始数据的完整性。



      C++将数组名参数视为数组第一个元素的地址。



      从技术上讲,这仍然是按值传递的,因为指针是原始地址的拷贝,但函数将使用指针来访问原始数组的内容。

      当且仅当声明函数的形参时,下面两个声明才是等价的:


      char*


      这两个声明都表明,arr是指向typeName的指针,但在编写函数代码时,可以像使用数组名那样使用arr来访问元素:arr[i]。



      即使在传递指针时,也可以将形参声明为const指针,来保护原始数据的完整性。



      由于传递数据的地址时,并不会传输有关数组长度的信息,因此通常将数组长度作为独立的参数来传递。



      另外,也可以传递两个指针(其中一个指向数组开头,另一个指向数组末尾的下一个元素),以指定一个范围,就像STL使用的算法一样。



      C++提供了3种表示C-风格字符串的方法:字符数组、字符串常量和字符串指针。



      它们的类型都是

    • 在函数声明前加上关键字inline
    • (char指针),因此被作为
    • 在函数定义前加上关键字inline
    • 类型参数传递给函数。



      C++使用空值(\0)来结束字符串,因此字符串函数检测空值字符来确定字符串的结尾。



      C++还提供了string类,用于表示字符串。



      函数可以接受string对象作为参数以及将string对象作为返回值。



      string类的方法size()可用于判断其存储的字符串的长度。



      C++处理结构的方式与基本类型完全相同,这意味着可以按值传递结构,并将其用作函数返回类型。



      然而,如果结构非常大,则传递结构指针的效率将更高,同时函数能够使用原始数据。



      这些考虑因素也适用于对象。



      C++函数可以是递归的,也就是说,函数代码中可以包括对函数本身的调用。



      C++函数名与函数地址的作用相同。



      通过将函数指针作为参数,可以传递要调用的函数的名称。


      第七章 函数探幽 7.1 C++内联函数
      内联函数是C++为提高程序运行速度所做的一项改进。



      常规函数和内联函数之间的主要区别不在于编写方式,而在于C++编译器如何将它们组合到程序中。



      要了解内联函数与常规函数的区别,必须深入到程序内部。



      编译过程的最终产品是可执行程序:由一组机器语言指令组成。



      运行程序时, *** 作系统将这些指令载入到计算机内存中,因此每条指令都有特定的内存地址。



      计算机随后将逐步执行这些指令。



      有时,将跳过一些指令,向前或向后跳到特定地址。



      常规函数调用也使程序跳到另一个地址(函数地址),并在函数结束时返回。



      C++内联函数提供了另一种选择,内联函数的编译代码与其他程序代码"内联"起来了。



      也就是说,编译器将使用相应的函数代码替换函数调用。



      对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。



      因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。



      如果程序在10个不同的地方调用同一个内联函数,则该程序将包含该函数代码的10个副本。



      应该有选择地使用内联函数。



      如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间将只占整个过程的很小一部分。



      如果代码执行时间很短,则内联调用就可以节省非内联调用使用的大部分时间。



      另一方面,由于这个过程相当快,因此尽管节省了该过程的大部分时间,但节省的时间绝对值并不大,除非该函数经常被调用。

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


      a = 25,b = 144 c = 13,c square = 169 Now c = 14 #define SQUARE(X) X*X


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

       
      程序清单: 
      << "a = " << a << ",b = " << b << "\n";
          cout << "c = " << c;
          cout << ",c square = " << square(c++) << "\n";
          cout << "Now c = " << c << "\n";
          return 0;
      }
      
      #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 = 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++;


      输出表明,内联函数和常规函数一样,也是按值来传递参数的。



      如果参数为表达式,则函数将传递表达式的值。



      这使得C++的内联功能远远胜过C-语言的宏定义。



      尽管程序没有提供独立的原型,但C++原型特性仍在起作用。



      这是因为在函数首次使用前出现的整个函数定义充当了原型。



      这意味着可以给square()传递int或者long值,将值传递给函数前,程序自动将这个值强制转换为double类型。


      🚢内联与宏


      inline工具是C++新增的特性。



      C语言使用预处理器语句#define来提供宏——内联代码的原始实现。

      例如:计算平方的宏:


      #define SQUARE(X) ((X)*(X))


      这并不是通过传递参数实现的,而是通过文本替换来实现的——X是"参数"的符号标记。


      引用是已定义的变量的别名(另一个名称)


      上述示例只有第一个能正常工作。

      可以通过使用括号来进行改进:


      int rats; int & rodents = rats; // makes rodents an alias for rats


      但仍然存在这样的问题,即宏不能按值传递。



      即使使用新的定义,SQUARE(c++)仍将c递增两次,但是上面程序中的内联函数square()能计算c的结果,传递它,以计算其平方值,然后将c递增一次。



      这里的目的不是演示如何编写C宏,而是要指出,如果使用C语言的宏执行了类似函数的功能,应考虑将其转换为C++内联函数。


      7.2 引用变量
      C++新增了一个复合类型:引用变量。



      char*



      例如:如果将twain作为clement变量的引用,则可以交替使用twain和clement来表示该变量。



      引用变量的主要用途是用作函数的形参,通过将引用变量用作参数,函数将使用原始数据,而不是其副本。



      这样除了指针外,引用也可以为函数处理大型结构提供了一种非常方便的途径。



      同时对于设计类来说,引用也是必不可少的。


      1、创建引用变量


      C和C++使用&符号来指示变量的地址。



      C++给&符号赋予了另一个含义,将其用来声明引用。

       
      
      例如:将rodents 作为 rats变量的别名,可以:


      int & 指 的是指向int的引用


      其中,&不是地址运算符,而是类型标识符的一部分。



      就像声明中的rats = 101,rodents = 101 rats = 102,rodents = 102 rats address = 0x61fe14, rodents address = 0x61fe14 指的是指向char的指针一样,int &



      上述引用声明允许将rats和rodents互换,它们指向相同的值和内存单元。

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

      using namespace std; int main() { int rats = 101; int & rodents = rats; // rodents is a reference cout

       
      
      运行结果:


      int & rodents = rats;


      请注意:下述语句中的&运算符不是地址运算符,而是将rodents的类型声明为 cout << ", rodents address = " << &rodents << endl; ,即指向int变量的引用。


      int rats = 101; int & rodents = rats; // rodents a reference int * prats = &rats; // prats a pointer


      但下面语句中的&运算符是地址运算符,其中&rodents表示rodents引用的变量的地址。


      *prats


      由此可知,rats和rodents的值和地址都相同。



      将rodents加1将影响这两个变量。


      更准确地说,rodents++ *** 作是将一个有两个名称的变量加1.


      指针和引用之间是有区别的。



      例如:可以创建指向rats的引用和指针。


      *解除引用运算符


      这样,表达式rodents和 int rat; int & rodent; rodent = rat; // No,you can't do this 都可以同rats互换,而表达式&rodents和prats都可以同&rats互换。



      从这一点来说,引用看上去像是伪装表示的指针(其中,int & rodents = rats; 被隐式理解 )。



      实际上,引用还是不同于指针的,除了表示法不同外,还有其它的区别。



      例如:必须在声明引用的同时将其初始化。



      而不能像指针那样,先声明,再赋值。


      int * const pr = & rats;


      注意:必须在声明引用变量时进行初始化。



      引用更接近const指针,必须在创建时进行初始化,一旦与某个变量关联起来,就将一直效忠于它。

      也就是说:

      *pr
       
      
      实际上是下述代码的伪装表示:


      rats = 101, rodents = 101 rats address = 0x61fe14, rodents address = 0x61fe14 bunnies = 50, rats = 50, rodents = 50 bunnies address = 0x61fe10, rodents address = 0x61fe14


      其中,引用rodents扮演的角色与表达式rodents = bunnies; 相同。

       
      程序清单: 
      << "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;
          
      }
      
      #include

      using namespace std; int main() { int rats = 101; int & rodents = rats; cout

       
      
      运行结果:


      rats = bunnies;


      最初,rodents引用的是rats,但随后程序试图将rodents作为bunnies的引用。


      wallet1 = 0 wallet2 = 0 << original values Using references to swap contents: wallet1 = 0 wallet2 = 0 << values swapped Using pointers to swap contents again: wallet1 = 0 wallet2 = 0 << values swapped again Trying to use passing by value: wallet1 = 0 wallet2 = 0 << swap failed


      乍一看,这种意图暂时是成功的,因为rodents的值从101变成了50.但仔细研究将发现,rats也变成了50,同时rats和rodents的地址相同,而地址与bunnies的地址不同。

      由于rodents是rats的别名,因此上述赋值语句与下面的语句等效:


      swapr(wallet1,wallet2); // pass variables swapp(&wallet1,&wallet2); // pass addresses of variables swapv(wallet1,wallet2); // passing values of variables


      也就是说,这意味着:将bunnies变量的值赋给rat变量。



      简而言之,可以通过初始化声明来设置引用,但不能通过赋值来设置。


      2、将引用用作函数参数


      引用经常被用作函数参数,使得函数中的变量名成为调用程序中的变量的别名,这种传递参数的方法称为按引用传递。



      按引用传递允许被调用的函数能够访问调用函数中的变量。



      C++新增的这项特性是对C语言的超越,C语言只能按值传递。



      按值传递导致被调用函数使用调用程序的值的拷贝。



      当然,C语言也允许避开按值传递的限制,采用按指针传递的方式。



      解决常见的计算机问题:交换两个变量的值,对使用引用和使用指针做一下比较。



      交换函数必须能够修改调用程序中的变量的值。



      这意味着按值传递变量将不管用,因为函数将交换原始变量的副本的内容,而不是变量本身的内容。



      但传递引用时,函数将可以使用原始数据。



      另一种方法是,传递指针来访问原始数据。

       
      程序清单: 
      << "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;
      }
      
      #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

       
      
      运行结果:


      int *p


      引用和指针方法都成功得交换了两个钱包(wallet)中的内容,而按值传递的方法没能完成这项任务。

       
      
      程序说明:


      void swapr(int & a, int & b); void swapv(int a, int b);


      按引用传递和按值传递看起来调用相同,只能通过原型或函数定义才能知道是如何传递的。



      然而,地址运算符(&)使得按地址传递(swapp())(类型声明void swapr(int & a, int & b); void swapp(int * p, int * q); 表明,p是一个int指针,因此与p对应的参数应为地址,如&wallet1)。

       
      
      比较函数swapr()(按引用传递)和swapv()(按值传递)的代码,唯一的外在区别是声明函数参数的方式不同:


      解除引用运算符*


      内在区别是:在swapr()中,变量a和b是wallet1和wallet2的别名,所以交换a和b的值相当于交换wallet1和wallet2的值;但在swapv()中,变量a和b是复制了wallet1和wallet2的值的新变量。



      因此交换a和b的值并不会影响wallet1和wallet2的值。

       
      
      比较函数swapr()(传递引用)和swapp()(传递指针),第一个区别是声明函数参数的方式不同:


      swapr(wallet1,wallet2);


      另一个区别是指针版本需要在函数使用p和q的整个过程中使用27 = cube of 3 27 = cube of 27



      应在定义引用变量时对其进行初始化。



      函数调用使用实参初始化形参,因此函数的引用参数被初始化为函数调用传递的实参。

      也就是说,下面的函数调用将形参a和b分别初始化为wallet1和wallet2:

      double refcube(const double &ra);


      3、 引用的属性和特别之处


      使用引用参数时,需要了解其一些特点。

       
      程序清单: 
      << 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;
      }
      
      #include

      using namespace std; double cube(double a); double refcube(double &ra); int main() { double x =3.0; cout

       
      
      运行结果:


    • 实参的类型正确,但不是左值;

    • refcube()函数修改了main()中的x值,而cube()没有,这提醒了我们为何通常按值传递。



      变量a位于cube()中,它被初始化为x的值,但修改a并不会影响到x。



      但由于refcube()使用了引用参数,因此修改ra实际上就是修改了x。



      如果意图是让函数使用传递给它的信息,而不对这些信息进行修改,同时又想使用引用,则应使用常量引用。

       
      
      例如:应在函数原型和函数头中使用const:


    • 使用const可以避免无意中修改数据的编程错误;

    • 如果这样做,当编译器发现代码修改了ra的值,将生成错误的消息。



      顺便说一句,如果要编写类似于上述示例 的函数(即使用基本数值类型),应采用按值传递的方式,而不要采用按引用传递的方式。


      临时变量、引用参数和const


      如果实参与引用参数不匹配,C++将生成临时变量。



      当前,仅当参数为const引用时,C++才允许这样做。



      何种情况下,C++将生成临时变量,以及为何对const引用的限制是合理的。

      • 首先,什么时候将创建临时变量呢?如果引用参数是const,则编译器将在下面两种情况下生成临时变量:

      • 使用const使函数能够处理const和非const实参,否则将只能接受非const数据;

      • 实参的类型不正确,但可以转换为正确的类型。



      左值参数是可以被引用的数据对象。



      例如:变量、数组元素、结构成员、引用和解除引用的指针都是左值。



      非左值包括字面常量(用引号括起的字符串除外,它们由地址表示)和包含多项的表达式。



      在C语言中,左值最初指的是可出现在赋值语句左边的实体,但这是引入关键字const之前的情况,现在,常规变量和const变量都可视为左值,因为可通过地址访问它们。



      但常规变量属于可修改的左值,而const变量属于不可修改的左值。



      注意:如果函数调用的参数不是左值或与相应的const引用参数的类型不匹配,则C++将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数来引用该变量。

      🚢应尽可能使用const
      • 将引用参数声明为常量数据的引用的理由有三个:

        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 struct free_throws { string name; int made; int attempts; float percent; };
        使用const引用使函数能够正确生成并使用临时变量。



      因此,应尽可能将引用参数声明为const。



      C++11 新增了另一种引用:右值引用(rvalue reference)。



      这种引用可指向右值,是使用&&声明的。


      void set_pc(free_throws & ft); // use-a reference to a structure


      新增右值引用的主要目的是:让库设计人员能够提供有些 *** 作的更有效实现。


      4、将引用用于结构


      引用非常适合用于结构和类(C++的用户定义类型)。



      确实,引入引用主要是为了用于这些类型的,而不是基本的内置类型。



      使用结构引用参数的方式与使用基本变量引用相同,只需要在声明结构参数时使用引用运算符&即可。

      void display(const free_throws & ft); //don't allow changes to strcture
       
      
      则可以这样编写函数原型,在函数中指向该结构的引用作为参数:

      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
       
      
      如果不希望函数修改传入的结构,可用const:

      能够将特性从一个类传递给另一个类的语言特性被称为继承
       
      程序清单: 
      << "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;
      }
      #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

       
      
      运行结果:

      继承的另一个特征是,基类引用可以指向派生类对象,而无需进行强制类型转换


      5、将引用用于类对象


      将类对象传递给函数时,C++通常的做法是使用引用。



      例如:可以通过使用引用,让函数将类string、ostream、istream、ofstream和ifstream等类的对象作为参数。


      6、对象、继承和引用


      ofstream对象可以使用ostream类的方法,这使得文件输入/输出的格式与控制台输入/输出相同。



      使得

    • 程序员能够修改调用函数中的数据对象


    • 简单来说,ostream是基类(因为ofstream是建立在它的基础之上的),而ofstream是派生类(因为它是从ostream派生而来的)。



      派生类继承了基类的方法,这意味着ofstream对象可以使用基类的特性,如格式化方法precision()和setf()。



      char * left (const char * str,int n = 1);



      这种特征的一个实际结果是,可以定义一个接受基类引用作为参数的函数,调用该函数时,可以将基类对象作为参数,也可以将派生类对象作为参数。



      例如:参数类型ostream &的函数可以接受ostream对象(如cout)或声明的ofstream对象作为参数。

      7、何时使用引用参数
      • 使用引用参数的主要原因有两个:

        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
        通过传递引用而不是整个数据对象,可以提高程序的运行速度。



      当数据对像较大时(如结构和类对象),第二个原因最重要。



      这些也是使用指针参数的原因。



      这是有道理的,因为引用参数实际上是基于指针的代码的另一个接口。


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


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



      • 如果数据对象很小,如内置数据类型或小型结构,则按值传递。



      • 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向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"的指针。



      如果程序经常需要抽取一个字符组成的字符串,而偶尔需要抽取较长的字符串,则这种默认值将很有帮助。



      默认值的设置必须通过函数原型。



      由于编译器通过查看原型来了解函数所使用的参数数目,因此函数原型也必须将可能的默认参数告知程序。



      方法是将值赋给原型中的参数。


      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


      例如,harpo()原型允许调用该函数时提供1个、2个、3个参数。


      Enter a string: forthcoming fort f


      ==实参按从左到右的顺序依次被赋给相应的形参,而不能跳过任何参数。

      ==因此,下面的调用是不允许的:


      i < n && str[i]


      默认参数只是提供了一种便捷的方式。



      在设计类时,可以通过使用默认参数,减少要定义的析构函数、方法以及方法重载的数量。

       
      程序清单: 
      << "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++] = '
      '; // set rest of string to '

      ' return p; }

      #include
      using namespace std;
      const int ArSize = 80;
      char * left(const char * str,int n = 1);
      int main()
      {
          char sample[ArSize];
          cout 


      运行结果:


      int len = strlen(str); n = (n < len) ? n : len; // the lesser of n and len char * p = new char[n+1];

      该程序使用new创建一个新的字符串,以存储被选择的字符。

      < n 测试让循环复制了n个字符后终止。


      int m = 0; while(m <= n && str[m] != 'while (m <= n && str[m]) ') m++; char * p = new char[m+1]; //use m instead of n in rest of code


      i



      测试的第二部分:表达式str[i],是要复制的字符的编码。



      遇到空值字符(其编码为0)后,循环将结束。


      这样,while循环将使字符串以空值字符结束,并将余下的空间设置为空值字符。



      另一种设置新字符串长度的方法是,将n设置为传递的值和字符串长度中较小的一个。



      double cube(double x); double cube(double & x);

      这将确保new分配的空间不会多于存储字符串所需的空间。


      例:当m的值等于n或到达字符串结尾时,下面的值循环都将终止:


      cout << cube(x);

      在str[m]不是空值字符时,表达式str[m] != '\0’的结果为true,否则为false。


      由于在&&表达式中,非零值被转换为true,而零被转换为false,因此也可以这样编写这个while测试:


      void dribble (char * bits); //overloaded void dribble (const char * cbits); //overloaded void dabble (char * bits); //not overloaded void drivel(const char * bits); //not overloaded

      7.4 函数重载
      函数重载指的是可以用多个同名的函数,因此对名称进行了重载。


      函数重载完成相同的工作,但使用不同的参数列表。




      函数重载的关键是函数的参数列表,也称为函数特征标(function signature)。



      如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量名是无关紧要的。



      C++允许定义名称相同的函数,条件是它们的特征标不同。


      如果参数数目和/或参数类型不同,则特征标也不同。



      一些看起来彼此不同的特征标是不能共存的。


      例如:


      long gronk (int n, float m); double gronk (int n, float m); //same signatures hence not allowed

      可能认为在此处使用函数重载,因为它们的特征标看起来不同。


      然而,从编译器的角度来考虑:


      long gronk (int n, float m); double gronk (float n, float m); //different signatures hence allowed


      参数x与double x原型和double & x原型都匹配,因此编译器无法确定究竟应使用哪个原型。


      为避免这种混乱,编译器在检查函数特征标时,将把类型引用和类型本身视为同一种特征标。



      匹配函数时,并不区分const和非const变量。



      char * left(const char * str,unsigned n); // two arguments char * left(const char * str); // one argument

      注意:是特征标,而不是函数类型使得可以对函数进行重载。


      例如:下面两个声明是互斥的:


      int x; short interval; //转换为: double x; //intended change of type short doubleerval; //unintended change of variable name

      因此,C++不允许以这种方式重载gronk()。

      返回类型可以不同,但特征标也必须不同:


      模板并不创建任何函数,而只是告诉编译器如何定义函数


      何时使用函数重载


      虽然函数重载很吸引人,但也不要滥用。



      仅当函数基本上执行相同的任务,但使用不同的数据时,才应采用函数重载。


      另外,是否可以通过使用默认参数来实现同样的目的。


      例如:可以用两个重载函数来代替面向字符串的left()函数:


      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.


      使用一个默认参数的函数要简单些,只需编写一个函数(而不是两个函数),程序也只需为一个函数(而不是两个)请求内存;需要修改函数时,只需修改一个。


      然而,如果需要使用不同类型的参数,则默认参数便不管用了,在这种情况下,应该使用函数重载。



      7.5 函数模板
      函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数,其中的泛型可用具体的类型(如int 或 double)替换。



      通过将类型作为参数传递给模板,可使编译器生成该类型的函数。



      由于模板允许泛型(而不是具体类型)的方式编写程序,因此有时也被称为通用编程。


      由于类型是用参数表示的,因此模板特性有时也被称为参数化类型(parameterized types)。




      例如:定义一个交换两个int值的函数。



      假设要交换两个double值,则一种方法是复制原来的代码,并用double替换所有的int。



      如果需要交换两个char 值,可以再次使用同样的技术。



      进行这种修改将浪费宝贵的时间,且容易出错。


      如果进行手工修改,则可能会漏掉一个int。


      如果进行全局查找和替换(如用double替换int)时,可能将:


      void Swap(int &a, int &b) { int temp; temp = a; a = b; b = temp; }

      C++的函数模板功能能自动完成这一过程,可以节省时间,而且更可靠。



      函数模板允许以任意类型的方式来定义函数。
      例如:可以建立一个交换模板:


      template

      void Swap(AnyType &a, AnyType &b) { AnyType temp; temp = a; a = b; b = temp; }


      第一行指出,要建立一个模板,并将类型命名为AnyType。



      关键字template 和 typename是必需的,除非可以使用关键字class代替typename。



      另外,必须使用尖括号。


      类型名可以任意选择(这里是AnyType),只要遵守C++命名规则即可;许多程序员都使用简单的名称,如 T。




      void Swap(double &a, double &b) { doouble temp; temp = a; a = b; b = temp; }



      需要交换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。

       
      程序清单:<< "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  
      
      #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
      	// 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. Original arrays: 07/04/1776 07/20/1969 Swapped arrays: 07/20/1969 07/04/1776


      程序说明:

      第一个Swap()函数接受两个int参数,因此编译器生成该函数的int版本。


      也就是说,用int替换所有的T,生成下面这样的定义:


      a = b


      程序员看不到这些代码,但编译器确实生成并在程序中使用了它们。


      第二个Swap()函数接受两个double参数,因此编译器将生成double版本。


      也就是说,用double替换T,生成下述代码:


      T c = a * b;


      注意:函数模板不能缩短可执行程序。



      对于上述程序清单,最终仍将由两个独立的函数定义,就像以手工方式定义了这些函数一样。



      最终的代码不包含任何模板,而只包含了为程序生成的实际函数。


      使用模板的好处是,它使生成多个函数定义更简单、更可靠。



      1、重载的模板


      需要对多个不同类型使用同一种算法的函数时,可使用模板。



      然而,并非所有的类型都使用相同的算法,为满足这种需求,可以像重载常规函数定义那样重载模板定义。


      和常规重载一样,被重载的模板的函数特征标必须不同。




      例如:下面程序新增一个交换模板,用于交换两个数组中的元素。



      原来的模板的特征标为(T &,T &),而新模板的特征标为(T [ ], T [ ],int)。



      注意,在后一个模板中,最后一个参数的类型为具体类型(int),而不是泛型。


      并非所有的模板参数都必须是模板参数类型。

       
      程序清单: 
      << "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 #include < 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;
      }
      
      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

      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 

      运行结果:

      struct job { char name[40]; double salary; int floor; };
       
      2、模板的局限性 
      假设有如下模板函数: 
      

      template
      // or template

      void f(T a,T b){ }

      通常,代码假定可执行哪些 *** 作。

      <,但如果T为结构,该假设便不成立:

      例如:下面的代码假定定义了赋值,但如果T为数组,这种假设将不成立:
       
      temp = a;
      a = b;
      b = temp;
       
      
      同样,下面的语句假设定义了


      if(a > b)

      另外,为数组名定义了运算符 > ,但由于数组名为地址,因此它比较的是数组的地址,而这种不是您希望的。


      下面的语句假定为类型T定义了乘法运算符,但如果T为数组、指针或结构,这种假设便不成立:


      int x; decltype(x) y; // make y the same type as x

      总之,编写的模板函数很可能无法处理某些类型。

       
      
      3、显式具体化


      假设定义了如下结构:


      decltype(x + x) xpy; // make xpy the same type as x + y xpy = x + y;

      另外,假设希望能够交换两个这种结构的内容。


      原来的模板使用下面的代码来完成交换:


      decltype(x + x) xpy = x + y;


      由于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
      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提供了解决方案。

      可这样使用该关键字:
       
      
      decltype(expression) var;

      给decltype提供的参数可以是表达式,因此在前面的模板函数ft()中,可使用下面的代码:
       
      
      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 *

      或者,将这两条语句合而为一:
       
      long indeed(int);
      decltype (indeed(3)) m;		// m is type int
       
      因此,可以这样修复前面的模板函数ft():


      template

      void ft(T1 x, T2 y) { decltype(x + x) xpy = x + y; }

      为确定类型,编译器必须遍历一个核对表。

      假设有如下声明:

      double xx = 4.4; decltype ((xx)) r2 = xx; // r2 is double & decltype (xx) w = xx; // w is double (Stage 1 match)
       
      
      则核对表的简化版如下:

      第一步:如果expression是一个没有用括号括起的标识符,则var的类型与该标识符的类型相同,包括const等限定符:
       
      
      xx = 98.6; (xx) = 98.6; // () don't affect use of xx


      第二步:如果expression是一个函数调用,则var的类型与函数的返回值类型相同:


      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


      注意:并不会实际调用函数。


      编译器通过查看函数的原型来获悉返回类型,而无需实际调用函数。




      第三步:如果expression是一个左值,则var为指向其类型的引用。



      这好像意味着前面的w应为引用类型。



      因为x是一个左值。


      但别忘了,这种情况已经在第一步处理过了,要进入第三步,expression不能是未用括号括起的标识符。


      那么,expression是什么时候将进入第三步呢?一种显而易见的情况是,expression是用括号括起的标识符:


      double h(int x, float y);

      顺便说一句,括号并不会改变表达式的值和左值性。

      例如:下面两条语句是等效的:
       
      


      第四步:如果前面的条件都不满足,则var的类型与expression的类型相同:


      请注意:虽然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++新增了一种声明和定义函数的语法。

      例如:使用内置类型来说明这种语法的工作原理:
       
       
      
      使用新增的语法可编写成这样:


      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位于作用域内,可以使用它们。



      7.6 总结
      C++扩展了C语言的函数功能,通过将inline关键字用于函数定义,并在首次调用该函数前提供其函数定义,可以使得C++编译器将该函数视为内联函数。


      也就是说,编译器不是让程序跳到独立的代码段,以执行函数,而是用相应的代码替换函数调用,只有在函数很短时才能采用内联方式。




      引用变量是一种伪装指针,它允许为变量创建别名(另一个名称)。



      引用变量主要被用作处理结构和类对象的函数的参数。


      通常,被声明为特定类型引用的标识符只能指向这种类型的数据;然而,如果一个类(如ofstream)是从另一个类(如ostream)派生出来的,则基类引用可以指向派生类对象。




      C++原型可以定义参数的默认值。



      如果函数调用省略了相应的参数,则程序将使用默认值;如果函数调用提供了参数值,则程序将使用这个值(而不是默认值)。



      只能在参数列表中从右到左提供默认参数。


      因此,如果为某个参数提供了默认值,则必须为该参数右边所有的参数提供默认值。




      函数的特征标是参数列表。



      程序员可以定义两个同名函数,只要其特征标不同。



      这被称为函数多态或函数重载。


      通常,通过重载函数来为不同的数据类型提供相同的服务。




      函数模板自动完成重载函数的过程。


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

      欢迎分享,转载请注明来源:内存溢出

      原文地址:https://54852.com/langs/568498.html

      (0)
      打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
      上一篇 2022-04-09
      下一篇2022-04-09

      发表评论

      登录后才能评论

      评论列表(0条)

        保存