C++ Primer Plus 学习记录(第七章节-函数-C++的编程模块-包含练习题答案)

本章和第八章介绍如何定义函数、给函数传递消息以及从函数那里获得信息。

本章首先复习函数是如何工作的,然后着重介绍如何使用函数来处理数组、字符串和结构,最后介绍递归和函数指针

7.1 复习函数的基本知识

要使用C++函数,必须要完成如下工作:

  • 提供函数定义
  • 提供函数原型
  • 调用函数

创建自己的函数时,必须自行处理3个方面——定义、提供原型和调用。
程序清单7.1演示了这个过程:

#include <iostream>

void simple();

int main()
{
	using namespace std;
	cout << "main() will call the simple() function:\n";
	simple();
	return 0;
}

void simple()
{
	using namespace std;
	cout << " I'm but a simple function.\n";
}

运行程序如下:
请添加图片描述
在每个函数定义中,都使用了一条using编译指令,这是因为每个函数都使用了cout。另一种方法是,在函数定义之前放置一条using编译指令或在函数中使用std::cout。

7.1.1 定义函数

函数可以分为两类:
一类是没有返回值的函数,被称为void函数;
可选的返回语句标记了函数的结尾,否则,函数将在右花括号处结束。

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

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

7.1.2 函数原型和函数调用

7.1.2.1 对于函数原型

首先,需要知道C++需要提供原型的原因。其次,由于C++要求提供原型,因此还应知道正确的语法。最后,应当感谢原型所做的一切。

1、为什么需要原型

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

...
double cube(double x);
...
double cube(side);

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

为何编译器需要原型,难道它就不能在文件中进一步查找,以了解函数是如何定义的吗?
这种方法的一个问题是效率不高。编译器在搜索文件的剩余部分时将必须停止对main()的编译。

一个更严重的问题是,函数甚至可能并不子啊文件中。C++允许将一个程序放在多个文件中,单独编译这些文件,然后再将它们组合起来。在这种情况下,编译器在编译main() 时,可能无权访问函数代码。如果函数位于库中,情况也是如此。避免使用函数原型的唯一方法是,在首次使用函数之前定义它,但这并不总是可行的。

C++编译的风格是将main()放在最前面,因此它通常提供了程序的整体结构。

2、原型的语法

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

double cube(double x);

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

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

void cheers(int);

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

在C++中,不指定参数列表时应使用省略号:

void say_bye(...);

通常,仅当与接受可变参数的C函数(如printf())交互时才需要这样做。

3、原型的功能

原型可以极大的降低程序出错的几率:

  • 编译器正确处理函数返回值;
  • 编译器检查使用的参数数目是否正确;
  • 编译器检查使用的参数类型是否正确。如果不正确,则转化为正确的类型(如果可能的话)。

原型自动将被传递的参数强制转换为期望的类型。(但在第8章函数重载可能导致二义性,因此不允许某些自动强制类型转换)

自动类型转换并不能避免所有可能的错误。当较大类型的值被自动转换为较小的类型时,有些编译器将发出警告,指出这可能会丢失数据。

仅当在有意义时,原型化才会导致类型转换。例如,原型不会讲整数转换为结构或指针。

在编译阶段进行的原型化被称为静态类型检查(static type checking)。可以看出,静态类型检查可捕获许多在运行阶段难以捕获的错误。

7.2 函数参数和按值传递

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

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

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

在函数中声明的变量(包括参数)是该函数私有的。函数调用时,计算机为这些变量分配内存;在函数结束时,计算机将释放这些变量使用的内存。这样的变量被称为局部变量,因此它们被限制在函数中。有助于确保数据的完整性。

在main()中声明一个x变量,同时也在另一个函数中也声明一个名为x的变量,它们完全不同,毫不相关。这样的变量也被称为自动变量,因为它们是在程序执行时自动被分配和释放的。

7.2.1 多个参数

原型中的变量名不必与定义中的变量名相同,而且可以省略

7.2.2 另外一个接受两个参数的函数

7.3 函数和数组

函数需要知道要对哪个数组进行累计,因此需要将数组名作为参数传递给它。为使函数通用,而不限于特定长度的数组,还需要传递数组长度。这里唯一的新内容是,需要将一个形参声明为数组名。下面来看一看函数头及其其他部分:

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

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

程序清单7.5
演示了如同使用数组名那样使用指针的情况。程序将数组初始化为某些值,并使用sum_arr()函数计算总数。注意sum_arr()函数使用arr时,就像是使用数组名一样。

#include <iostream>
const int ArSize = 8;
int sum_arr(int arr[], int n);
int main()
{
    using namespace std;
    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;
}

程序输出结果:
请添加图片描述

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

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

cookies == &cookies[0] // 数组名是第一个元素的地址

该规则有一些例外:

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

对于程序清单7.5中使用了下面的函数:
写法一

int sum_arr(int arr[], int n);
...
int sum = sum_arr(cookies, ArSize);

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

int 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[i] == *(arr + i)
&arr[i] == arr + 1

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

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

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

程序清单7.5实际上并没有将数组内容传递给参数,而是将数组的位置(地址)、包含的元素种类(类型)以及元素数目(n变量)提交给函数。有了这些信息,函数便可以使用原来的数组。

这种区别并不违反C++按值传递的方法,sum_arr()函数仍传递了一个值,这个值被赋给了一个新变量,但这个值是一个地址,而不是数组的内容。

sum_arr()函数将cookies的地址赋给指针变量arr,将ArSize赋给int变量n。

数组名和指针对应是好事吗?

确实是第一件好事。

一方面,将数组地址作为参数可以节省复制整个数组所需的时间和内存。如果数组很大,则使用拷贝系统开销非常大;程序不仅需要更多的计算机内存,还需要花费时间来复制大块的数据。
另一方面,使用原始数据增加了破坏数据的风险。经典C中是一个问题,但是C++中const限定符提供了解决这种问题的办法。

程序清单7.6表明,cookies和arr的值相同,它还演示了指针概念如何使sum_arr函数比以前更通用。该程序使用限定符std::而不是编译指令using来提供对cout和endl的访问权。

#include <iostream>
const int ArSize = 8;
int sum_arr(int arr[], int n);
int main()
{
    using namespace std;
    int cookies[ArSize] = {1,2,4,8,16,32,64,128};

    cout << cookies << " = array address, ";

    cout << sizeof cookies << " = sizeof cookies\n";

    int sum = sum_arr(cookies, ArSize);
    cout << "Total cookies eaten: " << sum << endl;

    sum = sum_arr(cookies, 3);
    cout << "First three eaters ate " << sum << " cookies.\n";

    sum = sum_arr(cookies + 4, 4);
    cout << "Last four eaters ate " << sum << " cookies.\n";

    return 0;
}

int sum_arr(int arr[], int n)
{
    using namespace std;
    int total = 0;

    cout << "\n" << arr << " = arr, ";
    cout << sizeof arr << " sizeof arr\n";

    for (int i = 0; i < n; i++)
        total = total + arr[i];
    return total;
}

程序输出:
请添加图片描述
地址值和数组的长度随系统而异。有些C++实现以十进制而不是十六进制格式显示地址,还有些编译器以十六进制显示地址时,会加上前缀0x。

程序说明

  • 首先,cookies和arr指向同一个地址,但sizeof cookies的值为32,而sizeof arr为4。这是由于sizeof cookies是整个数组的长度,而sizeof arr只是指针变量的长度(上述程序运行结果是从一个使用4字节地址的系统中获得的)。
  • 这也是必须显式地显示地传递数组长度,而不能在sum_arr()中使用sizeof arr的原因:因为本身并没有指出数组的长度。
  • 由于sum_arr()只能通过第二个参数获知数组中的元素数量,因此可以对函数”说谎“。例如,程序第二次使用该函数时,这样调用它:sum = sum_arr(cookies, 3);,通过告诉该函数cookies有3个元素,可以让它计算前3个元素的总和。
  • 还可以提供假的数组起始位置sum = sum_arr(cookies + 4, 4);cookies+4是第5个元素的地址。即计算第5、6、7、8个元素的总和。
  • 可以将&cookies[4],而不是cookies+4作为参数;它们的含义是相同的。

7.3.3 更多数组函数示例

7.3.3.1 房地产实例

编写特定的函数来处理特定的数据操作是有好处的:程序的可靠性更高、修改和调试更为方便。

构思程序将存储属性与操作结合起来,便是朝OOP思想迈进了重要的一步。

例如:
假设要使用一个数组来记录房地产的价值。

  1. 使用double类型,其相比于int和long取值范围更大,并且提供了足够多的有效位数来精确表似乎这些值
  2. 决定数组元素的数目:使用一个包含5个元素的double数组;这里不使用new创建动态数组
  3. 对房地产数组执行的操作:将值读入到数组中和显示数组内容;重新评估每种房地产的值
7.3.3.1.1 填充数组

函数的原型如下:

int fill_array(double ar[], int limit);
/*
该函数接受两个参数
一个是数组名
一个指定了要读取的最大元素数
返回值是实际读取的元素数

下面是函数的实现,这里使用负数来判定提早结束循环:

int fill_array(double ar[], int limit)
{
	using namespace std;
	double temp;
	int i;
	for (i = 0; i < limit; i++)
	{
		cout << "Enter value #" << (i + 1) << ": ";
		cin >> temp;
		if (!cin)//当cin出现需要bool值得地方时,该转换函数将被调用。如果最后一次读取成功了,则转换得到的bool值为true,否则为false。
		{
			cin.clear();
			while (cin.get() != '\n')
				continue;
			cout << "Bad input; input process terminated.\n";
			break;
		}
		else if (temp < 0)
			break;
		ar[i] = temp;
	}
	return i;
}

代码说明:

  • 代码中包含了对用户的提示
  • 如果用户输入的是非负值,则这个值将被赋给数组,否则循环结束。
  • 如果用户输入的都是有效值,则循环将在读取最大数目的值后结束
  • 循环完成的最后一项工作是将i值加1,循环结束后,i将比最后一个数组索引大于1,即等于填充的元素数目,然后函数返回这个值
7.3.3.1.2 显示数组及用const保护数组
  • 显示数组:将数组名和填充的元素数目传递给函数,然后函数使用循环显示每个元素
  • 确保函数不修改原始数组
  1. 使用普通参数保护自动实现,这是由于C++按值传递数据,函数使用的是数据的副本;
  2. 但是接受数组名的函数将使用原始数据,这也是能够添加数组的原因。为了防止无意中修改数组的内容,可以声明形参时使用关键字const:
void show_array(const double ar[], int n)
//指针ar指向的是常量数据;意味着不能使用ar修改该数据
//这不意味着原始数据必须是常量,而只是意味着不能再该函数中使用ar来修改这些和数据
//对于操作ar[0] += 10;违反限制,编译器将禁止这样做,会给出错误消息。

//C++将声明const double ar[]解释为const double *ar。因此,实际上就是说,ar指向的是一个常量值
  • 下面是show_array()函数的代码
void show_array(const double ar[], int n)
{
	using namespace std;
	for (int i = 0; i < n; i++)
	{
		cout << "Preperty #" << (i + 1) << ": $";
		cout << ar[i] << endl;
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值