C++学习笔记7:函数

作者: 云中布衣   分类:  学习笔记    热度: (304℃)   时间: 2017-7-5 9:48   标签: #始于C++而不止于C++    

函数可以看做程序员定义的一组操作。与内置操作符相同是,每个函数都会实现一系列的计算,然后(大多数时候)生成一个计算结果。但与操作符不同的是,函数有自己的函数名,而且操作数没有数量限制。与操作符一样,函数可以重载,这意味着同样的函数名可以对应多个不同的函数。这篇学习笔记,我们将要讨论函数(function)的定义和声明、函数的参数传递、函数的返回值、函数指针。然后具体的分析三类特殊的函数:内敛(inline)函数、类成员函数和重载函数。

1.函数的定义

函数由函数名以及一组操作数类型唯一地的表示。函数的操作数,也即形参 (parameter),在一对圆括号中声明,形参与形参之间以逗号分隔。函数执行的运算在一个称为函数体(function body)的块语句中定义。每一个函数都有一个相关联的返回类型(return type)。

C++语言使用调用操作符(call operator)(即一对圆括号)实现函数的调用。正如其他操作符一样,调用操作符需要操作数并产生一个结果。调用操作符的操作数是函数名和一组(有可能是空的)由逗号分隔的实参。函数调用的结果类型就是函数返回值的类型,该运算的结果本身就是函数的返回值。函数调用做了两件事,第一件事,用对应的实参初始化函数的形参,并将控制权转移给被调用函数,主调函数(calling function)的执行被挂起,被调函数(called function)开始执行。

函数体是一个作用域。函数体是一个语句块,定义了函数的具体操作。通常这个语句块包含在一对花括号中,形成了一个新的作用域。和其他的块语句一样,在函数体中可以定义变量,这种变量称为局部变量(local variable),它们只在该函数中才可以访问。

C++函数调用的形参和实参。类似于局部变量,函数的形参为函数提供了已命名的局部存储空间。他们之间的差别在于形参是在函数的形参表中定义的,并由调用函数时传递给函数的实参初始化。实参则是一个表达式,它可是变量或字面值常量,甚至可以是包含一个或几个操作符的表达式。在调用函数时,所传递的实参个数必须与函数的形参个数完全相同。与初始化式的类型必须与初始化对象的类型匹配一样,实参的类型也必须与其对应形参的类型完全匹配,也就是说实参必须具有与形参类型相同、或者能隐式转换为形参类型的数据类型。

函数的返回类型可以是内置类型(int或double)、类类型或符合类型(如int&或string*),还可以是void类型,表示该函数不返回任何值。函数不能返回另一个函数或者内置数组类型,但是可以返回指向函数的指针,或指向数组元素的指针的指针。

bool is_present(int *, int); //returns bool
int count(const string&, char); //returns int
Date &calendar(const char*); //returns reference to Date
void process(); // process does not return a value
int *foo_bar() ; // return pointer to first element of the array
需要注意的是,函数必须指定返回类型,在定义或声明函数时,没有显式指定返回类型是不合法的。函数的参数列表可以为空,但是不能省略。没有任何形参的函数可以用空形参表或含有单个关键字void的形参表来表示。形参表由一系列用逗号分隔的参数类型和(可选的)参数名组成。如果两个参数具有相同的类型,则其类型必须重复声明。参数表中不能出现同名的参数。类似地,局部于函数的变量也不能使用与函数的任意参数相同的名字。参数名是可选的,但在函数定义中,通常所有参数都要命名,参数必须要在命名后才能使用。
void process(){/*...*/}  //implicit void parameter list
void process(void){/* ... */} //equivalent declaration
int manip(int v1, v2) {/* ... */} //error
int manip(int v1, int v2) {/* .. */}  //ok
C++是一种静态强类型语言,对于每一次的函数调用,编译时都会检查其实参。调用函数时,传递过多的实参、忽略某个实参或者传递错误类型的实参,几乎肯定会导致严重的运行时错误!

2.参数传递

每次调用函数时,系统都会重新创建该函数所有的形参,此时所传递的实参将会初始化对应的形参。形参的初始化与变量的初始化一样,如果形参具有非引用类型,则赋值实参的值。如果形参为引用类型,则它只是实参的别名。

非引用形参:非引用形参表示对应实参的局部副本。对于这类形参的修改仅仅改变了局部副本的值。一旦函数执行结束,这些局部变量的值就没有了。

1)指针形参。函数的形参可以是指针,此时将复制实参指针。与其他非引用类型的形参一样,该类形参的任何改变也仅作用于局部副本。如果函数将新指针赋给形参,主调函数使用的实参指针的值没有改变。但是需要注意的是,如果函数形参是非const类型的指针,则函数可以通过指针实现赋值,修改指针所指向对象的值。当使用指针形参时,如需保护指针指向的值,则形参需要定义为指向const对象的指针。关于指针的初始化规则这里重申一下,可以将指向const对象的指针初始化为指向非const对象,但不可以让指向非const对象的指针指向const对象。

void use_ptr(const int *p){
    // use_ptr may read but not write to *p
}
2)const形参。在调用函数时,如果该函数使用非引用的非const形参,则既可以给该函数传递const实参也可以传递非const的实参。如果将形参定义为非引用的const类型,则在函数中不可以改变实参的局部副本。由于实参仍然是以副本的形式传递,因此传递给fcn的既可以是const对象,也可以是非const对象。有一点值得注意,尽管函数的形参是const,但是编译器却将形参被声明普通的非const类型。这种做法是为了支持对C语言的兼容,因为在C语言中,具有const形参或非const形参的函数并无区别。
void fcn(const int i) { /* fcn can read but not write to i */}
void fcn(int i){ /* ...*/} // error:redefines fcn(int)
对于非引用参数传递的复制实参并不是在所有的情况下都适合,不适宜复制实参的情况包括:当需要在函数中修改实参的值时;当需要以大型对象作为参数传递时。对于实际的应用而言,复制对象所付出的时间和存储空间代价往往过大;当没有办法实现对象的复制时。对于以上几种情况,有效的解决办法是将形参定义为引用或指针类型。

引用形参:与所有引用一样,引用形参直接关联到其所绑定的对象,而非这些对象的副本。定义引用时,必须使用与该引用绑定的对象初始化该引用。引用形参完全以相同的方式工作。每次调用函数,引用形参被创建并与响应的实参关联。

1)使用引用形参可以返回额外的信息。

2)利用const引用避免复制。在向函数传递大型对象时,需要使用引用形参。对于大部分类类型或者大型数组,复制实参的效率通常太低了,此外有些类类型是无法复制的。使用引用形参,函数可以直接访问实参对象而无须复制它。如果使用引用形参的唯一目的是避免复制实参,则应将形参定义为const引用。

3)更灵活的指向const的引用。如果函数具有普通的非const引用形参,则显然不能通过const对象进行调用。对于函数中普通的非const引用形参在使用时实际上是不太灵活的。这样的形参既不能用const对象初始化,也不能用字面值或产生右值的表达式的实参初始化。因此应该将不需要修改的引用形参定义为const引用,这样会更加的灵活。

4)传递指向指针的引用。我们可以通过形参int *&v1来传递指向指针的引用,从右往左理解,v1是一个引用,与指向int对象的指针相关联,v1只是传递进函数任意指针的别名。

vector和其他容器类型的形参:通常,函数不应该有vector或其他标准库容器类型的形参。调用含有普通的非引用vector形参的函数将会复制vector的每一个元素。从避免复制vector的角度出发,应考虑将形参声明为引用类型。但事实上,C++程序员更倾向于通过传递指向容器中需要处理的元素的迭代器来传递容器。

//pass iterators to the first and one past the last element to print
void print(vector<int>:: const_iterator beg, vector<int>::const_iterator end){
   while(beg != end){
        cout << **beg++;
        if(beg != end) cout << " "; // no space after last element
   }
}
数组形参:数组有两个特殊的性质,影响我们定义和使用作用在数组上的函数,一是不能赋值数组;二是使用数组名字时,数组名会自动转化为指向第一个元素的指针。因为数组不能复制,所以无法编写使用数组类型形参的函数。因为数组会自动转化为指针,因此处理数组的函数通常通过操纵指向数组中的元素的指针来处理数组。如果要编写一个函数,输出int数组的内容,可用下面三种方式指定数组形参:
// three equivalent definitions of printValues
void printValues(int*) { /* ... */}
void printValues(int[]) {/* ... */}
void printValues(int[10]) {/* ... */}
上面三种定义等价的,形参类型都是int*。通常,将数组形参直接定义为指针要比使用数组语法定义更好。这样就明确地表示,函数操纵的是指向数组元素的指针,而不是数组本身。由于忽略了数组的长度,形参定义中如果包含了数组长度则特别容易引起误解。当编译器检查数组形参关联的实参时,它只会检查实参是不是指针、指针的类型和数组元素的类型是否匹配,而不会检查数组的长度。

和其他类型一样,数组形参可定义为引用和非引用类型。大部分情况下,数组以普通的非引用类型传递,此时数组会悄悄地转换为指针。一般来说,非引用类型的形参会初始化为其相应实参的副本。而在传递数组时,实参是指向数组第一个元素的指针,形参复制的是这个指针的值而不是数组元素本身。函数操纵的是指针的副本,因此不会修改实参指针的值。然而,函数可以通过指针改变它所指向的数组元素的值。通过指针形参做的任何改变,都将修改数组元素本身。不需要修改数组形参元素时,函数应该将形参定义为指向const对象的指针。

如果通过引用传递数组,数组形参可声明为数组的引用,此时编译器不会将数组实参转化为指针,而是传递数组的引用本身。在这种情况下,数组的大小成为形参和实参类型的一部分。编译器会检查数组实参的大小与形参的大小是否匹配。

void printValues(int (&arr)[10]){/* ... */}

需要注意的是&arr两边的圆括号是必需的,因为下标操作符具有更高的优先级。通过后面模板技术,允许传递指向任意大小的数组的引用形参。

和其他数据一样,多维数组以指向0号元素的指针方式传递。多维数组的元素本身就是数组。除了第一维以外的所有维的长度都是元素类型的一部分,必须明确指定。
//first parameter is an array whose elements are arrays of 10 ints
void printValues(int (*matrix)[10], int rowSize);
void printValues(int matrix[][10], int rowSize);
再次强调*matrix两边的圆括号是必需的。第二种声明方式是采用数组语法定义多维数组,与一位数组一样,编译器忽略第一维的长度,所以最好不要把它包括在形参表内。

传递给函数的数组的处理:任何处理处理数组的程序都要确保程序停留在数组的边界内。

第一种方法是在数组本身放置一个标记检测数组结束。C风格字符串就是采用这种方法,它是一个字符数组,并且以空字符null作为结束的标记。处理C风格的字符串的程序就是使用这个标记停止数组元素的处理。

第二种就是使用标准库规范,传递指向数组第一个和最后一个元素的下一个位置的指针。这种编程风格由标准库使用的技术启发得到。只要正确计算指针使它们标记一段有效的元素范围,程序就是安全的。

第三种方法是将第二个形参定义为表示数组的大小,这种用法在C程序和标准化之前的C++程序中十分普遍。

main处理命令行选项:实际上主函数的实参是可选的,用来确定程序要执行的操作。两种风格的定义如下:

int main(int argc, char *argv[]){ ... }
int main(int argc, char **argv){ ... } 
含有可变形参的函数:C++中的省略符形参是为了编译使用了varagrs的C语言程序。对于C++程序只能将简单数据类型传递给含有省略符形参的函数。实际上,当需要传递给省略形参时,大多数类类型对象都不能正确的复制。在无法列举出传递给函数的所有实参的类型和数目时,可以使用省略符形参。省略符暂定了类型检查机制,他们的出现告知编译器,当调用函数时,可以有0或多个实参,而实参的类型未知。省略符形参有下列两种形式:
void foo(parm_list, ...);
void foo(...);
3.return 语句

return语句用于结束当前正在执行的函数,并将控制权返回给调用此函数的函数。return语句有以下两种形式:

return;
return expression;
没有返回值的函数:不带返回值的return语句只能用于返回类型为void的函数。在返回类型为void的函数中,return返回语句不是必须的,隐式的return发生在函数的最后一个语句完成时。一般情况下,返回类型是void的函数使用return语句是为了引起函数的强制结束,它类似于循环结构中的break语句。返回类型为void的函数通常不能使用第二种形式的return语句,但是它可以返回另一个返回类型同样是void的函数的调用结果,返回任何其他表达式的尝试都会导致编译时的错误。

具有返回值的函数:任何返回类型不是void的函数都必须返回一个值,而且这个返回值的类型必须和函数的返回类型相同,或者能隐式转化为函数的返回类型。

1)主函数main的返回值。返回类型不是void的函数必须返回一个值,但次规则有一个例外情况。允许主函数main没有返回值就可结束。如果程序控制执行到主函数main的最后一个语句都还没有返回,那么编译器会隐式地插入返回0的语句。关于主函数main返回的另一个特别之处在于如何处理它的返回值。我们可将主函数main返回的值视为状态指示器。返回0表示程序运行成功,其他大部分返回值则表示失败,非0返回值的意义因机器的不同而不同,为了是返回值独立于机器,cstdlib头文件定义了两个预处理变量,分别表示程序运行成功(return EXIT_FAILURE;)和失败(return EXIT_SUCCESS;)。

2)返回非引用类型。函数的返回值用于初始化在调用函数出创建的临时对象(temporary object)。在求解表达式时,如果需要一个地方存储其运算结果,编译器会创建一个没有命名的对象,这就是临时对象。用函数返回值初始化临时对象与用实参初始化形参方法一样。如果返回类型不是引用,在调用函数的地方会将函数返回值复制给临时对象。当函数返回非引用类型时,其返回值既可以是局部对象,也可以是求解表达式的结果。

// return plural version of word if ctr isn't 1
string make_plural(size_t ctr, const string &word, const string &ending)
{
    return (ctr == 1) ? word : word + ending;
}

3)返回对象的引用。当函数返回对象的引用时,没有复制返回值。相反返回的是对象本身。下面的栗子,形参和返回值类型都是指向const string对象的引用,调用函数和返回结果时都没有复制这些string对象。

// find shorter of two string
const string &shortedString(const string &s1, const string &s2)
{
    return s1.size() < s2.size() ? s1: s2;
}
4)千万不要返回局部对象的引用。当函数执行完毕时,将释放给局部对象的存储空间。此时,对局部对象的引用就会指向不确定的内存。确保返回引用安全的一个好方法就是,请自问,这个引用指向哪个在此之前存在的对象?

5)引用返回左值。返回引用的函数返回一个左值,因此这样的函数可用于任何要求使用左值的地方。给函数返回值赋值可能让人惊讶,由于函数返回的是一个引用,因此这是正确的,该引用是被返回元素的同义词。

#include <iostream>
#include <string>
using namespace std;
char &get_val(string &str, string::size_type ix){
    return str[ix];
}
int main(){
    string s("a value");
    cout << s << endl;
    get_val(s, 0) = 'A';
    cout << get_val(s,0) <<endl;
    return 0;
}
如果不希望引用返回值被修改,返回值应该声明为const:
const char &get_val(...
6)千万不要返回指向局部对象的指针。函数的返回类型可以是大多数类型。特别地,函数也可以返回指针类型。和返回局部对象的引用一样,指向局部对象的指针也是错误的。一旦函数结束,局部对象被释放,返回的指针就变成了指向不存在对象的悬垂指针。

递归:直接或间接调用自己的函数称为递归函数(recursion function)。递归必须要定义一个终止条件,否则函数就会永远的递归下去,这意味着函数会一直调用自身直到程序栈耗尽。有时候,这种现象称为“无限递归错误”(infinite recursion error)。需要注意的是,主函数main不能调用自身。

#include <iostream>
using namespace std;
int factorial(int val);
int main(){
    int num = 3;
    cout << factorial(num)<<endl;
    return 0;
}
int factorial(int val){
    if(val > 1){
        return val*factorial(val -1);
    }
    return 1;
}
4.函数声明

正如变量必须先声明后使用一样,函数也必须在被调用之前先声明。与变量的定义类似,函数的声明也可以和函数的定义分离。一个函数只能定义一次,但是可以声明多次。函数的声明由函数返回类型、函数名和形参列表组成。形参列表必须包括形参类型,但不必对形参命名。这是三个元素被称为函数原型(function prototype),函数原型描述了函数的接口。

为了方便灵活,习惯于把函数声明放在头文件中,而函数的定义放在源文件中,这样可以确保对于指定函数其所有声明保持一致。如果函数接口发生变化,则只要修改其唯一的声明即可。需要注意的是,定义该函数的源文件应包含声明该函数的头文件。

默认实参是一种虽然不普遍、但在多数情况下仍然适用的实参值。调用函数时,可以省略有默认值的实参,编译器会为我们省略的实参提供默认值。默认实参是通过给形参表中的形参提供明确的初始值来指定的。程序员可为一个或多个形参定义默认值。但是如果有一个形参具有默认实参,那么它后面所有的形参都必须有默认实参。

string screenInit(string::size_type height = 24, 
                  string::size_type width = 80, 
                  char background = ' ');
...
string screen;
screen = screenInit(); //ok
screen = screenInit(66); //ok
screen = screenInit(66, 256); //ok
screen = screenInit(66, 256, '#'); //ok
设计带有默认实参的函数,其中部分工作就是排列形参,使最少使用默认实参的形参排在最前面,最可能使用默认实参的形参排在最后。默认实参的初始化式可以是任何适当类型的表达式。我们既可以在函数声明也可以在函数定义中指定默认实参。但是,在一个文件中,只能为一个形参指定默认实参一次。通常应当在函数声明中指定默认实参,并将该声明放在合适的头文件中。如果在函数定义的形参表中提供默认实参,那么只有在包含该函数定义的源文件中调用该函数,默认实参才是有效的。

5.局部对象

在C++语言中,每个名字都有作用域,而每个对象都有生命期(lifetime)。名字的作用域指的是知道该名字的程序文本区。对象的生命周期则是在程序执行过程中对象存在的时间。在函数中定义的形参和变量的名字只存在于函数的作用域中。

自动对象:默认情况下局部变量的生命周期局限于所在函数的每次执行期间。只有当定义它的函数被调用时才存在的对象成为自动对象(automatic object)。自动对象在每次调用函数时创建和撤销。

静态局部对象:一个变量如果位于函数的作用域内,但生命期却跨越了这个函数的多次调用。那么我们应该将这样的对象定义为static(静态的)。静态局部对象(static local object)确保不迟于在程序执行流程第一次经过该对象的定义语句进行初始化。

6.内联函数

在大多数机器上,调用函数都要做很多的工作:调用前要先保存寄存器,并在返回时恢复;复制实参;程序还必须转向一个新的位置执行。而此时如果使用内联函数,则可以避免函数调用的开销,通常来说,所谓的内联函数就是将它在程序中每个调用点上内联地展开。在函数返回类型前加上关键字inline就可以将函数指定为内联函数。

// inline version: find longer of two strings
inline const string &
shorterString( const string &s1, const string &s2 ){
    return s1.size() < s2.size() ? s1 : s2;
}
需要注意的是内联说明(inline specification)对于编译器来说只是一个建议,编译器可以选择忽略这个建议。一般来说,内联机制适用于优化小的、只有几行的而且经常被调用的函数。大多数的编译器都不支持递归函数的内联。内联函数应该在头文件中定义,这一点不同于其他函数。

7.类的成员函数

成员函数的定义与普通函数的定义类似。和任何函数一样,成员函数也包含下面四个部分:函数返回类型;函数名;用逗号隔开的形参表(也可能是空的);包含在一对花括号里面的函数体。前三部分组成了函数原型,函数原型必须在类中定义,但是,函数体则既可以在类中也可以在类外定义。编译器隐式的将类内定义的成员函数当做内联函数。类的所有成员都必须在类定义的花括号里面声明,此后,就不能再为类增加任何成员。

bool same_isbn(const Sales_item &rhs)  const
        {return isbn == rhs.isbn; }

每个成员函数都有一个额外的、隐含的形参将该成员函数与调用该函数的类对象捆绑在一起。调用成员函数时,实际上是使用对象来调用的。每一个成员函数(static成员函数除外)都有一个额外的、隐含的形参this,在调用成员函数时,形参this初始化为调用函数的对象的地址。考虑下面的语句:

total.same_isbn(trans);
编译器会重写这个函数调用:
Sales_item::same_isbn(&total, trans);
现在我们再来理解跟在Sales_item成员函数声明形参表后面的const所起的作用了:const改变了隐含this形参类型。在调用total.same_isbn(trans)时,隐含的this形参将是一个指向total对象的const Sales_Item*类型的指针。
bool Sales_item::same_isbn(const Sales_item* const this, const Sales_item &rhs)
        {reture (this->isbn == rhs.isbn); }
用这种方式使用const的函数称为常量成员函数(const member function),由于this是指向const对象的指针,const成员函数不能修改调用该函数的对象。const对象、指向const对象的指针或引用只能用于调用其const成员函数,如果尝试用它们来调用非const成员函数,则是错误的。

在成员函数中,不必显式地使用this来访问被调用函数所属对象的成员。对这个类的成员的任何没有前缀的引用,都被假定为通过指针this实现的引用。当然你也可以在函数体中显式的使用this指针,尽管这是合法的,但是没有这个必要。由于this指针是隐式定义的,因此不需要在函数的形参表中包含this指针,实际上这样做也是非法的。

前面提到,类的成员函数既可以在类中也可以在类外定义。在类外定义的成员函数必须先在类内声明,在类外定义时必须指明他们是类的成员。可以用作用域操作符来指明成员函数是在某类的作用域范围内定义的。

double Sales_item::avg_price() const
{
        if(units_sold)
            return revenue/units_sold;
       else
            return 0;
}
编写Sales_item类的构造函数:在定义类时没有初始化它的数据成员,而是通过构造函数来初始化其数据成员。构造函数(constructor)是特殊的成员函数,与其他的成员函数不同,构造函数和类同名,而且没有返回类型。而与其他成员函数相同的是,构造函数也有形参列表(可能为空)和函数体。一个类可以有多个构造函数,每个构造函数必须有与其他构造函数不同数目或类型的形参。构造函数的形参指定了创建类类型对象时使用的初始化式。通常这些初始化式会用于初始化新创建对象的数据成员。构造函数通常应确保每个数据成员都完成了初始化。

没有形参的构造函数,我们通常称为默认构造函数(defalut constructor)。默认构造函数说明当定义对象却没有为它提供显式的初始化式时应该怎么办?和其他成员函数一样,构造函数也必须在类中声明,但可以在类中或类外定义。需要注意的是,构造函数是放在类的public部分的,通常构造函数会作为类接口的一部分。

//default constructor needed to initialize members of built-in type
Sales_item(): units_sold(0), revenue(0.0) { }
如上的构造函数的形参表好和函数体都为空,令人感兴趣的是冒号与定义(空)函数体的花括号之间的代码,这部分代码称为构造函数的初始化列表(constructor initializer list)。构造函数的初始化列表为类的一个或多个数据成员指定初值。它跟在构造函数的形参表之后,以冒号开头。构造函数的初始化式是一些列成员名,每个成员后面是括在圆括号中的初始值,多个成员的初始化用逗号隔开。没有在初始化列表中表述的成员皆被其默认构造函数自动初始化。

如果没有为一个类显式定义任何构造函数,编译器将自动为这个类生成默认构造函数,由编译器创建的默认构造函数通常称为合成默认构造函数(synthesized default constructor),它将依据变量初始化规则初始化类中的左右成员。

类代码文件的组织:通常将类的声明放置在头文件中,大多数情况下,在类外定义的成员函数则置于源文件中。

8.重载函数

出现在相同作用域中的两个函数,如果具有相同的名字而形参表不同,则称为重载函数(overloaded function)。函数的重载(function overloading)简化了程序的实现,使程序更容易理解。要理解函数重载,必须理解如何定义一组重载函数和编译器如何决定对某一调用使用哪个函数。任何程序都仅有一个main函数的实例,main函数不能重载。

函数重载和重复声明的区别:如果两个函数声明的返回类型和形参完全匹配,则将第二个函数声明视为第一个的重复声明。如果两个函数的形参列表完全相同,但返回类型不同,则第二个声明是错误的。函数不能仅仅基于不同的返回类型而实现重载。有些看起来不相同的形参本质上是相同的:

//each pair declares the same function
Record lookup(const Account &acct);
Record lookup(const Account&); // parameter names are ignored
typedef Phone Telno;
Record lookup(const Phone&);
Record lookup(const Telno&); // Telno and Phone are the same type
//default argument doesn't change the number of parameters
Record lookup(const Phone&, const Name&);
Record lookup(const Phone&, const Name& = "");
//const is irrelevant for nonreference paramters
Record lookup(Phone);
Record lookup(const Phone); // redeclaration

重载与作用域:在函数中局部声明的名字将屏蔽在全局作用域内声明的同名名字。这个关于变量名字的性质对于函数名同样成立。如果局部声明了一个函数,则该函数将屏蔽而不是重载在外层域中声明的同名函数,在C++中名字查找发生在类型检查之前。由此推论,每一个版本的重载函数都应在同一个作用域中声明。

函数匹配与实参转换:函数重载确定(overload resolution,即函数匹配 function matching)是将函数调用与重载函数集合中一个函数相关联的过程。通过自动提取函数调用中实际使用的实参与重载集合中各个函数提供的形参做比较,编译器实现该调用与函数的匹配。匹配有三种可能:存在最佳匹配,并生成调用该函数的代码;不存在匹配的函数,编译器报错;存在多个匹配,没有最佳选择,该调用具有二义性(ambiguous),编译器报错。

重载确定的三个步骤:第一确定候选函数(candidate function);第二选择可行函数(viable function);第三寻找最佳匹配(best match)(如果有的话)。如果含有多个形参的重载确定,则函数匹配会更加的复杂,此时需要注意调用二义性的出现。实参类型转换:为了确定最佳匹配,编译器将实参类型到相应的形参类型的转换划分等级。转换的等级以降序排列如下:

  1. 1)精准匹配(extract match)。
  2. 2)通过类型提升(promotion)实现的匹配。
  3. 3)通过标准转换(standard conversion)实现的匹配。
  4. 4)通过类类型转换(class-type conversion)实现的匹配。

需要注意是,通过类型提升实现的转换要其他标准转换;整数对象即使具有与枚举元素相同的值也不能用于调用期望获得枚举类型实参的函数,因为枚举类型的对象只能用同一枚举类型的另一个对象或一个枚举成员(enumerator)进行初始化。尽管无法将整型值传递给枚举类型的形参,但可以将枚举值传递给整型形参。仅当形参是引用或指针时,形参是否为const才有影响。

9.指向函数的指针

函数指针是指指向函数而非指向对象的指针。像其他指针一样,函数指针也指向某个特定的类型。函数类型由其返回类型以及形参表决定,而与函数名无关。

bool (*pf)(const string &, const string &); // pf points to function return bool
bool *pf(const string &, const string &);//declares a function named pf that returns a bool*
函数指针类型相当地冗长。使用typedef可以为指针类型定义同义词,可将函数指针的使用大大简化。
typedef bool (*cmpFcn)(const string&, const string &);

该定义表示cmpFcn是一种指向函数的指针类型的名字。该指针类型为“指向返回bool类型并带有两个const string引用形参的函数的指针。”在要使用这种函数指针类型时,只需直接使用cmpFcn即可,不必每次都把整个类型声明全部写出来。在引用函数名但又没有调用该函数,函数名将被自动解释为指向函数的指针。假设有函数:

// compares lengths of two strings
bool lengthCompare(const string &, const string &);
除了用作函数调用的左操作数以外,对lengthCompare的任何使用都被解释为如下类型的指针:
bool (*)(const string &, const string &);
指向函数的指针的初始化和赋值:函数指针只能通过同类型的函数或函数指针或者0值常量表达式进行初始化或赋值。一直引用函数函数名等效于在函数名上应用取地址操作符。将函数指针初始化为0,表示该指针不指向任何函数,指向不同函数类型的指针之间不存在转换。

通过指针调用函数:指向函数的指针可以用于调用它所指向的的函数。可以不需要使用解引用操作符,直接通过指针调用函数。

cmpFcn pf = lengthCompare;
lengthCompare("hi", "bye"); //direct call
pf("hi", "bye"); //equivalent call : pf1 implicitly dereferenced
(*pf)("hi", "bye"); // equivalent call:pf1 explicitly dereferenced

函数指针形参:函数的形参可以是指向函数的指针。这种形参可以用以下两种方式编写:

void useBigger(const string &, const string &, bool(const string &, const string &));
void useBigger(const string &, const string &, bool(*)(const string &, const string &))
返回指向函数的指针:函数可以返回指向函数的指针,但是正确写出这种返回类型相当不容易。

int (*ff(int))(int*, int)
使用typedef可使该定义更加简明易懂:

// PF is a pointer to a function returning an int, taking an int* and an int
typedef int(*PF)(int *, int)
PF ff(int); // ff returns a pointer to function
允许将形参定义为函数类型,但函数的返回类型则必须是指向函数的指针,而不能是函数。具有函数类型的形参所对应的实参将被自动转换为指向相应函数类型的指针。但是,当返回的是函数时,同样的转换操作则无法实现。

// func is a function type, not a pointer to function
typedef int func(int*, int);
void f1(func); // ok
func f2(int); //error
func * f3(int);//ok
指向重载函数的指针:C++语言允许使用函数指针指向重载的函数。指针的类型必须与重载的函数的一个版本精准匹配。如果没有精准匹配的函数,则对该指针的初始化或赋值都将导致编译错误。

extern void ff(vector<double>);
extern void ff(unsigned int);
//which function does pf1 refer to?
void (*pf1)(unsigned int) = &ff; // ff(unsigned int)

(完)

56.8K

发表评论:

© 云中布衣 2015 | Driven by EMLOG  | SiteMap | RunTime: 10.85ms&RSS  |   | 回到顶部

文章数量【230】 评论数量【156】 稳定运行【1031天】

Visitor IP Address【54.80.82.9】