第七章 函数
7.1 函数的基础知识
要使用函数,必须完成如下工作:
Ø 提供函数定义
Ø 提供函数原型
Ø 调用函数
7.1.1 函数的定义
函数总体来说可以分为两类,一类是没有返回值的,另一类是具有返回值的,两类的函数定义的格式如下
void functionName(parameterList) { statement(s) return; //可以有也可以没有 } typeName functionName(parameterList) { statement(s) return value; // value 的类型是typeName或者可以自动转换为typeName }
C++对于返回类型有限制,不能是数组,但可以是整型、浮点型、指针、结构和对象。函数内在遇到第一个返回语句将结束。 有返回值的函数,必须使用返回语句,返回值可以是常量、变量或者表达式。其结果必须可以转化为typeName类型。
7.1.2 函数原型和函数调用
函数原型描述了函数到编译器的接口,把函数的返回类型、参数列表(数量,类型和顺序)告知编译器。
函数原型是一条语句,必须以分号结束,可以通过复制函数头并添加分号来快速构建函数原型。
函数原型不要求提供变量名,但是拥有变量名的函数原型可以提高程序的可读性。
函数原型的作用有3个:
Ø 编译器正确处理函数返回值;
Ø 编译器检查使用的参数数目是否正确;
Ø 编译器检查使用的参数类型是否正确,如果不正确,则转化为正确的类型(如果可能)。
7.1.3 ANSI C和C++ 的函数原型的区别
在ANSI C中函数原型是可选的,而在C++中函数原型是必须的。
在C++中,圆括号中参数列表为空与在圆括号中使用void是等效的,意味着没有参数。在ANSI C中圆括号为空意味着不指出参数,将在后面定义参数列表。C++不指定参数列表应该使用省略号(…),例如:
void say_bye(...); //不指出参数,将在后面的定义中指出参数列表
C++中,通常仅当与可变参数C函数交互时才这样做。
7.2 函数参数与按值传递
按值传递参数时,将数值参数传递给函数,而后将其赋值给新变量。如:
double cube(double x); double a = 1.0; double volume = cube(a);
函数cube被调用时,将创建一个名为x的新变量,并初始化为a的值1.0。这样在函数内部对x的修改,将不会改变a的值。
形参:接收传递值的变量。
实参:传递给函数的值。
C++使用参数(argument)来表示实参,参量(parameter)来表示形参。
函数中声明的变量是函数私有的,函数被调用是,为这些变量分配内存,在函数结束时,释放这些内存。这样的变量成为局部变量。存储类型为自动存储。
7.3 函数和数组
看如下原型:
int sum_arr(int arr[], int n) //arr是数组,n是数组元素数量
参数arr是一个数组,但实际上在函数内部,arr并不是数组,而是一个指针。
在C++中,当且仅当用于函数头或函数原型时,int arr[]和int * arr等价。但是int arr[]可以具有提醒的作用,表示传递的指针为数组第一个元素的地址。
当把数组第一个元素的地址和数组元素的数目传递给函数时,并没有将函数的内容传递给函数,而是将数组第一个元素的地址,数组元素的类型和数组元素的数目传递给了函数。当传递常规变量时,函数将使用该变量的拷贝,淡传递数组时,函数将直接使用该数组,这一位置对数组内容的改变会改变原来的数组。
实际上传递数组时,所传递的地址也可以看做为普通变量,函数将使用指针的拷贝,把拷贝的指针指向其他地址,将不会修改原来的指针值。
传递数组地址可以节省复制整个数组内容所需的时间和内存,但是另一方面会对原数据带来风险。ANSI C和C++使用const限定符来解决这个问题。如下:
void show_arr(const double arr[], int n); //参数为指向const的指针 void show_arr(const double *arr, int n); //与上一个完全等价
这两个函数将不能在函数内部修改数组的内容,任何赋值操作将会被编译器报告错误。
可以用2种方式将关键字const用于指针。
1. 使指针指向常量;
2. 将指针本身声明为常量。
如下代码:
int n = 10; const int * p1 = &n; //可以修改p1的值,但是不可以修改*p1的值 int * const p2 = &n; //不可以修改p2的值,但是可以修改*p2的值
可以将const变量或者非const变量的地址赋值给指向const的指针,但是只可以将非const变量的地址赋值给常规指针。
当在函数内部不修改指针或者数组的内容时,应当使用指向const的指针。有2个理由:
Ø 可以避免无意的修改而导致的错误
Ø 使用const作为形参,可以接受const和非const实参,否则只能接受非const实参。
7.4 函数和二维数组
看如下原型:
int sum(int m[][4], int size); //size指定行数,4为列数 int sum(int(*m)[4], int size); //与上一个等价
上面的int (*m)[4]的圆括号必不可少。带括号表示m是一个数组,数组内元素的类型是指向含有4个元素数组的指针;不带括号表示m是一个指针的数组,作为形参就是int **类型。原因是[]运算符的优先级高于*,不加括号时,[]先作用于m,表示m是一个具有4个元素的数组,而元素的类型是int *;加括号时,*先作用于m,表示m是一个指针,所指类型为一个具有4个int型元素的数组。
7.5 函数和C-风格字符串
表示字符串的方式有3种:
Ø Char数组
Ø 用引号括起的字符串常量
Ø 被设置为字符串地址的char指针
C-风格字符串内置有’ ’,表示字符串的结尾,因此作为参数可以不用将元素的数量传递给函数。
7.6 函数和结构
函数处理结构和处理基本类型的变量类似。
需要注意的是,当结构很大时,应该选用指向const的指针来代替直接传递结构,这样做可以提高效率。
7.7 函数与array对象
Array为C++11的模板类。将array对象作为参数传递给函数时,将拷贝array的内容。
7.8 递归
C++函数可以自己调用自己,与C语言不同的是C++不允许main函数调用自己。这种功能成为递归。一般的格式如下:
void recurs(argumentList) { statements1 if (test) { recurs(arguments) } statements2 }
这里如果test值为false时,调用链将结束。
7.9 函数指针
函数指针的好处是可以将函数名作为参数传递给函数,这样可以给函数传递策略。当修改策略时,不用修改函数本身,只需要传递不同的函数指针实参,这就是委托协议。
使用函数指针必须完成以下工作:
Ø 获取函数地址
Ø 声明一个函数指针
Ø 使用函数指针调用函数
1) 获取函数地址
获取函数地址就是把函数名作为参数传递给其他函数,如:
process(think); //把think函数地址传递给process thought(think()); //把think函数的返回值传递给thought
2) 声明函数指针
声明方法如下:
double f(int); //f是函数 double(*pf1) (int); //pf1是函数指针 pf1 = f; //为函数指针赋值 double(*pf2) (int) = f; //声明时初始化
通常,为了声明特定类型的函数指针,可以首先编写函数原型,再用(*pf)替换函数名,这样pf就是这种类型的函数的指针。
3) 使用函数指针调用函数
使用函数指针有两种方式调用函数。一种是使用(*pf),另一种是直接使用pf。如下:
double x = f(4); //通过函数名调用函数 double y = (*pf1)(5); //通过函数指针调用函数 double z = pf2(6); //函数指针名直接调用函数
实际上函数名就是函数的起始地址,C++折衷考虑使用两种方式都可以调用函数。
7.10 深入探讨函数指针
先看下面几个等价的函数:
const double * f1(const double arr[], int n); const double * f2(const double[], int); const double * f3(const double *, int);
声明函数指针并赋予初值:
const double * (*p1) (const double*, int) = f1;
可以使用C++11的自动类型推断:
auto p2 = f2;
函数指针数组声明如下:
const double * (*pa[3]) (const double*, int) = { f1, f2, f3 };
注意这里[3]的位置。这里不用为*pa加括号是因为[]运算符的优先级高于*运算符。[]首先作用于pa,表示pa是一个具有3个元素的数组,元素的类型是函数指针。这里也不能用auto自动类型推断。因为自动类型推断只能用于单值初始化,而不能用于初始化列表。但是可以声明同样类型的数组:
auto pb = pa;
Pa和pb都是指向函数指针的指针,可以这样调用:
double av[3] = { 1.0, 2.0, 3.0 }; const double * px = pa[0](av, 3); const double * py = (*pb[0])(av, 3); double x = *pa[1](av, 3); double y = *(*pb[1])(av, 3);
可以使用自动类型推断简单声明指向整个数组的指针:
auto pc = &pa;
Pc的类型为
const double * (*(*pd)[3]) (const double*, int) = &pa;
使用typedef简化声明:
typedef double real; //real是double的别名 typedef const double * (*p_fun) (const double*, int); //p_fun是函数指针类型 p_fun p1 = f1; p_fun pa[3] = { f1, f2, f3 }; p_fun(*pd)[3] = &pa;
这里再说一下使用auto的利弊:
利:使用auto可以简化声明;
弊:可能提供错误的初值,使声明的类型不正确。