C++ Primer 6_函数
之前学过的基础性的东西我将不会在此再做重复,如果有一些我之前从未注意过或从未深思过的事情以及一些重点,我都将会在此写下。
函数的返回类型不能是数组类型或者函数类型,但可以是指向数组或函数的指针。
void fcn(const int i) {/* fcn 能够读取 i,但是不能向 i 写值 / } void fcn(int i) {/…*/ } //错误:重复定义了 fcn(int) 在 C++ 语言中,允许我们定义若干具有相同名字的函数, 不过前提是不同函数的形参列表应该有明显的区别。 因为顶层 const 被忽略掉了,所以在上面的代码中传入两个 fcn 函数的参数可以完全一样。 因此第二个 fcn 是错误的,尽管形式上有差异,但实际上它的形参和第一个 fcn 的形参没什么不同。
尽管不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式: //尽管形式不同,但这三个 print 函数是等价的 //每个函数都有一个 const int* 类型的形参 void print (const int* ) ; void print (const int[] ); //可以看出来,函数的意图是作用于一个数组 void print (const int[10]); //这里的维度表示我们期望数组含有多少元素,实际不一定
尽管表现形式不同,但上面的三个函数是等价的: 每个函数的唯一形参都是 const int* 类型的。 当编译器处理对 print 函数的调用时,只检查传入的参数是否是 const int* 类型: int i = 0, j[2]= {0,1}; print(&i); //正确: &i的类型是int* print(j); //正确: j转换成int*并指向j[0]
如果我们传给 print 函数的是一个数组, 则实参自动地转换成指向数组首元素的指针, 数组的大小对函数的调用没有影响。
和其他使用数组的代码一样,以数组作为形参的函数也必须确保使用数组时不会越界。
因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸, 调用者应该为此提供一些额外的信息。 ? 使用标记指定数组长度 管理数组实参的第一种方法是要求数组本身包含一个结束标记, 使用这种方法的典型示例是 C 风格字符串。 C 风格字符串存储在字符数组中,并且在最后一个字符后面跟着一个空字符。 函数在处理 C 风格字符串时遇到空字符停止: void print (const char *cp) { if (cp) //若cp不是一个空指针 while (*cp) //只要指针所指的字符不是空字符 cout << *cp++; //输出当前字符并将指针向前移动一个位置 } 这种方法适用于那些有明显结束标记且该标记不会与普通数据混淆的情况, 但是对于像 int 这样所有取值都是合法值的数据就不太有效了。 ? 使用标准库规范 void print (const int *beg, const int *end){ while (beg != end) cout << *beg++ << endl; } ? 显式传递一个表示数组大小的形参 void print (const int ia[], size_t size) { for (size_t i = 0 ; i != size; ++i) { cout << ia [i] << endl ; } }
数组引用形参: void print(int (&arr)[10]) { for (auto elem : arr) cout << elem << endl ; } &arr 两端的括号必不可少: f(int &arr[10]) //错误:将arr声明成了引用的数组 f(int (&arr)[10]) //正确:arr是具有10个整数的整型数组的引用
int *matrix[10]; // 10个指针构成的数组 int (*matrix)[10]; // 指向含有10个整数的数组的指针
有时我们确实需要给 main 传递实参, 一种常见的情况是用户通过设置一组选项来确定函数所要执行的操作。 例如,假定 main 函数位于可执行文件 prog 之内,我们可以向程序传递下面的选项: prog -d -o ofile data0 这些命令行选项通过两个(可选的)形参传递给 main 函数: int main (int argc, char *argv[]) { … }
第二个形参 argv 是一个数组,它的元素是指向 C 风格字符串的指针; 第一个形参 argc 表示数组中字符串的数量。
因为第二个形参是数组,所以 main 函数也可以定义成: int main (int argc, char *argv) { … } 其中 argv 指向 char。
当实参传给 main 函数之后, argv 的第一个元素指向程序的名字或者一个空字符串, 接下来的元素依次传递命令行提供的实参。 最后一个指针之后的元素值保证为 0。 以上面提供的命令行为例, argc 应该等于 5,argv 应该包含如下的 C 风格字符串: argv [0] = “prog” ; //或者argv [0]也可以指向一个空字符串 argv [1] = “-d” ; argv [2] = “-o” ; argv [3] = “ofile” ; argv [4] = “data0” ; argv [5] = 0 ;
当使用 argv 中的实参时,一定要记得可选的实参从 argv[1] 开始; argv[0] 保存程序的名字,而非用户输入。
为了编写能处理不同数量实参的函数,C++ 提供了三种主要的方法: ? 如果所有的实参类型相同,可以传递一个名为 initializer_list 的标准库类型(C++11); ? 如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板(C++11); ? C++ 还有一种特殊的形参类型(即省略符),可以用它传递可变数量的实参。即省略符形参,不过需要注意的是,这种功能一般只用于与 C 函数交互的接口程序。
如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用 initializer_list 类型的形参。 initializer_list 是一种标准库类型,用于表示某种特定类型的值的数组。 initializer_list 类型定义在同名的头文件中。
initializer_list 对象中的元素永远是常量值,我们无法改变 initializer_list 对象中元素的值。
如果想向 initializer_list 形参中传递一个值的序列,则必须把序列放在一对花括号内: // expected 和 actual 是 string 对象 if (expected != actual) error_msg( { " functionx" , expected, actual} ); else error_msg ( { “functionx” , “okay” }) ; 在上面的代码中我们调用了同一个函数 error_msg,但是两次调用传递的参数数量不同: 第一次调用传入了三个值,第二次调用只传入了两个。
含有 initializer_list 形参的函数也可以同时拥有其他形参。例如: void error_msg (ErrCode e, initializer_list i)
省略符形参是为了便于 C++ 程序访问某些特殊的 C 代码而设置的, 这些代码使用了名为 varargs 的 C 标准库功能。 通常,省略符形参不应用于其他目的。
省略符形参只能出现在形参列表的最后一个位置,它的形式无外乎以下两种: void foo(parm_list, …) ; void foo(…); 在第一种形式中,形参声明后面的逗号是可选的。
省略符形参应该仅仅用于 C 和 C++ 通用的类型。 特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
在含有 return 语句的循环后面应该也有一条 return 语句,如果没有的话该程序就是错误的。很多编译器都无法发现此类错误,VS 的能发现。
引用返回左值,可以像使用其他左值那样来使用返回引用的函数的调用, 特别是,我们能为返回类型是非常量引用的函数的结果赋值: char &get_val (string &str, string::size_type ix) return str[ix] ; } int main () { string s(“a value”); cout << s << endl ; // 输出a value get_val(s, 0) = ‘A’; // 将s[o]的值改为A cout << s << endl ; // 输出A value return 0; }
如果返回类型是常量引用,我们不能给调用的结果赋值: shorterString(“hi”, “bye”) = “X” ; //错误:返回值是个常量
列表初始化返回值: vector process() { // … // expected 和actual是string对象 if (expected. empty()) return { }; //返回一个空vector对象 else if (expected == actual) return { “functionx”, “okay” }; //返回列表初始化的vector对象 else return { “functionx”, expected, actual } ; } 如果函数返回的是内置类型(例如int),则花括号包围的列表最多包含一个值, 而且该值所占空间不应该大于目标类型的空间。 如果函数返回的是类类型,由类本身定义初始值如何使用。
我们允许 main 函数没有 return 语句直接结束。
main 函数不能调用它自己。
因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用。 虽然从语法上来说,要想定义一个返回数组的指针或引用的函数比较烦琐: int (func(int i))[10] { } 但是有一些方法可以简化这一任务,其中最直接的方法是使用类型别名: typedef int arrT[10]; // arrT 是一个类型别名,它表示的类型是含有 10 个整数的数组 // using arrT = int [10] ; // 上述的等价声明 arrT func(int i) { } // func 返回一个指向含有 10 个整数的数组的指针
可以按照以下的顺序来逐层理解 int (*func(int i))[10]; 该声明的含义: ? func(int i) 表示调用 func 函数时需要一个 int 类型的实参。 ? (*func(int i)) 意味着我们可以对函数调用的结果执行解引用操作。 ? (*func(int i))[10] 表示解引用 func 的调用将得到一个大小是 10 的数组。 ? int (*func(int i))[10] 表示数组中的元素是 int 类型。
在 C++11 新标准中还有一种可以简化上述 func 声明的方法,就是使用尾置返回类型。 任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或者数组的引用。 尾置返回类型跟在形参列表后面并以一个->符号开头,函数真正的返回类型跟在形参列表之后。 为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个 auto: auto func(int i) -> int (*)[10];
还有一种情况,如果我们知道函数返回的指针将指向哪个数组,就可以使用 decltype 关键字声明返回类型: int odd[] = {1,3,5,7,9} ; int even[] = {0,2,4,6,8} ; decltype(odd) *arrPtr(int i) { return (i % 2)? &odd : &even; } 有一个地方需要注意: decltype 并不负责把数组类型转换成对应的指针,所以 decltype 的结果是个数组, 要想表示 arrPtr 返回指针还必须在函数声明时加一个 * 符号。
main函数不能重载
不允许两个函数除了返回类型外其他所有的要素都相同。 假设有两个函数,它们的形参列表一样但是返回类型不同,则第二个函数的声明是错误的: Record lookup(const Account&) ; bool lookup(const Account&); //错误:与上一个函数相比只有返回类型不同
函数的对象。一个拥有顶层 const 的形参无法和另一个没有顶层 const 的形参区分开来: Record lookup (Phone) ; Record lookup (const Phone) ;//重复声明了Record lookup(Phone) Record lookup (Phone*) ; Record lookup (Phone* const) ;//重复声明了Record lookup(Phone*) 在这两组函数声明中,每一组的第二个声明和第一-个声明是等价的。
另一方面,如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的 const 是底层的: //对于接受引用或指针的函数来说,对象是常量还是非常量对应的形参不同 //定义了4个独立的重载函数 Record lookup (Account&);//函数作用于Account的引用 Record lookup (const Account&);//新函数,作用于常量引用 Record lookup (Account*) ;//新函数,作用于指向Account的指针 Record lookup (const Account*);//新函数,作用于指向常量的指针
当传递一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数。
一般来说,将函数声明置于局部作用域内不是一个明智的选择。但是为了说明作用域和重载的相互关系,我们将暂时违反这一原则而使用局部函数声明。
重载对作用域的一般性质并没有什么改变: 如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。 在不同的作用域中无法重载函数名: string read () ; void print (const string &) ; void print (double); //重载print函数 void fooBar (int ival) { bool read = false; //新作用域: 隐藏了外层的read string s = read ( ); //错误: read是一个布尔值,而非函数 //不好的习惯: 通常来说,在局部作用域中声明函数不是一个好的选择 void print (int); //新作用域: 隐藏了之前的print print ("value: "); //错误: print (const string &)被隐藏掉了 print (ival); //正确: 当前print (int)可见 print (3.14); //正确: 调用print(int),print (double)被隐藏掉了 } 大多数读者都能理解调用 read 函数会引发错误。因为当编译器处理调用 read 的请求时,找到的是定义在局部作用域中的 read。这个名字是个布尔变量,而我们显然无法调用一个布尔值,因此该语句非法。 调用 print 函数的过程非常相似。在 fooBar 内声明的 print(int) 隐藏了之前两个 print 函数,因此只有一个print 函数是可用的: 该函数以 int 值作为参数。
一旦在当前作用域中找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体。剩下的工作就是检查函数调用是否有效了。
在 C++ 语言中,名字查找发生在类型检查之前。
多次声明同一个函数也是合法的。 不过有一点需要注意,在给定的作用域中一个形参只能被赋予一次默认实参。 换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参, 而且该形参右侧的所有形参必须都有默认值。假如给定: typedef string::size_type sz; string screen (sz, sz, char = ’ '); 我们不能修改一个已经存在的默认值: string screen (sz, sz, char = ‘*’); //错误:重复声明 但是可以按照如下形式添加默认实参: string screen(sz = 24, sz = 80, char) ; //正确:添加默认实参
局部变量不能作为默认实参。函数之外(与函数处于同一作用域或高于函数作用域的)的变量可以作为默认实参。 // wd、def 和 ht 的声明必须出现在函数之外 sz wd = 80; char def = ’ '; sz ht(); string screen (sz = ht (), sz = wd, char = def);
当用作默认实参的名字在函数声明所在的作用域内解析,而这些名字的求值过程发生在函数调用时: void f2 () { def =’’; //改变默认实参的值 sz wd = 100; //隐藏了外层定义的wd,但是没有改变默认值 window = screen(); //调用screen(ht(), 80,’’) } 我们在函数 f2 内部改变了 def 的值,所以对 screen 的调用将会传递这个更新过的值。 另一方面,虽然我们的函数还声明了一个局部变量用于隐藏外层的 wd,但是该局部变量与传递给 sereen 的默认实参没有任何关系。
定义 constexpr 函数的方法与其他函数类似,不过要遵循几项约定: 函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条 return 语句。
如果我们用一个非常量表达式传入 constexpr 函数,比如 int 类型的对象 i,则返回值是一个非常量表达式: int i = 2; // i不是常量表达式 int a2[scale(i)]; // 错误:scale(i)不是常量表达式
constexpr 函数不一定返回常量表达式。
函数匹配从可行函数中选择与本次调用最匹配的函数。 在这一过程中,逐一检查函数调用提供的实参,寻找形参类型与实参类型最匹配的那个可行函数。 “最匹配”的基本思想是,实参类型与形参类型越接近,它们匹配得越好。
在我们的例子中,调用只提供了一个(显式的)实参,它的类型是 double。 如果调用 f(int) ,实参将不得不从 double 转换成 int。 另一个可行函数 f(double, double) 则与实参精确匹配。 精确匹配比需要类型转换的匹配更好,因此,编译器把 f(5.6) 解析成对含有两个 double 形参的函数的调用,并使用默认值填补我们未提供的第二个实参。
我们来分析如下的调用会发生什么情况: (42,2.56); 可行函数包括f(int, int)和 f(double,double)。 接下来,编译器依次检查每个实参以确定哪个函数是最佳匹配。 如果有且只有一个函数满足下列条件,则匹配成功。
如果在检查了所有实参之后没有任何一个函数脱颖而出,则该调用是错误的。编译器将报告二义性调用的信息。 编译器最终将因为这个调用具有二义性而拒绝其请求: 因为每个可行函数各自在一个实参上实现了更好的匹配,从整体上无法判断孰优孰劣。 看起来我们似乎可以通过强制类型转换其中的一个实参来实现函数的匹配, 但是在设计良好的系统中,不应该对实参进行强制类型转换。
调用重载函数时应尽量避免强制类型转换。 如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理。
分析函数调用前,我们应该知道小整型一般都会提升到 int 类型或更大的整数类型。 假设有两个函数,一个接受 int、另一个接受 short, 则只有当调用提供的是 short 类型的值时才会选择 short 版本的函数。 有时候,即使实参是一个很小的整数值,也会直接将它提升成 int 类型, 此时使用 short 版本反而会导致类型转换: void ff(int); void ff(short); ff(‘a’); // char提升成int,调用 f(int)
所有算术类型转换的级别都一样。 例如,从 int 向 unsigned int 的转换并不比从 int 向 double 的转换级别高。 举个具体点的例子,考虑 void manip(long); void manip(float); manip(3.14); //错误:二义性调用 字面值 3.14 的类型是 double,它既能转换成 long 也能转换成 float。 因为存在两种可能的算数类型转换,所以该调用具有二义性。
如果重载函数的区别在于它们的引用类型的形参是否引用了const,或者指针类型的形参是否指向const,则当调用发生时编译器通过实参是否是常量来决定选择哪个函数: Record lookup (Account&);//函数的参数是Account的引用 Record lookup (const Account&);//函数的参数是一个常量引用 const Account a; Account b; lookup (a);//调用lookup (const Account&) lookup (b);//调用lookup (Account& )
指针类型的形参也类似。 如果两个函数的唯一区别是它的指针形参指向常量或非常量, 则编译器能通过实参是否是常量决定选用哪个函数: 如果实参是指向常量的指针,调用形参是 const* 的函数; 如果实参是指向非常量的指针,调用形参是普通指针的函数。
要想声明一个可以指向该函数的指针,只需要用指针替换函数名即可: pf 指向一个函数,该函数的参数是两个 const string 的引用,返回值是 bool 类型 bool (*pf)(const string &, const string &); //未初始化
*pf 两端的括号必不可少。
当我们把函数名作为一个值使用时,该函数自动地转换成指针。 例如,按照如下形式我们可以将 lengthCompare 的地址赋给 pf: pf = lengthCompare; // pf 指向名为 lengthCompare 的函数 pf = &lengthCompare; //等价的赋值语句: 取地址符是可选的
此外,我们还能直接使用指向函数的指针调用该函数,无须提前解引用指针: bool b1 = pf(“hello”, “goodbye”) ; //调用 lengthCompare 函数 bool b2= (*pf)(“hello”, “goodbye”); //一个等价调用 bool b3 = lengthcompare(“hello”, “goodbye”); //另一个等价调用
当我们使用重载函数时,上下文必须清晰地界定到底应该选用哪个函数。如果定义了指向重载函数的指针 void ff(int*); void ff(unsigned int); void (*pf1)(unsigned int) = ff; // pf1 指向 ff(unsigned int) 编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配 void (*pf2)(int) = ff; //错误: 没有任何一个 ff 与该形参列表匹配 double (pf3)(int) = ff; //错误: ff 和 pf3 的返回类型不匹配
和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。 此时,形参看起来是函数类型,实际上却是当成指针使用: //第三个形参是函数类型,它会自动地转换成指向函数的指针 void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &)); //等价的声明:显式地将形参定义成指向函数的指针 void useBigger(const string &s1, const string &s2, bool (*pf)(const string &,const string &)) ;
我们可以直接把函数作为实参使用,此时它会自动转换成指针: //自动将函数 lengthCompare 转换成指向该函数的指针 useBigger(s1, s2, lengthCompare);
正如 useBigger 的声明语句所示,直接使用函数指针类型显得冗长而烦琐。 类型别名和 decltype 能让我们简化使用了函数指针的代码: // Func 和 Func2 是函数类型 typedef bool Func(const string &, const string &) ; typedef decltype(lengthCompare) Func2; //等价的类型 // FuncP 和 FuncP2 是指向函数的指针 typedef bool (*FuncP)(const string &, const string &) ; typedef decltype(lengthCompare) *FuncP2; //等价的类型
Func 和 Func2 是函数类型,而 FuncP 和 FuncP2 是指针类型。 可以使用如下的形式重新声明 useBigger: // useBigger 的等价声明,其中使用了类型别名 void useBigger(const string&, const string&, Func); void useBigger(const string&, const string&, FuncP2) ; 这两个声明语句声明的是同一个函数,在第一条语句中,编译器自动地将 Func 表示的函数类型转换成指针。同: //第三个形参是函数类型,它会自动地转换成指向函数的指针 void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &)); //等价的声明:显式地将形参定义成指向函数的指针 void useBigger(const string &s1, const string &s2, bool (*pf)(const string &,const string &)) ;
和数组类似,虽然不能返回一个函数,但是能返回指向函数类型的指针。 然而,我们必须把返回类型写成指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理。 与往常一样,要想声明一个返回函数指针的函数,最简单的办法是使用类型别名: using F = int(int*, int); // F 是函数类型,不是指针 using PF = int()(int, int); // PF 是指针类型
其中我们使用类型别名将 F 定义成函数类型,将 PF 定义成指向函数类型的指针。 必须时刻注意的是,和函数类型的形参不一样,返回类型不会自动地转换成指针。 我们必须显式地将返回类型指定为指针: PF f1(int); //正确: PF 是指向函数的指针,f1 返回指向函数的指针 F f1(int); //错误: F 是函数类型,f1 不能返回一个函数 F *f1(int); //正确: 显式地指定返回类型是指向函数的指针
当然,我们也能用下面的形式直接声明 f1: int(f1(int))(int, int);
我们还可以使用尾置返回类型的方式声明一个返回函数指针的函数: auto f1(int) -> int()(int, int);
如果我们明确知道返回的函数是哪一个,就能使用 decltype 简化书写函数指针返回类型的过程: string::size_type sumLength(const string &, const string &); string::size_type largerLength(const string &, const string &); // 根据其形参的取值,getFcn函数返回指向sumLength或者largerLength的指针 // decltype(sumLength) *getFcn(const string &); 书上原程序 decltype(sumLength) *getFcn = sumLength; decltype(largerLength) *getFcn = largerLength;
牢记当我们将 decltype 作用于某个函数时,它返回函数类型而非指针类型。 因此,我们显式地加上 * 以表明我们需要返回指针,而非函数本身。
|