(一)概述
本章内容包括:
- 函数的基本知识
- 设计处理数组函数
- 使用const指针参数
- 设计处理文本字符串的函数
- 设计处理结构的函数
- 设计处理string对象的函数
- 调用自身的函数(递归)
- 指向函数的指针
(二)函数的基本知识
自定义函数需要处理三个方面:
- 提供函数定义
- 提供函数原型
- 调用函数
具体例程如下所示:
#include <iostream>
using namespace std;
void Systemout(void); //函数定义
int main()
{
Systemout(); //调用函数
return 0;
}
void Systemout(void) //函数原型
{
cout << "system out" << endl;
}
运行结果如下所示:
定义函数
函数可以分为两类:有返回值的函数和没有返回值的函数。没有返回值的函数格式如下:
void FunctionName(parameterList)
{
Function(s);
return; //optional
}
有返回值的函数格式如下:
typeName FunctionName(parameterList)
{
Function(s);
return value; //value is type cast to type typeName
}
对于没有返回值的函数,不通过调用return语句返回具体的值。如果函数中存在return,但未返回具体值,代表从该条语句直接退出该函数。对于有返回值的函数,必须使用返回语句,将值返回给调用函数。值本身可以是常量、变量及表达式,但是函数类型必须和返回值类型保持一致(存在类型不一致的情况时,返回值类型会被强制转化为函数类型)。C++对于返回值的类型有一定的限制:不能是数组,但可以是其他任何类型(虽然不能直接返回数组,但可以将数组作为结构或对象组成部分来返回)。
函数在执行返回语句后结束,如果在函数内部包含多条返回语句,则函数在执行遇到的第一条返回语句后结束。
int FunctionName(int a, int b)
{
if(a > b)
{
return a;
}
else
{
return b;
}
Function(s); //该函数不会被执行
}
(二)函数和数组
在函数传递数组形参时,实际上并不是数组,而是一个指针,但是在函数内部可以将该指针看做是数组来处理。
(1)函数如何使用指针来处理数组
在大多数情况下,C++和C一样,可以将数组名看做指针(第一个元素的地址):
cookies == &cookies[0] //array name is address of first element
该规则存在一些例外。首先,数组声明使用数组名来标记存储位置,;其次,对数组名使用sizeof将得到整个数组的长度(以字节为单位);第三,将地址运算符&作用于数组名时,将返回整个数组的地址,例如 &cookies 将返回一个32字节内存块的地址(如果int为4个字节)。
int sum_arr(int arr[], int n);
int sum_arr(int *arr, int n);
int sum = sum_arr(cookies, ArSize);
第一条语句和第二条语句的定义都是正确的,int arr[]和int *arr的含义是相同的,都代表着arr是一个int指针。数组表示法(int arr[])提示用户,arr不仅指向int,还指向int数组的第一个int。第三条语句,cookies是第一个元素的地址,由于数组的元素类型为int,因此cookies的类型必须是int指针。
arr[i] == *(arr + i)
&arr[i] == arr + i
(2)将数组作为参数意味着什么
传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组。这种方式并没有改变C++按值传递的方法,函数仍然传递了一个值,只是这个值是通过自身的地址被传递,而不是自身内容。
将数组地址作为参数可以节省复制整个数组所需的时间和内存。
(三)指针和const
const关键字用于指针,第一种方法是让指针指向一个常量对象,这样可以防止使用该指针来修改所指向的值;第二种方法是将指针本身声明为常量,这样可以防止改变指针指向的位置。
1.第一种情形:常规变量的地址赋给const的指针
int age = 22; //(1)
const int * pt = &ag; //(2)
*pt += 1; //(3)
cin >> *pt; //(4)
(1)定义一个变量age。(2)pt指向一个const int(值为39)。不能使用pt来修改age的值,因为*pt的值为const,不能被修改。由于pt指针是const,而age不是const,可以通过修改age变量来修改age的值。
//通过指针修改值
*pt = 20; //invalid
//通过age变量修改值
age = 20; //valid
可以将常规变量的地址赋给常规指针,常规变量的地址赋给指向const的指针。因此还存在两种情形:const变量的地址赋给指向const的指针、const的地址赋给常规指针。(第二种情形不可行)
2.第二种情形:const变量的地址赋给const的指针
//const变量地址赋给const指针
const float g_earth = 9.80;
const float *pe = &g_earth; //valid
//const变量地址赋给常规指针
const float g_moon = 1.63;
float *pm = &g_moon; //invalid
C++禁止将const变量地址赋给非const指针,是为了数据的安全;如果非要突破这种限制,可以使用强制类型转换。
3.第三种情形:指针指向指针,常规指针赋给const指针
int age = 22; //(age ++) is a valid operation
int *pd = &age; //(*pd = 23) is a valid operation
const int *pt = pd; //(*pt = 23) is a invalid operation
进入两级间接关系是,与一级间接关系一样,将const和非const混合的指针赋值方式将不再安全。
尽可能的使用const
将指针参数声明为指向常亮数据的指针有两条理由:
- 这样可以避免由于无意间修改数据而导致的编程错误;
- 使用const使得函数能够处理const和非const实参,否则将只能接受非const数据。如果条件允许,则应将指针形参声明为指向const的指针。
int age = 22;
const int *pt = &age;
在第二个声明中,const只能防止修改pt指向的值(这里为22),而不能防止修改pt的值。也就是说们可以将一个新地址赋给pt:
int sage = 100;
pt = &sage; //Okay to point to another location,but can't change the value of sage
使用const的方式,使得无法修改指针的值:
int a = 10;
const int *ps = &a; //a pointer to const int
int * const pt = &a; //a const pointer to int
第二个声明中,ps为const,不允许使用ps来修改a的值,但是允许ps指向另一个地址。在第三个声明中,pt为const,pt只能指向a,但允许使用pt来修改a的值。简单来说,pt和ps都是const,*pt和ps不是const。
(四)函数和二维数组
二维数组作为函数的参数,那么必然是以地址作为参数进行传递的,因此相应的形参是一个指针。二维数组不同于一维数组,如何正常地声明指针至关重要。假设有如下代码:
int data[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
int total = sum(data, 3);
则函数sum()的原型是怎么定义的?二维数组的行数、列数是怎么作为参数传递的?
Data是一个数组名,该数组有三个元素。第一个元素本身是一个数组,有4个int值组成。因此data的类型是指向由4个int组成的数组的指针。
正确的原型如下所示:
int sum(int (*arr)[4], int size); //4个指向int的指针
int sum(int arr[][4], int size); //与上式含义完全相同,但可读性更强
int *arr[4]; //4个int组成的数组的指针
*注:arr的括号是不可缺少的,由于声明的是一个由4个指向int的指针组成的数组,而不是由一个指向由4个int组成的数组的指针。
(五)函数与结构
函数可以返回结构,与数组不同的是:数组名是数组第一个元素的地址,而结构名只是结构的名称。要想获得结构的地址,必须使用地址运算符&。
(1)传递和返回结构
当结构比较小时,按值传递结构最合理。例:
#include <iostream>
using namespace std;
struct travel_time
{
int hours;
int mins;
};
const int Mins_per_hr = 60; //每小时60分钟
travel_time sum(travel_time t1, travel_time t2);
void show_time(travel_time t);
int main()
{
travel_time day1 = { 5,45 };
travel_time day2 = { 4,55 };
travel_time total = sum(day1,day2);
cout << "Two-day total: ";
show_time(total);
return 0;
}
travel_time sum(travel_time t1, travel_time t2)
{
travel_time total;
total.mins = (t1.mins + t2.mins) % Mins_per_hr;
total.hours = t1.hours + t2.hours + (t1.mins + t2.mins) / Mins_per_hr;
return total;
}
void show_time(travel_time t)
{
cout << t.hours << " hours " << t.mins << " minutes\n";
}
(2)传递结构体的地址
- 调用函数时,将结构的地址(&a)而不是结构本身(a)传递给函数;
- 将形参声明为指向结构类型的指针,由于函数不应该修改结构,因此使用const修饰符;
- 由于形参是指针而不是结构,因此应用间接成员运算符(->),而不是成员运算符(.)
#include <iostream>
#include <cmath>
using namespace std;
struct polar
{
double distance; //distance from origin
double angle; //direction from origin
};
struct rect
{
double x; //horizontal distance from origin
double y; //vertical distance from origin
};
void rect_to_polar(const rect* pxy, polar* pda);
void show_polar(const polar *pda);
int main()
{
rect rplace;
polar pplace;
cout << "Enter the x and y value: ";
while (cin >> rplace.x >> rplace.y)
{
rect_to_polar(&rplace, &pplace);
show_polar(&pplace);
cout << "Next two numbers (q to quit): ";
}
cout << "Done.\n";
return 0;
}
void show_polar(const polar* pda)
{
const double Rad_To_Deg = 57.29577951;
cout << "distance = " << pda->distance;
cout << ", angle = " << pda->angle * Rad_To_Deg;
cout << "degrees.\n";
}
void rect_to_polar(const rect* pxy, polar* pda)
{
pda->distance = sqrt(pxy->x * pxy->x + pxy->y * pxy->y);
pda->angle = atan2(pxy->y, pxy->x);
}
(六)函数和string对象
#include <iostream>
#include <string>
using namespace std;
const int SIZE = 5;
void display(const string sa[], int n);
int main()
{
string list[SIZE];
cout << "Enter your " << SIZE << " favorite astronomical sights:\n";
for (int i = 0; i < SIZE; i++)
{
cout << i + 1 << " : ";
getline(cin, list[i]);
}
cout << "Your list:\n";
display(list, SIZE);
return 0;
}
void display(const string sa[], int n)
{
for (int i = 0; i < n; i++)
cout << i + 1 << " : " << sa[i] << endl;
}
(七)函数与array对象
在C++中,类对象是基于结构的,因此结构编程方面的有些考虑因素也适用于类。既可按值将对象传递给函数,也可将指向对象的指针传递给函数。
#include <iostream>
#include <array>
#include <string>
//constant data
const int Seasons = 4;
//位于std空间中的字符串类数组定义
const std::array<std::string, Seasons> Snames = { "Spring","Summer","Fall","Winter" };
//function to modify array object
void fill(std::array<double, Seasons> *pa);
//function that uses array object without modifying it
void show(std::array<double, Seasons> da);
int main()
{
std::array<double, Seasons> expenses;
fill(&expenses);
show(expenses);
return 0;
}
//对应的季节填入对应的花销
//地址传递
void fill(std::array<double, Seasons>* pa)
{
using namespace std;
for (int i = 0; i < Seasons; i++)
{
cout << "Enter " << Snames[i] << " expenses:";
cin >> (*pa)[i];
}
}
//显示每个季节对应的花销,并计算总共花销
//值传递
void show(std::array<double, Seasons> da)
{
using namespace std;
double total = 0.0;
cout << "\nEXPENSES\n";
for (int i = 0; i < Seasons; i++)
{
cout << Snames[i] << ": $" << da[i] << endl;
total += da[i];
}
cout << "Total Expenses: $" << total << endl;
}
运行结果:
(八)递归
函数自己调用自己称为递归。
8.1 包含一个递归调用的递归
如果递归函数调用自己,则被调用的函数也会调用自己,除非代码中包含终止调用的部分,否则将无限循环下去。一般可以将递归调用放在if语句中,以结束调用。一般形式如下:
void recurs(argumentlist)
{
statements1;
if(test)
recurs(arguments);
statements2;
}
在进行递归调用时,只要if语句判断为真,便会一直调用递归函数recurs,直至if语句判断为假,才会逐一调用statements2函数,直至整个递归全部调用完成。例:递归调用10次
#include <iostream>
void countdown(int n);
int main()
{
countdown(10);
return 0;
}
void countdown(int n)
{
using namespace std;
cout << "Counting down ..." << n << endl;
if (n > 0)
{
countdown(n - 1);
}
cout << n << " : Kaboom!\n";
}
递归函数在if判断为真时,从10到0,countdown函数 嵌套了10次,因此先输出counting down 11次,当if判断为假时,函数逐层退出,从0到10输出Kaboom。
8.2 包含多个递归调用的递归
例:
#include <iostream>
const int Len = 66;
const int Divs = 6;
void subdivide(char* ar, int low, int high, int level);
int main()
{
char ruler[Len];
int i;
int max = Len - 2;
int min = 0;
//初始化ruler数组
for (i = 1; i < Len - 2; i++)
{
ruler[i] = ' ';
}
ruler[Len - 1] = '\0';
ruler[min] = ruler[max] = '|';
std::cout << ruler << std::endl;
//开始递归调用
for (i = 1; i < Divs; i++)
{
subdivide(ruler, min, max, i);
std::cout << ruler << std::endl;
for (int j = 1; j < Len - 2; j++)
{
ruler[j] = ' ';
}
}
return 0;
}
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);
}
运行结果:
分析:mian函数先初始化了ruler数组并输出,即运行结果的第一行输出:ruler[0] = ‘|’; ruler[64] = ‘|’; ruler[65] = ‘\0’;
其次,通过for循环调用6次subdivide函数并输出;第一次调用时,ar[32] = ‘|’;由于level为1,输出了第二行显示的内容,将原先的部分等分为了两个部分;第二次调用时,两次嵌套,即4等分;第三次调用时,3次嵌套,即8等分;以此类推,最后一次,6次嵌套,即64等分。
(九)函数指针
与数据项相似,函数也有地址。函数的地址是存储器二进制代码的内存起始地址。编写一个函数的地址作为参数的函数,在函数调用时,当前函数能够直接找到被调用函数的地址,并且开始运行。与直接调用函数相比,调用地址的方式显的很笨拙,但是它允许在不同的时间传递不同函数的地址,即不同的时间使用不同的函数。
9.1 函数指针的基础知识
假设需要设计一个名为 stimate() 的函数,估算编写指定函数代码所需要的时间,并且希望不同的程序员都将使用该函数。对于所有用户来说,estimate() 中一部分代码是相同的,但是该函数允许每个程序员提供自己的算法来估算时间,为了现实这种目标,采用的机制是,将所需的算法函数的地址传递给 estimate()。为此,可以将工作分为以下三个部分:
- 获取函数的地址;
- 声明一个函数指针;
- 使用函数指针来调用函数;
(1)获取函数的地址
函数的地址即是函数名。假设 think() 是一个函数,则 think 就是该函数的地址。要将函数作为参数进行传递,必须传递函数名。
注:区分传递的是函数的地址还是函数的返回值。
process(think); //将think函数的地址传递给process函数
thought(think()); //将think函数的返回值传递给thought函数
(2)声明函数指针
声明指向某种数据类型的指针时,必须指定指针指向的类型。因此,声明指向函数的指针时,也必须指定指针指向的函数类型。
假设:一个估算时间的函数其原型如下:
double pam(int);
则正确的指针类型声明如下:
double (*pf)(int); //pf指向函数
//一个int类型参数
//返回类型为double
这与 pam() 声明类似,将 pam 替换成了 (*pf) 。由于 pam 为函数,因此 (*pf) 也是函数,pf 则是函数指针。
注:*pf(int) 意味着 pf() 是一个返回指针的函数,而 (*pf)(int) 意味着 pf 是一个指向函数的指针。
double (*pf)(int); //pf指向一个返回double类型的函数
double *pf(int); //函数返回一个double类型的指针
正确地声明pf后,可以将相应函数的地址赋给它:
double pam(int);
double (*pf)(int);
pf = pam; //pf指向pam()函数
注:pam() 的特征标和返回类型必须与 pf 相同;如果不相同,编译器将拒绝这种赋值。
double ned(double);
int ted(int);
double (*pf)(int);
pf = ned; //无效赋值;参数类型不匹配
pf = ted; //无效赋值;函数返回值类型不匹配
回头去看一下开始的 estimate() 函数。假设要将将要编写的代码行数和估算算法(如 pam() 函数)的地址传递给它,则其函数原型如下:
void estimate(int lines, double (*pf)(int));
声明中指出,第二个参数为函数指针,其形参类型为int,并且返回一个double类型的值;
estimate(50,pam);
(3)使用指针来调用函数
double pam(int);
double (*pf)(int);
pf = pam; //pf指向pam()函数
double x = pam(4); //使用函数名调用函数
double y = (*pf)(5); //使用函数指针调用函数
//C++允许使用函数名形式的pf:
double y = pf(5); //使用函数指针调用函数
9.2 函数指针示例
main() 函数两次调用 estimate() 函数,一次传递 betsy() 函数的地址,一次传递 pam() 函数的地址,分别使用 betsy() 和 pam() 函数计算所需要的时间。这种设计有助于之后的程序开发。当需要修改估算时间使用新的算法时,不要需要重新编写 estimate()函数, 只需要提供新的算法函数,并确保该函数的特征标和返回类型正确即可。
#include <iostream>
double betsy(int);
double pam(int);
void estimate(int lines, double (*pf)(int));
int main()
{
using namespace std;
int code;
cout << "How many lines of code dou you need? ";
cin >> code;
cout << "Here's Betsy's estimate:\n";
estimate(code,betsy);
cout << "Here's Pam's estimate:\n";
estimate(code, pam);
return 0;
}
double betsy(int lns)
{
return 0.05 * lns;
}
double pam(int lns)
{
return 0.03 * lns + 0.0004 * lns * lns;
}
void estimate(int lines, double (*pf)(int))
{
using namespace std;
cout << lines << " lines will take ";
cout << (*pf)(lines) << " hour(s)\n";
}
9.3 深入函数指针
const double * f1(const double ar[], int n);
const double * f2(const double [], int);
const double * f3(const double *, int);
三个函数的特征标看似不同,但实际上相同。在函数原型声明时可以省略标识符,但是在函数定义时必须提供标识符。
假设,声明一个指针pa,它可以指向这三个函数之一:
const double *(*p1)(const double *, int);
//在声明时进行初始化:
onst double *(*p1)(const double *, int) = f1;
auto p2 = f2; //使用C++11的自动类型推断功能,简化代码
//输出代码:
cout << (*p1)(av,3) << " : " << *(*p1)(av,3) << endl;
cout << p2(av,3) << " : " << *p2(av,3) << endl;
(*p1)(av,3) 和 p2(av,3) 都调用指向的函数(这里为 f1() 和 f2()),所以输出的是这两个函数的返回值,其返回值类型为const double *(即double值的地址)。因此在每一条的cout语句中,前半部显示的都是一个double值的地址。
*(*p1)(av,3) 和 *p2(av,3)表示的是地址上的内容。
怎么样通过一个函数指针来表示三个函数:
const double * (*pa[3])(const double *, int) = {f1, f2, f3};
pa是一个包含三个元素的数组,声明这样的数组需要使用 pa[3]。运算符 [ ] 的优先级高于*,因此*pa[3] 表示pa是一个包含三个指针的数组。上述声明中指出:特征标为const double *, int,且返回类型为 const double *的函数。
这里不能使用auto。因为自动类型推断只能用于单值初始化,而不能用于初始化列表。但是在声明数组 pa 后,声明同样类型的数组就可以了:
auto pb = pa;
//调用函数
const double * px = pa[0](av, 3);
const double * py = (*pb)[1](av, 3);
//获取指向的double值
double x = *pa[0](av, 3);
double y = *(*pb[1])(av, 3);
上述方式采用的是声明一个数组用来存放三个函数指针,与此不同的方式,创建一个指向整个数组的指针。数组名 pa 是指向函数指针的指针,因此指向整个数组的指针便是这样的指针,即指向指针的指针。有如下的声明方式:
auto pc = &pa; //C++11 automatic type deduction
//由三个函数指针构成的数组,声明的是数组
*pd[3] //an array of 3 pointers
//指向整个数组的指针,声明的是指针
(*pd)[3] //a pointer to an array of 3 elements
pd是一个指针,指向一个包含三个元素的数组。这些元素是什么形式,具体由 pa 的声明来描述,如下所示:
const double *(*(*pd)[3])(const double *, int) = &pa;
解释:pd指向的是数组,*pd就是数组,(*pd)[i] 代表数组中第 i 个元素,即函数指针。
函数调用方式:(*pd)[i](av, 3),而*(*pd)[i](av, 3)是返回的指针指向的值。亦可用 (*(*pd)[i])(av, 3)来调用函数,而*(*(*pd)[i])(av, 3)指向的是double值。
注:pa 与 &pa之间的差别,pa是数组名,表示地址,&pa是整个数组的地址(这里是三个指针块的地址)。
**&pa == *pa == pa[0];
综合例程:
#include <iostream>
const double* f1(const double ar[], int n);
const double* f2(const double[], int);
const double* f3(const double*, int);
int main()
{
using namespace std;
double av[3] = {1112.3, 1542.6, 2227.9};
//pointer to a function
const double* (*p1)(const double*, int) = f1;
auto p2 = f2;
//const double *(*p2)(const double *,int) = f2;
cout << "Using pointers to functions:\n";
cout << " Address Value\n";
cout << (*p1)(av, 3) << " : " << *(*p1)(av, 3) << endl;
cout << p2(av, 3) << " : " << *p2(av, 3) << endl;
//pa an array of pointers
const double* (*pa[3])(const double*, int) = { f1,f2,f3 };
auto pb = pa;
//const double* (**pb)(const double*, int) = pa;
cout << "\nUsing an array of a pointer to a function:\n";
cout << " Address Value\n";
for (int i = 0; i < 3; i++)
{
cout << pa[i](av, 3) << " : " << *pa[i](av, 3) << endl;
}
cout << "\nUsing a pointer to a pointer to a function:\n";
cout << " Address Value\n";
for (int i = 0; i < 3; i++)
{
cout << pb[i](av, 3) << " : " << *pb[i](av, 3) << endl;
}
//what about a pointer to an array of function pointers
cout << "\nUsing pointer to an array of pointers:\n";
cout << " Address Value\n";
auto pc = &pa;
//const double* (*(*pc)[3])(const double*, int) = &pa;
cout << (*pc)[0](av, 3) << " : " << *(*pc)[0](av, 3) << endl;
//hard way to declare pd
const double* (*(*pd)[3])(const double*, int) = &pa;
const double* pddata = (*pd)[1](av, 3);
cout << pddata << " : " << *pddata << endl;
cout << (*(*pd)[2])(av, 3) << " : " << *(*(*pd)[2])(av, 3) << endl;
return 0;
}
const double* f1(const double* ar, int n)
{
return ar;
}
const double* f2(const double ar[], int n)
{
return ar + 1;
}
const double* f3(const double ar[], int n)
{
return ar + 2;
}
运行结果: