C++ Primer Plus (6th) Chap7 函数--C++的编程模块 摘录

7.1 复习函数基本知识

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

提供函数定义;

提供函数原型;

调用函数;

库函数使已经定义和编译好的函数,同时可以提供标准库头文件提供其原型;

执行simple()时,将暂停执行main()中的代码;等函数simple()执行完毕后,继续执行main()中的代码。

7.1.1 定义函数

将函数分为有返回值和无返回值类型。没有返回值类型称为void函数。其通用格式如下:

void functionName(parameters)
{
        statement(s);
        (return); 
}

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

有返回值的函数将生成一个值,并将它返回给调用函数。

typeName functionName(parameters)
{
        statement(s);
        return value;
}

返回值本身可以是常量。变量,也可以是表达式,只是其结果的类型必须为typeName或可以被转化为typeName。

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

函数通过将返回值复制到指定CPU寄存器或内存单元中将其返回。随后,调用程序将查看该内存单元。返回程序和调用程序必须就该内存单元中存储的数据类型达成一致。函数原型将返回值类型告知调用程序,而函数定义命令被调用函数应返回什么类型的数据。

函数在执行返回语句后结束。如果函数包含多条返回语句(例如,它们位于不同的if else选项中),则函数在执行遇到的第一条返回语句后结束。

7.1.2 函数原型及函数调用

函数原型描述了函数到编译器的接口,也就是说,它将返回值的类型(如果有的话)以及参数的类型和数量告诉编译器。因此编译器知道应该检索多少个字节以及如何解释它们。

避免使用函数原型的唯一方法,在首次使用函数之前定义它,但这不总是可行的。

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

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

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

在C++中,括号为空与在括号中使用关键字void是等效的--意味着函数没有参数。

原型的功能:

编译器正确处理函数返回值;

编译器检查使用的参数数目是否正确;

编译器检查使用的参数类型是否正确;如果不正确,则转换为正确的类型;

C++自动将传递的值转换为原型中指定的类型,条件是两者都是算数类型。

函数重载可能导致二义性,因此不允许某些自动强制类型转换。

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

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

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

7.2 函数参数和按值传递

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

C++标准使用参数(argument)来表示实参,使用参量(parameter)来表示形参。

在函数中声明的变量(包括参数)是该函数私有的。此类变量被称为局部变量,因为它们被限制在函数中。

7.2.1 多个参数

在声明或者定义参数时,在函数头使用由逗号分隔的参数声明列表。

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

void func(parameter1, parameter2...)

如果两个参数的类型相同,则必须分别指定每个参数的类型。

函数原型中的变量名不必与定义的变量名相同,而且可以省略。然而,提供变量名将使原型更容易理解,尤其是两个参数的类型相同时。这样,变量名可以提醒参量与参数间的对应关系。

形参与其他局部变量的主要区别是,形参从调用函数哪里获得自己的值,而局部变量从函数内部定义获得自己的值。

7.3 函数和数组

函数是处理复杂类型(如数组和结构)的关键。

int sum_arr(int arr[], int n);

方括号指出arr是一个数组,而空括号表明,可以将任何长度的数组传递给该函数。但实际情况并非如此:arr实际上并不是数组,而是一个指针。

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

在大多数情况下,C++将数组名视为指针。该规则也有一些例外。首先,数组声明使用数组名来标记存储位置;其次,对数组名使用sizeof将得到整个数组的长度;最后,将地址运算符&用于数组名时,将返回整个数组的地址。

在C++中,当也仅当用于函数头或函数原型中,int* arr与int arr[]的含义才相同。它们都意味着arr是一个int指针。然而,数组表示法提醒用户,arr不仅指向int,还指向int数组的第一个int。当指针指向数组的第一个元素时,本书使用数组表示法;而当指针指向一个独立的值时,使用指针表示法。

arr[i] = *(arr + i);
&arr[i] = arr + i;

7.3.2 将数组作为参数意味着声明

传递常规变量时,函数将使用该变量的拷贝;

但传递数组时,函数将使用原来的数组。

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

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

void func(int arr[], int size); // good prototype
void func(int arr[size]);       // bad prototype

7.3.3 更多数组函数示例

构思程序时将存储属性和操作结合起来,便是超OOP思想迈进的一大步。

为防止函数无意中修改数组的内容,可在声明形参时使用关键字const。

void func(const int arr[], int size);

7.3.4 使用数组区间的函数

对于处理数组的C++函数,必须将数组中的数据类型、数组的起始位置和数组中元素数量提交给它;传统的C++方法是,将指向数组起始处的指针作为一个参数,将数组长度作为第二个参数。

另一个给函数提供所需信息的方法,即指定元素区间(range),这可以通过传递两个指针来完成:一个指针标识数组的开头,另一个指针标识数组的尾部。

7.3.5 指针和const

int val1 = 5;
const int* pt = &val1; // const 修饰int* ,所以值不能变,地址可变。
int* const pt = &val1; // const 修饰pt, 所以地址不能变,值可以变。

可以将指针指向一个常量,防止使用该指针修改所指向的值。

可以将指针本身声明为常量,防止修改指针指向的地址。

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

7.4 函数和二维数组

为编写将二维数组作为参数的函数,必须牢记,数组名被视为其地址。

int arr2[5][4];
int sum(int (*arr2d)[4], int size); // 指针数组,(*)是必要的
int sum(int arr2d)[][4], int size); // 指针数组,可读性更强


arr2[r][c] == *(*(arr2 + r) + c); // same thing

7.6 函数和结构

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

将结构作为参数传递,并在需要时将结构用作返回值使用。然而,按值传递结构有一个缺点,如果结构非常大,则复制结构将增加内存要求,降低系统运行的速度。

7.6.1 传递和返回结构

当结构很小时,按值传递结构最合理。

7.6.3 传递结构地址

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

将传值改成传地址需要修改三个地方;

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

        将形参声明维指向val的指针,即typeName*。

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

7.9 递归

递归----C++函数可以自己调用自己。main()除外。

7.9.1 包含一个递归调用的递归

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

void recurs(argumentlist)
{
    statement1
    if (test-condition)
        recurs(arguments)
    statement2
}

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

递归调用将导致一系列有趣的事情。是要if语句为true,每个recurs()调用都将执行statement1,然后在调用recurs(),而不会执行statement2。当if语句为false时,当前调用将执行statement2.当前调用结束后,程序控制权将返回调用它的recurs(),而该recurs()将执行statement2部分,然后结束,并将控制权返回给前一个调用,以此类推。

void countdown(int n)
{
	cout << "Counting down ..." << n << endl;
	if (n > 0)
		countdown(n - 1);
	cout << n << ": Kaboom!\n";
}


int main()												
{
	countdown(5);	
	return 0;
}

/*Counting down ...5
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: Kaboom!*/

7.9.2 包含多个递归调用的递归

递归方法有时被称为分而治之策略(divide-and-conquer-strategy)。如果要求的递归层次很多,这将导致函数调用数(以及存储的变量数)翻倍,这种递归方式将是一种很糟糕的选择。

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

void recurs()
{
	const int Len = 66;
	const int Divs = 6;
	char ruler[Len];

	for (int i = 0; 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 (int i = 1; i < Divs; ++i)
	{
		subdivide(ruler, min, max, i);
		cout << ruler << endl;
		for (int j = 1; j < Len - 2; ++j)
		{
			ruler[j] = ' ';
		}
	}
}

/*
|                                                               |
|                               |                               |
|               |               |               |               |
|       |       |       |       |       |       |       |       |
|   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
*/

7.10 函数指针

函数的地址是存储其机器语言代码的内存的开始地址。

例如,可以编写将另一个函数的地址作为参数的函数。这样第一个函数将能够找到第二个函数,并运行它。与直接调用另一个函数相比,这种方法很笨拙,但它允许在不同的时间传递不同函数的地址,这意味着可以在不同的时间使用不同的函数。

将程序员要使用的算法函数的地址传递给调用函数,必须要完成下面工作:

        获取函数的地址;

        声明一个函数指针;

        使用函数指针来调用函数;

获取函数的地址很简单,只要使用函数名(后面不跟参数和函数调用符号())即可。

thought(think);    // 将函数think传入
thought(think());  // 将函数think的返回值传入

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

声明指向函数的指针时,也必须指定指针指向的函数类型。这意味着声明应指定函数的返回类型以及函数的特征标(参数列表)。

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

为提供正确的优先级,必须在声明中使用括号*func_p括起。括号的优先级比*运算符高。

double func(int);         // prototype
double (*func_p)(int);    // func_p is a function pointor
func_p = func;            // func_p points to the func function

void extimate(int, double (*func_p)(int)); // prototype
estimate(50, (*func_p)(5)); // OK, 提示代码正在使用函数指针
estimate(50, func_p(5));    // OK

7.10.3 深入探讨函数指针

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

// declaration
const double* (*pf1)(const double ar[], int n);

// function pointer initialization
const double* (*pf2)(const double ar[], int n);

// 使用类型推导
auto p3 = f3;

// 函数指针数组
const double* (*pf[3])(const double ar[], int n) = {f1, f2, f3};

[]运算符优先级高于*运算符,因此*pf[3]表明pa是一个包含三个指针的数组。

自动类型推导只能用于单值初始化,而不能用于初始化列表。

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

7.10.4 使用typedef

typedef typeName aliasName;

7.11 总结

函数是C++的编程模块,要使用函数,必须提供函数定义和原型,并调用函数。

函数定义是实现函数功能的代码;

函数原型描述了函数的接口:传递给函数的值的数目和种类以及函数的返回类型。

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

在默认情况下,C++函数按值传递参数。这意味着函数定义的形参是新的变量,它们被初始化为函数调用所提供的值。以此,C++函数通过拷贝、保护了原始数据的完整性。

C++处理结构的方式与基本类型完全相同。

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

C++函数名和函数地址的作用相同。通过函数指针作为参数,可以传递要调用函数的名称。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值