C++ Primer 5th 第6章 函数
正如第一章所说:C++的函数是一个能够完成一个功能的模块或者说是一段命名了的代码块。
如下图所示,函数可以重载,是一段实现某些功能命名了的代码块。
一个完整的函数定义有四部分:
1.返回类型
2.函数名
3.形参
4.函数体
对于返回类型来说,return语句返回的类型必须和返回类型一致,或者可以转换。
对于函数名来说,允许多个函数使用相同的名字,当参数不同时,形成重载。对函数名使用调用运算符,则执行函数。
对于形参来说,可以是传值的,也可以是传引用的。多个形参使用逗号来分割,分割的逗号并非逗号运算符。
对于函数体来说,必须是在一对花括号包围的块中。
在函数体中必须要有return语句,或者返回某种特定类型,或者返回void,由于人为因素,main函数和返回类型为void的函数可以没有return语句,编译器将在其末尾自动插入相应的return语句。
函数的调用过程是首先初始化被调函数的形参,然后将控制权交给被调函数,接下来被调函数开始执行函数体,当遇到return语句时,返回相应的值,并交还控制权。
实参是形参的初始值,形参的初始化是依次对应的,第一个形参对应第一个实参,第二个形参对应第二个实参,以此类推。虽然形参和实参之间是一一对应的关系,但C++并没有规定实参初始化形参的先后次序,只规定了对应次序。这和表达式中对象的求值顺序是一样的。有几个形参,就要提供几个实参,所以形参一定会被初始化。
另外,当提供的实参和形参类型不一致时,则会发生隐式转换,这也对类型之间能够互相转换提出了要求。
任意两个形参之间不允许同名,另外,在一个函数内的最外层作用域不允许定义和形参一样名字的局部变量,例如:
void f(int a) // 形参a { // 函数最外层作用域 int b; // 正确,局部变量和函数内最外层作用域 { // 子作用域 int a; // 正确,子作用域隐藏了外层的形参a int b; // 正确,子作用域隐藏了外层的局部变量b } // 子作用域结束 int a; // 错误,函数内最外层作用域局部变量与形参同名 }
形参的名字是可选的,但是我们却无法使用一个未命名的形参,当形参不会被用到时,可以不命名。但需要注意的是,即使形参不会被使用,也需要传递给他实参以初始化。
大多数类型都可以作为函数的返回类型,void也是一种特殊的类型,表示函数不返回任何值。函数不能直接返回数组,因为数组不能被拷贝。同样,函数的形参也不能是数组,因为数组不能被拷贝。另外,函数的返回类型也不能是函数。要返回这些类型,需要使用指针或者引用。
函数体内和形参定义的都是局部变量,因为函数体是一个块,一个块就是一个作用域。函数体外定义的变量则是全局变量,存在于程序的整个执行过程中,这些对象在程序启动时就被创建,程序结束时才消亡。
对于普通的局部变量来说,当程序的执行路径经过它时,它被创建,当到达它所在块的尾部时,它被销毁。它们将只存在于块的执行期间,被称为自动对象。
可以在函数的内部定义一种创建后但直到程序结束才被销毁的变量,这种变量也是局部的,但却不是自动的,而是静态的,称之为局部静态对象。
静态对象如果没有被初始化,那么它将自我初始化为0。
和其他名字一样,函数的名字在使用前也必须被声明。类似变量,函数只能被定义一次,但是可以声明多次。如果一个函数不会被使用,那么我们可以只声明。
在函数的声明中,可以不为形参提供名字。
函数声明有三个要素:返回类型,函数名,形参类型。这三个要素描述了函数的接口,函数声明也称为函数原型,其他版本的翻译或者其他编程语言中也称为函数签名。
函数应该在头文件中声明,源文件中定义。
含有函数声明的头文件应该被函数定义的源文件包含,以便于编译器验证定义和声明是否匹配。
每次调用参数时,都会重新创建实参,并用实参初始化形参。形参的类型决定了和实参交互的方式。传值是拷贝一份实参的值,传引用则是直接绑定到实参上。对于指针和传值方式是一样的,都是复制一份值。
传引用则是绑定到实参上,对形参的操作就是对实参的操作,使用引用可以避免拷贝,到实参对象较大或者不支持拷贝时,传引用就派上用场了。如果无需改变引用的值,那最好将引用形参声明为const引用。
关于const形参在被实参初始化时,会忽略实参的顶层const。在定义形参时,尽量使用const引用,因为const引用不会在不经意间改变实参,同时对实参的类型匹配范围较广,可以接受const实参和非const实参,以及接受字面值。
数组的两个特殊性质对定义和使用作用在数组的函数有影响,这两个性质分别是:不允许拷贝数组以及使用数组时,会自动转化成指针。
因为不允许拷贝数组,所以不允许传值调用数组,另外数组会被转换成指针,所以给函数传递一个数组时,将转换成指针,并且丢失数组长度信息。
因为数组传递给函数后,自动转换为指针,丢失了数组长度,因此需要为传入的数组提供一些额外的信息来确定数组处理的结束。
第一种方法是,数组本身就有结束标记,比如C风格字符串,末尾隐式有个'\0'的空字符,函数在处理C风格字符串时,遇到空字符自动停止。
第二种方法是,使用标准库的首尾迭代器:begin和end
第三种方法是,传入一个额外的实参来显式提供数组大小。
在C++中允许将形参定义成对数组的引用,此形参将绑定对应的实参数组上。需要注意的是,不像数组实参转换成指针形参时会丢失长度信息那样,引用形参要求严格匹配实参的数组长度。对于数组引用的形参其数组长度是固定的,限制了函数的可用性。
main函数是不允许被重载的,main函数也可以接受参数,它的形参有2个,第一个是一个int,第二个是二维数组或者元素是C风格字符串的数组,其定义形式为
int main(int argc, char const *argv[]) int main(int argc, char const **argv)
其中argc的值是参数的个数,参数的个数统计中包括程序名,例如在命令行中如下执行main程序
$ ./main 1 2 3
其中main是第一个参数,1是第二个参数,以此类推,总共4个参数。而字符串“main”存储在argv[0],字符串“1”存储在argv[1]中,以此类推。argv最后一个元素是argv[5],注意这里不是argv[4],不是从0到4,而是0到5,最后一个值argv[5]是自动添加的,总是为0。
有时,我们并不能一定确定有些函数到底有多少个参数,这些就需要形参数量可变的函数。对于形参数量可变的函数,在C++中有两种方法,一是使用一种initializer_list的标准库类型,另外就是使用可变参数模板。这里先了解第一种。
对于可变数量的形参,在C语言中使用省略符(...),这种方法是C++从C语言继承而来的,但是很少使用。
initializer_list类型其实就是一种const vector类型,两者的差别是vector中的元素不是const的,initializer_list中的元素是const的,另外使用initializer_list来处理可变数量的形参其实也是一种错觉,形参的数量并没有变化,形参的数量就是一个initializer_list的对象,只不过这个对象可以使用列表初始化来进行初始化({ }),所以看起来像是接受了数量不固定的形参,因此这里的initializer_list也可以使用vector来代替,只不过vector不能防止无意间元素的修改。
initializer_list的使用如下:
void error_msg(initializer_list<string> il) { for (auto beg = il.begin(); beg != il.end(); ++beg) cout << *beg << " " ; cout << endl; }
这里的形参il可以被这样的实参初始化
error_msg({"functionX", expected, actual}); error_msg({"functionX", "okay"});
上面两种调用都是可以的,仔细观察可以发现这就是列表初始化,vector也具有同样的初始化方式,因此上面的initializer_list<string> il完全可以换成vector<string> v。
前面说过return语句的作用是返回值并交还控制权,return语句有两种形式,
return;
return expression;
前面也说过,返回类型为void的函数可以没有return语句,编译器将在其末尾自动插入相应的return语句。造成这种结果的原因是那些老的程序员偷懒不愿意在程序的末尾多写这一条语句,所以标准允许省略,而让编译器自动添加。
关于return;这种形式,也可以被写成第二种,不够要求就是被写成第二种形式时,expression必须是另一个返回void的函数,这种形式通常用于递归函数。强行令void函数返回非void类型将导致编译错误。
在有返回值的情况下,需要使用第二种形式的return语句,这种形式要求return语句返回的对象类型和函数定义时的返回类型一致,或者能进行转换。否则编译错误。
return语句返回值时和初始化一个变量或形参一样,返回值就将结果值拷贝一份到调用点。如果返回引用,则被这种返回方式告知不要去复制,直接把需要的对象返回。通常是把引用形参传入的对象返回,如果形参不是引用类型,那么一般返回的是局部对象的引用,这么做是非法的。
返回值和返回引用的区别就是怎么去返回,直接把用的对象返回回去,还是复制一份返回回去。对于返回值来说,返回的值是右值;对于返回引用来说,返回的值是左值。具体情况还是要看应用场景。
前面多次提了数组在函数中应用的限制,那么具体如何定义使用数组的引用形参和返回数组引用呢?具体如下:
对于数组形参的声明可以使用这么几种形式:
void fun(int *array); void fun(int array[]); void fun(int array[10]); void fun(int (&array)[10]);
前面三种形式是等价的,最终的效果都是一个不知道长度的指针,虽然第三个指定了数组长度,但是最终还是会被编译器忽略。
第四种则是对数组的引用,数组的类型和长度都是数组的一部分,因此第四个参数,形参和实参必须严格在类型和长度上匹配,这就限定了该函数所能接受实参的范围。
对于返回数组的函数,不能直接返回数组,我们可以使用数组指针或者数组引用,可使用如下这几种形式:
int (*fun(int i)) [10]; auto fun(int i) -> int (*)[10]; int arr[5]; decltype(arr) *fun(int i); typedef int arr[10]; arr* fun(int i);
第一种解读方式是先看fun(int i),这是一个函数,该函数有个返回值,函数返回值之后,值被用来组建新的表达式,形式为int (* value) [10],这样解读就可以明白了,这是一个函数声明,函数名是fun,形参是int,返回一个长度为10,元素为int类型的数组指针。
第二种是C++11的新的形式,称作尾置返回类型,其格式是auto代替原返回类型,接下来是函数名和形参列表,之后一个->符号,然后才是真正的返回类型。
第三种是使用decltype来推断类型,因为decltype推断除arr是个数组,而函数不允许直接返回数组,使用加上*来变成返回指针。
第四种是使用别名,同样地,函数不允许直接返回数组,使用加上*来变成返回指针。
第四种方法也可以用于数组形参的声明,但略繁琐。
接下来是函数的重载,在一个程序中,允许多个函数使用同一个名字,比如某个动作,属于同一名字,但是具体行为可能略有差异,使用同一个名字便于记忆和理解,这时就可以使用函数重载。对于重载的函数它们的要么是形参数量不同,要么是形参类型不同。函数重载时,对于顶层const的属性是忽略不计的,因此拥有顶层cosnt的形参不能和另一个没有顶层const形参区分开来。
但对于底层const是能够区分的,虽然有时能够相互转换,但是编译器会优先选择相对应的版本,也就是说非底层const实参优先匹配非const形参的函数。
重载的函数之间是存在作用域的,不同作用域的函数无法重载。例如,对下面的函数重载就有作用域的限制:
string read(); void print(const string &); void print(double); // 对print的重载 void fooBar(int ival) { // 新子作用域 bool read = false; // read覆盖了外层的read string s = read(); // 错误,read是个bool对象,不是函数 void print(int); // 新子作用域,覆盖了外面的print print("Value: "); // 错误,子作用域内只有一个接受int的print函数 print(ival); // 正确,与子作用域匹配 print(3.14); // 正确,与子作用域匹配,但是是经过了隐式的类型转换 }
从上面的例子可以看到,编译器是首先寻找对函数名的声明,一旦在一个作用域找到了,就不再去别的作用域寻找,剩下的就是检查调用是否有效。
还有一些情况是,对于有些函数,我们每次需要对形参使用一个默认的值,我们可以定义这样的函数,其形参包含一个默认值,如果传递了实参就用实参去初始化形参,如果没有传递,则使用默认值。其格式如下:
void fun(int i, int j = 10)
{
/* ... */
}
对于上面的函数,我们在定义形参j时给了一个默认值10,当我们调用上面的函数时,可以给形参j提供值,也可以不提供使用默认的值。
同前面提到过的类似,形参和实参的位置是一一对应的。
给形参提供默认值时,有个特殊的规定,那就是,一旦一个形参提供了默认值,那么该形参后面的其他形参也必须提供默认值。
另外,一个函数只能定义一次,但声明可以多次。函数形参的默认值也可以被放在声明中,既然允许多次声明,那么也就能多次给形参提供默认值,但是多次提供默认值给形参的要求是这些拥有不同默认值的形参不能在一个作用域内。换句话说,同一个形参在同一个作用域内不能多次被赋予默认值。不同的文件是属于不同的作用域,所以在不同的文件中,可以提供不同的形参默认值。
当一个函数规模较小,流程直接,又被频繁调用的时候,就可以被定义为内联函数。指定一个函数为内联函数,是使用inline关键词来指定。
对于inline修饰的函数是向编译器发出内嵌请求,但是编译器并不一定接受。
constexpr在C++11中感觉比较鸡肋,constexpr函数必须要返回类型和所有形参都是字面值类型,而且函数体只是一条return语句。在不要求用于初始化constexprt对象时,constexpr函数就是个普通函数。
在分离编译中,我们使用头文件保护(#ifndef)来防止重复包含头文件,另外,我们也可以使用assert和NDEBUG来选择性的执行调试代码。
assert是一种预处理的宏,该宏类似于函数调用,assert需要的是一个表达式,其格式如下:
assert (expression);
使用assert需要事先包含头文件cassert,assert的行为类似于if语句,只不过与if相反,当expression成立时,即为真,它什么都不做而已,而为假时,它直接终止程序并报错。
assert的行为是依赖一个名为NDEBUG的预处理变量。当定义了这个NDEBUG预处理变量,assert就被忽略了。默认情况下是没有定义NDEBUG的,所以assert生效。
我们可以在编译时,取消NDEBUG中的代码,方法是在编译时使用选项 -D NDEBUG 来取消。
另外,编译器还预定义了5个非常有用的预处理变量,具体如下:
__func__ // 当前函数名
__FILE__ // 当前文件名
__LINE__ // 当前代码行
__TIME__ // 当前时间
__DATE__ // 当前日期
它们的使用如下:
#include <iostream> using namespace std; int main(int argc, char const *argv[]) { cout << "current function name:" << __func__ << endl; cout << "current file name:" << __FILE__ << endl; cout << "current line number:" << __LINE__ << endl; cout << "current time:" << __TIME__ << endl; cout << "current data:" << __DATE__ << endl; return 0; }
执行结果如下
函数重载的匹配分为3个步骤,分别如下:
1.确定候选函数
2.确定可行函数
3.寻找最佳匹配
对于上面3个,其中第一步是确定所有的重载函数集合,集合中的函数称为候选函数。候选函数有几个特点,一是属于同一作用域,二是属于拥有相同的名字,三是在调用点可见。
当确定了候选函数后,就执行第二步,确定可行函数,步骤是检查形参与实参数量的匹配,以及检查形参和实参类型的匹配。
最后就是匹配最佳函数,匹配的原则是:实参和形参的类型越接近,则匹配的越好。如果在经过上面3个步骤之后,没有唯一一个函数脱颖而出,则编译器报错。
在实参和形参匹配的过程中,可能会进行实参到形参的转换,这个转换被划分为几个等级,具体如下:
1.精确匹配
2.const转换
3.类型提升
4.算术类型转换
5.类类型转换
对于函数指针,我们可以定义指向函数的指针,其格式类似于返回数组指针,只是数组指针后面跟的是数组长度,而函数指针跟的是形参列表,形式如下:
int (*fun(int i)) (int, int);
对比返回数组指针的函数:
int (*fun(int i)) [10];
可以看到,一个后跟数组长度,一个后跟函数形参列表。解读方式依旧,先看最内层fun(int i),这是一个函数,该函数有个返回值,函数返回值之后,值被用来组建新的表达式,形式为int (* value) ( int , int ),这样解读就可以明白了,这是一个函数声明,函数名是fun,形参是int,返回一个返回类型为int、形参是(int,int)类型的函数指针。
和数组一样,对于函数名的使用将自动转换成指针,例如下面的例子:
bool lengthCompare(const string &, const string &); // 声明一个返回值是bool,形参是2个const string引用的函数 bool *pf(const string &, const string &); // 声明一个返回值是bool,形参是2个const string引用的函数指针 pf = lengthCompare; // 令pf指向lengthCompare pf = &lengthCompare; // 等价的使用方法,取地址符可选
对函数指针的使用也类似于数组指针,可以直接使用而无需解引用,如下:
bool b1 = pf("hello", "goodbye"); // 直接调用lengthCompare bool b2 = (*pf)("hello", "goodbye"); // 解引用后调用lengthCompare bool b3 = lengthCompare("hello", "goodbye"); // 等价的调用
对于返回函数指针的函数,其方式和返回数组指针一样,也有如下几种方式
typedef int (*pf) (int, int); pf fun(int, int); int (*fun(int, int)) (int, int); auto fun(int, int) -> int (*) (int, int); int exam(int, int); decltype(exam) *fun(int, int);
第一种是别名
第二种是直接定义一个返回函数指针的函数
第三种是尾置返回类型
第四种是使用decltype,同样地,也需要显式的加上*来指定为指针类型。
练习6.1:实参和形参的区别是什么?
实参是形参的初始值,实参用于初始化形参
练习6.2:请指出下列函数哪个有错误,为什么?应该如何修改这些错误呢?
(a) int f() { string s; // ... return s; } (b) f2(int i) { /* ... */ } (c) int calc(int v1, int v1) { /* ... */ } (d) double square (double x) return x * x;
(a)返回值和返回类型不一致,应该将返回类型改为string
(b)缺少返回值,应该再函数名前加上void
(c)函数体错误,函数体是一对花括号括起来的块
(d)函数体错误,函数体是一对花括号括起来的块
练习6.3:编写你自己的fact函数,上机检查是否正确。
#include <iostream> using namespace std; void fact() { cout << "my fact function\n"; } int main(int argc, char const *argv[]) { fact(); return 0; }
练习6.4:编写一个与用户交互的函数,要求用户输入一个数字,计算生成该数字的阶乘。在main函数中调用该函数。
#include <iostream> using namespace std; int factorial() { cout << "intput a number:"; int i = 0, sum = 1; cin >> i; while (i > 0) { sum *= i--; } return sum; } int main(int argc, char const *argv[]) { cout << factorial() << endl; return 0; }
练习6.5:编写一个函数输出其实参的绝对值。
#include <iostream> using namespace std; void ABS(int i) { i >= 0 ? cout << i : cout << -i; } int main(int argc, char const *argv[]) { cout << "input a number:"; int i; cin >> i; ABS(i); cout << endl; return 0; }
练习6.6:说明形参、局部变量以及局部静态变量的区别。编写一个函数,同时用到这三种形式。
形参是函数的一部分,是自动创建的,属于局部变量.局部变量包括形参和函数体内定义的变量.局部静态变量则是定义在函数体内,但是一直到程序结束时才被销毁
#include <iostream> using namespace std; void fun(int i) { int sum = 1; sum += i; static int n = 10; sum *= n; } int main(int argc, char const *argv[]) { fun(7); return 0; }
练习6.7:编写一个函数,当它第一次被调用时返回0,以后每次被调用返回值加1。
#include <iostream> using namespace std; int fun() { static int n = 0; return n++; } int main(int argc, char const *argv[]) { fun(); return 0; }
练习6.8:编写一个名为Chapter6.h 的头文件,令其包含6.1节练习(第184页)中的函数声明。
// Chapter6.h
#ifndef CHAPTER6_H #define CHAPTER6_H int fact(int); #endif
练习6.9:编写你自己的fact.cc 和factMain.cc ,这两个文件都应该包含上一小节的练习中编写的 Chapter6.h 头文件。通过这些文件,理解你的编译器是如何支持分离式编译的。
//file fact.cc #include "Chapter6.h" int fact(int val) { return val; }
//file factMain.cc #include <iostream> #include "Chapter6.h" using namespace std; int main(int argc, char const *argv[]) { fact(10); return 0; }
练习6.10:编写一个函数,使用指针形参交换两个整数的值。在代码中调用该函数并输出交换后的结果,以此验证函数的正确性。
#include <iostream> #include "Chapter6.h" using namespace std; void SWAP(int *p1, int *p2) { int temp = *p1; *p1 = *p2; *p2 = temp; } int main(int argc, char const *argv[]) { int i = 22, j = 5; cout << "before swap:i=" << i << ",j=" << j << endl; SWAP(&i, &j); cout << "after swap:i=" << i << ",j=" << j << endl; return 0; }
练习6.11:编写并验证你自己的reset函数,使其作用于引用类型的参数。
#include <iostream> using namespace std; void reset(int &i) { i *= 2; } int main(int argc, char const *argv[]) { int j = 19; reset(j); cout << j << endl; return 0; }
练习6.12:改写6.2.1节中练习6.10(第188页)的程序,使其引用而非指针交换两个整数的值。你觉得哪种方法更易于使用呢?为什么?
#include <iostream> #include "Chapter6.h" using namespace std; void SWAP(int &r1, int &r2) { int temp = r1; r1 = r2; r2 = temp; } int main(int argc, char const *argv[]) { int i = 22, j = 5; cout << "before swap:i=" << i << ",j=" << j << endl; SWAP(i, j); cout << "after swap:i=" << i << ",j=" << j << endl; return 0; }
引用版本的方法更易于使用,方便,可读性强.
练习6.13:假设T是某种类型的名字,说明以下两个函数声明的区别:一个是void f(T), 另一个是 void f(&T)。
第一个是传值形参,第二个是传引用形参,传值形参需要拷贝,引用无需拷贝
练习6.14:举一个形参应该是引用类型的例子,再举一个形参不能是引用类型的例子。
当需要改变实参的值时,应该使用引用.当实参无法被引用形参绑定时,则不能是引用.例如:
#include <iostream> using namespace std; void fun1(int &i) { i *= 2; } void fun2(int &i) { /*...*/ } int main(int argc, char const *argv[]) { int j = 5; fun1(j); //需要改变j的值 fun2(5); //错误,此时无法绑定到字面值 return 0; }
练习6.15:说明find_char函数中的三个形参为什么是现在的类型,特别说明为什么s是常量引用而occurs是普通引用?为什么s和occurs是引用类型而c不是?如果令s是普通引用会发生什么情况?如果令occurs是常量引用会发生什么情况?
因为不需要也不允许改变s的值,同时为了避免拷贝s,所以s是const引用.occurs的值是可能被改变的,所以不能是const,另外occurs的值需要被保存,所以不可以使用局部变量,而要用引用.c的值不是引用则允许传递一个左值或者右值.如果s是普通引用,那么s可能会被改变.如果occurs是const引用,则程序无法改变字符出现的顺序.
练习6.16:下面的这个函数虽然合法,但是不算特别有用。指出它的局限性并设法改善。
bool is_empty(string& s) { return s.empty(); }
形参是非const引用,只能绑定string的非常量对象,无法绑定const对象或者字符串字面值.应该将形参修改为const引用
练习6.17:编写一个函数,判断string对象中是否含有大写字母。编写另一个函数,把string对象全都改写成小写形式。在这两个函数中你使用的形参类型相同吗?为什么?
#include <iostream> using namespace std; bool has_upper(const string &s) { for (auto c : s) { if (isupper(c)) return true; } return false; } string& to_lower(string &s) { for (auto &c : s) c = tolower(c); return s; } int main(int argc, char const *argv[]) { return 0; }
不相同,因为一个不会改变实参,一个需要改变实参
练习6.18:为下面的函数编写函数声明,从给定的名字中推测函数具备的功能。
(a) 名为 compare 的函数,返回布尔值,两个参数都是 matrix 类的引用。
(b) 名为 change_val 的函数,返回vector<int>的迭代器,有两个参数:一个是int,另一个是vector<int>的迭代器。
(a) bool compare(const matrix &m1, const matrix &m2); (b) vector<int>::iterator change_val(const int i, vector<int>::iterator it);
练习6.19:假定有如下声明,判断哪个调用合法、哪个调用不合法。对于不合法的函数调用,说明原因。
double calc(double); int count(const string &, char); int sum(vector<int>::iterator, vector<int>::iterator, int); vector<int> vec(10); (a) calc(23.4, 55.1); (b) count("abcda", 'a'); (c) calc(66); (d) sum(vec.begin(), vec.end(), 3.8);
(a)不合法,参数数量不匹配
(b)合法
(c)合法
(d)合法
练习6.20:引用形参什么时候应该是常量引用?如果形参应该是常量引用,而我们将其设为了普通引用,会发生什么情况?
当不需要改变实参的值或者是个字面值类型时应该是常量引用.如果改为普通引用,则只能绑定非const实参,且实参需要是左值.
练习6.21:编写一个函数,令其接受两个参数:一个是int型的数,另一个是int指针。函数比较int的值和指针所指的值,返回较大的那个。在该函数中指针的类型应该是什么?
#include <iostream> using namespace std; int fun(int i, int * const p) { if (p == 0) throw; (*p > i) ? *p : i; } int main(int argc, char const *argv[]) { return 0; }
指针的类型应该是const int类型.
练习6.22:编写一个函数,令其交换两个int指针。
#include <iostream> using namespace std; void fun(int *p1, int *p2) { int temp = *p1; *p1 = *p2; *p2 = temp; } int main(int argc, char const *argv[]) { return 0; }
练习6.23:参考本节介绍的几个print函数,根据理解编写你自己的版本。依次调用每个函数使其输入下面定义的i和j:
int i = 0, j[2] = { 0, 1 };
#include <iostream> using namespace std; void print(const int i) { cout << i; } void print(const int *b, const int *e) { while (b != e) cout << *b++ << '\t'; } int main(int argc, char const *argv[]) { int i = 0; int j[2] = {0, 1}; print(i); cout << endl; print(begin(j), end(j)); cout << endl; return 0; }
练习6.24:描述下面这个函数的行为。如果代码中存在问题,请指出并改正。
void print(const int ia[10]) { for (size_t i = 0; i != 10; ++i) cout << ia[i] << endl; }
该函数遍历形参传入的数组,缺陷是函数的形参并不能直接传入数组,数组传入形参时是转换成指针进行处理的,转换成指针时,数组维度信息丢失,也即不检查数组的长度.
#include <iostream> using namespace std; void print(const int ia[], const int size) { for (size_t i = 0; i != size; ++i) cout << ia[i] << endl; } int main(int argc, char const *argv[]) { return 0; }
练习6.25:编写一个main函数,令其接受两个实参。把实参的内容连接成一个string对象并输出出来。
#include <iostream> using namespace std; int main(int argc, char const *argv[]) { string s = argv[1]; s += argv[2]; cout << s << endl; return 0; }
练习6.26:编写一个程序,使其接受本节所示的选项;输出传递给main函数的实参的内容。
#include <iostream> using namespace std; int main(int argc, char const *argv[]) { for (int i = 1; i != argc; ++i) cout << argv[i] << '\t'; return 0; }
练习6.27:编写一个函数,它的参数是initializer_list<int>类型的对象,函数的功能是计算列表中所有元素的和。
#include <iostream> using namespace std; int fun(const initializer_list<int> &i_list) { int sum = 0; for (auto i : i_list) sum += i; return sum; } int main(int argc, char const *argv[]) { cout << fun({1, 2, 3, 4, 5, 6, 7, 8, 9}) << endl; return 0; }
练习6.28:在error_msg函数的第二个版本中包含ErrCode类型的参数,其中循环内的elem是什么类型?
const string类型
练习6.29:在范围for循环中使用initializer_list对象时,应该将循环控制变量声明成引用类型吗?为什么?
应该,因为initializer_list对象中的元素是常量.
练习6.30:编译第200页的str_subrange函数,看看你的编译器是如何处理函数中的错误的。
error: non-void function 'str_subrange' should return a value
练习6.31:什么情况下返回的引用无效?什么情况下返回常量的引用无效?
当返回的是局部变量的引用时无效,当需要修改变量时,返回常量引用无效.
练习6.32:下面的函数合法吗?如果合法,说明其功能;如果不合法,修改其中的错误并解释原因。
int &get(int *array, int index) { return array[index]; } int main() { int ia[10]; for (int i = 0; i != 10; ++i) get(ia, i) = i; }
合法,因为返回的引用是对指针解引用后的值的引用,解引用后的值是非局部变量.
练习6.33:编写一个递归函数,输出vector对象的内容。
#include <iostream> #include <vector> using namespace std; void fun(vector<int>::iterator b, vector<int>::iterator e) { if (b == e) return; cout << *b << '\t'; return fun(++b, e); } int main(int argc, char const *argv[]) { return 0; }
练习6.34:如果factorial 函数的停止条件如下所示,将发生什么情况?
if (val != 0)
因为负数没有阶乘,所以传入负数时,将产生未定义行为.
练习6.35:在调用factorial 函数时,为什么我们传入的值是 val-1 而非 val--?
如果是val--,将始终使用val的值去调用递归函数,这样造成死循环.
练习6.36:编写一个函数的声明,使其返回数组的引用并且该数组包含10个string对象。不用使用尾置返回类型、decltype或者类型别名。
string (&fun()) [10];
练习6.37:为上一题的函数再写三个声明,一个使用类型别名,另一个使用尾置返回类型,最后一个使用decltype关键字。你觉得哪种形式最好?为什么?
using ref_arr = string (&)[10]; ref_arr fun(); auto fun() -> string (&)[10]; string arr[10]; decltype(arr) & fun();
尾置返回形式最好,易于书写和阅读.
练习6.38:修改arrPtr函数,使其返回数组的引用。
decltype(odd) & arrPtr(int i)
练习6.39:说明在下面的每组声明中第二条语句是何含义。如果有非法的声明,请指出来。
(a) int calc(int, int); int calc(const int, const int); (b) int get(); double get(); (c) int *reset(int *); double *reset(double *);
(a)非法,顶层const无法与没有顶层const的形参区分开来
(b)非法,函数重载不通过返回值来区分
(c)合法
练习6.40:下面的哪个声明是错误的?为什么?
(a) int ff(int a, int b = 0, int c = 0); (b) char *init(int ht = 24, int wd, char bckgrnd);
(b)的声明是错误的,因为一旦声明一个带默认值的形参,则其后面声明的形参都必须带有默认值.
练习6.41:下面的哪个调用是非法的?为什么?哪个调用虽然合法但显然与程序员的初衷不符?为什么?
char *init(int ht, int wd = 80, char bckgrnd = ' '); (a) init(); (b) init(24, 10); (c) init(14, '*');
(a)调用非法,因为参数数量没有匹配成功.
(c)与程序的初衷不符,第二个实参将不会与第三个形参结合,而是与第二个形参进行自动提升转换后结合.
练习6.42:给make_plural函数(参见6.3.2节,第201页)的第二个形参赋予默认实参's', 利用新版本的函数输出单词success和failure的单数和复数形式。
#include <iostream> using namespace std; string make_plural(size_t ctr, const string &word, const string &ending = "s") { return (ctr > 1) ? word + ending : word; } int main(int argc, char const *argv[]) { cout << make_plural(1, "success") << endl; cout << make_plural(5, "success", "es") << endl; cout << make_plural(1, "failure") << endl; cout << make_plural(5, "failure", "") << endl; return 0; }
练习6.43:你会把下面的哪个声明和定义放在头文件中?哪个放在源文件中?为什么?
(a) inline bool eq(const BigInt&, const BigInt&) {...} (b) void putValues(int *arr, int size);
(a)将声明和定义都放在头文件中,因为内联函数允许在程序中多次定义,并且仅有函数头声明是不够的,还需要函数的定义
(b)放在头文件和源文件中.因为可以让编译器检查函数的声明和定义是否一致
练习6.44:将6.2.2节(第189页)的isShorter函数改写成内联函数。
inline bool isShorter(const string &s1, const string &s2) { return s1.size() < s2.size(); }
练习6.45:回顾在前面的练习中你编写的那些函数,它们应该是内联函数吗?如果是,将它们改写成内联函数;如果不是,说明原因。
视情况而定,对于频繁调用,流程直接切规模较小的函数则应该改写成内联函数.
练习6.46:能把isShorter函数定义成constexpr函数吗?如果能,将它改写成constexpr函数;如果不能,说明原因。
可以,在不要求常量表达式的上下文中时,可以定义成constexpr.
练习6.47:改写6.3.2节(第205页)练习中使用递归输出vector内容的程序,使其有条件地输出与执行过程有关的信息。例如,每次调用时输出vector对象的大小。分别在打开和关闭调试器的情况下编译并执行这个程序。
#include <iostream> #include <vector> using namespace std; void fun(vector<int>::iterator b, vector<int>::iterator e) { if (b == e) return; #ifndef NDEBUG cout << "vector size:" << e - b << endl; #endif cout << *b << '\t'; return fun(++b, e); } int main(int argc, char const *argv[]) { vector<int> v{1, 2, 3, 4, 5, 6, 7, 8, 9}; fun(v.begin(), v.end()); cout << endl; return 0; }
练习6.48:说明下面这个循环的含义,它对assert的使用合理吗?
string s; while (cin >> s && s != sought) { } //空函数体 assert(cin);
合理,因为while循环退出的条件是cin输入流状态不正常,或者输入的string对象与sought相等.当执行到assert时,要么是cin状态异常,要么是s==sought,使用assert来判断cin是否正常以决定是否退出程序.
练习6.49:什么是候选函数?什么是可行函数?
候选函数就是在一次函数调用中,确定与被调函数同名且作用域有效的函数集合.可行函数则是在候选函数中选出与被调函数的参数数量和类型一致的函数集合.
练习6.50:已知有第217页对函数f的声明,对于下面的每一个调用列出可行函数。其中哪个函数是最佳匹配?如果调用不合法,是因为没有可匹配的函数还是因为调用具有二义性?
(a) f(2.56, 42) (b) f(42) (c) f(42, 0) (d) f(2.56, 3.14)
(a)
void f(int,int)
void f(double,double=3.14)
没有最佳匹配,存在二义性
(b)
void f(int)
void f(double,double=3.14)
第一个是最佳匹配
(c)
void f(int,int)
void f(double,double=3.14)
第一个是最佳匹配
(d)
void f(int,int)
void f(double,double=3.14)
第二个是最佳匹配
练习6.51:编写函数f的4个版本,令其各输出一条可以区分的消息。验证上一个练习的答案,如果你回答错了,反复研究本节的内容直到你弄清自己错在何处。
#include <iostream> #include <vector> using namespace std; void f() { cout << "f1" << endl; } void f(int) { cout << "f2" << endl; } void f(int, int) { cout << "f3" << endl; } void f(double, double = 3.14) { cout << "f4" << endl; } int main(int argc, char const *argv[]) { f(2.56, 42); f(42); f(42, 0); f(2.56, 3.14); return 0; }
练习6.52:已知有如下声明:
void manip(int , int); double dobj;
请指出下列调用中每个类型转换的等级(参见6.6.1节,第219页)。
(a) manip('a', 'z'); (b) manip(55.4, dobj);
(a)(b)都将实参转换为形参类型.(a)是第3等级,(b)是第4等级
练习6.53:说明下列每组声明中的第二条语句会产生什么影响,并指出哪些不合法(如果有的话)。
(a) int calc(int&, int&); int calc(const int&, const int&); (b) int calc(char*, char*); int calc(const char*, const char*); (c) int calc(char*, char*); int calc(char* const, char* const);
(a)(b)第二条都与前一条形成重载
(c)不合法,重载无法区分顶层const
练习6.54:编写函数的声明,令其接受两个int形参并且返回类型也是int;然后声明一个vector对象,令其元素是指向该函数的指针。
#include <iostream> #include <vector> using namespace std; int fun(int, int); vector<int(*)(int, int)> v; int main(int argc, char const *argv[]) { return 0; }
练习6.55:编写4个函数,分别对两个int值执行加、减、乘、除运算;在上一题创建的vector对象中保存指向这些函数的指针。
#include <iostream> #include <cassert> #include <vector> using namespace std; int fun1(int i, int j) { return i + j; } int fun2(int i, int j) { return i - j; } int fun3(int i, int j) { return i * j; } int fun4(int i, int j) { assert(j != 0); return i / j; } int main(int argc, char const *argv[]) { vector<int (*)(int, int)> v; v.push_back(fun1); v.push_back(fun2); v.push_back(fun3); v.push_back(fun4); return 0; }
练习6.56:调用上述vector对象中的每个元素并输出结果。
#include <iostream> #include <cassert> #include <vector> using namespace std; int fun1(int i, int j) { return i + j; } int fun2(int i, int j) { return i - j; } int fun3(int i, int j) { return i * j; } int fun4(int i, int j) { assert(j != 0); return i / j; } int main(int argc, char const *argv[]) { vector<int (*)(int, int)> v; v.push_back(fun1); v.push_back(fun2); v.push_back(fun3); v.push_back(fun4); for (auto i : v) { cout << i(25, 5) << '\t'; } cout << endl; return 0; }