函数的基本知识
- 创建函数时,必须完成如下工作:提供函数定义;提供函数原型;调用函数。
- C++对于函数返回值的类型有一定的限制:不能是数组,但可以是其他任何类型——整数、浮点数、指针,甚至可以是结构和对象。
- 函数原型不要求提供变量名,有类型列表就足够了。通常,在原型的参数列表中,可以包括变量名,也可以不包括。原型中的变量名相当于占位符,因此不必与函数定义中的变量名相同。
- 函数原型的功能:
- 编译器正确处理函数返回值;
- 编译器检查使用的参数数目是否正确;
- 编译器检查使用的参数类型是否正确,如果不正确,则转换为正确的类型(如果可能的话)。在编译阶段进行的原型化被称为静态类型检查(static type checking)。
函数参数和按值传递
- 用于接收传递值的变量被称为形参;传递给函数的值被称为实参。
- 如果函数的两个参数的类型相同,则必须分别指定每个参数的类型,而不能像声明常规变量那样,将声明组合在一起;原型中的变量名不必与定义中的变量名相同,而且可以省略;提供变量名将使原型更容易理解,尤其是两个参数的类型相同时。
- 对于
(10 * 9) / (2 * 1)和(10 / 2) * (9 / 1),前者将计算90 / 2,得到45;后者将计算为5 * 9,得到45。当数字非常大时,这种交替进行乘除运算的策略可以防止中间结果超出最大的浮点数。
默认参数
在C++中,定义函数时可以给形参指定一个默认的值,这样调用函数时如果没有给这个形参赋值(没有对应的实参),那么就使用这个默认的值。也就是说,调用函数时可以省略有默认值的参数。如果用户指定了参数的值,那么就使用用户指定的值,否则使用参数的默认值。
所谓默认参数,指的是当函数调用中省略了实参时自动使用的一个值,这个值就是给形参指定的默认值。
C++规定,默认参数只能放在形参列表的最后,而且一旦为某个形参指定了默认值,那么它后面的所有形参都必须有默认值。实参和形参的传值是从左到右依次匹配的,默认参数的连续性是保证正确传参的前提。
函数和数组
在C++中,当(且仅当)用于函数头或函数原型中,
int *arr和int arr[]的含义才是相同的。它们都意味着arr是一个int指针。将指针(包括数组名)加1,实际上是加上了一个与指针指向的类型的长度(以字节为单位)相等的值。对于遍历数组而言,使用指针加法和数组下标时是等效的。传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组。实际上,这种区别并不违反C++按值传递的方法,函数仍传递一个值,这个值被赋给一个新变量,但这个值是一个地址,而不是数组的内容。
将数组地址作为参数可以节省复制整个数组所需的时间和内存。
对于处理数组的C++函数,必须将数组中的数据种类、数组的起始位置和数组中元素数量提交给它;传统的C/C++方法是,将指向数组起始处的指针作为一个参数,将数组长度作为第二个参数(指针指出数组的位置和数据类型);还有另一种给函数提供所需信息的方法,即指定元素区间(range),这可以通过传递两个指针来完成:一个指针标识数组的开头,另一个指针标识数组的尾部。
写个例子加深一下各方面理解:
#include <iostream> const int Max = 5; int fill_array(double ar[], int limit); void show_array(const double ar[], int n); // do not change data void revalue(double r, double ar[], int n); int main() { using namespace std; 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; Please enter a number: "; } revalue(factor, properties, size); show_array(properties, size); } cout << "Done.\n"; cin.get(); cin.get(); return 0; } 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) // 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) { using namespace std; 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; }指针和
const可以用两种不同的方式将
const关键字用于指针。第一种方法是让指针指向一个常量对象,这样可以防止使用该指针来修改所指向的值,第二种方法是将指针本身声明为常量,这样可以防止改变指针指向的位置。举个例子来说明一下:
int age = 39; const int * pt = &age;pt的声明并不意味着它指向的值实际上就是一个常量,而只是意味着对pt而言,这个值是常量。例如,pt指向age,而age不是const。可以直接通过age变量来修改age的值,但不能使用pt指针来修改它。同时,该声明中的const只能防止修改pt的值(这里是39),而不能防止修改pt的值。也就是说,可以将一个新的地址赋给pt。再比如:
int sloth = 3; const int * ps = &sloth; // a pointer to const int int * const finger = &sloth; // a const pointer to int在最后一个声明中,关键字
const的位置与以前不同,这种声明格式使得finger只能指向sloth,但允许使用finger来修改sloth的值。也就是,地址是常量。如果愿意的话,还可以声明指向
const对象的const指针:double trouble = 2.98; const double * const stick = &trouble;综上来讲,其实有四种组合关系:将常规变量的地址赋给常规指针;将常规变量的地址赋给指向
const的指针;将const变量的地址赋给指向const的指针;将const的地址赋给常规指针。这里面,第四种是不可行的,其他都可行。将指针参数声明为指向常量数据的指针有两条理由:a. 这样可以避免由于无意间修改数据而导致的编程错误;b. 使用
const使得函数能够处理const和非const实参,否则将只能接受非const数据。如果条件允许,则应将指针形参声明为指向const的指针。
函数和C-风格字符串
- 将字符串作为参数时意味着传递的是地址,但可以使用
const来禁止对字符串参数进行修改。 - C-风格字符串有内置的结束字符,这意味着不必将字符串长度作为参数传递给函数,而函数可以使用循环依次检查字符串中的每个字符,直到遇到结尾的空值字符为止。
函数和结构
涉及到函数传递时,结构变量的行为更接近于基本的单值变量:
- 可以将一个结构赋给另一个结构;
- 可以按值传递结构,就像普通变量那样;在这种情况下,函数将使用原始结构的副本;函数也可以返回结构;但这样有一个缺点,如果结构非常大,则复制结构将增加内存要求,降低系统运行的速度;
- 也可以传递结构的地址,用指针来访问结构的地址;与数组名就是数组第一个元素的地址不同的是,结构名只是结构的名称,要获得结构的地址,必须使用地址运算符
&; - 另外,C++还使用
&表示引用变量,按引用传递也是特别重要的一种方式。
递归
函数指针
总结
函数是C++的编程模块。要使用函数,必须提供定义和原型,并调用该函数。函数定义是实现函数功能的代码;函数原型描述了函数的接口; 传递给函数的值的数目和种类以及函数的返回类型,函数调用使得程序将参数传递给函数,并执行函数的代码。
在默认情况下,C++函数按值传递参数。这意味着函数定义中的形参是新的变量,它们被初始化为函数调用所提供的值。因此,C++函数通过使用拷贝,保护了原始数据的完整性。
C++将数组名参数视为数组第一个元素的地址。从技术上讲,这仍然是按值传递的,因为指针是原始地址的拷贝,但函数将使用指针来访问原始数组的内容。当且仅当声明函数的形参时,下面两个声明才是等价的:
typeName arr[];
typeName * arr;
这两个声明都表明,arr是指向typeName的指针,但在编写函数代码时,可以像使用数组名那样使用arr来访问元素:arr[i]。即使在传递指针时,也可以将形参声明为const指针,来保护原始数据的完整性。由于传递数据的地址时,并不会传输有关数组长度的信息,因此通常将数组长度作为独立的参数来传递。另外,也可传递两个指针(其中一个指向数组开头,另一个指向数组末尾的下一个元素),以指定一个范围,就像STL使用的算法一样。
C++提供了3种表示C-风格字符串的方法:字符数组、字符串常量和字符串指针。它们的类型都是char*(char指针),因此被作为char*类型参数传递给函数。C++使用空值字符(\0)来结束字符串,因此字符串函数检测空值字符来确定字符串的结尾。
C++还提供了string类,用于表示字符串。函数可以接受string对象作为参数以及将string对象作为返回值。string类的方法size()可用于判断其存储的字符串的长度。
C++处理结构的方式与基本类型完全相同,这意味着可以按值传递结构,并将其用作函数返回类型。然而,如果结构非常大,则传递结构指针的效率将更高,同事函数能够使用原始数据。这些考虑因素也适用于类对象。
C++函数可以是递归的,也就是说,函数代码中可以包括对函数本身的调用。
C++函数名与函数地址的作用相同。通过将函数指针作为参数,可以传递要调用的函数的名称。