C++证道之路第七章函数——C++的编程模块

本文详细介绍了C++中函数的使用,包括函数定义、原型的作用、值传递、数组与指针的关系、函数和字符串对象处理、结构、递归以及函数指针的概念。还讨论了const关键字的应用和函数参数的不同形式。
摘要由CSDN通过智能技术生成

1,复习函数的基本知识 

要使用C++的函数,必须完成如下工作。

提供函数的定义。
提供函数的原型。
调用函数。

定义函数:

可以将函数分为两类:没有返回值的函数和有返回值的函数。没有返回值的函数被称为void函数,其通用格式如下:

void functionName(parameterList)

{

statement(s);

return;

}

有返回值的函数将生成一个值,并将它返回给调用函数。

其通用格式如下:

void functionName(parameterList)

{

statement(s);

return value;

}

C++对于返回值的类型有一定的限制:不能是数组,但可以是其他的任何类型——整数,浮点数,指针,甚至可以是结构和对象!(有趣的是,虽然C++函数不能直接返回数组,但可以将数组作为结构或对象组成部分来返回。)

为什么需要原型?

原型描述了函数到编译器的接口,也就是说,它将函数返回值的类型(如果有的话)以及参数的 类型和数量告诉编译器。

原型的语法

通常,在原型的参数罗列表中,可以包括变量名,也可以不包括,原型中的变量名,相当于占位符,一次不必与函数定义中的变量名相同。

为与基本的C兼容,ANSI C中的原型是可选的,但在C++中,原型是必不可少的。

在C++中,括号为空与在括号中使用关键字void是等效的——意味着函数没有参数。

在ANSI C 中,括号为空意味着不指出参数——这意味着将在后面定义参数列表。在C++中,不指定参数列表时应使用省略号。

原型的功能

具体来说,原型确保一下几点:

编译器正确处理函数返回值;

编译器检查使用的参数数目是否正确;

编译器检查使用的参数类型是否正确,如果不正确,则转换为正确的类型(如果可能的话)。

通常,原型自动将被传递的参数强制转换为期望的类型。

在编译阶段进行的原型化被称为静态类型检查。

2.函数参数和按值传递

C++通常按值传递参数,这意味着将数值参数传递给函数,而后者将其赋给一个新的变量。

用于接受传递值的变量被称为形参。传递给函数的值被称为实参。

在函数声明的变量(包括参数)是该函数私有的。在函数被调用时,计算机将为这些变量分配内存;在函数结束时,计算机将释放这些变量使用的内存(有些C++文献将分配和释放内存称为创建和毁坏变量,这样似乎更激动人心。)这样的变量被称为局部变量,因为它们被限制在函数中。

cin.get()函数读取所有的输入字符,包括空格和换行符,而cin>>跳过空格和换行符。

3.函数和数组

在大多数情况下 ,C++和C语言一样,也将数组名视为指针。C++将数组名解释为其第一个元素的地址。

先数组声明时候用数组名来标记储存位置:其次,对数组名使用sizeof 将得到整个数组的长度(以字节为单位):第三,将地址运算符&用于数组名时,将返回整个数组的地址。

一定要记住:

arr[i] == *(ar+i)
&arr[i] == ar+i
//arr为数组名
//ar为指针

数组名与指针对应时好事吗?确实是一件好事。将数组地址作为参数可以节省复制整个数组所需的时间和内存。如果数组很大,则使用拷贝的系统开销将非常大;程序不仅需要更多的计算机内存,还需要花费时间来复制大块的数据。另一方面,使用原始数据增加了破坏数据的风险。

const限定符提供了解决这种问题的方法。

指针本身并没有指出数组的长度

通过数据类型和设计适当的函数来处理数据,然后将这些函数组合成一个程序。有时也称为自下而上的程序设计。因为设计过程从组件到整体进行。这种方法非常适合于oop——它们首先强调的是数据表示和操纵。而传统的过程性编程倾向于从上而下的程序设计,首先指定模块化设计方案。然后再研究细节。这两种方法都很有用。最终的产品都是模块化程序。

使用数组区间的函数。

对于处理数组的C++函数,必须将数组中的数据种类,数组的起始位置和数组中元素数量提交给它:传统的C/C++方法是,将指向数组起始处的指针作为一个参数,将数组长度作为第二个参数(指针指出数组的位置和数据类型),这样便给函数提供了找到所有数据所需的信息。

还有另一种给函数提供所需信息的方法,即指定元素区间,这可以通过传递两个指针来完成;

一个指针标识数组的开头,另一个指针标识数组的尾部。

C++标准模板库(STL,我后面会说)将区间方法广义化了。STL 方法使用“超尾”概念来指定区间。也就是说,对于数组而言,标识数组结尾的参数将是指向最后一个元素后面的指针。

根据指针减法规则,end-begin是一个整数值,等于数组的元素数目。

将const用于指针有一些很微妙的地方,(指针看起来总是很微妙),可以用两种不同 的方式将const关键字用于指针。第一种方法是让指针指向一个常量对象,这样可以防止使用该指针来修改所指向的值。第二种方法是将指针本身声明为常量,这样可以防止改变指针指向的位置。

int age = 39;
const int * pt = &age;
//该声明指出pt指向的const int,因此不能使用pt来修改这个值。换句话来说,*pt的值是const,不能被修改。

我们经常将常规变量的地址赋给唱给指针,而这里将常规变量的地址赋给指向const的指针。因此还会有两种可能:

将const变量的地址赋给指向const的指针或将const的地址赋给常规指针。这两种操作都可行吗?第一种可以,第二种不行。

第二种其实可用强制类型转换来突破这种限制。

const int **pp2;
int *p1;
const int  n = 13;
pp2 = &p1;
*pp2 = &n;
*p1 = 10;

上述代码将非const地址(&p1)赋给了const指针(pp2),因此可以使用p1来修改const数据。因此,仅当只有一层间接关系(如指针指向基本数据类型)时,才可以将非const地址或指针赋给const指针。

注意:如果数据类型本身并不是指针,则可以将const数据或非const数据的地址赋给指向const的指针,但只能将非const数据的地址赋给非const指针。

尽可能使用const

将指针参数声明为指向常量数据的指针有两条理由:

这样可以避免由于无意间修改数据而导致的编程错误;

使用const使得函数能够处理const和非const实参,否则将只能接受非const数据。

如果条件允许,则应将指针形参声明为指向const的指针。

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

第二个声明中的const只能防止修改pt指向的值,而不能防止修改pt的值。也就是说,可以将一个新地址赋给pt。

4.函数和二位数组

为编写将二维数组作为参数的函数,必须牢记,数组名被视为其地址,因此,相应的形参是一个指针,就像一位数组一样。

int data[3][4] = {{1,2,3,4},{4,3,2,1},{4,6,5,6}};
int total = sum(data,3);

data 是一个数组名,它有三个元素,第一个元素本身是一个数组,由4个int值组成。因此data的类型是指向由4个int组成的数组的指针。

因此正确的sum声明如下:

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

其中括号是必不可少的,因为下面的声明将声明一个由4个指向int的指针组成的数组,而不是由一个指向由4个int组成的数组的指针:另外函数参数不能是数组。

int *ar2[4];

还有另外一种格式,可读性更强。

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

5.将C风格字符串作为参数的函数

假设要将字符串作为参数传递给函数,则表示字符串的方式有三种:

char数组;

用引号括起来的字符串常量;

被设置为字符串的地址的char指针。

6,函数和结构

为结构编写函数比为数组编写函数要简单的多。虽然结构变量和数组一样,都可以储存多个数据项,但在涉及到函数时,结构变量的行为更接近于基本的单值变量。也就是说,与数组不同,结构将其数据组合成单个实体或数据对象,该实体被视为一个整体。前面讲过,可以将一个结构赋给另外一个结构。同样,也可以按值传递结构,就像普通变量那样。在这种情况下,函数将使用原始结构的副本,另外,函数也可以返回结构。与数组名就是数组第一个元素的地址不同的是,结构名只是结构的名称,要获得结构的地址,必须使用地址运算符&。

传递和返回结构

当结构比较小时,按值传递结构最合理。

7.函数和string对象

虽然C风格字符串和string对象的用途几乎相同,但与数组相比,string对象与结构的更相似。

8.函数与array对象

在C++中,类对象象是基于结构的,因此结构编程方面的有些考虑因素也使用于类。

请注意,模板array并非只能存储基本数据类型,它还可以存储对象。

9.递归

C++函数有一种有趣的特点——可以调用自己(然而,与C语言不同的是,C++不允许main()调用自己)

如果递归函数调用自己,则被调用的函数也将调用自己,将无限循环下去,除非代码中包含终止调用链接的内容。

通常的方法是将递归调用放在if语句中。

例如:

void recurs(argumentlist)
{
    statements1
    if (test1)
        recurs(arguments)
    statements2
}

 test最终为false,调用链将断开。

注意:

每个递归调用都创建一套自己的变量。

10.函数指针

与数据项相似,函数也有地址。函数的地址是储存器机器语言代码的内存的开始地址。

可以编写将另一个函数的地址作为参数的函数。这样第一个函数能够找到第二个函数并运行它。与直接调用另一个函数相比,这种方法很笨拙,但它允许在不同的时间传递不同函数的地址,这意味着可以在不同的时间 使用不同的函数。

获取函数的地址

获取函数的地址很简单:只要使用函数名(后面不跟参数)即可。

要将函数作为参数进行传递,必须传递函数名。一定要区分传递的是函数的地址还是函数的返回值。

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

声明函数指针

声明指向某种数据类型的指针时,必须指定指针指向的类型。同样,声明指向函数的指针时,也必须指定指针指向的函数类型。这意味着声明应指定函数的返回类型以及函数的特征值(参数列表)。

提示:

通常,要声明指向特定类型的函数的指针,可以首先编写这种函数的原型,然后用(*pt)替换函数名。这样pt就是这类函数的指针。

为提供正确的运算符优先级,必须在声明中使用括号将*pt括起。

使用指针来调用函数

(*pt)扮演的角色与函数名相同,因此使用(*pt)时,只需将它看做函数名即可。

历史与逻辑

为什么pt和*pt等价呢?

一种学派认为,由于pt是函数指针,而*pt是函数。因此应将(*pt)()用作函数调用。

另一种学派认为,由于函数名是指向该函数的指针,指向函数的指针的行为应与函数名相似,因此应将pt()用作函数调用使用。C++进行了折中——这两种方式都是正确的,或者至少是允许的,虽然它们在逻辑上是冲突的。主宰认为这种折中粗糙之前,应该想到,容忍逻辑上无法自圆其说的观点正是人类思维活动的特点。

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 *ar;//这两者的含义完全相同

auto自动类型推断只能用于单值初始化,而不能用于初始化列表。

  • 45
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值