Chinaunix首页 | 论坛 | 博客
  • 博客访问: 9707
  • 博文数量: 9
  • 博客积分: 270
  • 博客等级: 二等列兵
  • 技术积分: 105
  • 用 户 组: 普通用户
  • 注册时间: 2011-06-10 09:34
文章分类
文章存档

2014年(2)

2011年(7)

我的朋友
最近访客

分类: C/C++

2011-06-11 19:57:25

 

这里主要讲函数的通信,也就是函数是如何进行数据交换的。函数的不同主要体现在参数上,包括参数的类型是内部数据类型,数组还是程序员定义的类型,以及实参是否被改变。此外,还有函数名重载,缺省参数值,内联函数,参数的提升等等。这篇文章不是C++语法介绍,所以我只是简要介绍了函数语法知识,更多的是把函数的使用中容易忽略的地方拿出来。所以想看函数基础介绍的同学可能会失望。

1 C++函数基本语法

使用函数时,必须在三个地方统一代码:函数声明、函数定义、函数调用。任何两处不统一都是错误的,除非函数的定义能充当函数声明时,声明和定义不一致或许不会出错。

1.1 函数声明

函数声明也叫函数原型,包括返回类型、函数名、参数列表以及最后的一个分号。C++在处理函数调用之前,必须先看到该函数的声明或者定义。如果一个函数在不同源文件中被调用,那么每个源文件中都必须有声明。如果函数没有返回值,那么返回类型必须是void,如果什么都不写,默认返回类型是int。要避免不写返回类型。函数声明往往都写在一个单独的头文件中。虽然原型中可以只写函数参数类型,不写参数名,但是最好还是写上。有些人并不是在源文件开始处声明函数原型,而是在调用函数中声明,这是软件工程中重要的论题。

1.2 函数定义

函数定义包括返回类型、函数名、形参列表、花括号括起来的函数体。如果函数返回类型是voidreturn语句可有可无,如果有的话不要带任何参数。如果函数返回类型不是void,那么至少要有一个return,并且return后必须带一个值。函数定义在一个程序中只能出现一次,但是函数原型可以有多个。

1.3 函数调用

函数调用使用了调用运算符,但是大多数人并不知道。如果函数的参数列表为空,那么在声明和定义时可以什么也不写,也可以写一个void。但是调用时不能写void

2 参数提升和类型转换

如果函数的实参和形参如果个数一样,但是对应类型不一致,有可能会出错,有可能不会。假如是两种类型无法转换,那么一定出错。但是如果可以转换的话,就可以进行提升和隐式类型转换。在进行任何计算之前,都会将小类型转到大类型,这些提升是安全的。假如提升之后仍然不能完全匹配,实参就会进行隐式类型转换:任何数字数据都能转换为其他数字类型,但是有可能会精度下降。实参0可以转换为任何数字类型或者指针类型。有些时候,我们就是要让实参和形参不匹配,通用的方法就是使用显式类型转换。这样可以让维护人员明白编写者的意图。

如果实际返回类型和声明的返回类型不一致,也可以使用上述规则。

不要因为有参数提升和隐式转换,就可以不在乎参数匹配问题,最好的方法还是让参数一一对应。因为使用参数提升和隐式转换没有什么好处。

3 参数传递

3.1 按值传递

函数的实参可以是变量、符号常量、表达式、字面值。

形参和函数外某个变量的名字即便重复也没有关系,它们不会指向同一个地址。调用函数时,形参得到定义,分配了内存空间,并被实参的值初始化。所以,形参的值是实参值的独立副本。形参的值在函数体内可以被修改。但是实参的值不在函数作用域内,值的传递是单向的。在函数体内改变了形参的值,这种改变不会传回到形参被撤销的调用者空间。

3.2 按指针传递

指针变量包含另一个变量的内存地址,所以这很危险。指针不能指向任意类型的变量。我们有责任决定指针要指向什么类型的变量。指针被创建时也没有有效值,它必须被初始化或被赋值。可以用NULL、同种类型的另一个指针变量包含的值以及相应类型变量的地址来为指针赋值。*运算符是间接引用运算符,它的作用域仅仅是一个名字,而不是其后的多个名字。

指针可以帮助改变客户空间中实参的值。按指针传递参数需要注意以下几点:函数原型部分使用*;函数体内使用*p;调用函数时使用&。按指针传递参数存在大量可能出现错误的情况,不管程序有多么小,也要测试其正确性。每一个程序员都会在指针问题上出错,只是有人出错少并且修正快。所以出现这些问题时不要批判自己。

关于前面讨论的值的类型转换规则,在指针部分一律不再适用。

3.3 按引用传递

引用变量和指针变量一样,指向另一个内存空间,包括另一个变量的内存地址。在定义引用变量时,要说明它所指向的变量的类型。可以定义任何类型的引用,它可以被定义、分配内存空间、初始化、被撤销。但是,和指针类型不同的是,引用类型的变量只能指向一个内存空间。这个空间的类型必须和引用本身的类型一致。引用变量不能放弃它指向的位置,也不能指向另一个位置。引用一旦被初始化,就不能被改变而指向另一个变量,其原因是引用和变量永远在一起,直到超出作用域或者消亡才会分开。因此,引用类型变量必须在声明的时候初始化。如果不这样,就会丧失将此引用指向某个内存空间的机会,使它成为无用的变量。初始化一个引用之后,对于引用的赋值只能修改数据而不能修改数据的地址,因为引用只是提供一个别名而已。所以,使用int& ia = a;ia进行了初始化后,再写ia = b,只能使得a被赋予b值,而不能让ia指向b的地址。

初始化引用变量时,不需要对引用指向的变量应用运算符。对指针而言,*paa是等效的,但是对于引用而言,iaa是等效的,不需要任何运算符。这是引入引用的两个原因。*pa是对指针的“间接引用”,而不是“间接指针”,而对引用却没有“间接引用”操作。这是由于历史原因引起的,不需要追究。

按引用传递参数时,我们需要这样做:函数原型中参数是引用类型;函数体中不需要间接引用操作的参数名;函数调用时不需要地址运算符。这种形式在函数重载中会有些麻烦。

使用引用传递参数时,实参必须和形参完全一致,C++不支持不同类型的引用隐式转换。显式转换虽然可以但是没有任何作用。

三种参数传递方式中,按值传递最简单,也没有副作用,按指针传递最复杂,有副作用。当实参不会被函数改变值的时候,最好使用按值传递,这可以让别人明白。如果需要函数改变实参的值,就使用按引用传递。

3.4 结构

结构作为参数时,规则和内部类型一样。但是在对结构使用指针传递参数时,有一些地方需要注意。结构中的域的获取有两种操作符:.à。当结构很大时,要使用按值传递,会极大地消耗运行时间和空间,影响程序性能。尽管使用指针传递会对实参带来无意的损害,但是按值传递的不足更为严重。因为如果对实参进行了无意的修改,这是应该测试出来的错误。既想不按值传递参数,又要表达不对实参进行修改,最好的方法就是使用引用,并且配合const关键字(const不是必需的,它更多时候为了让维护人员能明白这个参数是不会被函数修改的)

3.5 数组

数组常常以与按指针传递类似的模式传递。这是传递数组参数的唯一模式。把数组作为参数时要指定:函数头中数组名后带[];函数体中使用数组分量;函数调用时使用数组名。比如,函数头可以写void F(int a[]);调用时写F(array);函数头也可以写void F(int* a);函数体中可以写a[i]或者*(a+i)

无论写那种方式,都不能区分出数组参数是输入还是输出。好的做法当然是使用const做说明性标记。总之,如果某个参数不会被函数修改,或者我们本意不想让它被修改,就使用const关键字,这样可以防止无意修改,也使得程序清晰。虽然const的规则可能麻烦,但是不要放弃使用它,高级语言的复杂性是为了提高可读性。

3.6 类型转换

C++对类型转换有严格的规定。按值传递时,值、结构、数组不能进行转换,显式转换也不行。不同结构也不能转换。但是使用按指针或者引用传递结构和数组时,情况就不一样了。假如函数形参是A结构的指针,我们要将B结构的指针作为实参传递,而且AB的结构一致,可以使用强制类型转换(A*)。对数组一样可以。但是这很不好,一旦A或者B发生变化,这个方法就会引起错误。

3.7 返回值

C++函数是按值返回结果的。它可以返回内部数据类型值或者结构值。但是不能返回数组。函数返回值需要三个地方一致:返回类型、返回表达式、接收返回值的变量。函数可以返回指针或者引用。如果返回了结构的指针,可以避免复制一个大的结构,但是却有可能对返回值进行修改。这个时候可以将返回值定义为指向const对象的指针来保证返回地址不能修改它指向的值。假如实参是const的,形参不是,那么会出错。假如返回表达式返回的是const的值,而接收返回值的变量不是const的,这也会出错。

使用指针作为返回值需要注意,如果返回的那个指针在函数体内指向的是函数作用域定义的变量,或者说返回的是函数作用域内定义的指针,有可能到了函数体外,这个指针已经不存在了。比较安全的是返回指向堆内存的指针或者指向客户空间中的变量的指针。所以,函数返回地址最安全的做法就是用于动态内存管理,返回的是指向堆内存的指针。

返回引用也可以避免复制结构。但是,缺省状态下,引用就是常量。如果将返回的引用值赋值给一个引用变量,一般不会有什么作用,因为引用变量必须在声明时初始化。所以,引用值作为返回值唯一用法就是初始化。如果用一个非引用类型的变量接收引用返回值,就会发生复制数据的操作,引用的好处就没有了。

这里讨论的内容很多很复杂,这是因为指针和引用本身就很复杂。在返回指针或引用时,一定要考虑两点:是否真的能得到性能上的好处?是否会破坏程序的完整性?

4 内联函数

内联函数提供了模块化优点,也没有上下文切换的开销。Inline函数适用于函数体特别小,并且多次调用的情况。假如某个函数的函数体很小,但是它仅调用几次,这就没有必要使用内联,因为性能改善不明显,并且因为可执行程序大小增加了,可能会导致额外的交换(内存页段、高速缓冲命中等)。对很多函数而言,使用inline并不会带来性能上的改变。同时,inline是一个建议,有的编译程序发现inline函数很长时,它就把这个函数作为普通函数处理。有的编译程序不接受带有控制程序的内联函数。

类的成员函数有两种定义方法。一种是在类规则说明的区域仅仅声明函数原型,然后到这个区域外去定义函数;另一种是直接在类规则说明区域内定义函数。后一种情况的函数缺省为inline的。前一种情况就必须写inline才行。

5 参数缺省值

    参数的缺省值应该在函数原型而不是函数定义中指定。在同一个文件中,一个参数只能使用一个缺省值。当函数的定义和使用在同一个文件中时,如果该文件中没有用到函数原型,那么缺省值可以出现在函数定义中。如果函数原型和函数定义都放在函数被调用的文件中,两者之间只能有一个指定缺省值。相同的函数原型中只能有一个指定缺省值。形参名可以没有,直接使用缺省值。C++只能对最右边的参数设置缺省值。

    当函数经常使用某个相同值时,缺省值很有用。如果指定的参数值只是在特别情况下才使用,是否使用缺省值就值得讨论了。使用缺省参数值可以简化一些维护,虽然不是全部的,但是可以使用缺省参数值时,一定不要错过这个机会。比如这样一个例子:函数 inline void F() {x=20;}, 很多地方都需要调用这个函数。但是有可能极少时候需要将x设置为30,这个时候再写一个函数F1()显然不太好。所以可以将函数改为inline void F(int t){x=t;}。原来已经写好的调用必须一一改正。这不是一件很难的工作,确实一项很复杂繁琐的工作。所以可以这样改:inline void F(int t = 20){x= t;}

6 函数名重载

C++中,同一个函数名,可以用于全局函数,也可以用于类的成员函数,也可以用于不同类的成员函数。因为是在不同的作用域,所以函数名可以一样,不会有冲突。但是,在同一个作用域中,程序员定义的标识符必须是唯一的。不过,C++允许函数名重载,区分它们依赖于参数个数和参数类型,函数名在分辨重载函数时是不起作用的,返回值也不起作用。使用函数重载,就要让重载的函数拥有类似的操作。

当两个重载函数拥有相同的参数个数,而参数类型之间能进行类型转换时,容易出现问题。比如,long F(long x)double F(double x),调用时实参为int型,那么就会出现二义性错误。

按值传递参数时,const是多余的,因此它也不能用来区分重载函数。从一个类型到其引用的转换也是无关紧要的,也不能用于区分函数。但是指针和被指的类型可以区分重载函数,常量和非常量指针、常量和非常量引用也能区分重载函数。

函数重载和函数缺省值都能用于软件改进,但是这样的特性应该尽量少用,因为它们十分复杂,还有许多不为人所注意的细节和特殊情况。并不是每一个看到这样代码的人都理解或者知道这样的特性。

阅读(440) | 评论(0) | 转发(0) |
0

上一篇:没有了

下一篇:《我的大学》——爱情篇(原创)

给主人留下些什么吧!~~