【C++ Primer Plus】第七章:函数

第七章:函数

使用函数的步骤:

  • 提供函数定义
  • 提供函数原型
  • 调用函数

C++对于返回值的类型有一定的限制:不能是数组。有趣的是,虽然C++函数不能直接返回数组,但可以将数组作为结构或对象组成部分来返回。

7.1 函数原型

7.1.1 原型的工作原理

通常,函数通过将返回值复制到指定的CPU寄存器或内存单元中来将其返回。随后,调用程序将查看该内存单元。例如:

double volume = cube(side);

首先,原型告诉编译器,cube( )有一个double参数。如果程序没有提供这样的参数,原型将让编译器能够捕获这种错误。其次,cube( )函数完成计算后,将把返回值放置在指定的位置——可能是CPU寄存器,也可能是内存中。然后调用函数(这里为main( ))将从这个位置取得返回值。由于原型指出了cube( )的类型为double,因此编译器知道应检索多少个字节以及如何解释它们。如果没有这些信息,编译器将只能进行猜测,而编译器是不会这样做的。

7.1.2 为什么需要原型?

你可能还会问,为何编译器需要原型,难道它就不能在文件中进一步查找,以了解函数是如何定义的吗?这种方法的一个问题是效率不高。编译器在搜索文件的剩余部分时将必须停止对main( )的编译。一个更严重的问题是,函数甚至可能并不在文件中。C++允许将一个程序放在多个文件中,单独编译这些文件,然后再将它们组合起来。在这种情况下,编译器在编译main( )时,可能无权访问函数代码。如果函数位于库中,情况也将如此。避免使用函数原型的唯一方法是,在首次使用函数之前定义它,但这并不总是可行的。另外,C++的编程风格是将main()放在最前面,因为它通常提供了程序的整体结构。

然而,函数原型不要求提供变量名,有类型列表就足够了。通常,在原型的参数列表中,可以包括变量名,也可以不包括。原型中的变量名相当于占位符,因此不必与函数定义中的变量名相同。

在C++中,括号为空与在括号中使用关键字void是等效的——意味着函数没有参数。在C++中,不指定参数列表时应使用省略号:

void say_bay(...);

7.1.3 原型的功能

  • 编译器正确处理函数返回值;
  • 编译器检查使用的参数数目是否正确;
  • 编译器检查使用的参数类型是否正确。如果不正确,则转换为正确的类型(如果可能的话)。

如果写程序去计算下列的一个计算式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DIHSoPpE-1679998401953)(函数原型.assets/image-20230310174226164.png)]

正确的做法是先算51/6,再去算50/5,然后将它们的结果相乘。而不是先去算分子的乘积,再去算分母的乘积,最后将他们相除。这样得到的乘积将比先进行乘法运算得到的小。例如,对于(10 * 9)/(2 * 1)和(10 / 2)(9 / 1),前者将计算90/2,得到45,后者将计算为59,得到45。这两种方法得到的结果相同,但前者的中
间值(90)大于后者。因子越多,中间值的差别就越大。当数字非常大时,这种交替进行乘除运算的策略可以防止中间结果超出最大的浮点数。

在C++中,当(且仅当)用于函数头或函数原型中,int *arr和int arr[ ]的含义才是相同的。它们都意味着arr是一个int指针。

int sum(int arr[],int n);
int sum(int arr*,int n);

对于数组而言,当前存在两个恒等式需要记忆:

arr[i] == *(arr + i);
&arr[i] == arr+i;

传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组。将数组作为参数并没有意味着C++打破了其按值传递的方法,而是碰巧这个值是一个地址,而不是数组的内容。函数仍传递了一个值,这个值被赋给了一个新变量,还是和之前的按值传递一样。

7.2 const的用法

我们把函数当成一种数据类型,函数的实体当成对象。

指针与const:

int age = 39;
const int*pt = &age;

该声明指出:pt指向一个const int,因此,不能使用pt来修改这个值。pt的声明并不意味着它指向的值实际上就是一个常量,而只是意味着对pt而言,这个值是常量。

7.2.1 修饰普通变量:

在变量定义前面加上const即可。const变量在定义的时候要进行初始化,并且之后不能再给它赋值,否则会报错。

7.2.2 修饰指针

const修饰指针有三种用法:
    ①const在*号前出现:指针指向的数据是不可变的,指针指向了某块内存,那么这块内存的数据不可更改了。
    ②const在*号后出现:指针本身不可变,指针自身是一个地址,地址不可变,也就是指针的指向不可变。
    ③上面两种情况结合一下。

const int *p    //情况一
int const *p    //情况一
int * const p    //情况二
const int * const p   //情况三

可以发现,const\*和变量名称p之间的时候,意思是数据不能变,其他时候都是指针指向不能变。

7.3 函数与二维数组

int data[3][4] = {
   
   {1,2,3,4},{9,8,7,6},{2,4,6,8}};

Data是一个数组名,该数组有3个元素。第一个元素本身是一个数组,有4个int值组成。因此data的类型是指向由4个int组成的数组的指针,因此正确的原型如下:

int sum(int (*arr)[4], int size);

其中的括号必不可少。因为下面的声明将声明一个由4个指向int的指针组成的数组,而不是由一个指向由4个int组成的数组的指针:另外,函数参数不能是数组。关于这点,也可以简单的概括为:数组里面的元素是什么,接受它的参数就应当是什么类型。

int *arr2[4];

还有一种格式,这种格式与上述原型的含义完全相同,但可读性更强:

int sum(int arr[][4],int size);

上述两个原型都指出,ar2是指针而不是数组。还需注意的是,指针类型指出,它指向由4个int组成的数组。因此,指针类型指定了列数,这就是没有将列数作为独立的函数参数进行传递的原因。

由于arr指向数组(它的元素是由4个int组成的数组)的第一个元素(元素0),因此表达式arr + r指向
编号为r的元素。因此arr[r]是编号为r的元素。由于该元素本身就是一个由4个int组成的数组,因此arr[r]是由4个int组成的数组的名称。将下标用于数组名将得到一个数组元素,因此arr[r][c]是由4个int组成的数组中 的一个元素,是一个int值。必须对指针arr执行两次解除引用,才能得到数据。最简单的方法是使用方括号两次:ar2[r][c]。然而,如果不考虑难看的话,也可以使用运算符*两次:

arr[r][c] == * ( * ( arr +  r ) + c );

这份代码在声明参数arr时,没有使用const,因为这种技术只能用于指向基本类型的指针,而arr是指向指针的指针。

7.4 函数与C风格字符串

表示字符串的方式有三种:

  1. char数组;
  2. 用引号括起的字符串常量(也称字符串字面值);
  3. 被设置为字符串的地址char指针;

但上述3种选择的类型都是char指针(准确的说是char*),因此也可以将其作为字符串处理函数的参数。

char ghost[15] = "galloping";
char * str = "galumphing";
int n1 = strlen(ghost);
int n2 = strlen(str);
int n3 = strlen("wwwoshi");

也就是说将字符串作为参数来传递,但实际传递的是字符串第一个字符的地址。这也意味着字符串函数原型应将其表示字符串的形参声明为char*类型。

C-风格字符串与常规char数组之间的一个重要区别是,字符串有内置的结束字符(前面讲过,包含字符,但不以空值字符结尾的char数组只是数组,而不是字符串)。这意味着不必将字符串长度作为参数传递给函数,而函数可以使用循环依次检查字符串中的每个字符,直到遇到结尾的空值字符为止。

7.5 函数与string对象

声明一个string对象数组,并将该数组传递给一个函数以显示其内容;

string list[4];
getline(cin,list[i]);//获取string的方式

7.6 函数与array对象

假设使用一个array对象来存储一年四季的开支:

std::array<double,4> expenses;

要使用array类,需要包含头文件array,而名称array位于名称空间std中。如果只是显示expenses的内容,可以按值传递expenses:

show(expenses);

如果要修改对象expenses,则需将该对象的地址传递给函数:

fill(&expenses);

相应的函数声明如下:

void show(std::array<double,4> da);
void fill(std::array<double,4> *pa);

7.7 递归

C++函数有个特点:可以自己调用自己,这种功能被称为递归。然而,与C语言不同的是,C++不允许main()调用自己。在需要将一项工作不断分为两项较小的类似的工作时,递归非常有用。

7.8 函数指针

与数据项相似,函数也有地址。函数的地址是存储其机器语言代码的内存的开始地址。通常,这些地址对用户而言,既不重要,也没有什么用处,但对程序而言,却很有用。例如,可以编写将另一个函数的地址作为参数的函数。这样第一个函数将能够找到第二个函数,并运行它。与直接调用另一个函数相比,这种方法很笨拙,但它允许在不同的时间传递不同函数的地址,这意味着可以在不同的时间使用不同的函数。

使用步骤:

  1. 获取函数的地址;
  2. 声明一个函数指针;
  3. 使用函数指针来调用函数;

7.8.1 如何获取函数地址?

只需要使用函数名即可。如果think()是一个函数,则think就是该函数的地址。要将函数作为参数进行传递,必须传递函数名。

一定要区分传递的是函数的地址还是函数的返回值:

process(think); //传递的是函数地址;
process(think());//传递的是这个函数的返回值;

7.8.2 如何声明函数指针?

声明指向某种数据类型的指针时,必须指定指针指向的类型。同样,声明指向函数的指针时,也必须指定指针指向的函数类型。这意味着声明应指定函数的返回类型以及函数的特征标(参数列表)。也就是说,声明应像函数原型那样指出有关函数的信息。

double pam(int);
double (*pf) (int);
//通常,要声明指向特定类型的函数的指针,可以首先编写这种函数的原型,然后用(*pf)替换函数名。这样pf就是这类函数的指针。为提供正确的运算符优先级,必须在声明中使用括号将*pf括起。括号的优先级比*运算符高,因此*pf(int)是一个返回指针的函数,而(*pf)(int)意味着pam是一个指向函数的指针。

因为pam是函数,因此(*pf)也是函数,而如果(*pf)是函数,则pf就是函数指针;

7.8.3 如何赋值?

double pam(int);
double (*pf) (int);
pf = pam

注意:pam()的特征标和返回类型必须与pf相同。

7.8.4 如何使用指针来调用函数?

double pam(int);
double (*pf) (int);
pf = pam
double x = pam(4);

double y = (*pf)(5);
double y = pf(5);
//这两种都可以。第一种格式虽然不太好看,但它给出了强有力的提示--代码正在使用函数指针。虽然在逻辑上他们的逻辑是互相冲突的,但这两种都是合理的。
const double * f1(const double ar[],int n);
const double * f2(const double [],int);
const double * f3(const double * , int);//这三种声明方式一模一样。在函数原型中,可以省略标识符。因此,const double ar [] 可以简化为const double[],而const double *ar可以简化成const double*。因此,上述所有函数特征标的含义都相同。

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[3]。该声明的其他部分指出了数组包含的元素是什么样的。运算符[]的优先级高于*,因此*pa[3]表明pa是一个包含三个指针的数组因此,pa是一个包含三个指针的数组,其中每个指针都指向这样的函数,即将const double *和int作为参数,并返回一个const double *。

这里能否使用auto呢?不能。自动类型推断只能用于单值初始化,而不能用于初始化列表,但声明数组pa后,声明同样类型的数组就很简单了:

auto pb = pa;//数组名是指向第一个元素的指针,因此pa和pb都是指向函数指针的指针。
pa[0](av,3);//调用函数
pb[2](av,4);

可做的另一件事是创建指向整个数组的指针。由于数组名pa是指向函数指针的指针,因此指向数组的指针将是这样的指针,即它指向指针的指针。这听起来令人恐怖,但由于可使用单个值对其进行初始化,因此可使用auto:

auto pc = &pa;
*pd[3];//一个有3个地址的数组
(*pd)[3];//一个指向里面有三个元素的数组的指针

换句话说,pd是一个指针,它指向一个包含三个元素的数组。这些元素是什么呢?由pa的声明的其他部分描述,结果如下:

const double *(*(*pd)[3]) (const double *, int) = &pa;

要调用函数,需认识到这样一点:既然pd指向数组,那么pd就是数组,而(*pd)[i]是数组中的元素,即函数指针。因此,较简单的函数调用是(pd)i,而(pd)i是返回的指针指向的值。也可以使用第二种使用指针调用函数的语法:使用((pd)[i])(av,3)来调用函数,而((*pd)[i])(av,3)是指向的double值。

请注意pa(它是数组名,表示地址)和&pa之间的差别。正如您在本书前面看到的,在大多数情况下,pa都是数组第一个元素的地址,即&pa[0]。因此,它是单个指针的地址。但&pa是整个数组(即三个指针块)的地址。从数字上说,pa和&pa的值相同,但它们的类型不同。一种差别是,pa+1为数组中下一个元素的地址,而&pa+1为数组pa后面一个12字节内存块的地址(这里假定地址为4字节)。另一个差别是,要得到第一个元素的值,只需对pa解除一次引用,但需要对&pa解除两次引用:

**&pa == *pa == pa[0];

这个示例可能看起来比较深奥,但指向函数指针数组的指针并不少见。实际上,类的虚方法实现通常都采用了这种技术(参见第13章)。所幸的是,这些细节由编译器处理。

7.9 使用typedef进行简化

除auto外,C++还提供了其他简化声明的工具。关键字typedef让您能够创建类型别名:

typedef double real;

这里采用的方法是,将别名当做标识符进行声明,并在开头使用关键字typedef。因此,可将p_fun声明为程序清单7.19使用的函数指针类型的别名:

typedef const double *(*p_fun)(const double *,int);//p_fun now a type name
p_fun p1 = f1;

然后使用这个别名来简化代码:

p_fun pa[3] = {f1,f2,f3};
p_fun (*pd)[3] = &pa;

使用typedef可减少输入量,让您编写代码时不容易犯错,并让程序更容易理解。通常,当函数的参数是一个回调函数时,就可能会使用 typedef 来简化声明。

7.10 总结

函数是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++函数名与函数地址的作用相同。通过将函数指针作为参数,可以传递要调用的函数的名称。

猜你喜欢

转载自blog.csdn.net/weixin_43717839/article/details/129821802