文章目录
- 6.1
- 6.2
- 6.3
- 6.4
- 6.5
- 6.6
- 6.7
- 6.8
- 6.10
- 6.11
- 6.12
- 6.13
- 6.14
- 6.15
- 6.16
- 6.17
- 6.18
- 6.19
- 6.20
- 6.21
- 6.22
- 6.23
- 6.24
- 6.25
- 6.26
- 6.27
- 6.28
- 6.29
- 6.30
- 6.31
- 6.32
- 6.33
- 6.34
- 6.35
- 6.36
- 6.37
- 6.38
- 6.39
- 6.40
- 6.41
- 6.42
- 6.43
- 6.44
- 6.45
- 6.46
- 6.47
- 6.48
- 6.49
- 6.50
- 6.51
- 6.52
- 6.53
- 6.54
- 6.55
- 6.56
6.1
形参出现在函数定义的地方,形参列表可以包含 0 个、1 个或多个形参,多个形参之间以逗号分隔。形参规定了一个函数所接受数据的类型和数量。
实参出现在函数调用的地方,实参的数量与形参一样多。实参的主要作用是初始化形参,并且这种初始化过程是一一对应的,即第一个实参初始化第一个形参、第二个实参初始化第二个形参,以此类推。实参的类型必须与对应的形参类型匹配。
6.2
(a)是错误的,因为函数体返回的结果类型是 string,而函数的返回值类型是 int,二者不一致且不能自动转换
(b)是错误的,因为函数缺少返回值类型
(c)是错误的,同一个函数如果含有多个形参,则这些形参的名字不能重复;另外,函数体左侧的花括号丢失了
(d)是错误的,因为函数体必须放在一对花括号内。
6.3
#include <iostream>
using namespace std;
int fact(int n){int sum{1};while (n>1){sum*=n;--n;}
}
int main() {cout<< fact(5) <<endl;return 0;
}
6.4
#include <iostream>
using namespace std;
int fact(int n){int sum{1};while (n>1){sum*=n;--n;}
}
int main() {int n;cout<<"请输入一个数字:";cin>>n;cout<< n<<"的阶乘是:" <<fact(n) <<endl;return 0;
}
6.5
#include <iostream>
using namespace std;unsigned m_abs(int n){if(n>=0)return n;elsereturn -n;
}
int main() {int n;cout<<"请输入一个数字:";cin>>n;cout<< n<<"的绝对值是:" <<m_abs(n) <<endl;return 0;
}
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;cout << "请输入两个数:";}return 0;
}
6.7
#include <iostream>using namespace std;unsigned func(){static int n{0};return n++;
}int main() {for (int i = 0; i < 10; ++i) {cout<< func()<<"次" <<endl;}return 0;
}
6.8
#ifndef CHAPTER6
#define CHAPTER6int fact(int n);
unsigned m_abs(int n);#endif
6.10
#include <iostream>using namespace std;void swap(int *p1, int *p2) {int tmp = *p1;*p1 = *p2;*p2 = tmp;
}int main() {int a=1,b=2;cout<<"a="<<a<<" "<<"b="<<b <<endl;swap(&a,&b);cout<<"a="<<a<<" "<<"b="<<b <<endl;return 0;
}
6.11
#include <iostream>using namespace std;void reset(int &n) {n = 50;
}int main() {int n = 100;cout << "调用函数之前n的值为" << n << endl;reset(n);cout << "调用函数之后n的值为" << n << endl;return 0;
}
6.12
#include <iostream>using namespace std;void swap(int &p1, int &p2) {int tmp = p1;p1 = p2;p2 = tmp;
}int main() {int a=1,b=2;cout<<"a="<<a<<" "<<"b="<<b <<endl;swap(a,b);cout<<"a="<<a<<" "<<"b="<<b <<endl;return 0;
}
与使用指针相比,使用引用交换变量的内容从形式上看更简单一些,并且无须额外声明指针变量,也避免了拷贝指针的值。
6.13
void f(T) 的形参采用的是传值方式,也就是说,实参的值被拷贝给形参,形参和实参是两个相互独立的变量,在函数 f 内部对形参所做的任何改动都不会影响实参的值。
void f(&T) 的形参采用的是传引用方式,此时形参是对应的实参的别名,形参绑定到初始化它的对象。如果我们改变了形参的值,也就是改变了对应实参的值。
6.14
基于对引用传递优势的分析,我们可以举出几个适合使用引用类型形参的例子:
当函数的目的是交换两个参数的内容时应该使用引用类型的形参;
当参数是 string 对象时,为了避免拷贝很长的字符串,应该使用引用类型。
在其他情况下可以使用值传递的方式,而无须使用引用传递,例如求整数的绝对值或者阶乘的程序。
6.15
ind_char 函数的三个参数的类型设定与该函数的处理逻辑密切相关,原因分别如下:
对于待查找的字符串 s 来说,为了避免拷贝长字符串,使用引用类型;同时我们只执行查找操作,无须改变字符串的内容,所以将其声明为常量引用。
对于待查找的字符 c 来说,它的类型是 char,只占 1 字节,拷贝的代价很低,而且我们无须操作实参在内存中实际存储的内容,只把它的值拷贝给形参即可,所以不需要使用引用类型。
对于字符出现的次数 occurs 来说,因为需要把函数内对实参值的更改反映在函数外部,所以必须将其定义成引用类型;但是不能把它定义成常量引用,否则就不能改变所引的内容了。
6.16
一是容易给使用者一种误导,即程序允许修改变量 s 的内容;二是限制了该函数所能接受的参数类型,我们无法把 const 对象、字面值常量或者需要进行类型转换的对象传递给普通的引用形参
6.17
#include <iostream>
#include<string>
using namespace std;
bool isICapital(const string&s){for(auto c:s){if (c>='A'&&c<='Z')return true;}return false;
}
void change(string&s){for(auto &c:s){if (c>='A'&&c<='Z')c+=32;}
}
int main() {string s ="AcasdAZ";change(s);cout<< s <<endl;cout<< isICapital(s) <<endl;return 0;
}
第一个函数的任务是判断 string 对象中是否含有大写字母,无须修改参数的内容,因此将其设为常量引用类型。第二个函数需要修改参数的内容,所以应该将其设定为非常量引用类型。
6.18
bool compare(const matrix &, const matrix &);
vector<int>::iterator change_val(int, vector<int>::iterator);
6.19
(a)是非法的,函数的声明只包含一个参数,而函数的调用提供了两个参数,因此无法编译通过。
(b)是合法的,字面值常量可以作为常量引用形参的值,字符 a 作为 char 类型形参的值也是可以的。
(c)是合法的,66 虽然是 int 类型,但是在调用时自动转换为 double 类型。
(d)是合法的,vec.begin() 和 vec.end() 的类型都是形参所需的 vector::iterator ,第三个实参 3.8 可以自动转换为形参所需的 int 类型。
6.20
当函数对参数所做的操作不同时,应该选择适当的参数类型。如果需要修改参数的内容,则将其设置为普通引用类型;否则,如果不需要对参数内容做任何更改,最好设为常量引用类型。
6.21
#include <iostream>
#include<string>using std::cout,std::endl,std::string;inline int comp(const int x,const int *p){return (x>*p)?x:(*p);
}int main() {int x = 50;int y = 500;int *p = &x;cout<< comp(y,p) <<endl;return 0;
}
6.22
第一个函数以值传递的方式使用指针,所有改变都局限于函数内部,当函数执行完毕后即不会改变指针本身的值,也不会改变指针所指的内容。
第二个函数同样以值传递的方式使用指针,但是函数内部通过解引用的方式直接访问内存并修改了指针所指的内容。
第三个函数的参数形式是 int &* ,其含义是(从右向左理解),该参数是一个引用,引用的对象是内存中的一个 int 指针,使用这种方式可以把指针当成对象,交换指针本身的值。需要注意的是,最后一个函数既然交换了指针,当然解引用该指针所得的结果也会相应发生改变。
#include <iostream>
using namespace std;// 该函数既不交换指针,也不交换指针所指的内容
void SwapPointer1(int *p, int *q) {int *temp = p;p = q;q = temp;cout << "p 的值是:" << p << ",q 的值是:" << q << endl;
}// 该函数交换指针所指的内容
void SwapPointer2(int *p, int *q) {int temp = *p;*p = *q;*q = temp;cout << "p 的值是:" << p << ",q 的值是:" << q << endl;
}// 该函数交换指针本身的值,即交换指针所指的内存地址
void SwapPointer3(int *&p, int *&q) {int *temp = p;p = q;q = temp;cout << "p 的值是:" << p << ",q 的值是:" << q << endl;
}int main() {int a = 5, b = 10;int *r = &a, *s = &b;cout << "交换前:" << endl;cout << "r 的值是:" << r << ",s 的值是:" << s << endl;cout << "r 所指的值是:" << *r << ",s 所指的值是:" << *s << endl;SwapPointer1(r, s);cout << "交换后:" << endl;cout << "r 的值是:" << r << ",s 的值是:" << s << endl;cout << "r 所指的值是:" << *r << ",s 所指的值是:" << *s << endl;cout << endl;a = 5, b = 10;r = &a, s = &b;cout << "交换前:" << endl;cout << "r 的值是:" << r << ",s 的值是:" << s << endl;cout << "r 所指的值是:" << *r << ",s 所指的值是:" << *s << endl;SwapPointer2(r, s);cout << "交换后:" << endl;cout << "r 的值是:" << r << ",s 的值是:" << s << endl;cout << "r 所指的值是:" << *r << ",s 所指的值是:" << *s << endl;cout << endl;a = 5, b = 10;r = &a, s = &b;cout << "交换前:" << endl;cout << "r 的值是:" << r << ",s 的值是:" << s << endl;cout << "r 所指的值是:" << *r << ",s 所指的值是:" << *s << endl;SwapPointer3(r, s);cout << "交换后:" << endl;cout << "r 的值是:" << r << ",s 的值是:" << s << endl;cout << "r 所指的值是:" << *r << ",s 所指的值是:" << *s << endl;return 0;
}
6.23
#include <iostream>
using std::cout,std::endl,std::string;void print(const int x){cout<< x <<endl;
}void print(const int*x,const size_t len){for (int i = 0; i < len; ++i)cout<< *(x+i) <<" ";cout<<endl;}
int main() {int i = 0,j[2]={0,1};print(i);print(j,2);return 0;
}
6.24
上述 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
#include <iostream>
using namespace std;int main(int argc, char *argv[]) {string str;for (int i = 0; i != argc; ++i) {str += argv[i];}cout << str << endl;return 0;
}
6.26
#include <iostream>
using namespace std;int main(int argc, char *argv[]) {for (int i = 0; i != argc; ++i)cout<< "arg["<<i<<"]="<<argv[i] <<endl;return 0;
}
6.27
#include <iostream>
#include<initializer_list>
using namespace std;int sum(const initializer_list<int>i){int s{0};for (auto beg = i.begin(); beg!=i.end();++beg)s+=(*beg);return s;
}
int main() {cout<< sum({1,2,3})<<endl;cout<< sum({1,1,1,1,1})<<endl;return 0;
}
6.28
initializer_list 的所有元素类型都是 string,因此 const auto &elem : il 推断得到的 elem 的类型是 const string & 。使用引用是为了避免拷贝长字符串,把它定义为常量的原因是我们只需读取字符串的内容,不需要修改它(另外, initializer_list 列表中的元素原本属性就是 const,想修改也无法修改)
6.29
引用类型的优势主要是可以直接操作引用的对象以及避免拷贝较为复杂的类型对象和容器对象。因为 initializer_list 对象的元素永远是常量值,所以我们不可能通过设定引用类型来更改循环控制变量的内容。只有当 initializer_list 对象的元素类型是类类型或容器类型(比如 string)时,才有必要把范围 for 循环的循环控制变量设为引用类型。
6.30
#include <iostream>
using namespace std;// 因为含有不正确的返回值,所以这段代码无法通过编译
bool str_subrange(const string &str1, const string str2) {// 大小相同:此时用普通的相等性来判断结果作为返回值if (str1.size() == str2.size())return str1 == str2; // 正确:== 运算符返回布尔值// 得到较短 string 对象大小auto size = (str1.size() < str2.size())? str1.size() : str2.size();// 检查两个 string 对象的对应字符是否相等,以较短的字符长度为限for (decltype(size) i = 0; i != size; ++i) {if (str1[i] != str2[i])return; // 错误 #1:没有返回值,编译器将报告这一错误}// 错误 #2:控制流可能尚未返回任何值就结束了函数的执行// 编译器可能检查不出这一错误
}int main(int argc, char **argv) {return 0;
}
6.31
如果引用所引的是函数开始之前就已经存在的对象,则返回该引用是有效的;如果引用所引的是函数的局部变量,则随着函数结束局部变量也失效了,此时返回的引用无效。
当不希望返回的对象被修改时,返回对常量的引用
6.32
该函数是合法的。get 函数接受一个整型指针,该指针实际指向一个整型数组的首元素,另外还接受一个整数表示数组中某个元素的索引值。它的返回值类型是整型引用,引用的对象是 array 数组的某个元素。当 get 函数执行完毕后,调用者得到实参数组 array 中索引为 index 的元素的引用。
6.33
#include <iostream>
#include <vector>
using namespace std;// 递归函数输出 vector<int> 的内容
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);return 0;
}
6.34
因为原文中递归函数的参数类型是 int,所以理论上用户传入 factorial 函数的参数可以是负数。按照原程序的逻辑,参数为负数时函数的返回值是 1.
如果修改递归函数的停止条件,则当参数的值为负时,会依次递归下去,执行连续乘法操作直至溢出。因此,不能把 if 语句的条件改成上述形式。
6.35
如果把 val - 1 改成 val-- ,则出现一种我们不期望看到的情况,即变量的递减操作与读取变量值的操作共存于同一条表达式中,这时有可能产生未定义的值
6.36
要想使函数返回数组的引用并且该数组包含 10 个 string 对象,可以按照如下所示的形式声明函数:
string (&func())[10];
上述声明的含义是:func() 表示调用 func 函数无须任何实参,(&func()) 表示函数的返回结果是一个引用,(&func())[10] 表示引用的对象是一个维度为 10 的数组,string (&func())[10] 表示数组的元素是 string 对象。
6.37
typedef string arr[10];
arr &func();auto func() -> string (&)[10];string str[10];
decltype(str) &func();
6.38
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)的第二个声明是非法的。它的意图是声明另外一个函数,该函数只接受整型常量作为实参,但是因为顶层 const 不影响传入函数的对象,所以一个拥有顶层 const 的形参无法与另一个没有顶层 const 的形参区分开来。
(b)的第二个声明是非法的。它的意图是通过函数的返回值区分两个同名的函数,但是这不可行,因为 C++ 规定重载函数必须在形参数量或形参类型上有所区别。如果两个同名函数的形参数量和类型都一样,那么即使返回类型不同也不行。
(c)的两个函数是重载关系,它们的形参类型有区别。
6.40
a、正确的
b、错误的,默认实参之后必须是默认实参
6.41
(a)是非法的,该函数有两个默认实参,但是总计有三个形参,其中第一个形参并未设定默认实参,所以要想调用该函数,至少需要提供一个实参。
(b)是合法的,本次调用提供了两个实参,第一个实参对应第一个形参 ht,第二个实参对应第二个形参 wd,其中 wd 的默认实参没有用到,第三个形参 bckgrnd 使用它的默认实参。
(c)在语法上是合法的,但是与程序的原意不符。从语法上来说,第一个实参对应第一个形参 ht,第二个实参的类型虽然是 char,但是它可以自动转换为第二个形参 wd 所需的 int 类型,所以编译时可以通过,但这显然违背了程序的原意,正常情况下,字符 * 应该被用来构成背景。
6.42
#include <iostream>
#include<string>
using std::cout,std::endl,std::string;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(2,"success","es")<<endl;cout<< "failure--" <<make_plural(2,"failure")<<endl;return 0;
}
6.43
(a)应该放在头文件中。因为内联函数的定义对编译器而言必须是可见的,以便编译器能够在调用点内联展开该函数的代码,所以仅有函数的原型不够。并且,与一般的函数不同,内联函数有可能在程序中定义不止一次,此时必须保证在所有源文件中定义完全相同,把内联函数的定义放在头文件中可以确保这一点。
(b)是函数声明,应该放在头文件中。
6.44
inline bool isShorter(const string &s1, const string &s2) {return s1.size() < s2.size();
}
6.45
简单的一句换可以改写为内联函数
6.46
显然 isShorter 函数不符合 constexpr 函数的要求,它虽然只有一条 return 语句,但是返回的结果调用了标准库 string 类的 size() 函数和 < 比较符,无法构成常量表达式,因此不能改写成 constexpr 函数。
6.47
#include <iostream>
#include <vector>
#define NDEBUG
using namespace std;// 递归函数输出 vector<int> 的内容
void print(vector<int> vInt, unsigned index) {unsigned sz = vInt.size();// 设置在此处输出调试信息#ifndef NDEBUGcout << "vector 对象的大小是:" << sz << endl;#endif // NDEBUGif (!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);return 0;
}
6.48
该程序对 assert 的使用有不合理之处。在调试器打开的情况下,当用户输入字符串 s 并且 s 的内容与 sought 不相等时,执行循环体,否则继续执行 assert(cin); 语句。换句话说,程序执行到 assert 的原因可能有两个,一是用户终止了输入,二是用户输入的内容正好与 sought 的内容一样。如果用户尝试终止输入(事实上用户总有停止输入结束程序的时候),则 assert 的条件为假,输出错误信息,这与程序的原意是不相符的。
当调试器关闭时,assert 什么也不做。
6.49
当程序中存在多个同名的重载函数时,编译器需要判断调用的是其中哪个函数,这时就有了候选函数和可行函数两个概念。
函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数。候选函数具备两个典型特征:一是与被调用的函数同名;二是其声明在调用点可见。
函数匹配的第二步是考查本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数。可行函数也有两个特征:一是其形参数量与本次调用提供的实参数量相等;二是每个实参的类型与对应的形参类型相同或者能转换成形参的类型。
可行函数是指形参数量与本次调用提供的实参数量相等且每个实参的类型都与对应的形参类型相同或者能够转换成形参类型的函数。
最佳匹配是指该函数每个实参的匹配都不劣于其他可行函数需要的匹配且至少有一个实参的匹配优于其他可行函数提供的匹配。
6.50
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
#include <iostream>
void f();
void f(int);
void f(int, int);
void f(double, double);
using namespace std;int main() {// f(2.56, 42);f(42);f(42, 0);f(2.56, 3.14);return 0;
}void f() {cout << "f()" << endl;
}void f(int i) {cout << "f(int)" << endl;
}void f(int i, int j) {cout << "f(int, int)" << endl;
}void f(double d1, double d2 = 3.14) {cout << "f(double, double)" << endl;
}
6.52
(a)发生的参数类型转换是类型提升,字符型实参自动提升成整型。
(b)发生的参数类型转换是算术类型转换,双精度浮点型自动转换成整型。
6.53
(a)是合法的,两个函数的区别是它们的引用类型的形参是否引用了常量,属于底层 const,可以把两个函数区分开来。
(b)是合法的,两个函数的区别是它们的指针类型的形参是否指向了常量,属于底层 const,可以把两个函数区分开来。
(c)是非法的,两个函数的区别是它们的指针类型的形参本身是否是常量,属于顶层 const,根据本节介绍的匹配规则可知,向实参添加顶层 const 或者从实参中删除顶层 const 属于精确匹配,无法区分两个函数。
6.54
int func(int, int);
vector<decltype(func) *> vF;
6.55
#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};return 0;
}
6.56
#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};for (auto p : vF) {// 遍历 vector 中的每个元素,依次调用四则运算函数compute(i, j, p);}return 0;
}