函数及练习题解
1、基础
函数(function) 包含返回类型(return type)、函数名、由0个或多个*形参(parameter)*组成的列表以及函数体。
调用函数可以通过调用运算符(call operator)。调用运算符的形式之一是一对圆括号 (),作用于一个表达式,该表达式是函数或者指向函数的指针;圆括号内是一个用逗号隔开的 实际参数(argument,简称实参) 列表,用来初始化函数形参。调用表达式的类型就是函数的返回类型。
编写与调用函数
//定义函数:fact 用来阶乘
// val的阶乘是val * (val - 1) * (val - 2) . . . * ((val - (val - 1)) * 1)
int fact(int val)
{
int ret = 1; // 局部变量,用于保存计算结果
while (val > 1)
ret *= val--; // 把ret和val的来积赋给ret,然后将val减1
return ret; // 返回结果
}
//调用函数
int main()
{
int j = fact(5); // j equals 120, i.e., the result of fact(5)
cout << "5! is " << j << endl;
return 0;
}
函数调用完成两项工作:一是用实参初始化对应的形参,二是将控制权从主调函数转移给被调函数。
此时,主调函数(calling function) 的执行被暂时中断,被调函数(called function) 开始执行。
遇到 return 语句时,函数结束执行过程,完成两项工作:一是返回return语句中的值(有的话);二是将控制权从被调函数转回主调函数。
形参和实参
实参是形参的初始值,两者的顺序、数量要匹配。
对于上一段代码中的fact函数:
fact("hello");
fact();
fact(23, 34);
fact(3.4);
只有第四个能成功调用,因为浮点数能转换成int。第一个是类型不能转换,二三是实参数量不对。
函数的形参列表
函数的形参列表可以为空,但是不能省略。形参列表中的形参通常用逗号隔开,每个形参都是含有一个声明符的声明,即使两个形参类型一样,也必须把两个类型声明都写出来。
void f1() {
/* ... */ } // 隐式地定义空形参列表
void f2(void) {
/* ... */ } // 显式地定义空形参列表
int f3(int v1, v2) {
/* ... */ } // 错误
int f4(int v1, int v2) {
/* ... */ } // 正确
函数的任意两个形参不能同名,而且函数最外层作用域中的局部变量也不能使用与函数形参一样的名字。
形参的名字是可选的,但是由于无法使用未命名的形参,所以形参一般都应该有个名字,即使某个形参不被函数使用,也必须为它提供一个实参。
函数的返回类型不能是数组类型或者函数类型,但可以是指向数组或函数的指针。
练习 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) {
/* ... */ }
(c)是错误的,同一个函数如果含有多个形参,则这些形参的名字不能重复;另外,函数体左侧的花括号缺失了。
修改后的程序应该是:
double square (double x) {
return x * x; }
(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;
return 0;
}
练习 6.5:编写一个函数输出其实参的绝对值。
答:
#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;
return 0;
}
1.1 局部对象
对象的 生命周期(lifetime) 是程序执行过程中对象存在的一段时间。
形参和函数体内定义的变量仅在函数的作用域内可见,局部变量会隐藏在外层作用域中同名的其他所有声明中。
函数体外定义的对象存在于程序的整个执行过程中,程序启动时创建,程序结束时销毁。
自动对象
对于普通局部变量对应的对象,函数在定义它的地方创建该对象,在块末尾销毁它。存在于块执行期间的对象称为 自动对象(automatic object) 。
形参和局部变量都是自动对象,形参对应的自动对象用实参的值初始化;局部变量对应的自动对象如果含初始值,则用初始值初始化,如果不含则未定义。
局部静态对象
局部静态对象(local static object) 是将局部变量定义成 static 类型,其声明收齐贯穿函数调用及之后的时间。
练习 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;
}
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;
}
return 0;
}
1.2 函数声明
类似于变量,函数在使用前必须声明,只能定义一次,但可以是声明多次。如果函数没用用到,可以只声明不定义。
函数的声明与函数的定义类似,不包含函数体,也可以不包含形参。
函数的返回类型、函数名、形参类型描述了函数接口。函数声明也称作函数原型(function prototype)。
在头文件中进行函数声明
建议函数在头文件中声明,原文件中定义。
在头文件中声明函数就能确保同一函数的所有声明保持一致,如果想改变函数接口,只需改变一条声明即可。
练习 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
1.3 分离式编译
分离式编译(separate compilation) 允许我们把程序按照逻辑关系分割到几个文件中去,每个文件独立编译。这一过程通常会产生后缀名是 .obj(Windows) 或 .o(UNIX) 的文件,该文件包含 对象代码(object code)。之后编译器把对象文件 链接(link) 在一起形成可执行文件。
实例看练习:
练习 6.9:编写你自己的 fact.cc 和 factMain.cc ,这两个文件都应该包含上一小节的练习中编写的 Chapter6.h 头文件。通过这些文件,理解你的编译器是如何支持分离式编译的。
答:
fact.cpp:
#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.cpp:
#include <iostream>
#include "Chapter6.h"
using namespace std;
int main()
{
int num;
cout << "请输入一个数:";
cin >> num;
cout << num << " 的阶乘是:" << fact(num) << endl;
return 0;
}
前两个命令先将两个文件分别编译,第三个命令将两个 .o 文件链接成可执行程序。
也可以直接编译两个文件。
也可以直接对 cpp 文件编译链接。
2、参数传递
每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。形参初始化的机理与变量初始化一样。
当形参是引用类型时,我们说它对应的实参被 引用传递(passed by reference) 或者函数被传引用调用(called by reference)。引用形参是它绑定的对象的别名。
当形参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递(passed by value) 或者函数被传值调用(called by value)。
2.1 传值参数
初始化非引用类型形参,初始值被拷贝被形参,对形参的改动不会影响实参。
指针形参
指针形参也一样,形参指针的值与实参一样,但两者不是一个对象。可以通过形参指针来操作它所指的对象。
// 该函数接受一个指针,然后将指针所指的位置为0
void reset(int *ip) {
*ip = 0; // 改变指针ip所指对象的值
ip = 0; // 只改变了ip的局部拷贝,实参未被改变
}
调用 reset 函数之后, 实参所指的对象被置为0,但是实参本身并没有改变:
int i = 42;
reset(&i); // 改变i的值而非i的地址
cout << "i = " << i << endl; // 输出i = 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;
return 0;
}
// 在函数体内部交换了两个形参指针本身的值,未能影响实参
void mySwap(int *p, int *q){
int *tmp = p;
p = q;
q = tmp;
}
2.2 传引用参数
通过使用引用形参,函数可以改变实参的值。
// 该函数接受一个int对象的引用,然后将对象的位置为0
void reset(int &i) // i是传给reset函数的对象的另一个名字
{
i = 0; // 改变了i所引对象的值
}
引用形参绑定初始化它的对象。当调用 reset 时,i 绑定到传给函数的 int 对象,此时改变 i 也就是改变 i 所引用对象的值。
int j = 42;
reset(j); // j采用传引用方式,它的值被改变
cout << "j = " << j << endl; // 输出j = 0
i 是 j 的另一个名字,函数中对 i 赋 0 ,就是对实参 j 赋0。
使用引用避免拷贝
拷贝大的类类型对象或容器对象比较低效,另外有的类类型(如IO类型)根本就不支持拷贝操作,这时只能通过引用形参访问该类型的对象。
如果函数无须改变引用形参的值,最好将其声明为常量引用。
使用引用形参返回额外信息
一个函数只能返回一个值,然而有时函数需要同时返回多个值,这个时候可以使用引用形参让函数返回额外信息。
练习 6.11:编写并验证你自己的 reset 函数,使其作用于引用类型的参数。
答:
#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;
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;
return 0;
}
与使用指针相比,使用引用交换变量的内容从形式上看更简单一些,并且无须额外声明指针变量,也避免了拷贝指针的值。
在C++中,建议使用引用形参替代指针形参。
练习 6:13:假设 T 是某种类型的名字,说明以下两个函数声明的区别:
一个是 void f(T), 另一个是 void f(&T)。
答:
第一个函数是传值调用,对T的修改不会影响实参;第二个是传引用调用,对T的修改会影响到实参。
练习 6.14:举一个形参应该是引用类型的例子,再举一个形参不能是引用类型的例子。
答:当函数要返回多个值、操作大容量的容器、大对象时使用引用;
对整数求阶乘则不需要传引用。
练习 6.15:说明 find_char 函数中的三个形参为什么是现在的类型,特别说明为什么 s 是常量引用而 occurs 是普通引用?
为什么 s 和 occurs 是引用类型而 c 不是?
如果令 s 是普通引用会发生什么情况?
如果令 occurs 是常量引用会发生什么情况?
答:find_char 函数的三个参数的类型设定与该函数的处理逻辑密切相关,原因分别如下:
对于待查找的字符串 s 来说,为了避免拷贝长字符串,使用引用类型;同时我们只执行查找操作,无须改变字符串的内容,所以将其声明为常量引用。
对于待查找的字符 c 来说,它的类型是 char ,只占1字节,拷贝的代价很低,而且我们无须操作实参在内存中实际存储的内容,只把它的值拷贝给形参即可,所以不需要使用引用类型。
对于字符出现的次数 occurs 来说,因为需要把函数内对实参值的更改反应在函数外部,所以必须将其定义成引用类型,但是不能把它定义成常量引用,否则就不能改变所引的内容了。
2.3 const 形参和实参
关于const的更多细节可以参考primer第一章
形参有顶层 const 时,传给它常量的对象或者非常量对象都可以:
void fun(const int i){
}//调用时可以传入 const int 或者 int
指针或引用形参与 const
可以使用非常量对象初始化一个底层 const 形参(只有引用和指针有底层const),因为可以将底层 const 引用绑定或使底层 const 指向一个非常量对象身上。但反过来不可以。
尽量使用常量引用
把函数不会改变的形参定义成普通引用会极大地限制函数所能接受的实参类型,同时也会给别人一种误导,即函数可以修改实参的值。此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型。
练习 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 对象全部改写成小写形式。
在这两个函数中你使用的形参类型相同吗?为什么?
答:
#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;
}
return 0;
}
不相同,第一个不用修改实参内容,传进来的参数可能是非常量,也可能是常量(顶层const),所以要形参使用常量引用类型。
第二个需要修改实参内容,应将其设定为非常量引用。
练习 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.18:假定有如下声明,判断哪个调用合法、哪个调用不合法。对于不合法的函数调用,说明原因。
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)是合法的,int 可以转换为 double。
(d)是合法的,vec.begin() 和 vec.end() 的类型都是形参所需的 vector::iterator,第三个实参 3.8 可以自动转换为形参所需的 int 类型。
练习 6.20:引用形参什么时候应该是常量引用?如果形参应该是常量引用,而我们将其设为了普通引用,会发生什么情况?
答:如果需要修改参数的内容,则将其设置为普通引用类型;否则,如果不需要对参数内容做任何更改,最好设为常量引用类型。
如果把一个本来应该是常量引用的形参设成了普通引用类型,有可能遇到几个问题:
一是容易给使用者一种误导,即程序允许修改实参的内容;
二是限制了该函数所能接受的实参类型,无法把 const 对象、字面值常量或者需要类型转换的对象传递给普通的引用形参。
2.4 数组形参
数组不允许拷贝,使用数组时会被转换成指针。所以将数组传递给函数时传递的是指向数组首元素的指针。
尽管不能以传值方式传递数组,但是可以把形参写成类似数组的形式:
// 尽管形式不同,但这三个print函数是等价的
// 每个函数都有一个const int*类型的形参
void print(const int*);
void print(const int[]); // 可以看出来,函数的意图是作用于一个数组
void print(const int[10]); // 这里的维度表示我们期望数组含有多少元素,实际不一定
int i = 0, j[2] = {
0, 1};
print(&i); // 正确:&i的类型是int*
print(j); // 正确: j转换成int*并指向j[0]
因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外信息。管理指针形参有三种常用的技术:
- 使用标记指定数组长度
第一个方法,让数组本身包含一个结束标记。C风格字符串就是使用最后一个空字符来标记。
- 使用标准库规范
第二个方法,使用指向数组尾后的指针,可以使用end()
得到数组的尾后指针。
- 显式传递一个表示数组大小的形参
第三个方法,使用一个数表示数组大小。
数组形参和 const
如果函数不需要对数组元素执行写操作,应该把数组形参定义成指向 const 的指针。只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针。
数组引用形参
形参可以是数组的引用,引用绑定到对应的实参上,也就是绑定到数组上:
//正确: 形参是数组的引用,维度是类型的一部分
void print(int (&arr) [10]) {
for (auto elem : arr)
cout << elem << endl;
}
// &arr两端的括号必不可少:
// f(int &arr[10]) // 错误:将arr声明成了引用的数组
// f(int (&arr)[10]) // 正确:arr是具有10个整数的整型数组的引用
传递多维数组
将多维数组传递给函数时,真正传递的是指向数组首元素的指针,数组第二维(以及后面所有维度)的大小是数组类型的一部分,不能省略。
// matrix指向数组的首元素,该数组的元素是由10个整数构成的数组
void print(int (*matrix)[10], int rowSize) {
/* ... */ }
// *matrix两端的括号必不可少:
int *matrix[10]; // 10个指针构成的数组
int (*matrix)[10]; // 指向含有10个整数的数组的指针
// 等价定义
void print(int matrix[][10], int rowSize) {
/* ... */ }
练习 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;
return 0;
}
练习 6.22:编写一个函数,令其交换两个 int 指针。
答:
交换两个 int 指针就是交换两个指针的值,即交换指针所指的内存地址。
#include<iostream>
#include<typeinfo>
using namespace std;
//使用指针的引用从而改变指针的值。
int Swap(int *&a, int *&b){
int *temp = a;
a = b;
b = temp;
}
int main(){
int a{
1}, b{
2};
int *ap = &a, *bp = &b;
cout << ap << " " << bp << endl;
Swap(ap, bp); //不能直接传入 &a,&b 因为这两个是 a b 的地址,是常量。
cout << ap << " " << bp << endl;
cout << "a = " << *ap << '\n' << "b = " << *bp << endl;
return 0;
}
练习 6.23:参考本节介绍的几个 print 函数,根据理解编写你自己的版本。
依次调用每个函数使其输入下面定义的 i 和 j:
答:
实现了三个版本的 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;
print2(&i, 1);
//计算得到数组j的容量
print2(j, sizeof(j) / sizeof(*j));
cout << endl;
auto b = begin(j);
auto e = end(j);
print3(b, e);
return 0;
}
练习 6.24:描述下面这个函数的行为。如果代码中存在问题,请指出并改正。
void print(const int ia[10])
{
for (size_t i = 0; i != 10; ++i)
cout << ia[i] << endl;
}
答:rint 函数的定义存在一个潜在风险,即虽然我们期望传入的数组维度是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;
}
2.5 main:处理命令行选项
可以在命令行中向 main 函数传递参数,形式如下:
int main(int argc, char *argv[]) {
/*...*/ }
int main(int argc, char **argv) {
/*...*/ }
- argc 表示数组中字符串的数量
- argv 是一个数组,数组元素是指向 C 风格字符串的指针。
当使用argv中的实参时,一定要记得可选的实参从argv[1]开始;argv[0]保存程序的名字,而非用户输入。
练习 6.25:编写一个 main 函数,令其接受两个实参。把实参的内容连接成一个 string 对象并输出出来。
答:
#include<iostream>
#include<string>
using namespace std;
int main(int argc, char **argv){
string result{
};
for(int i = 1;i != argc; ++i){
result += argv[i];
}
cout << result << endl;
return 0;
}
练习 6.26:编写一个程序,使其接受本节所示的选项;输出传递给 main 函数的实参内容。
答:
说实话这题没看明白啥意思。
#include <iostream>
using namespace std;
int main(int argc, char **argv){
for (int i = 1; i != argc; ++i){
cout << "argc[" << i << "]:" << argv[i] << endl;
}
return 0;
}
2.6 含有可变形参的函数
C++11新标准提供了两种主要方法处理实参数量不定的函数:
- 如果实参类型相同,可以使用 initializer_list 标准库类型;
- 如果实参类型不同,可以定义可变参数模板。
- C++还可以使用省略符形参传递可变数量的实参,但这种功能一般只用在与C函数交换的接口程序中。
initializer_list 形参
initializer_list 是一种标准库类型,定义在头文件 initializer_list 中,表示某种特定类型的值的数组。
提供如下操作:
- 和 vector 一样,initializer_list 也是一种模板类型,定义对象时,必须说明列表中所含元素的类型。
- 和 vector 不一样的是,initializer_list 对象中的元素永远是常量值,无法改变。
使用如下的形式编写输出错误信息的函数,使其可以作用于可变数量的实参:
void error_msg(initializer_list<string> il)
{
for (auto beg = il.begin(); beg != il.end(); ++beg)
cout << *beg << " " ;
cout << endl;
}
如果想向 initializer_list 形参传递一个值的序列,则必须把序列放在一对花括号内:
// expected和actual是string对象
if (expected != actual)
error_msg({
"functionX", expected, actual});
else
error_msg({
"functionX", "okay"});
含有 initializer_list 形参的函数也可以同时拥有其他形参,下面例子将会展示,同时对 initializer_list 的遍历也可以使用范围 for 语句:
void error_msg(ErrCode e, initializer_list<string> il)
{
cout << e.msg() << ": ";
for (const auto &elem : il)
cout << elem << " " ;
cout << endl;
}
省略符形参
省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为 varargs 的C标准库功能。通常,省略符形参不应该用于其他目的。
省略符形参应该仅仅用于C和C++通用的类型,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
省略符形参只能出现在形参列表的最后一位:
void foo(parm_list, ...);
void foo(...);
练习 6.27:编写一个函数,它的参数是 initializer_list 类型的对象,函数的功能是计算列表中所有元素的和。
答:
#include <iostream>
#include <initializer_list>
using namespace std;
int iCount(initializer_list<int> il){
int count = 0;
// 遍历il的每一个元素
for (const auto &val : il)
count += val;
return count;
}
int main(){
// 使用列表初始化的方式构建initializer_list<int>对象
// 然后把它作为实参传递给函数iCount
cout << "sum of 1,6,9:" << iCount({
1, 6, 9 }) << endl;
cout << "sum of 4,5,9,18: " << iCount({
4, 5, 9, 18 }) << endl;
cout << "sum of 10,10,10,10,10,10,10,10,10: "
<< iCount({
10, 10, 10, 10, 10, 10, 10, 10, 10 }) << endl;
return 0;
}
练习 6.28:在 error_msg 函数的第二个版本中包含 ErrCode 类型的参数,其中循环内的 elem 是什么类型?
答:const string &。
练习 6.29:在范围 for 循环中使用 initializer_list 对象时,应该将循环控制变量声明成引用类型吗?为什么?
答:引用类型的优势主要是可以直接操作所引用的对象以及避免拷贝较为复杂的类型对象和容器对象。因为 initializer_list 对象的元素永远是常量值,所以我们不可能通过设定引用类型来更改循环控制变量的内容。
只有当 initializer_list 对象类型是类类型或容器类型(比如 string)时,才有必要把范围 for 循环的循环控制变量设为引用类型。
总之initializer_list 中的对象不能修改就行了。
3、返回类型和return语句
return 语句终止当前正在执行的函数并将控制权返回到调用该函数的地方,有两种形式:
return;
return expression;
3.1 无返回值函数
无返回值的 return 用于返回类型是 void 的函数中。如果没有显式 return 语句,最后一行会隐式执行 return。
要想让程序在中间退出可以使用 return。
3.2 有返回值函数
只要函数的返回类型不是 void,该函数内的每条 return 语句就必须返回一个值,并且返回值的类型必须与函数的返回类型相同,或者能隐式地转换成函数的返回类型(main 函数例外)。
关于return,有以下两点要注意:
- return 语句没有返回值是错误的,编译器能检测到这个错误。
- 在含有 return 语句的循环后面应该也有一条 return 语句,否则程序就是错误的,但很多编译器无法发现此错误。
值是如何被返回的
函数返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。如果函数返回引用类型,则该引用仅仅是它所引用对象的一个别名。
string make_plural(size_t ctr, const string &word, const string &ending){
return (ctr > 1) ? word + ending : word;
}
这个函数的返回值就是 word 的副本或者一个临时 string 对象。
const string &shorterString(const string &s1, const string &s2){
return s1.size() <= s2.size() ? s1:s2;
}
这个函数返回两个实参的引用之一,不会拷贝。
不要返回局部对象的引用或指针
函数完成后,它里面的存储空间也会被释放,所以不能返回函数内局部变量的引用。
// 严重错误: 这个函数试图返回局部对象的引用
const string &manip()
{
string ret;
// 以某种方式改变一下ret
if (!ret.empty())
return ret; // 错误:返回局部对象的引用!
else
return "Empty"; // 错误:"Empty"是一个局部临时量
}
返回局部对象的引用是错的;返回局部对象的指针也是错的,只能通过初始化临时量或者实参的引用的方式返回。
引用返回左值
调用返回引用的函数得到左值,其他类型得到右值。可以像使用其他左值那样来使用返回引用的函数调用,包括为其赋值:
char &get_val(string &str, string::size_type ix)
{
return str[ix]; // get_val assumes the given index is valid
}
int main()
{
string s("a value");
cout << s << endl; // prints a value
get_val(s, 0) = 'A'; // changes s[0] to A
cout << s << endl; // prints A value
return 0;
}
但是返回常量引用的就不能对其赋值了,道理大家都懂。
列表初始化返回值
C++11规定,函数可以返回用花括号包围的值的列表。同其他返回类型一样,列表也用于初始化表示函数调用结果的临时量。如果列表为空,临时量执行值初始化;否则,返回的值由函数的返回类型决定。
- 如果函数返回内置类型,则列表内最多包含一个值,且该值所占空间不应该大于目标类型的空间。
- 如果函数返回类类型,由类本身定义初始值如何使用。
主函数 main 的返回值
如果函数的返回类型不是 void,那么它必须返回一个值,除了,main 函数可以没有 return 语句直接结束。如果控制流到达了 main 函数的结尾处并且没有return语句,编译器会隐式地插入一条返回0的 return 语句。
main 函数的返回值可以看作是状态指示器。返回0表示执行成功,返回其他值表示执行失败,其中非0值的具体含义依机器而定。
为了使 main 函数的返回值与机器无关,头文件 cstdlib 定义了 EXIT_SUCCESS 和 EXIT_FAILURE 这两个预处理变量,分别表示执行成功和失败。
int main()
{
if (some_failure)
return EXIT_FAILURE; // 定义在cstdlib头文件中
else
return EXIT_SUCCESS; // 定义在cstdlib头文件中
}
因为他们是预处理变量,所以既不能在前面加上 std::,也不能在using声明中出现。
递归
如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为 递归函数(recursive function)。
// 计算val!,即1 * 2 * 3 . . . * val
int factorial(int val)
{
if (val > 1)
return factorial(val-1) * val;
return 1;
}
在递归函数中,一定有某条路径是不包含递归调用的,否则函数会一直递归下去,直到程序栈空间耗尽为止。
相对于循环迭代,递归的效率较低,但在某些情况下使用递归可以增加代码的可读性。
循环迭代适合处理线性问题(如链表,每个节点有唯一前驱、唯一后继),
而递归适合处理非线性问题(如树,每个节点的前驱、后继不唯一)。
练习 6.30:编译第200页的str_subrange函数,看看你的编译器是如何处理函数中的错误的。
答:
#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;
}
}
练习 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");
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);
return 0;
}
练习 6.34:如果 factorial 函数的停止条件如下所示,将发生什么?if (val != 0)
答:如果递归函数的参数类型是 int,所以理论上用户传入 factorial 函数的参数可以是负数。按照原程序的逻辑,参数是负数时函数的返回值是1.
如果修改递归函数的停止条件,则当参数的值为负时,会依次递归下去,执行连续乘法操作直至溢出。因此,不能把 if 语句的条件改成上述形式。
练习 6.35:在调用 factorial 函数时,为什么我们传入的值是 val-1 而非 val–?
答:如果把传入的值 val-1 改成 val–,则出现一种我们不期望看到的情况,即变量的递减操作与读取变量值的操作共存于同一条表达式中,加号运算符也未规定求值顺序,所以有可能产生未定义的值。
3.3 返回数组指针
因为数组不能拷贝,所以函数不能返回数组,但可以返回数组的指针或引用。
可以使用类型别名简化返回的数组指针或引用:
typedef int arrT[10]; // arrT is a synonym for the type array of ten ints
using arrtT = int[10]; // equivalent declaration of arrT; see § 2.5.1 (p. 60)
arrT* func(int i); // func returns a pointer to an array of five ints
func 返回一个指向含有 10 个整数的数组的指针。
声明一个返回数组指针的函数
先来看一下指向数组的指针是怎么定义的:
int arr[10];
int (*p)[10] = &arr;
先从括号里面看 p 是一个指针,再看两侧,说明p是个指向含有十个整数数组的指针。
类似的,返回数组指针的函数形式如下:
Type (*function(parameter_list))[dimension]
其中 Type 表示元素类型,dimension 表示数组大小,(*function (parameter_list)) 两端的括号必须存在,如果没有这对括号,函数的返回类型将是指针的数组。
没有使用类型别名的 func 函数:
int (*func(int))[10];
// 可以按下面解释理解,也可以对照 p 的定义理解。
// func(int i)表示调用func函数时需要一个int类型的实参
// (*func(int i))意味着可以对函数调用的结果执行解引用操作
// (*func(int i))[10]表示解引用func的调用将得到一个大小是10的数组
// int(*func(int i))[10]表示数组中的元素是int类型
使用尾至返回类型
尾至返回类型(trailing return type) 可以简化 func 的声明。任何函数都能使用尾至返回,但是对返回类型复杂的函数有效。
尾至返回类型跟在形参列表后面并以一个 -> 开头。为了表示函数真正的返回类型在形参列表之后,需要在本应出现返回类型的地方添加 auto 关键字。
// func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
auto func(int i) -> int(*)[10];
使用 decltype
如果我们知道函数返回的指针将指向哪个数组,就可以使用 decltype 关键字声明返回类型。但 decltype 并不会把数组类型转换成指针类型,所以还要在函数声明中添加一个 * 符号。
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.36:编写一个函数声明,使其返回数组的引用并且该数组包含10个 string 对象。不用使用尾置返回类型、decltype 或者类型别名。
答:
string (&func())[10];
练习 6.37:
答:
using arrofstring= string [10];
arrofstring &func();
auto func -> string (&)[10];
string arr[10];
decltype(arr) &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; // 返回一个指向数组的指针
}
答:
int odd[] = {
1,3,5,7,9};
int even[] = {
0,2,4,6,8};
decltype(odd) &arrPtr(int i)
{
return (i % 2) ? odd : even; // 返回一个指向数组的指针
}
4、函数重载
同一作用域内的几个名字相同但形参列表不同的函数叫做 重载(overloaded)函数。
void print(const char *cp);
void print(const int *beg, const int *end);
void print(const int ia[), size_t ze);
编译器会根据传递的实参类型推断使用哪个函数:
int j[2] = {
0,1};
print("Hello World"); // calls print(const char*)
print(j, end(j) - begin(j)); // calls print(const int*, size_t)
print(begin(j), end(j)); // calls print(const int*, const int*)
定义重载函数
不允许两个函数除了返回类型以外的其他所有要素都相同。
Record lookup(const Account&);
bool lookup(const Account&); // 错误:与上一个函数相比只有返回类型不同
判断两个形参的类型是否相异
形参相异的才叫重载,相同就是重复定义了。
// each pair declares the same function
Record lookup(const Account &acct);
Record lookup(const Account&); // parameter names are ignored
typedef Phone Telno;
Record lookup(const Phone&);
Record lookup(const Telno&); // Telno and Phone are the same type
重载和 const 形参
顶层 const 不影响传入函数的对象,一个拥有顶层 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*); // 新函数,作用于指向常量的指针
const_cast 和 重载
const_cast 可以用于函数的重载。
当函数的实参是常量时,返回的结果仍然是常量的引用。
// 比较两个string对象的长度,返回较短的那个引用
const string &shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
当函数的实参不是常量时,将得到普通引用。
string &shorterString(string &s1, string &s2)
{
auto &r = shorterString(const_cast<const string&>(s1),
const_cast<const string&>(s2));
return const_cast<string&>(r);
}
调用重载的函数
函数匹配(function matching) 也叫做 重载确定(overload resolution),是指编译器将函数调用与一组重载函数中的某一个进行关联的过程。
编译器首先将调用的实参与重载集合中每一个函数的形参进行比较,然后根据比较的结果决定到底调用是哪个函数。
调用重载函数时有三种可能的结果:
- 编译器找到一个与实参 最佳匹配(best match) 的函数,并生成调用该函数的代码。
- 编译器找不到任何一个函数与实参匹配,发出 无匹配(no match) 的错误信息。
- 有一个以上的函数与实参匹配,但每一个都不是明显的最佳选择,此时编译器发出 二义性调用(ambiguous call) 的错误信息。
练习 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++规定重载函数必须在形参数量或形参类型上有所区别。如果两个同名函数的形参数量和类型都一样,那么即使返回类型不同也不行。
(c)的两个函数是重载关系,它们的形参类型有区别。
4.1 重载与作用域
在内层作用域声明的名字,将覆盖外层的同名实体。
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(doub1e)被隐藏掉了
}
5、特殊用途语言特性
5.1 默认实参
默认实参 作为形参的初始值出现在形参列表中。可以为一个或多个形参定义默认值,不过一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
使用默认实参调用函数
调用含有默认实参的函数时可以省略该实参:
string window;
window = screen(); // equivalent to screen(24,80,' ')
window = screen(66);// equivalent to screen(66,80,' ')
window = screen(66, 256); // screen(66,256,' ')
window = screen(66, 256, '#'); // screen(66,256,'#')
默认参数负责填补函数调用缺少的尾部实参(右侧的),下面的第一个方法省略头部的就会出错,第二个将字符的ascll值传给第一个参数,后两个默认初始化。
window = screen( , , '?');
window = screen('?');
默认实参声明
虽然多次声明同一个函数是合法的,但是同一作用域同一函数的声明的一个形参只能被赋予一次默认实参:
// 表示高度和宽度的形参没有默认位
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);
string window = screen(); // 调用screen(ht(), 80,' ')
void f2()
{
def = '*'; // 改变默认实参的值
sz wd = 100; // 隐藏了外层定义的wd,但是没有改变默认值
window = screen(); // 调用screen(ht(), 80, '*')
}
用作默认实参的名字在函数声明所在的作用域内解析,这些名字的求值过程发生在函数调用时。比如 f2() 里面的def,f2() 内的 wd 与 screen 的默认参数无关,所以没生效。
练习 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错误,因为默认实参只能位于形参列表的尾部。
练习 6.41:下面的哪个调用是非法的?为什么?哪个调用虽然合法但显然与程序员的初衷不符?为什么?
char *init(int ht, int wd = 80, char bckgrnd = ' ');
(a) init();
(b) init(24,10);
(c) init(14,'*');
答:a不正确,至少需要传入一个形参对第一个参数初始化。
b正确,两个参数对前两个形参初始化。
c正确,语法上是合法的,但是与程序的原意不符。从语法上来说,第一个实参对应第一个形参 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;
return 0;
}
5.2 内联函数和 constexpr 函数
内联函数可避免函数调用的开销
内联函数会在每个调用点上“内联地”展开,省去函数调用所需的一系列工作。
定义内联函数时需要在函数的返回类型前添加关键字 inline。
// 内联版本:寻找两个string对象中较短的那个
inline const string &
shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
一般来说,内联机制适用于优化规模较小、流程直接、调用频繁的函数。内联函数中不允许有循环语句和 switch 语句,否则函数会被编译为普通函数。
constexpr 函数
constexpr 函数是指能用于常量表达式的函数。constexpr 函数的返回类型及所有形参的类型都得是字面值类型。函数体内只能有一条return语句:
constexpr int new_sz() {
return 42; }
constexpr int foo = new_sz(); // ok: foo is a constant expression
constexpr 函数的返回值可以不是一个常量。
// 如果arg是常量表达式,则scale(arg)也是常量表达式
constexpr size_t scale(size_t cnt)
{
return new_sz() * cnt;
}
int arr[scale(2)]; //正确:scale(2)是常量表达式
int i = 2; //i 不是常量表达式
int a2[scale(i)]; //错误:scale(i) 不是常量表达式。
和其他函数不同,内联函数和 constexpr 函数可以在程序中多次定义。因为在编译过程中,编译器需要函数的定义来随时展开函数,仅有函数的声明是不够的。对于某个给定的内联函数或 constexpr 函数,它的多个定义必须完全一致。因此内联函数和 constexpr 函数通常定义在头文件中。
练习 6.43:你会把下面的哪个声明和定义放在头文件中?哪个放在源文件中?为什么?
(a) inline bool eq(const BigInt&, const BigInt&) {
...}
(b) void putValues(int *arr, int size);
答:a 应该放到头文件中,在编译过程中,编译器需要函数的定义来随时展开函数,仅有函数的声明是不够的。对于某个给定的内联函数或 constexpr 函数,它的多个定义必须完全一致。因此内联函数和 constexpr 函数通常定义在头文件中。
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.46:能把isShorter函数定义成constexpr函数吗?如果能,将它改写成constxpre函数;如果不能,说明原因。
答:constexpr 函数是指能用于常量表达式的函数,constexpr 函数的返回类型和所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条 return 语句。
显然 isShorter 函数不符合 constexpr 函数的要求,它虽然只有一条 return 语句,但是返回的结果调用了标准库 string 类的 size() 函数和 < 比较符,无法构成常量表达式,因此不能改写成 constexpr 函数。
5.3 调试帮助
assert 预处理宏
assert 是一种预处理宏,定义在 cassert 头文件中。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。assert 宏使用一个表达式作为它的条件:
assert(expr);
首先对 expr 求值,如果表达式为假(即0),assert 输出信息并终止程序的执行;如果
表达式为真(即非0),assert什么也不做。
因为他是预处理变量,所以既不能在前面加上 std::,也不能在using声明中出现。
NDEBUG 预处理变量
assert 的行为依赖于于一个名为 NDEBUG 的预处理变量的状态。
- 如果定义了 NDEBUG,则 assert 什么也不做;
- 默认状态下没有定义 NDEBUG,此时 assert 将执行运行时检查。
可以用 #define 定义NDEBUG,也可以是命令行选项定义预处理变量:
g++ -D NDEBUG main.c
可以使用 NDEBUG 编写自己的条件调试代码:
void print(const int ia[], size_t size)
{
#ifndef NDEBUG
// _ _func_ _ is a local static defined by the compiler that holds the function's name
cerr << _ _func_ _ << ": array size is " << size << endl;
#endif
// ...
func 是 const char 的静态数组用于输出当前函数的名字。
还有4个对调试有用的名字:
练习 6.47:改写6.3.2节练习中使用递归输出 vector 内容的程序,使其有条件地输出与执行过程有关的信息。例如,每次调用时输出 vector 对象的大小。分别在打开和关闭调试器的情况下编译并执行这个程序。
void print(vector<int> vInt, unsigned index){
unsigned sz = vInt.size();
if (!vInt.empty() && index < sz){
cout << vInt[index] << endl;
print(vInt, index + 1);
}
}
答:
#include <iostream>
#include <vector>
using namespace std;
void print(vector<int> vInt, unsigned index){
#ifndef NDEBUG
cout << "length: " << vInt.size() << endl;
#endif
size_t 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};
print(v, 0);
return 0;
}
练习 6.48:说明下面这个循环的含义,它对assert的使用合理吗?
string s;
while (cin >> s && s != sought) {
} // 空函数体
assert(cin);
答:该程序对 assert 的使用有不合理之处。
程序执行到 assert 的原因可能有两个,一是用户终止了输入,二是用户输入的内容正好与 sought 的内容一样。如果用户尝试终止输入(事实上用户总有停止输入结束程序的时候),则 assert 的条件为假,输出错误信息,这与程序的原意是不相符的。
6、函数匹配
确定候选函数与可行函数
函数实参类型与形参类型越接近,它们匹配得越好。
重载函数集中的函数称为 候选函数(candidate function):
- 一是与被调用的函数同名;
- 二是其声明在调用点可见。
可行函数(viable function):
- 一是形参数量与函数调用所提供的实参数量相等;
- 二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。
如果没找到可行函数,编译器讲报告无匹配函数的错误。
调用重载函数时应该尽量避免强制类型转换。
练习 6.49:什么是候选函数?什么是可行函数?
答:当程序中存在多个同名的重载函数时,编译器需要判断调用的是其中哪个函数,这时就有了候选函数和可行函数两个概念。函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数。函数匹配的第二步是考查本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数。
练习 6.50:已知有第217页对函数 f 的声明,对于下面的每一个调用列出可行函数。其中哪个函数是最佳匹配?如果调用不合法,是因为没有可匹配的函数还是因为调用具有二义性?
void f();
void f(int);
void f(int,int);
void f(double, double = 3.14);
(a) f(2.56, 42)
(b) f(42)
(c) f(42, 0)
(d) f(2.56, 3.14)
答:
(a) f(2.56, 42) //void f(double, double = 3.14) or void f(int,int) 具有二义性。
(b) f(42) //void f(int) or void f(double, double = 3.14). void f(int) best.
(c) f(42, 0) //void f(int,int) or void f(double, double = 3.14). first is best
(d) f(2.56, 3.14) //void f(int,int) or void f(double, double = 3.14). second is best
练习 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);
return 0;
}
6.1 实参类型转换
为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体如下:
所有算术类型转换的级别都一样。
如果重载函数的区别在于它们的引用或指针类型的形参是否含有底层 const,或者指针类型是否指向 const,则调用发生时编译器通过实参是否是常量来决定函数的版本。
Record lookup(Account&); // 函数的参数是Account的引用
Record lookup(const Account&); // 函数的参数是一个常量引用
const Account a;
Account b;
lookup(a); // 调用lookup(const Account&)
lookup(b); // 调用lookup(Account&)
练习 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,能与普通指针区分。
c有影响,两个函数的区别是它们的指针类型的形参本身是否是常量,属于顶层 const,根据本节介绍的匹配规则可知,向实参添加顶层 const 或者从实参中删除顶层 const 属于精确匹配,无法区分两个函数。
7、函数指针
要想声明一个可以指向某种函数的指针,只需要用指针替换函数名称即可。
// 比较两个string对象的长度
bool lengthCompare(const string &, const string &);
// pf指向一个函数,该函数的参数是两个const string的引用,返回值是bool类型
bool (*pf)(const string &, const string &); // uninitialized
*pf 两端的括号必不可少!!!如果不写这对括号,则 pf 是一个返回值为 bool 指针的函数:
// 声明一个名为pf的函数,该函数返回bool*
bool *pf(const string &, const string &);
使用函数指针
可以直接使用指向函数的指针来调用函数,无须提前解引用指针。
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)
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);
调用函数时可以直接把函数当做实参,它会自动转换为指针。
关键字 decltype 作用于函数时,返回的是函数类型,而不是函数指针类型。
使用类型别名和 decltype 能简化函数指针的使用:
//function
typedef bool func(const string&, const string&);
typedef decltype(lengthCompare) func2; //等价
//pointer to function
typedef bool (*pfun)(const string &, const string &);
typedef decltype(lengthCompare) *pfun2; //等价
useBigger 就可以换成如下的声明:
void useBigger(const string &s1, const string &s2, func);
void useBigger(const string &s1, const string &s2, pfun);
返回指向函数的指针
程序无法返回函数,但能返回函数指针。
使用类型别名比较简单:
//function
using f = int(int*, int);
typedef int f(int*, int);
//pointer to function
using pf = int(*)(int*, int);
typedef int (*f)(int *, int);
和函数类型的形参不一样,返回类型不会自动转换为指针,需要显示指定:
pf f1(int); //正确:f1返回指向函数的指针。
f f1(int); //错误:f1不能返回一个函数
f *f1(int); //正确:显式指定返回的是函数的指针。
不使用类型别名:
int (*f1(int))(int *, int);
类比函数指针:
int (*p)(int *, int);
将 p 换为函数的声明即可表明这个函数返回的是函数指针。
使用尾至返回类型:
auto f1(int) -> int (*) (int *, int);
**练习 6.54**:编写函数的声明,令其接受两个 int 形参并返回类型也是 int;然后声明一个 vector 对象,令其元素是指向该函数的指针。
答:
int func(int, int);
vector<decltype(func)*> fv;
练习 6.55:编写4个函数,分别对两个 int 值执行加、减、乘、除运算;在上一题创建的 vector 对象中保存指向这些函数的指针。
答:
看下一题
练习 6.56:
答:
#include <iostream>
#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 mul(int a, int b)
{
return a * b;
}
int dev(int a, int b)
{
return a / b;
}
int main(int argc, char **argv)
{
int a = atoi(argv[1]);
int b = atoi(argv[2]);
int func(int, int);
decltype(func) *arr[4] = {
add, sub, mul, dev};
vector<decltype(func) *> fv(arr, arr + 4);
vector<string> S = {
"a + b = ", "a - b = ", "a * b = ", "a / b = "};
int i = 0;
for (auto fun : fv)
{
cout << S[i++] << fun(a, b) << endl;
}
return 0;
}
关注博仔不迷路,一起学好C++