[C++ primer] 第六章习题答案
6.1
实参时形参的初始值。
形参是定义在函数参数列表中的局部变量, 等待被实参初始化。
实参是调用函数时给形参赋予的初始值。
6.2
(a)返回类型不匹配
(b)函数没有标明返回类型
(c)形参命名冲突
(d)函数体必须有{}
6.4
#include <iostream>
#include <stdexcept>
#include <vector>
using namespace std;
int func(int num)
{
if (num == 1 || num == 0)
return 1;
else
return num * func(num - 1);
}
int main()
{
int a;
cin >> a;
cin.ignore();
int res = func(a);
cout << res << endl;
}
6.5
#include <iostream>
#include <stdexcept>
#include <vector>
using namespace std;
int func(int num)
{
return abs(num);
}
int main()
{
int a;
cin >> a;
cin.ignore();
int res = func(a);
cout << res << endl;
}
6.6
形参是定义在函数参数列表中的局部变量,等待被实参初始化
局部变量仅在函数的作用域内可见,还会隐藏在外层作用域中同名的其他所有声明中。形参和函数体内部定义的变量统称为局部变量。(仅在块内有效)
局部静态变量在函数的执行路径第一次经过对象定义语句时初始化,直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。可以将局部变量定义成static类型获得。
6.8
建议变量和函数皆在头文件中声明,在源文件中定义。
函数声明时不需要函数体,用一个分号代替即可。形参也可以省略(建议保留,增加可读性)
6.10
每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。
当实参拷贝给形参是,他们之间还是独立的对象,此过程被称为值传递。(改变形参不会影响到实参)
当形参是引用类型时,它将绑定到对应的实参上。被称为引用传递。
在c++中 建议使用引用类型的形参替代指针。
#include <iostream>
#include <stdexcept>
#include <vector>
using namespace std;
void func(int *p, int *q)
{
int temp = *p;
*p = *q;
*q = temp;
}
int main()
{
int a = 1, b = 2;
func(&a, &b);
cout << a << b << endl;
}
6.12
如果函数无须改变引用参数的值,最好将其声明为常量引用
可以使用引用避免拷贝
#include <iostream>
#include <stdexcept>
#include <vector>
using namespace std;
void func(int &p, int &q)
{
int temp = p;
p = q;
q = temp;
}
int main()
{
int a = 1, b = 2;
func(a, b);
cout << a << b << endl;
}
6.13
void f(T),在传参时,会把T类型的变量整个复制一份,此时对T的修改不会造成实参改变
void f(&T),在传参时,只会传递类型为T的实参的引用,可以对实参进行修改
6.15
find_char函数不应该在函数内部修改s的值,只读不写,因此该参数应该使用常量引用。
需要在函数内部修改occurs的值,所以是普通引用
c不是引用类型是根据函数的用途决定的,如果c是引用,则用户在调用find_char(s, ‘o’, ctr)时会出错,因为不能用一个字面值去初始化非常量引用。
当使用实参初始化形参时会忽略掉顶层const。当形参有顶层const时,传给他常量对象或者非常量对象都是可以的
指向常量的指针不能用于修改所指向对象的值(自以为是性)
要想指向常量对象的地址,只能使用指向常量的指针
尽量使用常量引用
6.16
如果实参是const string类型,则会报错——不能把普通引用绑定到const对象上
6.17
#include <iostream>
#include <stdexcept>
#include <vector>
#include <string>
using namespace std;
bool hasUpper(const string &s)
{
for (string::size_type i = 0; i != s.size(); i++)
{
if (isupper(s[i]))
return true;
}
return false;
}
void toLower(string &s)
{
for (string::size_type i = 0; i != s.size(); i++)
{
if (isupper(s[i]))
s[i] = tolower(s[i]);
}
}
int main()
{
string s = "KjdhuhjhksdjhkkkJHKJH";
cout << hasUpper(s) << endl;
toLower(s);
cout << s << endl;
}
6.18
(a) bool compare(const matrix &A, const matrix &B);
(b) vector<int>::iterator change_val(int num, vector<int>::iterator res);
6.19
(a)不合法,calc函数只有一个参数
(b)(c)(d)合法
6.20
当函数不会改变参数值的时候,应该将形参设为常量引用。若其该为常量引用,而我们将其设为普通引用,当函数内部改变其值,将不会报错,引发不必要的错误
6.21
数组形参:
不允许拷贝数组,使用数组时通常会将其转换成指针,——无法以值传递的方式使用数组参数,当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针
由于数组是以指针的形式传递给函数的,所以函数不知道数组的大小,指针就可能会发生越界问题。有三种方法可以解决这一点问题。
1. 使用标记指定数组长度:要求数组本身包含一个结束标记(比如C风格字符串,函数在遇到空字符停止)。适用于有明显结束标记且标记不会与普通数据混淆的情况
2. 使用标准库规范:传递指向数组首元素和尾后元素的指针。
3. 显式传递一个表示数组大小的形参:专门定义一个表示数组大小的形参。
#include <iostream>
#include <stdexcept>
#include <vector>
#include <string>
using namespace std;
int func(int *p, int val)
{
return (*p > val) ? *p : val;
}
int main()
{
int a = 4, b = 7;
cout << func(&a, b) << endl;
}
6.22
#include <iostream>
#include <stdexcept>
#include <vector>
#include <string>
using namespace std;
void func(int *&p, int *&q)
{
int *temp = p;
p = q;
q = temp;
}
int main()
{
int a = 4, b = 7;
int *p = &a, *q = &b;
func(p, q);
cout << *p << *q << endl;
}
6.24
不能用传值的方式传递数组。
应该定义为:
void print(const int (&ia)[10])
6.25
使用argv中的实参时,一定要记得可选参数从argv[1]开始,argv[0]保存的是程序的名字。
#include <iostream>
#include <stdexcept>
#include <vector>
#include <string>
using namespace std;
int main(int argc, char **argv)
{
string res = "";
for (int i = 1; i < argc; i++)
{
res += argv[i];
res += " ";
}
cout << res << endl;
return 0;
}
6.27
如果函数的实参数量未知但所有实参的类型都相同:使用initializer_list类型的形参
initializer_list中的元素永远是const类型
含有initializer_list形参的函数也可以同时拥有其他形参
省略符形参是为了便于C++程序访问某些特殊的C代码设置的。应该仅仅用于C和C++通用的类型。(注意:大多数类类型的对象在传递给省略符形参时都无法正确拷贝)
省略符形参只能出现在形参列表的最后一个位置
省略符形参所对应的实参无需类型检查
#include <iostream>
#include <stdexcept>
#include <vector>
#include <string>
using namespace std;
int func(initializer_list<int> num)
{
int sum = 0;
for (auto i = num.begin(); i != num.end(); i++)
{
sum += *i;
}
return sum;
}
int main(int argc, char **argv)
{
cout << func({ 1, 2, 3, 1 }) << endl;
}
6.28
const string &
6.29
在范围for循环中使用initializer_list对象时,应该将循环变量声明成引用类型。这是因为考虑到复制实参的问题,声明成引用,可以避免复制实参,提高了效率。
6.30
return语句终止当前正在执行的函数并将控制权返回到调用该函数的地方
return语句的返回值类型必须与函数的返回类型相同,或者能隐式得转换成函数的返回类型
函数返回的值用于初始化调用点的一个临时量,该临时量是函数调用的结果
如果函数返回引用,则该引用仅是它所引对象的一个别名
不要返回局部对象的引用或指针(函数完成后,它所占用的存储空间也随之被释放掉)
调用运算符的优先级和点运算符、箭头运算符的优先级一样
函数的返回类型决定函数调用是否是左值:引用返回左值,其它类型返回右值
如果返回类型是常量引用,则不能对返回类型进行赋值
函数可以返回花括号包围的值的列表,如果列表为空,临时量执行值初始化,否则返回的值由函数的返回类型决定
允许main函数没有return 语句直接结束。(编译器将隐式地插入一个return 0)
递归时,main函数不能调用自己
严重性 代码 说明
错误 C2561 “func”: 函数必须返回值
6.31
返回局部引用时无效,返回局部定义的常量引用无效。要确保返回的引用有效,就要确定引用所返回的是在函数之前已经存在的某个对象。
6.32
合法,其功能是从0递增,初始化一个数组。
6.33
#include <iostream>
#include <stdexcept>
#include <vector>
#include <string>
using namespace std;
void func(vector<int>::const_iterator beg, vector<int>::const_iterator end)
{
if (beg != end)
{
cout << *beg++ << endl;
func(beg, end);
}
}
int main(int argc, char **argv)
{
vector<int> temp = { 0, 1, 2, 3 };
func(temp.begin(), temp.end());
}
6.34
如果传入的val < 0,则递归永远不会停止
6.35
val – 返回的是val的值,相当于又把val当作参数传递,递归将永远不会停止,并且第一次递归不断重复执行。
6.36
因为数组不能被拷贝,所以函数不能返回数组,不过函数可以返回数组的指针或引用。可以使用类型别名简化过程
- 声明一个返回数组指针的函数:数组的维度跟在形参列表之后
Type (*function(parameter_list))[dimension]
- 使用尾置返回类型:任何函数的定义都可以使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效。
auto func(int i) -> int(*)[10]//返回一个指针,该指针指向含有10个整数的数组
- 使用decltype:已知函数的返回值时,可以使用关键字decltype表示返回类型为指针
string (&func(string (&str)[10]))[10]
6.37
(1) typedef string arr[10];
using arr = string[10];//c++11
arr& func(arr& str);
(2) auto func(string (&str)[10]) -> string(&)[10];
(3) string temp[10];
decltype(temp)& func(decltype(temp)& str);
6.39
如果同一作用域内几个函数名字相同但形参列表不同,我们称之为重载函数。
main函数不能重载, 不允许两个函数除了返回类型外其他所有要素都相同
顶层const不影响传入函数的对象,一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开
如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载
调用重载函数时有三种可能的结果:
- 编译器找到一个与实参最佳匹配的函数,生成调用该函数的代码
- 找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配的错误信息
- 有多余一个函数可以匹配,但每一个都不是明显的最佳选择,此时也将发生错误 (二义性调用)
不要把函数声明放在局部作用域内
(a)不合法。属于顶层const形参,第二行是重复声明,与第一行含义相同。
(b)不合法。 不允许两个函数除了返回类型外其他所有要素都相同
(c)合法。参数类型不同,属于函数重载。
6.40
默认实参
调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。
一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值
使用默认实参时,只要在调用函数时省略该形参就可以了。
在给定的作用域中一个形参只能被赋予一次默认实参,函数的后续声明只能为之前没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须有默认值
局部变量不能作为默认实参
(a)正确
(b)错误,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值
6.41
(a)非法,第一个参数无默认值,应该初始化赋值。
(b)(c)合法
6.42
#include <iostream>
#include <stdexcept>
#include <vector>
#include <string>
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 **argv)
{
string str1 = "hello";
string str2 = "world";
cout << make_plural(2, str1) << endl;
cout << make_plural(1, str2) << endl;
}
6.43
内联函数
内联函数可以避免函数调用时的开销,在函数返回类型前面加上关键字inline就可以将其声明为内联函数
内联机制用于优化规模较小,流程直接,频繁调用的函数,很多编译器都不支持内联递归函数
constexpr函数
constexpr函数是指能用于常量表达式的函数。其中函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return 语句。
constexpr函数的返回值不一定是一个常量,跟调用类型有关
nline函数和constexpr函数可以在函数中多次定义,但是通常将其定义在头文件中。(多个定义必须完全一致)
(a)内联函数通常定义在头文件中
(b)普通函数的声明,一般也放在头文件中
6.44
inline bool isShorter(const string &s1, const string &s2)
{
return s1.size() < s2.size();
}
6.45
练习题中的函数短小的,应该被定义成内联函数。改写为内联函数只需要在函数声明前加inline关键字就可以。
6.46
不可以,参数是string类型,不是字面值。
6.47
程序有时会包含一些用于调试的代码,但是这些代码只在开发程序时使用,当应用程序编写完成准备发布时,要先屏蔽掉调试代码,此时用到两种预处理功能:assert和NDEBUG
assert预处理宏:assert(expr):包含一个表达式,expr为真时,assert什么也不做,为假时输出信息并终止程序。包含在cassert头文件中。通常用于检查不能发生的条件
NDEBUG预处理变量:assert依赖于一个NDEBUG的预处理变量的状态,如果定义了NDEBUG,assert什么也不做,默认状态下NDEBUG是未定义的。编译器也可以预先定义该变量。
可以使用#define NDEBUG来关闭调试状态
也可以使用NDEBUG编写自己的条件调试代码。如果NDEBUG未定义,将执行#ifndef到#endif之间的代码,如果定义了NDEBUG,这些代码将被忽略。
用于调试:
_ func _ :一个静态数组,存放函数的名字
_ FILE _ :存放文件名的字符串字面值
_ LINE _ :存放当前行号的整形字面值
_ TIME _ :存放文件编译时间的字符串字面值
_ DATE _ :存放文件编译日期的字符串字面值
#include <iostream>
#include <stdexcept>
#include <vector>
#include <string>
using namespace std;
#define NDEBUG
void func(vector<int>::const_iterator beg, vector<int>::const_iterator end)
{
#ifndef NDEBUG
cerr << __func__ << endl;
#endif // !
if (beg != end)
{
cout << *beg++ << endl;
func(beg, end);
}
}
int main(int argc, char **argv)
{
vector<int> temp = { 0, 1, 2, 3 };
func(temp.begin(), temp.end());
}
6.48
不合理,函数的意义是让用户进行输入,直到输入的字符串是sought是停止。因此assert (cin)一直为真,这条语句也就没有意义。可以改为:assert ( s != sought)
6.49
函数匹配的第一步是选定本次调用对应的重载函数集。集合中的函数被称为候选函数,候选函数有两个特征:一是与被调用的函数同名,二是其声明在调用点可见。
第二步是考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,称为可行函数。可行函数有两个特征:一是形参数量与本次提供的实参数量相同,二是每个实参的类型与对应形参类型相同,或能转换成形参的类型。
第三步是考察实参类型是否与形参匹配。
(如果没找到可行函数,编译器将报告无匹配函数的错误)
实参类型与形参类型越接近,匹配的越好
注意避免二义性调用
6.50
(a)不合法,有二义性调用
(b)合法,void f(int)
(c)合法, void f(int, int)
(d)合法,void f(double, double = 3.14)
6.52
为了确定最佳匹配,编译器将实参类型到形参类型的转换划分为几个等级,具体排序:
- 精确匹配
- 通过const转换实现的匹配
- 通过类型提升实现的匹配
- 通过算术类型转换或者指针转换实现的匹配
- 通过类类型转换实现的匹配
函数匹配和const形参:
实参是const -> 形参是const的函数
实参非const -> 形参非const的函数
(a)类型提升
(b)算术类型转换
6.53
(a)合法,会根据传入的实参是否时const类型决定使用哪个函数。
(b)合法,会根据传入的实参是否时const类型决定使用哪个函数。
(c)非法,与第一行含义相同,属于重复定义。
6.54
要想声明一个可以指向函数的指针,只需要用指针替换函数名即可。
当我们把函数名作为一个值使用时,该函数自动地转换成指针。
此外,还可以直接使用只想函数地指针调用该函数,无须提前解引用指针。
在指向不同函数类型地指针间不存在转换规则。但可以为指针赋一个nullptr或0,表示指针没有指向任何一个函数
当我们使用重载函数时,上下文必须清晰地界定到底应该选用哪个函数。编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中地某一个精确匹配。
形参不可以是函数类型,但形参可以是指向函数的指针。(会将函数形参转换成指向函数的指针)
虽然不能返回一个函数,但是可以返回一个指向函数的指针。
using func1 = int(int, int);
vector<func1*> fvec1;
int func(int, int);
typedef decltype(func) *func2;
vector<func2> fvec2;
using func3 = int(*)(int, int);
vector<func3> fvec3;
6.56
#include <iostream>
#include <stdexcept>
#include <vector>
#include <string>
using namespace std;
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return (a - b); }
int multiply(int a, int b) { return a * b; }
int divide(int a, int b) { return b != 0 ? a / b : 0; }
int main()
{
typedef int(*p) (int, int);
vector<p> vec{ add, sub,multiply, divide };
for (auto f : vec) {
cout << f(1, 1) << endl;
}
getchar();
}