首页 > 代码库 > 函数

函数

迭代的是人,递归的是神。—— L.Peter Dautsch

 1、函数声明

  在C++程序里,完成某件工作的一种典型方式就是调用一个函数去做那件事情。定义函数是你刻画怎样完成某个操作的一种方式。一个函数只有在预先声明之后才能调用。

  在一个函数声明中,需要给出函数的名字,这个函数返回的值的类型(如果有的话),以及在调用这个函数时必须提供的参数的个数和类型。例如

Elem* next_elem();
char* strcpy(char* to, const char* from);
void exit(int );

  参数传递的语义等同于初始化的语义。参数的类型被逐个检查,如果需要就会做隐式的参数类型转换。

  在函数声明中可以包含参数的名字。这样做可能对读程序的人有所帮助,但编译器将简单地忽略掉这样的名字。以void作为返回值类型表示这个函数不返回值。

  1)函数定义

  在程序里调用的每个函数都必须在某个地方定义(仅仅一次)。一个函数定义也就是一个给出了函数体的函数声明。一个函数的定义和对它的所有声明必须都描述了同样的类型。不过,这里并不把参数名字作为类型的一部分。

  函数可以定义为inline。

  2)静态变量

  局部变量将在运行线程达到其定义时进行初始化。按照默认方式,这件事发生在函数的每次调用中,且函数的每个调用有自己的一份局部变量副本。如果一局部变量被声明为static,那么将只有惟一的一个静态分配的对象,它被用于在该函数的所有调用中表示这个变量。这个对象将只在执行线程第一次到达它的定义时初始化。

  静态变量在一个函数里定义,只能在这个函数里通过变量名访问,但它的存在并不依赖于函数的调用。因此,这种变量可以用于保存需要一直存在,以便在这个函数内部使用的信息。

  静态变量为函数提供了一种“存储器”,使我们不必去引进可能被其他函数访问或破坏的全局变量。

2、参数传递

  当一个函数被调用时,将安排好其形式参数所需要的存储,各个形式参数将用对应的实际参数进行初始化。参数传递的语义与初始化的语义完全相同。特别是,需要对照着每一个形式参数检查与之对应的实际参数的类型,并执行所有标准的或者用户定义的类型转换。

  通过引用方式传递大的对象,比通过值传递的效率更高一些。在这种情况下,可以将有关的参数声明为const,以指明使用这种参数仅仅是为了效率的原因,而不是想让调用函数能够修改对象的值。

  1)数组参数

  如果将数组作为函数的参数,传递的就是到数组的首元素的指针。也就是说,类型T[]作为参数传递时将被转换为一个T*。这也就意味着,对数组参数的某个元素的赋值,将改变实际参数数组中那个元素的值。换句话说,数组与其他类型不同,数组不会也不能按值的方式传递。

  多维数组的情况更加诡异,但通常可以改用指针的数组,这样做并不需要特别的处理。

3、返回值

  一个没有声明为void的函数都必须返回一个值(main()是特殊的)。与此相反,void函数就不能返回值。

  返回值由返回语句描述。一个调用自己的函数被称为是递归的。函数里可以出现多个返回语句。

  就像参数传递的语义一样,函数返回值的语义也与初始化的语义相同,可以认为返回语句所做的就是去初始化一个具有返回类型的匿名变量。每当一个函数被调用时,就会建立起它的所有参数和局部变量的一套新副本。在该函数返回后,这些存储又会被另做他用。所以,绝不能返回指向局部变量的指针,因为被指位置中内容的改变情况是无法预料的。

4、重载函数名

  将同一个名字用于在不同类型上操作的函数的情况称为重载。

  重载解析与被考虑的函数声明的顺序无关。重载解析中将不考虑返回类型。这样规定的理由就是要保持对重载的解析只是针对单独的运算符或函数调用,与调用 的环境无关。

  在不同的非名字空间作用域里声明的函数不算是重载。

  对一个函数,声明的重载版本过少或者过多都有可能导致歧义性。一些C++新手会被编译器报告出的歧义性错误弄得急躁起来。更有经验的程序员则欣赏这种错误信息,将它们看做是很有用的关于设计错误的指示器。

5、默认参数

  默认参数的类型将在函数声明时检查,在调用时求值。只可能对排列在最后的那些参数提供默认参数。例如,

int f(int , int  = 0, char* = 0);  // ok
int g(int = 0, int = 0, char*);  // 错误
int h(int = 0, int , char* = 0); // 错误

6、未确定数目的参数

  对于有些参数而言,我们没办法确定在各个调用中所期望的所有参数的个数和类型。声明这种函数的方式就是在参数表的最后用省略号(...)结束,省略号表示“还可能有另外一些参数”。例如

int printf(const char*...);

7、指向函数的指针

  对一个函数只能做两件事:调用它,或者取得它的地址。通过一个函数的地址而得到的指针,可以在后面用于调用这个函数。

  在指向函数的指针的声明中也需要给出参数类型,就像函数声明一样。在指针赋值时,完整的函数类型必须完全匹配。

  指向函数的指针的数组常常很有用。例如,我的基于鼠标的编辑器里的菜单系统,就是利用指向函数的指针的数组实现的,这些函数表示各种各样的操作。

typedef void (*PF)();
PF edit_ops[] = {  // 编辑操作
    &cut, &paste, &copy, &search
};
PF file_ops[] = {  // 文件管理
    &open, &append, &close, &write
};

  要理解指向函数的指针的表达能力,一种方式就是试着写这种代码而不是用函数指针——也不用它们的更具良好行为的兄弟:虚函数。通过把新函数插入运算符表等方式,就可以在运行中修改这种菜单。在运行中构造出新菜单也同样非常容易。

8、宏

  宏在C语言里极其重要,而在C++里用得就少多了。关于宏的第一规则是:绝不应该去使用它,除非你不得不这样做。

  宏名字不能重载,而且宏预处理器不能处理递归函数调用。

  有一种宏的使用几乎不可能避免。指令#ifdef identifier将条件性地导致随后的输入被忽略,直到遇到一个#endif指令。

9、忠告

【1】质疑那些非const的引用参数;如果你想要一个函数去修改其参数,请使用指针或者返回值;

【2】当你需要尽可能减少参数复制时,应该使用const引用参数;

【3】广泛而一致地使用const;

【4】避免宏;

【5】避免不确定数目的参数;

【6】不要返回局部变量的指针或者引用;

【7】当一些函数对不同的类型执行概念上相同的工作时,请使用重载;

【8】在各种整数上重载时,通过提供函数去消除常见的歧义性;

【9】在考虑使用指向函数的指针时,请考虑虚函数或模板是不是更好的选择;

【10】如果你必须使用宏,请使用带有许多大写字母的丑陋的名字;