《C++ Primer》学习笔记(六):C++模块设计——函数

欢迎关注WX公众号:【程序员管小亮】

专栏C++学习笔记

《C++ Primer》学习笔记/习题答案 总目录

——————————————————————————————————————————————————————

📚💻 Cpp-Prime5 + Cpp-Primer-Plus6 源代码和课后题

C++模块设计——函数

1、函数基础

函数是一个命名了的代码块,通过调用函数执行相应的代码。函数可以有0个或多个参数,而且(通常)会产生一个结果。可以重载函数,也就是说,同一个名字可以对应几个不同的函数。

一个典型的 函数(function) 定义包括:返回类型(return type)函数名字、由0个或多个形式参数(parameter,简称形参)组成的 列表函数体(function body)。函数执行的操作在语句块中说明,即函数体。

n的阶乘是从1到n所有数字的乘积,程序如下:

// 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;     	// 返回结果
}

程序通过 调用运算符(call operator) 来执行函数。调用运算符的形式之一是一对圆括号 (),作用于一个表达式,该表达式是函数或者指向函数的指针;圆括号内是一个用逗号隔开的 实际参数(argument,简称实参) 列表,用来初始化函数形参。调用表达式的类型就是函数的返回类型。

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 语句中的值(如果有的话)。
  • 将控制权从被调函数转移回主调函数,函数的返回值用于初始化调用表达式的结果,之后继续完成调用所在的表达式的剩余部分。

实参是形参的初始值,两者的顺序和类型必须一一对应,相应的数量也要一致。

函数的形参列表可以为空,但是不能省略。

void f1() { /* ... */ }      // 隐式地定义空形参列表
void f2(void) { /* ... */ }  // 显式地定义空形参列表

形参列表中的形参通常用逗号隔开,每个形参都是含有一个声明符的声明,即使两个形参类型一样,也必须把两个类型声明都写出来。

int f3(int v1, v2) { /* ... */ }      // 错误
int f4(int v1, int v2) { /* ... */ }  // 正确

函数的任意两个形参不能同名,而且函数最外层作用域中的局部变量也不能使用与函数形参一样的名字。形参的名字是可选的,但是由于无法使用未命名的形参,所以形参一般都应该有个名字,即使某个形参不被函数使用,也必须为它提供一个实参。

函数的返回类型不能是数组类型或者函数类型,但可以是指向数组或函数的指针。

1)局部对象

在C++语言中,名字有作用域,对象有 生命周期(lifetime)

  • 名字的作用域是程序文本的一部分, 名字在其中可见;
  • 对象的生命周期是程序执行过程中该对象存在的一段时间。

形参和函数体内定义的变量统称为 局部变量(local variable),仅在函数的作用域内可见。

只存在于块执行期间的对象称为 自动对象(automatic object),当块的执行结束后,块中创建的自动对象的值就变成未定义的了。形参是一种自动对象,在函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁。

局部静态对象(local static object) 在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序结束才被销毁,对象所在的函数结束执行并不会对它产生影响。在变量类型前添加关键字 static 可以定义局部静态对象。

size_t count calls() {
	static size_t ctr = 0; // 调用结束后,这个值仍然有效
	return ++ctr;
}
int main () {
	for (size_t i = 0; i != 10; ++i)
		cout << count_calls() << endl;
	return 0;
}

如果局部静态对象没有显式的初始值,它将执行值初始化,内置类型的局部静态变量初始化为0 。

2)函数声明

和其他名字一样,函数的名字也必须在使用之前声明。和变量类似,函数只能定义一次,但可以声明多次,函数声明也叫做 函数原型(function prototype)。函数的声明和函数的定义非常类似,唯一的区别是函数声明无须函数体,用一个分号
替代即可。

函数应该在头文件中声明,在源文件中定义。定义函数的源文件应该包含含有函数声明的头文件,编译器负责验证函数的定
义和声明是否匹配。

3)分离式编译

分离式编译(separate compilation) 允许我们把程序按照逻辑关系分割到几个文件中去,每个文件独立编译。这一过程通常会产生后缀名是 .obj(Windows) 或 .o(UNIX) 的文件,该文件包含 对象代码(object code)。之后编译器把对象文件 链接(link) 在一起形成可执行文件。

2、参数传递

每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。形参初始化的机理与变量初始化一样。

形参的类型决定了形参和实参交互的方式:

  • 当形参是引用类型时,它对应的实参被 引用传递(passed by reference),函数被 传引用调用(called by reference)。引用形参是它对应实参的别名。
  • 当形参不是引用类型时,形参和实参是两个相互独立的对象,实参的值会被拷贝给形参(值传递,passed by value),函数被 传值调用(called by value)

1)传值参数

如果形参不是引用类型,则函数对形参做的所有操作都不会影响实参。

使用指针类型的形参可以访问或修改函数外部的对象。

#include <iostream>
using namespace std;
int main()
{
	int n = 0, i = 42;
	int *p = &n, *q = &i; // p指向n; q指向i
	*p = 42;			  // n的值改变; p不变
	p = q;				  // p现在指向了i; 但是i和n的值都不变
	
	cout << "n:" << n << endl;
	cout << "i:" << i << endl;
	system("pause");
	return 0;
}

在这里插入图片描述
指针形参的行为与之类似:

// 该函数接受一个指针,然后将指针所指的位置为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

在这里插入图片描述
如果想在函数体内访问或修改函数外部的对象,建议使用引用形参代替指针形参。

2)传引用参数

通过使用引用形参,函数可以改变实参的值。

#include <iostream>
using namespace std;

int main()
{
	int n = 0, i = 42;
	int &r = n; 		// r绑定了n(即r是n的另一个名字)
	r = 42;				// 现在n的值是42
	r = i;				// 现在n的值和i相同
	cout << "n = " << n << endl;
	system("pause");
	return 0;
}

在这里插入图片描述
引用形参的行为与之类似:

// 该函数接受一个int对象的引用,然后将对象的位置为0
void reset(int &i)  			// i是传给reset函数的对象的另一个名字
{
    i = 0;  					// 改变了i所引对象的值
}

引用形参绑定初始化它的对象,即引用直接传入对象而无须传递对象的地址。

int j = 42;
reset(j);						// j采用传引用方式,它的值被改变
cout << "j = " << j << endl;	// 输出j = 0

在这里插入图片描述
使用引用形参可以避免拷贝操作,拷贝大的类类型对象或容器对象比较低效,另外有的类类型(如IO类型)根本就不支持拷贝操作,这时只能通过引用形参访问该类型的对象。

除了内置类型、函数对象和标准库迭代器外,其他类型的参数建议以引用方式传递。

如果函数无须改变引用形参的值,最好将其声明为常量引用。

一个函数只能返回一个值,然而有时函数需要同时返回多个值,这个时候可以使用引用形参让函数返回额外信息。

3)const形参和实参

关于 const,写了一个博客——【C++100问】深入理解理解顶层const和底层const,如果有必要,可以写一个这个系列的文章。

当形参有顶层 const 时,传递给它常量对象或非常量对象都是可以的。

void fcn(const int i) { /* fcn能够读取i,但是不能向i写值 */ }

调用 fcn 函数时,既可以传入 const int 也可以传入 int。忽略掉形参的顶层 const 可能产生意想不到的结果:

void fcn(const int i) { /* fcn能够读取i,但是不能向i写值 */ }
void fcn(int i) { /* ... */ } 	// 错误:重复定义了fcn(int)

可以使用非常量对象初始化一个底层 const 形参,但是反过来不行。

把函数不会改变的形参定义成普通引用会极大地限制函数所能接受的实参类型,同时也会给别人一种误导,即函数可以修改实参的值。此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型。

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]

因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外信息。管理指针形参有三种常用的技术:

  • 要求数组本身包含一个结束标记;
  • 传递指向数组首元素和尾后元素的指针;
  • 专门定义一个表示数组大小的形参。

以数组作为形参的函数必须确保使用数组时不会越界。

如果函数不需要对数组元素执行写操作,应该把数组形参定义成指向 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) { /* ... */ }

5)main:处理命令行选项

可以在命令行中向 main 函数传递参数,形式如下:

int main(int argc, char *argv[]) { /*...*/ }
int main(int argc, char **argv) { /*...*/ }
  • 第一个形参 argc 表示数组中字符串的数量;
  • 第二个形参 argv 是一个数组,数组元素是指向C风格字符串的指针。

当实参传递给 main 函数后,argv 的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参,最后一个指针之后的元素值保证为0。

当使用argv中的实参时,一定要记得可选的实参从argv[1]开始;argv[0]保存程序的名字,而非用户输入。

Visual Studio2013 中可以设置 main 函数调试参数:

在这里插入图片描述

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 对象不会拷贝列表中的元素,拷贝后,原始列表和副本共享元素。initializer_list 对象中的元素永远是常量值。如果想向 initializer_list 形参传递一个值的序列,则必须把序列放在一对花括号内。

// expected和actual是string对象
if (expected != actual)
    error_msg({"functionX", expected, actual});
else
    error_msg({"functionX", "okay"});

因为 initializer_list 包含 beginend 成员,所以可以使用范围 for 循环处理其中的元素。

省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为 varargs 的C标准库功能。通常,省略符形参不应该用于其他目的。

省略符形参应该仅仅用于C和C++通用的类型,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。

void foo(parm_list, ...);
void foo(...);

3、返回类型和return语句

return 语句有两种形式,作用是终止当前正在执行的函数并返回到调用该函数的地方。

return;
return expression;

1)无返回值函数

没有返回值的 return 语句只能用在返回类型是 void 的函数中。返回 void 的函数可以省略 return 语句,因为在这类函数的最后一条语句后面会隐式地执行 return

通常情况下,如果 void 函数想在其中间位置提前退出,可以使用 return 语句,return 的这种用法有点类似于用 break 语句退出循环。

void swap (int &vl , int &v2) {
	// 如果两个值是相等的,则不需要交换,直接退出
	if (v1 == v2)
		return;
	// 如果程序执行到了这里,说明还需要继续完成某些功能
	int tmp = v2;
	v2 = v1;
	v1 = tmp;
	// 此处无须显式的return语句
}

一个返回类型是 void 的函数也能使用 return 语句的第二种形式,不过此时 return 语句的 expression 必须是另一个返回 void 的函数,强行令 void 函数返回其他类型的表达式将产生编译错误。

2)有返回值函数

return 语句的第二种形式提供了函数的结果。只要函数的返回类型不是 void,该函数内的每条 return 语句就必须返回一个值,并且返回值的类型必须与函数的返回类型相同,或者能隐式地转换成函数的返回类型(main 函数例外)。

  • return 语句没有返回值是错误的,编译器能检测到这个错误。
  • 在含有 return 语句的循环后面应该也有一条 return 语句,否则程序就是错误的,但很多编译器无法发现此错误。

函数返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。如果函数返回引用类型,则该引用仅仅是它所引用对象的一个别名。

// 严重错误: 这个函数试图返回局部对象的引用
const string &manip()
{
    string ret;
    // 以某种方式改变一下ret
    if (!ret.empty())
        return ret;   		// 错误:返回局部对象的引用!
    else
        return "Empty";		// 错误:"Empty"是一个局部临时量
}

函数不应该返回局部对象的指针或引用,因为一旦函数完成,局部对象将被释放,指针将指向一个不存在的对象。


如果函数返回指针、引用或类的对象,则可以使用函数调用的结果访问结果对象的成员。

调用一个返回引用的函数会得到左值,其他返回类型得到右值。

C++11规定,函数可以返回用花括号包围的值的列表。同其他返回类型一样,列表也用于初始化表示函数调用结果的临时量。如果列表为空,临时量执行值初始化;否则,返回的值由函数的返回类型决定。

  • 如果函数返回内置类型,则列表内最多包含一个值,且该值所占空间不应该大于目标类型的空间。

  • 如果函数返回类类型,由类本身定义初始值如何使用。

    vector<string> process()
    {
        // . . .
        // expected和actual是string对象
        if (expected.empty())
            return {};  								// 返回一个vector对象
        else if (expected == actual)
            return {"functionX", "okay"};  // 返回列表初始化的vector对象
        else
            return {"functionX", expected, actual};
    }
    

如果函数的返回类型不是 void,那么它必须返回一个值,除了,main 函数可以没有 return 语句直接结束。如果控制流到达了 main 函数的结尾处并且没有return语句,编译器会隐式地插入一条返回0的 return 语句。

main 函数的返回值可以看作是状态指示器。返回0表示执行成功,返回其他值表示执行失败,其中非0值的具体含义依机器而定。

为了使 main 函数的返回值与机器无关,头文件 cstdlib 定义了 EXIT_SUCCESSEXIT_FAILURE 这两个预处理变量,分别表示执行成功和失败。

int main()
{
    if (some_failure)
        return EXIT_FAILURE; // 定义在cstdlib头文件中
    else
        return EXIT_SUCCESS; // 定义在cstdlib头文件中
}

建议 使用预处理变量 EXIT_SUCCESSEXIT_FAILURE 表示 main 函数的执行结果。

如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为 递归函数(recursive function)

// 计算val!,即1 * 2 * 3 . . . * val
int factorial(int val)
{
    if (val > 1)
        return factorial(val-1) * val;
    return 1;
}

在递归函数中,一定有某条路径是不包含递归调用的,否则函数会一直递归下去,直到程序栈空间耗尽为止。

相对于循环迭代,递归的效率较低,但在某些情况下使用递归可以增加代码的可读性。

  • 循环迭代适合处理线性问题(如链表,每个节点有唯一前驱、唯一后继),
  • 而递归适合处理非线性问题(如树,每个节点的前驱、后继不唯一)。

main函数不能调用它自身。

3)返回数组指针

因为数组不能被拷贝,所以函数不能返回数组,但可以返回数组的指针或引用。

返回数组指针的函数形式如下:

Type (*function(parameter_list))[dimension]    

其中 Type 表示元素类型,dimension 表示数组大小,(*function (parameter_list)) 两端的括号必须存在,如果没有这对括号,函数的返回类型将是指针的数组。

int (*func(int i))[10];

// func(int i)表示调用func函数时需要一个int类型的实参
// (*func(int i))意味着可以对函数调用的结果执行解引用操作
// (*func(int i))[10]表示解引用func的调用将得到一个大小是10的数组
// int(*func(int i))[10]表示数组中的元素是int类型

C++11允许使用 尾置返回类型(trailing return type) 简化复杂函数声明。尾置返回类型跟在形参列表后面,并以一个 -> 符号开头。为了表示函数真正的返回类型在形参列表之后,需要在本应出现返回类型的地方添加 auto 关键字。

// func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
auto func(int i) -> int(*)[10];

任何函数的定义都能使用尾置返回类型,但是这种形式更适用于返回类型比较复杂的函数。

如果我们知道函数返回的指针将指向哪个数组,就可以使用 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;  // 返回一个指向数组的指针
}

4、函数重载

同一作用域内的几个名字相同但形参列表不同的函数叫做 重载函数

void print(const char *cp);
void print(const int *beg, const int *end); 
void print (const int ia[), size_t ze);

函数的名字仅仅是让编译器知道它调用的是哪个函数,而函数重载可在一定程度上减轻程序员起名字、记名字的负担。

main 函数不能重载。

不允许两个函数除了返回类型以外的其他所有要素都相同。

Record lookup(const Account&); 
bool lookup(const Account&); 	// 错误:与上一个函数相比只有返回类型不同

顶层 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 可以用于函数的重载。

当函数的实参是常量时,返回的结果仍然是常量的引用。

// 比较两个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) 的错误信息。

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)被隐藏掉了
}

在C++中,名字查找发生在类型检查之前。

5、特殊用途语言特性

  • 默认实参
  • 内联函数
  • constexpr 函数

1)默认实参

默认实参 作为形参的初始值出现在形参列表中。可以为一个或多个形参定义默认值,不过一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。

typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');

调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。

如果想使用默认实参,只要在调用函数的时候省略该实参即可。

虽然多次声明同一个函数是合法的,但是在给定的作用域中一个形参只能被赋予一次默认实参。函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。

// 表示高度和宽度的形参没有默认位
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, '*')
}

2)内联函数和constexpr函数

内联函数会在每个调用点上“内联地”展开,省去函数调用所需的一系列工作。

定义内联函数时需要在函数的返回类型前添加关键字 inline

// 内联版本:寻找两个string对象中较短的那个
inline const string &
shorterString(const string &s1, const string &s2)
{
    return s1.size() <= s2.size() ? s1 : s2;
}

在函数声明和定义中都能使用关键字 inline,但是建议只在函数定义时使用。

一般来说,内联机制适用于优化规模较小、流程直接、调用频繁的函数。内联函数中不允许有循环语句和 switch 语句,否则函数会被编译为普通函数。


constexpr 函数是指能用于常量表达式的函数。constexpr 函数的返回类型及所有形参的类型都得是字面值类型。

另外C++11标准要求 constexpr 函数体中必须有且只有一条 return 语句,但是此限制在C++14标准中被删除。

constexpr 函数的返回值可以不是一个常量。

// 如果arg是常量表达式,则scale(arg)也是常量表达式
constexpr size_t scale(size_t cnt) 
{ 
    return new_sz() * cnt; 
}

scale 的实参是常量表达式时,它的返回值也是常量表达式;反之则不然:

int arr[scale(2)];  // 正确:scale(2)是常量表达式
int i = 2;          // i不是常量表达式
int a2[scale(i)];   // 错误:scale(i)不是常量表达式

constexpr 函数不一定返回常量表达式。

和其他函数不同,内联函数和 constexpr 函数可以在程序中多次定义。因为在编译过程中,编译器需要函数的定义来随时展开函数,仅有函数的声明是不够的。对于某个给定的内联函数或 constexpr 函数,它的多个定义必须完全一致。因此内联函数和 constexpr 函数通常定义在头文件中。

3)调试帮助

1_assert 预处理宏

assert 是一种预处理宏。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。

assert (expr);

首先对 expr 求值,如果表达式为假(即0),assert 输出信息并终止程序的执行;如果
表达式为真(即非0),assert什么也不做。

2_NDE8UG 预处理变量

assert 的行为依赖于于一个名为 NDEBUG 的预处理变量的状态。

  • 如果定义了 NDEBUG,则 assert 什么也不做;
  • 默认状态下没有定义 NDEBUG,此时 assert 将执行运行时检查。

可以使用 #define 语句定义 NDEBUG,从而关闭调试状态。

常用的几个对于程序调试很有用的名字:

变量名称内容
__func__输出当前调试的函数的名称
__FILE__存放文件名的字符串字面值
__LINE__存放当前行号的整型字面值
__TIME__存放文件编译时间的字符串字面值
__DATE__存放文件编译日期的字符串字面值

6、函数匹配

函数实参类型与形参类型越接近,它们匹配得越好。

重载函数集中的函数称为 候选函数(candidate function)

  • 一是与被调用的函数同名;
  • 二是其声明在调用点可见。

可行函数(viable function)

  • 一是形参数量与函数调用所提供的实参数量相等;
  • 二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。

如果没找到可行函数,编译器讲报告无匹配函数的错误。

调用重载函数时应该尽量避免强制类型转换。

1)实参类型转换

为了确定最佳匹配,编译器将~参类型到形参类型的转换戈 分成儿个等级 具体卡11
如下所示:

  1. 精确匹配 包括以下情况:
    • 实参类型和形参类型相同
    • 实参从数组类型或函数类型转换成对应的指针类型
    • 向实参添加顶层 const 或者从实参中删除顶层 const
  2. 通过 const 转换实现的匹配
  3. 通过类型提升实现的匹配
  4. 通过算术类型转换或指针转换实现的匹配
  5. 通过类类型转换实现的匹配

所有算术类型转换的级别都一样。

如果重载函数的区别在于它们的引用或指针类型的形参是否含有底层 const,或者指针类型是否指向 const,则调用发生时编译器通过实参是否是常量来决定函数的版本。

Record lookup(Account&);    	// 函数的参数是Account的引用
Record lookup(const Account&);  // 函数的参数是一个常量引用
const Account a;
Account b;
lookup(a);  					// 调用lookup(const Account&)
lookup(b);  					// 调用lookup(Account&)

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 作用于函数时,返回的是函数类型,而不是函数指针类型。

函数可以返回指向函数的指针。但返回类型不会像函数类型的形参一样自动地转换成指针,必须显式地将其指定为指针类型,即加上 *

如果想要更多的资源,欢迎关注 @我是管小亮,文字强迫症MAX~

回复【福利】即可获取我为你准备的大礼,包括C++,编程四大件,NLP,深度学习等等的资料。

想看更多文(段)章(子),欢迎关注微信公众号「程序员管小亮」~

在这里插入图片描述

参考文章

  • 《C++ Primer》
©️2020 CSDN 皮肤主题: 酷酷鲨 设计师:CSDN官方博客 返回首页