欢迎关注WX公众号:【程序员管小亮】
专栏C++学习笔记
《C++ Primer》学习笔记/习题答案 总目录
——————————————————————————————————————————————————————
📚💻 Cpp-Prime5 + Cpp-Primer-Plus6 源代码和课后题
第6章 - C++模块设计——函数
练习6.1
实参和形参的区别的什么?
解:
形参出现在函数定义的地方,形参列表可以包含0个,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
,而函数的返回值类型是 int
,二者不一致且不能自动转换。
修改后的程序是:
string f() {
string s;
// ...
return s;
}
(b)是错误的,因为函数缺少返回值类型。如果该函数确实不需要返回任何值,则程序应该修改为:
void f2(int i) { /* ... */ }
©是错误的,同一个函数如果含有多个形参,则这些形参的名字不能重复;另外,函数体左侧的花括号缺失了。
修改后的程序应该是:
int calc(int v1, int v2) { /* ... */ return ; }
(d)是错误的,因为函数体必须放在一对花括号内。
修改后的程序是:
double square (double x) { return x * x; }
练习6.3
编写你自己的 fact
函数,上机检查是否正确。注:阶乘。
解:
int fact(int i)
{
if (i < 0)
return -1;
int sum = 0;
sum = i > 1 ? i * fact(i - 1) : 1;
return sum;
}
练习6.4
编写一个与用户交互的函数,要求用户输入一个数字,计算生成该数字的阶乘。在 main
函数中调用该函数。
解:
#include <iostream>
using namespace std;
int fact(int i)
{
if (i < 0)
return -1;
int sum = 0;
sum = i > 1 ? i * fact(i - 1) : 1;
return sum;
}
int main()
{
int val, sum = 0;
cin >> val;
cout << val << " 的阶乘是 " << fact(val) << endl;
system("pause");
return 0;
}
练习6.5
编写一个函数输出其实参的绝对值。
解:
根据参数类型的不同,我们可以分别求整数的绝对值和浮点数的绝对值。从通用性的角度出发,不妨设定参数类型是双精度浮点数 double
。
#include <iostream>
#include <cmath>
using namespace std;
double myABS(double val)
{
if (val < 0)
return val * -1;
else
return val;
}
double sysABS(double val)
{
return abs(val);
}
int main()
{
double num;
cout << "请输入一个数:";
cin >> num;
cout << num << " 的 myABS 绝对值是 " << myABS(num) << endl;
cout << num << " 的 sysABS 绝对值是 " << sysABS(num) << endl;
system("pause");
return 0;
}
myABS
函数使用 if-else
分支语句判断实参是正数还是负数,从而计算实参的绝对值。sysABS
函数直接调用 cmath
头文件的 abs
函数实现同样的功能。
练习6.6
说明形参、局部变量以及局部静态变量的区别。编写一个函数,同时达到这三种形式。
解:
形参和定义在函数体内部的变量统称局部变量,它们对函数而言是局部的,仅在函数的作用域内可见,函数体内的局部变量又分为普通局部变量和静态局部变量。
对于形参和普通局部变量而言,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁它。我们把只存在于块执行期间的对象称为自动对象。
这几个概念的区别是:
- 形参是一种自动对象,函数开始时为形参申请内存空间,我们调用函数时提供的实参初始化形参对应的自动对象。
- 普通变量对应的自动对象也容易理解,我们在定义该变量的语句处创建自动对象,如果定义语句提供了初始值,则用该值初始化;否则,执行默认初始化。当该变量所在的块结束后,变量失效。
- 局部静态变量比较特殊,它的生命周期贯穿函数调用及之后的时间。局部静态变量对应的对象称为局部静态对象,它的生命周期从定义语句处开始,直到程序结束才终止。
下面的程序同时使用了形参、普通局部变量和静态局部变量:
#include <iostream>
using namespace std;
double myAdd(double val1, double val2){ // val1和val2是形参
double result = val1 + val2; // result是普通局部变量
static unsigned iCnt = 0; // iCnt是静态局部变量
++iCnt;
cout << "该函数已经累计执行了" << iCnt << "次" << endl;
return result;
}
int main(){
double num1, num2;
cout << "请输入两个数:";
while (cin >> num1 >> num2)
{
cout << num1 << "与" << num2 << "的求和结果是:"
<< myAdd(num1, num2) << endl;
}
system("pause");
return 0;
}
练习6.7
编写一个函数,当它第一次被调用时返回0,以后每次被调用返回值加1。
解:
#include <iostream>
using namespace std;
unsigned myCnt(){
static unsigned iCnt = -1;
++iCnt;
return iCnt;
}
int main(){
cout << "请输入任意字符后按回车键继续" << endl;
char ch;
while (cin >> ch)
{
cout << "函数myCnt()执行的次数是:" << myCnt() << endl;
}
system("pause");
return 0;
}
练习6.8
编写一个名为 Chapter6.h
的头文件,令其包含6.1节练习中的函数声明。
解:
#ifndef CHAPTER6_H_INCLUDED
#define CHAPTER6_H_INCLUDED
int fact(int);
double myABS(double);
double sysABS(double);
#endif // CHAPTER6_H_INCLUDED
练习6.9 : fact.cc | factMain.cc
编写你自己的 fact.cc
和 factMain.cc
,这两个文件都应该包含上一小节的练习中编写的 Chapter6.h
头文件。通过这些文件,理解你的编译器是如何支持分离式编译的。
解:
fact.cc
:
#include "Chapter6.h"
using namespace std;
int fact(int val)
{
if (val < 0)
return -1;
int ret = 1;
for (int i = 1; i != val; ++i){
ret *= i;
}
return ret;
}
factMain.cc
:
#include <iostream>
#include "Chapter6.h"
using namespace std;
int main()
{
int num;
cout << "请输入一个数:";
cin >> num;
cout << num << " 的阶乘是:" << fact(num) << endl;
system("pause");
return 0;
}
练习6.10
编写一个函数,使用指针形参交换两个整数的值。
在代码中调用该函数并输出交换后的结果,以此验证函数的正确性。
解:
#include <iostream>
using namespace std;
// 在函数体内部通过解引用操作改变指针所指的内容
void mySwap(int *p, int *q){
int tmp = *p;
*p = *q;
*q = tmp;
}
int main(){
int a = 5, b = 10;
int *r = &a, *s = &b;
cout << "交换前:a = " << a << ",b = " << b << endl;
// 指针形参
mySwap(r, s);
// 引用形参(建议)
// mySwap(&a, &b);
cout << "交换后:a = " << a << ",b = " << b << endl;
system("pause");
return 0;
}
请特别注意,下面的程序无法满足题目要求:
// 在函数体内部交换了两个形参指针本身的值,未能影响实参
void mySwap(int *p, int *q){
int *tmp = p;
p = q;
q = tmp;
}
练习6.11
编写并验证你自己的 reset
函数,使其作用于引用类型的参数。注:reset
即置0。
解:
#include <iostream>
using namespace std;
void reset(int &i){
i = 0;
}
int main(){
int num = 5;
cout << "重置前:num = " << num << endl;
reset(num);
cout << "重置前:num = " << num << endl;
system("pause");
return 0;
}
练习6.12
改写6.2.1节练习中的程序,使其引用而非指针交换两个整数的值。你觉得哪种方法更易于使用呢?为什么?
解:
#include <iostream>
using namespace std;
void mySwap(int &p, int &q){
int tmp = p;
p = q;
q = tmp;
}
int main(){
int a = 5, b = 10;
int *r = &a, *s = &b;
cout << "交换前:a = " << a << ",b = " << b << endl;
mySwap(a, b);
cout << "交换后:a = " << a << ",b = " << b << endl;
system("pause");
return 0;
}
与使用指针相比,使用引用交换变量的内容从形式上看更简单一些,并且无须额外声明指针变量,也避免了拷贝指针的值。
在C++中,建议使用引用形参替代指针形参。
练习6.13
假设 T
是某种类型的名字,说明以下两个函数声明的区别:
一个是 void f(T)
, 另一个是 void f(&T)
。
解:
-
void f(T)
的形参采用的是传值方式,也就是说,实参的值被拷贝给形参,形参和实参是两个相互独立的变量,在函数f
内部对形参所做的任何改动都不会影响实参的值。 -
void f(&T)
的形参采用的是传引用方式,此时形参是对应的实参的别名,形参绑定到初始化它的对象。如果改变了形参的值,也就是改变了对应实参的值。
下面的程序说明了这两个函数声明的区别:
#include <iostream>
using namespace std;
void a(int);
void b(int&);
int main(){
int s = 0, t = 10;
a(s);
cout << s << endl;
b(t);
cout << t << endl;
system("pause");
return 0;
}
void a(int i){
++i;
cout << i << endl;
}
void b(int& j){
++j;
cout << j << endl;
}
练习6.14
举一个形参应该是引用类型的例子,再举一个形参不能是引用类型的例子。
解:
与值传递相比,引用传递的优势主要体现在三个方面:
- 一是可以直接操作引用形参所引的值;
- 二是使用引用形参可以避免拷贝大的类类型对象或容器类型对象;
- 三是使用引用形参可以帮助从函数中返回多个值。
基于对引用传递优势的分析,举出几个适合使用引用类型的例子:
- 第一,当函数的目的是交换两个参数的内容时,应该使用引用类型的形参;
- 第二,当参数是
string
对象时,为了避免拷贝很长的字符串,应该使用引用类型。
在其他情况下可以使用值传递的方式,而无需使用引用传递,例如求整数的绝对值或者阶乘的程序。
练习6.15
说明 find_char
函数中的三个形参为什么是现在的类型,特别说明为什么 s
是常量引用而 occurs
是普通引用?
为什么 s
和 occurs
是引用类型而 c
不是?
如果令 s
是普通引用会发生什么情况?
如果令 occurs
是常量引用会发生什么情况?
解:
find_char
函数的三个参数的类型设定与该函数的处理逻辑密切相关,原因分别如下:
- 对于待查找的字符串
s
来说,为了避免拷贝长字符串,使用引用类型;同时我们只执行查找操作,无须改变字符串的内容,所以将其声明为常量引用。 - 对于待查找的字符
c
来说,它的类型是char
,只占1字节,拷贝的代价很低,而且我们无须操作实参在内存中实际存储的内容,只把它的值拷贝给形参即可,所以不需要使用引用类型。 - 对于字符出现的次数
occurs
来说,因为需要把函数内对实参值的更改反应在函数外部,所以必须将其定义成引用类型,但是不能把它定义成常量引用,否则就不能改变所引的内容了。
练习6.16
下面的这个函数虽然合法,但是不算特别有用。指出它的局限性并设法改善。
bool is_empty(string& s) { return s.empty(); }
解:
本程序把参数类型设为非常量引用,这样做有几个缺陷:
- 一是容易给使用者一种误导,即程序允许修改
s
的内容; - 二是限制了该函数所能接收的实参类型,我们无法把
const
对象、字面值常量或者需要进行类型转换的对象传递给普通的引用形参。
bool is_empty(const string& s) { return s.empty(); }
练习6.17
编写一个函数,判断 string
对象中是否含有大写字母。
编写另一个函数,把 string
对象全部改写成小写形式。
在这两个函数中你使用的形参类型相同吗?为什么?
解:
- 第一个函数的任务是判断
string
对象中是否有大写字母,无需修改参数的内容,因此将其设为常量引用类型。 - 第二个函数的任务是转换
string
对象中的字母为小写字母,需要修改参数的内容,所以应该将其设定为非常量引用类型。
程序如下:
#include <iostream>
#include <string>
using namespace std;
bool HasUpper(const string& str){
for (auto c : str)
if (isupper(c))
return true;
return false;
}
void ChangeToLower(string& str){
for (auto &c : str)
c = tolower(c);
}
int main(){
cout << "请输入一个字符串:" << endl;
string str;
cin >> str;
if (HasUpper(str)){
ChangeToLower(str);
cout << "转换后的字符串是:" << str << endl;
}
else{
cout << "该字符串不含大写字母,无需转换!" << endl;
}
system("pause");
return 0;
}
练习6.18
为下面的函数编写函数声明,从给定的名字中推测函数具备的功能。
(a) 名为 compare
的函数,返回布尔值,两个参数都是 matrix
类的引用。
(b) 名为 change_val
的函数,返回 vector
的迭代器,有两个参数:一个是 int
,另一个是 vector
的迭代器。
解:
(a)的函数声明是:
bool compare(const matrix&, matrix&);
(b)的函数声明是:
vector<int>::iterator change_val(int, vector<int>::iterator);
练习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)是合法的,字面值常量可以作为常量引用形参的值,字符 ‘a’ 作为 char
类型形参的值也是可以的。
©是合法的,66
虽然是 int
类型,但是在调用函数时自动转换为 double
类型。
(d)是合法的,vec.begin()
和 vec.end()
的类型都是形参所需的 vector<int>::iterator
,第三个实参 3.8
可以自动转换为形参所需的 int
类型。
练习6.20
引用形参什么时候应该是常量引用?如果形参应该是常量引用,而我们将其设为了普通引用,会发生什么情况?
解:
当函数对参数所做的操作不同时,应该选择适当的参数类型。如果需要修改参数的内容,则将其设置为普通引用类型;否则,如果不需要对参数内容做任何更改,最好设为常量引用类型。
如果把一个本来应该是常量引用的形参设成了普通引用类型,有可能遇到几个问题:
- 一是容易给使用者一种误导,即程序允许修改实参的内容;
- 二是限制了该函数所能接受的实参类型,无法把
const
对象、字面值常量或者需要类型转换的对象传递给普通的引用形参。
练习6.21
编写一个函数,令其接受两个参数:一个是 int
型的数,另一个是 int
指针。
函数比较 int
的值和指针所指的值,返回较大的那个。
在该函数中指针的类型应该是什么?
解:
函数实际上比较的是第一个实参的值和第二个实参所指数组首元素的值。因为两个参数的内容都不会被修改,所以指针的类型应该是 const int*
。
程序如下:
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
int myCompare(const int val, const int *p){
return (val > *p) ? val : *p;
}
int main(){
srand((unsigned)time(NULL));
int a[10];
for (auto &i : a)
i = rand() % 100;
cout << "请输入一个数:";
int j;
cin >> j;
cout << "您输入的数与数组首元素中较大的是:" << myCompare(j, a) << endl;
cout << "数组的全部元素是:" << endl;
for (auto i : a)
cout << i << " ";
cout << endl;
system("pause");
return 0;
}
练习6.22
编写一个函数,令其交换两个 int
指针。
解:
为了全面性,一共实现了三个不同版本的函数:
- 第一个函数以值传递的方式使用指针,所有改变都局限于函数内部,当函数执行完毕后既不会改变指针本身的值,也不会改变指针所指的内容。
- 第二个函数同样以值传递的方式使用指针,但是在函数内部通过解引用的方式直接访问内存并修改了指针的方式。
- 第三个函数的参数形式是
int*&
,其含义是,该参数是一个引用,引用的对象是内存中的一个int
指针,使用这种方式可以把指针当成对象,交换指针本身的值。需要注意的是,最后一个函数既然交换了指针,当然解引用该指针所得的结果也会相应发生改变。
#include <iostream>
using namespace std;
// 该函数既不交换指针,也不交换指针所指的内容
void SwapPointer1(int *p, int *q){
int *temp = p;
p = q;
q = temp;
}
// 该函数交换指针所指的内容
void SwapPointer2(int *p, int *q){
int temp = *p;
*p = *q;
*q = temp;
}
// 该函数交换指针本身的值,即交换指针所指的内存地址
void SwapPointer3(int &p, int &q){
int tmp = p;
p = q;
q = tmp;
}
int main(){
int a = 5, b = 10;
int *p = &a, *q = &b;
cout << "交换前:" << endl;
cout << "p的值是:" << p << ",q的值是:" << q << endl;
cout << "p所指的值是:" << *p << ",q所指的值是:" << *q << endl;
SwapPointer1(p, q);
cout << "交换后:" << endl;
cout << "p的值是:" << p << ",q的值是:" << q << endl;
cout << "p所指的值是:" << *p << ",q所指的值是:" << *q << endl;
cout << endl;
cout << "*********************************************************" << endl;
cout << endl;
int c = 5, d = 10;
int *r = &c, *s = &d;
cout << "交换前:" << endl;
cout << "p的值是:" << r << ",q的值是:" << s << endl;
cout << "p所指的值是:" << *r << ",q所指的值是:" << *s << endl;
SwapPointer2(r, s);
cout << "交换后:" << endl;
cout << "p的值是:" << r << ",q的值是:" << s << endl;
cout << "p所指的值是:" << *r << ",q所指的值是:" << *s << endl;
cout << endl;
cout << "*********************************************************" << endl;
cout << endl;
int e = 5, f = 10;
int *x = &e, *y = &f;
cout << "交换前:" << endl;
cout << "p的值是:" << x << ",q的值是:" << y << endl;
cout << "p所指的值是:" << *x << ",q所指的值是:" << *y << endl;
//SwapPointer3(e, f);
SwapPointer3(*x, *y);
cout << "交换后:" << endl;
cout << "p的值是:" << x << ",q的值是:" << y << endl;
cout << "p所指的值是:" << *x << ",q所指的值是:" << *y << endl;
system("pause");
return 0;
}
练习6.23
参考本节介绍的几个 print
函数,根据理解编写你自己的版本。
依次调用每个函数使其输入下面定义的 i
和 j
:
int i = 0, j[2] = { 0, 1 };
解:
实现了三个版本的 print
函数:
- 第一个版本不控制指针的边界,
- 第二个版本由调用者指定数组的维度,
- 第三个版本使用C++11新规定的
begin
和end
函数限定数组边界。
满足题意的程序如下所示:
#include <iostream>
using namespace std;
// 参数是常量整型指针
void print1(const int *p){
cout << *p << endl;
}
// 参数有两个,分别是常量整型指针和数组的容量
void print2(const int *p, const int sz){
int i = 0;
while (i != sz) {
cout << *p++ << endl;
++i;
}
}
// 参数有两个,分别是数组的首尾边界
void print3 (const int *b, const int *e){
for (auto q = b; q != e; ++q){
cout << *q << endl;
}
}
int main(){
int i = 0, j[2] = { 0, 1 };
print1(&i);
print1(j);
cout << endl;
cout << "*********************************************************" << endl;
cout << endl;
print2(&i, 1);
//计算得到数组j的容量
print2(j, sizeof(j) / sizeof(*j));
cout << endl;
cout << "*********************************************************" << endl;
cout << endl;
auto b = begin(j);
auto e = end(j);
print3(b, e);
system("pause");
return 0;
}
练习6.24
描述下面这个函数的行为。如果代码中存在问题,请指出并改正。
void print(const int ia[10])
{
for (size_t i = 0; i != 10; ++i)
cout << ia[i] << endl;
}
解:
当想把数组作为函数的形参时,有三种可供选择的方式:
- 一是声明为指针,
- 二是声明为不限维度的数组,
- 三是声明为维度确定的数组。
实际上,因为数组传入函数时实参自动转换成指向数组首元素的指针,所以这三种方式是等价的。
由之前的分析可知,print
函数的参数实际上等同于一个常量整型指针 const int*
,形参 ia
的维度10,只是我们期望的数组维度,实际上不一定。即使实参数组的真实维度不是10,也可以正常调用 print
函数。
上述 print
函数的定义存在一个潜在风险,即虽然我们期望传入的数组维度是10,但实际上任意维度的数组都可以传入。如果传入的数组维度较大,print
函数输出数组的前10个元素,不至于引发错误;相反如果传入的数组维度不足10,则 print
函数将强行输出一些未定义的值。
修改后的程序是:
void print(const int ia[], const int sz)
{
for (size_t i = 0; i != sz; ++i)
cout << ia[i] << endl;
}
练习6.25
编写一个 main
函数,令其接受两个实参。把实参的内容连接成一个 string
对象并输出出来。
解:
满足题意的程序如下所示:
#include <iostream>
#include <string>
using namespace std;
int main(int argc, char **argv){
string str;
for (int i = 0; i != argc; ++i)
str += argv[i];
cout << str << endl;
system("pause");
return 0;
}
int argc
用来表示命令行参数的个数;char *argv[]
用来取得所输入的参数;
练习6.26
编写一个程序,使其接受本节所示的选项;输出传递给 main
函数的实参内容。
解:
满足题意的程序如下所示:
#include <iostream>
using namespace std;
int main(int argc, char **argv){
for (int i = 0; i != argc; ++i){
cout << "argc[" << i << "]:" << argv[i] << endl;
}
system("pause");
return 0;
}
练习6.27
编写一个函数,它的参数是 initializer_list<int>
类型的对象,函数的功能是计算列表中所有元素的和。
解:
满足题意的程序如下所示,注意 iCount
的参数是 initializer list
对象,在调用该函数时,我们使用了列表初始化的方式生成实参。
#include <iostream>
#include <initializer_list>
using namespace std;
int iCount(initializer_list<int> il){
int count = 0;
// 遍历il的每一个元素
for (auto val : il)
count += val;
return count;
}
int main(){
// 使用列表初始化的方式构建initializer_list<int>对象
// 然后把它作为实参传递给函数iCount
cout << "1,6,9的和是:" << iCount({ 1, 6, 9 }) << endl;
cout << "4,5,9,18的和是:" << iCount({ 4, 5, 9, 18 }) << endl;
cout << "10,10,10,10,10,10,10,10,10的和是"
<< iCount({ 10, 10, 10, 10, 10, 10, 10, 10, 10 }) << endl;
system("pause");
return 0;
}
练习6.28
在 error_msg
函数的第二个版本中包含 ErrCode
类型的参数,其中循环内的 elem
是什么类型?
解:
initializer_list<string>
的所有元素类型都是 string
,因此 const auto &elem : il
推断得到的 elem
的类型是 const strings
。
使用引用是为了避免拷贝长字符串,把它定义为常量的原因是,我们只需读取字符串的内容,不需要修改它。
练习6.29
在范围 for
循环中使用 initializer_list
对象时,应该将循环控制变量声明成引用类型吗?为什么?
解:
引用类型的优势主要是可以直接操作所引用的对象以及避免拷贝较为复杂的类型对象和容器对象。因为 initializer_list
对象的元素永远是常量值,所以我们不可能通过设定引用类型来更改循环控制变量的内容。
只有当 initializer_list
对象类型是类类型或容器类型(比如 string
)时,才有必要把范围 for
循环的循环控制变量设为引用类型。
练习6.30
编译第200页的str_subrange
函数,看看你的编译器是如何处理函数中的错误的。
解:
编译器信息:VS2013,W10。
#include <iostream>
using namespace std;
bool str_subrange(const string &str1, const string &str2){
if (str1.size() == str2.size())
return str1 == str2;
auto size = (str1.size() < str2.size()) ? str1.size() : str2.size();
for (decltype(size)i = 0; i != size; ++i){
if (str1[i] != str2[i])
return;
}
}
编译错误信息:
编译器发现一个严重错误,即 for
循环中的 return
语句是非法的,函数的返回值类型是布尔值,而该条 return
语句没有返回任何值。
事实上还存在另一个严重错误,按照程序逻辑,for
循环有可能不会中途退出而是一直执行完毕,此时显然缺少一条 return
语句处理这种情况。遗憾的是,编译器无法发现这一错误。
练习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;
}
解:
合法。
get
函数接受一个整型指针,该指针实际指向一个整型数组的首元素,另外还接受一个整数表示数组中某个元素的索引值。它的返回值类型是整型引用,引用的对象是 arry
数组的某个元素。当 get
函数执行完毕,调用者得到实参数组 arry
中索引为 index
的元素引用。
在 main
函数中,首先创建一个包含10个整数的数组,名字是 ia
。请注意,由于 ia
定义在函数内部,所以 ia
不会执行默认初始化操作,如果此时我们直接输出 ia
每个元素的值,则这些值都是未定义的。
接下来进入循环。每次循环使用 get
函数得到数组 ia
中第 i
个元素的引用,为该引用赋值 i
,也就是说,为第 i
元素赋值 i
。循环结束时,ia
的元素依次被赋值为0~9。
#include <iostream>
using namespace std;
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;
for (int i = 0; i < 10; i++)
{
printf("%d ", ia[i]);
}
printf("\n");
system("pause");
//return 0;
}
练习6.33
编写一个递归函数,输出 vector
对象的内容。
解:
#include <iostream>
#include <vector>
using namespace std;
void print(vector<int> vInt, unsigned index){
unsigned sz = vInt.size();
if (!vInt.empty() && index < sz){
cout << vInt[index] << endl;
print(vInt, index + 1);
}
}
int main()
{
vector<int> v{ 1, 3, 5, 7, 9, 11, 13, 15 };
print(v, 0);
system("pause");
return 0;
}
练习6.34
如果 factorial
函数的停止条件如下所示,将发生什么?
if (val != 0)
解:
如果递归函数的参数类型是 int
,所以理论上用户传入 factorial
函数的参数可以是负数。按照原程序的逻辑,参数是负数时函数的返回值是1.
如果修改递归函数的停止条件,则当参数的值为负时,会依次递归下去,执行连续乘法操作直至溢出。因此,不能把 if
语句的条件改成上述形式。
练习6.35
在调用 factorial
函数时,为什么我们传入的值是 val-1
而非 val--
?
解:
int factorial(int val){
if (val < 1)
return factorial(val-1) * val;
return 1;
}
如果把传入的值 val-1
改成 val--
,则出现一种我们不期望看到的情况,即变量的递减操作与读取变量值的操作共存于同一条表达式中,这时有可能产生未定义的值。
练习6.36
编写一个函数声明,使其返回数组的引用并且该数组包含10个 string
对象。
不用使用尾置返回类型、decltype
或者类型别名。
解:
因为数组不能被拷贝,所以函数不能直接返回数组,但是可以返回数组的指针或引用。
要想使函数返回数组的引用并且该数组包含10个 string
对象,可以按照如下所示的形式声明函数:
string (&func())[10];
上述声明的含义是:
func()
表示调用 func
函数无须任何实参,(&func())
表示函数的返回结果是一个引用,(&func())[10]
表示引用的对象是一个维度为10的数组,string (&func())[10]
表示数组的元素是 string
对象。
练习6.37
为上一题的函数再写三个声明,一个使用类型别名,另一个使用尾置返回类型,最后一个使用 decltype
关键字。
你觉得哪种形式最好?为什么?
解:
使用类型别名:
typedef string arr[10];
arr& func();
使用尾置返回类型:
auto func()->string(&) [10];
使用 decltype
关键字:
string str[10];
decltype(str)& func();
个人觉得尾置返回类型最好,就一行代码,非常简便,同时这种形式对于返回类型比较复杂的函数最有效。
练习6.38
修改 arrPtr
函数,使其返回数组的引用。
解:
原始函数:
满足题意的函数是:
int odd[] = {1, 3, 5, 7, 9};
int even[] = {0, 2, 4, 6, 8};
// 返回一个指针,该指针指向含有5个整数的数组
decltype(odd) &arrPtr(int i)
{
return (i % 2) ? odd : even; // 返回数组的引用
}
练习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
的形参无法与另一个没有顶层 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);
解:
在上面的两个声明中,(a)是正确的而(b)是错误的。它们都用到了默认实参,但是 C++规定一旦某个形参被赋予了默认实参,则它后面的所有形参都必须有默认实参。
这一规定是为了防范可能出现的 二义性,显然(b)违反了这一规定。
练习6.41
下面的哪个调用是非法的?为什么?哪个调用虽然合法但显然与程序员的初衷不符?为什么?
char *init(int ht, int wd = 80, char bckgrnd = ' ');
(a) init();
(b) init(24,10);
(c) init(14,'*');
解:
(a)是非法的,该函数有两个默认实参,但是总计有三个形参,其中第一个形参并未设定默认实参,所以要想调用该函数,至少需要提供一个实参。
(b)是合法的,本次调用提供了两个实参,第一个实参对应第一个形参 ht
,第二个实参对应第二个形参 wd
,其中 wd
的默认实参没有用到,第三个形参 bckgrnd
使用它的默认实参。
©在语法上是合法的,但是与程序的原意不符。从语法上来说,第一个实参对应第一个形参 ht
,第二个实参的类型虽然是 char
,但是它可以自动转换为第二个形参 wd
所需的 int
类型,所以编译时可以通过,但这显然违背了程序的原意,正常情况下,字符 *
应该被用来构成 bckgrnd
。
练习6.42
给 make_plural
函数的第二个形参赋予默认实参’s’, 利用新版本的函数输出单词 success
和 failure
的单数和复数形式。
解:
原版函数:
新版函数:
#include<iostream>
#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(){
cout << "success的单数形式是:" << make_plural(1, "success", "es") << endl;
cout << "success的复教形式是:" << make_plural(2, "success", "es") << endl;
// 一般情况下调用该函数只需要两个实参
cout << "failure的单数形式是:" << make_plural(1, "failure") << endl;
cout << "failure的单数形式是:" << make_plural(2, "failure") << endl;
system("pause");
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节的isShorter
函数改写成内联函数。
解:
原版函数:
新版函数:
inline bool isShorter(const string &s1, const string &s2)
{
return s1.size() < s2.size();
}
练习6.45
回顾在前面的练习中你编写的那些函数,它们应该是内联函数吗?
如果是,将它们改写成内联函数;如果不是,说明原因。
解:
在本章前面实现的函数中,大多规模较小且流程直接,适合于设置为内联函数;如果以后遇到一些代码行数较多的函数,就不适合了。
举两个例子:
- 练习6.11中的
reset
函数改写后的形式是:
inline void reset(int &i)
{
i=0;
}
- 练习6.21中的
myCompare
函数改写后的形式是:
inline int mycompare(const int val, const int *p)
{
return (val > *p) ? val : *p;
}
练习6.46
能把isShorter
函数定义成constexpr
函数吗?
如果能,将它改写成constxpre
函数;如果不能,说明原因。
解:
constexpr
函数是指能用于常量表达式的函数,constexpr
函数的返回类型和所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条 return
语句。
显然 isShorter
函数不符合 constexpr
函数的要求,它虽然只有一条 return
语句,但是返回的结果调用了标准库 string
类的 size()
函数和 <
比较符,无法构成常量表达式,因此不能改写成 constexpr
函数。
练习6.47
改写6.3.2节练习中使用递归输出 vector
内容的程序,使其有条件地输出与执行过程有关的信息。
例如,每次调用时输出 vector
对象的大小。
分别在打开和关闭调试器的情况下编译并执行这个程序。
解:
原版函数:
新版函数:
#include<iostream>
#include<vector>
using namespace std;
// 递归函数输出vector<int>的内容
void print(vector<int> vInt, unsigned index)
{
unsigned sz = vInt.size();
// 设置在此处输出调试信息
#ifndef NDEBUG
cout << "vector对象的大小是:" << sz << endl;
#endif // NDEBUG
if(!vInt.empty() && index < sz)
{
cout << vInt[index] << endl;
print(vInt, index + 1);
}
}
int main()
{
vector<int> v = { 1, 3, 5, 7, 9, 11, 13, 15 };
print(v, 0);
system("pause");
return 0;
}
打开调试器时,每次递归调用 print
函数都会输出 vector对象的大小是:8
。
练习6.48
说明下面这个循环的含义,它对assert的使用合理吗?
string s;
while (cin >> s && s != sought) { } // 空函数体
assert(cin);
解:
该程序对 assert
的使用有不合理之处。
在调试器打开的情况下,当用户输入字符串 s
并且 s
的内容与 sought
不相等时,执行循环体,否则继续执行 assert(cin);
语句。换句话说,程序执行到 assert
的原因可能有两个,一是用户终止了输入,二是用户输入的内容正好与 sought
的内容一样。如果用户尝试终止输入(事实上用户总有停止输入结束程序的时候),则 assert
的条件为假,输出错误信息,这与程序的原意是不相符的,应该用
assert(s==sought);
当调试器关闭时,assert
什么也不做。
练习6.49
什么是候选函数?什么是可行函数?
解:
当程序中存在多个同名的重载函数时,编译器需要判断调用的是其中哪个函数,这时就有了候选函数和可行函数两个概念。
函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数。
候选函数具备两个典型特征:
- 一是与被调用的函数同名,
- 二是其声明在调用点可见。
函数匹配的第二步是考查本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数。
可行函数也有两个特征:
- 一是其形参数量与本次调用提供的实参数量相等,
- 二是每个实参的类型与对应的形参类型相同或者能转换成形参的类型。
练习6.50
已知有第217页对函数 f
的声明,对于下面的每一个调用列出可行函数。
其中哪个函数是最佳匹配?
如果调用不合法,是因为没有可匹配的函数还是因为调用具有二义性?
(a) f(2.56, 42)
(b) f(42)
(c) f(42, 0)
(d) f(2.56, 3.14)
解:
- 可行函数是指形参数量与本次调用提供的实参数量相等且每个实参的类型都与对应的形参类型相同或者能转换成形参类型的函数。
- 最佳匹配是指该函数每个实参的匹配都不劣于其他可行函数需要的匹配且至少有一个实参的匹配优于其他可行函数提供的匹配。
根据上述分析,我们可以推断出:
-
f(2.56, 42)
的可行函数是void f(int, int)
和void f(double, double=3.14)
。但是最佳匹配不存在,因为这两个可行函数各有所长。对于这次调用来说,如果只考虑第一个实参2.56
,我们发现,void f(double, double=3.14)
能够精确匹配,但是要想匹配第二个参数,int
类型的实参42
必须转换成double
类型。如果考虑第二个实参42
,我们发现,void f(int, int)
能够精确匹配,但是要想调用void f(int, int)
就必须把第一个double
类型的实参2.56
转换成int
类型。最终的结果是这两个可行函数各自在一个实参上实现了更好的匹配,但是把它们比较起来无从判断孰优孰劣,因此编译器将因为这个调用具有二义性而拒绝其请求。 -
f(42)
的可行函数是void f(int)
和void f(double, double=3.14)
,其中最佳匹配是void f(int)
,因为参数无须做任何类型转换。 -
f(42, 0)
的可行函数是void f(int, int)
和void f(double, double=3.14)
,其中最佳匹配是void f(int, int)
,因为参数无须做任何类型转换。 -
f(2.56, 3.14)
的可行函数是void f(int, int)
和void f(double, double=3.14)
,其中最佳匹配是void f(double, double=3.14)
,因为参数无须做任何类型转换。
练习6.51
编写函数 f
的4版本,令其各输出一条可以区分的消息。
验证上一个练习的答案,如果你的回答错了,反复研究本节内容直到你弄清自己错在何处。
解:
#include<iostream>
using namespace std;
void f(){
cout << "该函数无须参数" << endl;
}
void f(int){
cout << "该函数有一个整型参数" << endl;
}
void f(int, int){
cout << "该函数有两个整型参数" << endl;
}
void f(double a, double b = 3.14){
cout << "该函数有两个双精度浮点型参数" << endl;
}
int main(){
//f(2.56, 42); // 报错
f(42);
f(42, 0);
f(2.56, 3.14);
system("pause");
return 0;
}
练习6.52
已知有如下声明:
void manip(int ,int);
double dobj;
请指出下列调用中每个类型转换的等级。
(a) manip('a', 'z');
(b) manip(55.4, dobj);
解:
(a)发生的参数类型转换是类型提升,字符型实参自动提升成整型。
(b)发生的参数类型转换是算术类型转换,双精度浮点型自动转换成整型。
练习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)是合法的,两个函数的区别是它们的引用类型的形参是否引用了常量,属于底层 const
,可以把两个函数区分开来。
(b)是合法的,两个函数的区别是它们的指针类型的形参是否指向了常量,属于底层 const
,可以把两个函数区分开来。
©是非法的,两个函数的区别是它们的指针类型的形参本身是否是常量,属于顶层 const
,根据本节介绍的匹配规则可知,向实参添加顶层 const
或者从实参中删除顶层 const
属于精确匹配,无法区分两个函数。
练习6.54
编写函数的声明,令其接受两个 int
形参并返回类型也是 int
;然后声明一个 vector
对象,令其元素是指向该函数的指针。
解:
int func(int, int);
vector<decltype(func)* > vF;
练习6.55
编写4个函数,分别对两个 int
值执行加、减、乘、除运算;在上一题创建的 vector
对象中保存指向这些函数的指针。
解:
#include<iostream>
#include<vector>
using namespace std;
int func1(int a, int b){
return a + b;
}
int func2(int a, int b){
return a - b;
}
int func3(int a, int b){
return a * b;
}
int func4(int a, int b){
return a / b;
}
int main(){
decltype(func1) *p1 = func1, *p2 = func2, *p3 = func3, *p4 = func4;
vector<decltype(func1)*>vF = { p1, p2, p3, p4 };
system("pause");
return 0;
}
练习6.56
调用上述 vector
对象中的每个元素并输出结果。
解:
#include<iostream>
#include<vector>
using namespace std;
int func1(int a, int b){
return a + b;
}
int func2(int a, int b){
return a - b;
}
int func3(int a, int b){
return a * b;
}
int func4(int a, int b){
return a / b;
}
void Compute(int a, int b, int(*p)(int, int)){
cout << p(a, b) << endl;
}
int main(){
int i = 5, j = 10;
decltype(func1) *p1 = func1, *p2 = func2, *p3 = func3, *p4 = func4;
vector<decltype(func1)*>vF = { p1, p2, p3, p4 };
// 遍历vector中的每个元素,依次调用四则运算函数
for (auto p : vF){
Compute(i, j, p);
}
system("pause");
return 0;
}
转载:https://blog.csdn.net/TeFuirnever/article/details/103671594