本章内容概览
- 基本知识
- 函数原型
- 按值传递函数参数
- 函数和数组
- const指针参数
- 函数和字符串
- 递归
- 指向函数的指针
函数基本知识
要使用C++函数必须完成以下工作:
- 提供函数定义
- 提供函数原型
- 调用函数
void函数:
void functionName(parameterList)
{
statements
}
有返回值函数:
typeName functionName(parameterList)
{
statements
return value;
}
返回值可以是整数、浮点数、指针、结构和对象等。
函数原型
函数原型在C++中是必须的,可以不提供变量名,有类型就够了:
double cube(double);
原型确保以下几点:
- 编译器正确处理函数返回值
- 编译器检查使用的参数数目是否正确
- 编译器检查使用的参数类型是否正确。不正确则转化为正确的类型(如果可能)
注意,仅当有意义时,原型化才会导致类型转化(参数),例如:原型不会将整数转化为指针或结构。
函数参数和按值传递
接受传递值的变量称为形参,传递给函数的值称为实参。在函数调用时,计算机为函数中声明的变量分配内存,函数结束时自动释放,这被称为局部变量。
一个函数原型:
void fifi(float a, float b);
之后调用:
float c= 1.0f;
float d = 1.0f;
fifi(c, d)
c,d按形参位置传递给函数a,b。
函数和数组
将数组作为形参的函数原型:
int sum_arr(int arr[], int n);
arr是一个指针。
之前说过,C++将数组名解释为其第一个元素的地址,即函数传递的参数是一个地址,也就是形参类型为指针,所以上述原型等价于:
int sum_arr(int* arr, int n);
在C++中的函数头和函数原型中,int*
等价于int[]
,但在函数体中并不等价。
如果我们调用上述函数:
int a[10] = {};
sum_arr(a, 10);
我们将数组首地址传递给函数,即将该地址赋予arr指针。这与常规变量不同,常规变量传给函数的是其拷贝,而这里是传地址,也就是该函数可以修改真的数组。
不过由于可以真的修改原数组,所以这种做法可能有风险,比如我们不想修改数组里的元素,只想使用的话,可以使用const 限定符来定义函数原型:
void show_array(const double ar[], int n);
如果在函数体中试图修改传入的数组,编译器就会报错。而函数原型将const double[]
解释为const double* ar
。
const和指针
比如,我们声明一个指向常量的指针pt:
int age = 39;
const int* pt = &age;
该声明指出,pt指向一个const int(39),所以不能使用pt来修改该值,也就是*pt
的值为const,不能修改:
*pt += 1; //invalid
cin >> *pt; //invalid
这样就很微妙,我们可以直接修改age的值,但不能通过pt来修改age的值:
*pt = 20; //invalid
age = 20; //valid
我们向下面这样声明指针变量:
const float g_earth = 9.80;
const float* pe = &g_earth; //valid
const float g_moon = 1.63;
float* pm = &g_moon; //invalid
第一种可行,但第二种不行。第一种情况,既不能直接修改g_earth的值,也不能通过pe修改。第二种情况下,如果将g_moon地址赋给pm,则可以使用pm修改g_moon的值,这样的话g_moon的const限定符就没意义了,所以C++禁止这种做法。
如果将指针指向指针,这会更复杂。如果是一级间接关系,将非const指针赋给const指针是可以的:
int age = 39;
int* pd = &age;
const int* pt = pd;
但二级间接关系不行,如果可以进入二级间接关系,就可以可以这么做:
const int** pp2;
int *p1;
const int n =13;
pp2 = &p1; //假设可以
**pp2 = &n; //valid
*p1 = 10; //valid
上述代码将p1地址赋给const pp2,因此可以用p1来修改const数据,这不安全,所以不能这么使用。
将指针参数声明为指向常量数据的指针的理由(即使用const):
- 可以避免由于无意间修改数据而导致的编程错误。
- 使用const使得函数能够处理const和非const实参,否则只能接受非const实参。
函数和二维数组
将二位数组作为函数形参的形式:
int sum(int (*ar2)[4], int size);
括号是必须的,表明参数是指针而不是数组,即一个指向由4个int组成数组的指针,下面的是不行的:
int *ar2[4]
上述声明是一个由4个指向int的指针组成的数组。
等价原型:
int sum(int ar2[][4], int size);
形参的指针类型声明了列数,size中指定了行数。
函数和C风格字符串
表示字符串的三种方式:
- char数组
- 用引号括起的字符串常量
- 被设置为字符串的地址的char指针
这三种类型都是char*
,所以在函数中作为形参时可以直接这么声明:
void int c_in_str(char* str);
同时可以使用const限定符:
void int c_in_str(const char* str);
函数无法返回字符串,但可以返回字符串的地址:
char* buildstr(const char*str);
函数和结构体
结构体可以直接作为变量类型在函数中使用:
struct travel_time
{
int hours;
int mins;
};
travel_time sum(travel_time t1, travel_time t2);
这时候传入函数的实参是结构体的副本,返回值也是。
不过还可以传递结构体的地址作为实参,声明函数时形参是结构体指针,同时应该使用const限定符:
void show_polar(const polar* pda);
使用函数时:
show_polar(&polar1);
函数和string对象
string对象可以像结构体那样作为变量直接传入,如果要使用多个字符串,可以声明一个string数组:
void display(const string sa);
void display(const string sa[], int n);
递归
函数自己调用自己即递归,例子:
void recurs(argumentList)
{
statements1
if (test)
recurs(arguments)
statements2
}
递归一定要有出口条件,否则会无限递归。
函数指针
和数据项一样,函数也有地址。
比如编写要估算某种代码执行的时间的函数estimate(),要完成以下工作:
- 获取函数的地址
- 声明一个函数指针
- 使用函数指针来调用函数
获取函数地址使用函数名即可。声明函数指针需要与原型对应,比如:
double pam(int);
针对上述原型,声明指针为:
double (*pf)(int);
这与原型方式一致,只是将函数名换为(*pf)
,也就是pam和(*pf)
都是函数,那么pf就是函数指针。声明函数指针后,就可以将函数地址赋予它:
pf = pam;
上面的estimate()函数可以这么声明:
void estimate(int lines, double (*pf)(int));
第二个参数是一个函数指针,调用该函数:
estimate(50, pam);
我们还可以使用函数指针调用函数:
double x = pam(4);
double y = (*pf)(5);
C++还允许这么使用:
double y = pf(5);
深入讨论函数指针
下面一些函数原型,它们等价:
const double* f1(const double ar[], int n);
const double* f2(const double [], int);
const douvle* f3(const double*,int);
如果我们要定义一个函数指针指向上述三个函数之一:
const double* (*p1)(const double*, int);
还可以声明的时候初始化:
const double* (*p1)(const double*, int) = f1;
使用auto关键字会超级简单:
auto p2 = f2;
现在(*p1)(av,3)
和p2(av,3)都调用指向的函数,即f1(),f2()。
如果要使用三个函数,定义一个函数指针数组很方便:
coust double* (*pa[3])(const double*, int) = {f1, f2, f3};
运算符[]优先级高于*
,*pa[3]
表明pa是一个包含3个指针的数组。这时候就不能使用auto进行简单地类型推断了,不过定义好函数指针数组后就可以使用auto:
auto pb = pa;
要调用函数的话,可以这样:
const double* px = pa[0](av, 3);
const double* py = (*pb[1])(av, 3);
第二种更直观。
然后如何创建指向整个数组的指针,我们可以使用auto:
auto pc = &pa;
如果要自己声明的话,可以这么做:
const double* (*(*pd)[3]) (const double*, int) = &pa;
*pd[3]
表明是一个3个指针的数组,(*pd)[3]
表明是一个指向三个元素数组的指针。
要调用函数的话,pd是一个指针,那么*pd
就是数组,(*pd)[i]
是数组中的元素,即函数指针,比如(*pd)[i](av, 3)
,更直观的方式是(*(*pd)[i])(av, 3)
。
除了使用auto进行简化,C++还提供了typedef,针对类型可以使用typedef进行类型别名:
typedef double real;
对于函数指针还可以:
typedef const double* (*p_fun)(const double*, int);
p_fun p1 = f1;
p_fun就成为了函数指针类型别名,然后可以:
p_fun pa[3] = {f1,f2,f3};
p_fun (*pd)[3] = &pa;