C++ Primer Plus 学习笔记
第 7 章 函数 —— C++ 的编程模块
复习函数的基本知识
要使用 C++ 函数,必须完成如下工作:
- 提供函数定义;
- 提供函数原型;
- 调用函数。
库函数是已经定义和编译好的函数,同时可以使用标准库头文件提供原型,因此只需正确调用即可。而自定义函数必须自行处理这 3 个方面——定义、提供原型和调用。
定义函数
可以将函数分成两类:没有返回值的函数和有返回值的函数。
没有返回值的函数被称为void
函数,其通用格式如下:
void functionName(parameterList)
{
statement(s)
return; // optional
}
parameterList
指定了传递给函数的参数类型和数量,可选返回语句标记了函数的结尾,否则,函数将在右花括号处结束。
有返回值的函数其通用格式如下:
typeName functionName(parameterList)
{
statements
return vlaue; // value is type cast to type typeName
}
对于有返回值的函数,必须使用返回语句,以便将值返回给调用函数。值本身可以是常量、变量,也可以是表达式,只是其结果的类型必须为typeName
类型或可以被转换为typeName
。函数将最终的值返回给调用函数。C++ 对于返回值的类型有一定的限制:不能是数组,但可以是其他类型——整数、浮点数、指针,甚至可能是结构和对象!(可以将数组作为结构和对象的组成部分返回。)
通常,函数通过将返回值复制到指定的 CPU 寄存器或内存单元中将其返回。随后,调用函数将查看该内存单元。
函数原型和函数调用
为什么需要原型
原型描述了函数到编译器的接口,它将函数返回值的类型(如果有的话)以及参数的类型和数量告诉编译器。
避免使用函数原型的唯一方法是,在首次使用函数之前定义它,但这并不总是可行的。另外,C++ 的编程方格是将main()
放在最前面,因为它通常提供了程序的整体结构。
原型的语法
函数原型是一条语句,因此必须以分号结束。获得原型最简单的方法是,复制函数定义中的函数头,并添加分号。
通常,在原型的参数列表中,可以包括变量名,也可以不包括。原型中的变量名相当于占位符,因此不必与函数定义中的变量名相同。
在 C++ 中,原型是必不可少的。参数列表为空和使用关键字void
是等效的——意味着函数没有参数。
在 C++ 中,不指定参数列表时应使用省略号,通常,仅当与接受可变参数的 C 函数交互时才需要这样做。
原型的功能
原型确保以下几点:
- 编译器正确处理函数返回值;
- 编译器检查使用的参数数目是否正确;
- 编译器检查使用的参数类型是否正确。如不正确,则转换为正确的类型(如果可能的话)。
C++ 自动将传递的值转换为原型中指定的类型,条件是两者都是算术类型。
自动类型转换并不能避免所有可能的错误。
仅当有意义时,原型化才会导致类型转换。例如,原型不会将整型转换为结构或指针。
在编译阶段进行的原型化被称为静态类型检查(static type checking)。静态类型检查可捕获许多在运行阶段非常难以捕获的错误。
函数参数和按值传递
C++ 通常按值传递参数,将数值参数传递给函数,函数将其赋给一个新的变量。
用于接受传递值的变量被称为形参,传递给函数的值被称为实参。C++ 标准使用参数(argument)来表示实参,使用参量(parameter)来表示形参。
在函数中声明的变量(包括参数)是该函数私有的,被称为局部变量,被限制在函数中。也被称为自动变量。
多个参数
函数可以有多个参数,在调用时,用逗号将这些参数分开。
在定义函数时,在函数头中使用逗号分隔的参数声明列表。如果函数的两个参数类型相同,则必须分别指定每个参数的类型,而不能像声明常规变量那样,将声明组合在一起。
原型中的变量名不必与定义中的变量名相同,而且可以省略。然而,提供变量名将使原型更容易理解,尤其是两个参数的类型相同时。这样,变量名可以提醒参量和参数间的相应关系。
形参与其他局部变量的主要区别是,形参从调用函数那里获得自己的值,而其他变量是从函数中获得自己的值。
函数和数组
函数如何使用指针来处理数组
在大多数情况下,C++ 与 C 语言一样,也将数组名视为指针,该规则有一些例外。首先,数组声明使用数组名来标记存储位置;其次,对数组名使用sizeof()
将得到整个数组的长度;第三,将地址运算符&
用于数组名时,将返回整个数组的地址。
在 C++ 中,当(且仅当)用于函数头或函数原型中,int arr[]
和int *arr
的含义才是相同的。
arr[i] == *(arr + i) \\values in two notations
&arr[i] == arr + i \\addresses in two notations
遍历数组时,使用指针加法和数组下标是等效的。
将数组作为参数意味着什么
将数组作为参数,是将数组的位置(地址)传递给了函数。传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组。这种区别并不违反 C++ 按值传递的方法,传递了一个值,这个值被赋给了一个新的变量,这个值是地址,而不是数组的内容。因此,无法使用sizeof()
来获悉原始数组的长度。
数组名与指针对应是一件好事,将数组地址作为参数可以节省复制整个数组的时间和内存。另一方面,使用原始数据增加了破坏数据的风险。
用 const 保护数组
为防止函数无意中修改数组的内容,可在声明形参时使用关键字const
。该声明表示,指针指向的的常量数据,不能使用指针修改该数据,但并不意味着原始数组必须是常量。
使用数组区间的函数
处理数组的 C++ 函数,必须将数组中的数据种类、数组的起始位置和数组中元素数量提交给它;舍传统 C/C++ 方法是,将指向数组起始位处的指针作为第一个参数,将数组长度作为第二个参数。
另一种方法是,指定元素区间,可以通过传递两个指针,一个指针标识数组的开头,另一个指针标识数组的结尾。C++ 标准库(STL)将区间方法广义化,使用“超尾”概念来指定区间。也就是说,对于数组而言,标识数组结尾的参数将是指向最后一个元素后面的指针。
指针和 const
可以用两种不同的方式将const
关键字用于指针。第一种方式是让指针指向一个常量对象,可以防止使用该指针来修改所指向的值。第二种方法是将指针本身声明为常量,这样可以防止改变指针指向的位置。
可以将常规变量赋给常规指针,也可将常规变量赋给指向const
的指针,还可以将const
变量赋给指向const
的指针,但不能将const
的地址赋给常规指针。
只有一层间接关系(如指针指向基本数据类型)时,才可以将非const
地址或指针赋给const
指针。
注意:如果数据类型本身并不是指针,则可以将const
数据或非const
数据的地址赋给指向const
的指针,但只能将非const
数据地址赋给非const
指针。
将指针参数声明为指向常量数据的指针有两条理由:
- 这样可以避免由于无意间修改数据而导致的编程错误;
- 使用
const
使用得函数能够处理const
和非const
实参,否则将只能接受非const
数据。
如果条件允许,则应将指针形参声明为指向const
的指针。
函数和二维数组
为编写将二维数组作为参数的函数,必须牢记,数组名被视为指针,因此,相应的形参是一个