zl程序教程

您现在的位置是:首页 >  后端

当前栏目

C++基础知识要点--函数(Primer C++ 第五版 · 阅读笔记)

C++基础知识笔记 函数 -- 阅读 要点 primer
2023-09-14 09:14:59 时间

C++基础知识要点–函数

函数基础

函数的形参列表

函数的形参列表可以为空,但是不能省略。要想定义一个不带形参的函数,最常用的办法是书写一个空的形参列表。不过为了与C语言兼容,也可以使用关键字void表示函数没有形参:

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

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

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

任意两个形参都不能同名,而且函数最外层作用域中的局部变量也不能使用与函数形参一样的名字。

函数返回类型

  • 大多数类型都能用作函数的返回类型。
  • 一种特殊的返回类型是void,它表示函数不返回任何值。函数的返回类型不能是数组类型函数类型,但可以是指向数组或函数的指针
  • 我们将在6.3.3节(第205页)介绍如何定义一种特殊的函数,它的返回值是数组的指针(或引用),在6.7节(第221页)将介绍如何返回指向函数的指针。

局部对象

在C++语言中,名字有作用域,对象有生命周期。理解这两个概念非常重要。

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

自动对象

  • 对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁它。
  • 我们把只存在于块执行期间的对象称为自动对象( automatic object)。当块的执行结束后,块中创建的自动对象的值就变成未定义的了。
  • 形参是一种自动对象。函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁。

局部静态对象 ( static )

  • 某些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间。
  • 可以将局部变量定义成 static类型 从而获得这样的对象。局部静态对象(local static object)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。

举个例子,下面的函数统计它自己被调用了多少次,这样的函数也许没什么实际意义,但是足够说明问题:

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;
}

这段程序将输出从1到10(包括10在内)的数字。

  • 在控制流第一次经过ctr的定义之前,ctr被创建并初始化为0。每次调用将ctr加1并返回新值。
  • 每次执行count_calls函数时,变量ctr的值都已经存在并且等于函数上一次退出时ctr的值
  • 因此,第二次调用时ctr的值是1,第三次调用时ctr的值是2,以此类推。

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

函数声明

  • 和其他名字一样,函数的名字也必须在使用之前声明。类似于变量,函数只能定义一次,但可以声明多次。唯一的例外是如15.3节(第535页)将要介绍的,如果一个函数永远也不会被我们用到,那么它可以只有声明没有定义。
  • 函数的声明和函数的定义非常类似,唯一的区别是函数声明无须函数体,用一个分号替代即可。
  • 因为函数的声明不包含函数体,所以也就无须形参的名字。事实上,在函数的声明中经常省略形参的名字。尽管如此,写上形参的名字还是有用处的,它可以帮助使用者更好地理解函数的功能:

在头文件中进行函数的声明

  • 回忆之前所学的知识,我们建议变量在头文件中声明,在源文件中定义。
  • 与之类似,函数也应该在头文件中声明而在源文件中定义。

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

分离式编译

举个例子,假设fact函数的定义位于一个名为fact.cc 的文件中,它的声明位于名为chapter6.h 的头文件中。显然与其他所有用到fact 函数的文件一样,fact.cc应该包含Chapter6.h头文件。另外,我们在名为factMain.cc 的文件中创建main函数,main函数将调用fact函数。要生成可执行文件(executable file),必须告诉编译器我们用到的代码在哪里。对于上述几个文件来说,编译的过程如下所示:

$ CC factMain.cc fact.cc # generates factMain.exe or a.out
$ CC factMain.cc fact.cc -o main # generates main or main.exe
  • 其中,CC 是编译器的名字,$是系统提示符,#后面是命令行下的注释语句。接下来运行可执行文件,就会执行我们定义的main函数。
  • 如果我们修改了其中一个源文件,那么只需重新编译那个改动了的文件。大多数编译器提供了分离式编译每个文件的机制,这一过程通常会产生一个后缀名是 .obj(Windows)或 .o(UNIX)的文件,后缀名的含义是该文件包含对象代码(object code)。

参数传递

  • 和其他变量一样,形参的类型决定了形参和实参交互的方式。如果形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参。
  • 当形参是引用类型时,我们说它对应的实参被引用传递(passed by reference)或者函数被传引用调用( called by reference)。和其他引用一样,引用形参也是它绑定的对象的别名;也就是说,引用形参是它对应的实参的别名。
  • 当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递( passed by value))或者函数被传值调用(called by value)。

传值参数

当初始化一个非引用类型的变量时,初始值被拷贝给变量。此时,对变量的改动不会影响初始值。

指针形参
指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值:

int n= 0, i = 42;
int *p = &n, *q = &i ;         //p指向n;q指向i
*p = 42;                       //n的值改变;p不变
p = q;                         //p现在指向了i;但是i和n的值都不变

指针形参的行为与之类似:

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

熟悉C的程序员常常使用指针类型的形参访问函数外部的对象。在C+语言中,建议使用引用类型的形参替代指针。

传引用参数

  • 回忆过去所学的知识,我们知道对于引用的操作实际上是作用在引用所引的对象上,引用形参的行为与之类似。通过使用引用形参,允许函数改变一个或多个实参的值。
    举个例子,我们可以改写上一小节的reset程序,使其接受的参数是引用类型而非指针:
//该函数接受一个int对象的引用,然后将对象的值置为0
void reset(int &i)            //i是传给reset函数的对象的另一个名字
{
	i = 0;                   //改变了i所引对象的值
}

和其他引用一样,引用形参绑定初始化它的对象。当调用这一版本的reset函数时,i绑定我们传给函数的int对象,此时改变i也就是改变i所引对象的值。此例中,被改变的对象是传入reset的实参。
调用这一版本的reset函数时,我们直接传入对象而无须传递对象的地址:

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

在上述调用过程中,形参i仅仅是j的又一个名字。在reset内部对i的使用即是对j的使用.

使用引用避免拷贝

  • 拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。
  • 如果函数无须改变引用形参的值,最好将其声明为常量引用
//比较两个string 对象的长度
bool isshorter(const string &s1, const string &s2){
	return sl.size() < s2.size() ;
}

使用引用形参返回额外信息

  • 一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。

定义函数使得它能够既返回位置也返回出现次数?

  1. 一种方法是定义一个新的数据类型,让它包含位置和数量两个成员。
  2. 还有另一种更简单的方法,我们可以给函数传入一个额外的引用实参,令其保存字符出现的次数:
//返回s中c第一次出现的位置索引
//引用形参occurs负责统计c出现的总次数
string::size_type find_char (const string &s, char c, string::size_type &occurs)
{
	auto ret = s.size() ;         //第一次出现的位置(如果有的话)
	occurs = 0;                   //设置表示出现次数的形参的值
	for (decltype(ret) i = 0; i != s.size(); ++i){
		if (s[i] == c){
			if (ret == s.size() )
				ret = i ;            //记录c第一次出现的位置
			++oCcurs;            //将出现的次数加1
		}
	}
	return ret;            //出现次数通过occurs隐式地返回
}

const形参和实参

  • 顶层const作用于对象本身
const int ci = 42;                //不能改变ci, const是顶层的
int i = ci;                       //正确:当拷贝ci时,忽略了它的顶层 const
int *const p = &i ;               //const是顶层的,不能给p赋值
*p = 0;                           //正确:通过p改变对象的内容是允许的,现在i变成了0

和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层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)

在C++语言中,允许我们定义若干具有相同名字的函数,不过前提是不同函数的形参列表应该有明显的区别。因为顶层const被忽略掉了,所以在上面的代码中传入两个fcn函数的参数可以完全一样。

指针或引用形参与const

  • 形参的初始化方式和变量的初始化方式是一样的。
  • 我们可以使用非常量初始化一个底层const对象,但是反过来不行;
  • 同时一个普通的引用必须用同类型的对象初始化。
int i = 42;
const int *cp = &i;                //正确:但是cp不能改变i
const int &r = i;                  //正确:但是r不能改变i
const int &r2 = 42;                //正确
int *p = cp;                       //错误:p的类型和cp 的类型不匹配
int &r3 = r;                       //错误:r3的类型和r的类型不匹配
int &r4 = 42;                      //错误:不能用字面值初始化一个非常量引用

将同样的初始化规则应用到参数传递上可得如下形式:

int i = 0 ;
const int ci = i;
string::size_type ctr = 0;
reset(&i);                      //调用形参类型是int*的reset函数
reset(&ci);                     //错误:不能用指向const int对象的指针初始化int*
reset(i);                        //调用形参类型是int&的reset函数
reset(ci);                       //错误:不能把普通引用绑定到const对象ci上
reset(42);                       //错误:不能把普通应用绑定到字面值上
reset(ctr) ;                     //错误:类型不匹配,ctr是无符号类型
//正确:find_char的第一个形参是对常量的引用
find_char( "Hello world! " , 'o', ctr) ;
  • 要想调用引用版本的reset,只能使用int类型的对象,而不能使用字面值、求值结果为int的表达式、需要转换的对象或者const int类型的对象。类似的,要想调用指针版本的 reset 只能使用int*。
  • 另一方面,我们能传递一个字符串字面值作为find_char的第一个实参,这是因为该函数的引用形参是常量引用,而C++允许我们用字面值初始化常量引用。

尽量使用常量引用

  • 把函数不会改变的形参定义成(普通的)引用是一种比较常见的错误,这么做带给函数的调用者一种误导,即函数可以修改它的实参的值。
  • 此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型。就像刚刚看到的,我们不能把 const对象、字面值或者需要类型转换的对象传递给普通的引用形参。

数组形参

数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:

  • 不允许拷贝数组
  • 以及使用数组时(通常)会将其转换成指针。

因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数。因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。

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

  1. 使用标记指定数组长度
    C风格字符串时遇到空字符停止
  2. 使用标准库规范
    管理数组实参的第二种技术是传递指向数组首元素和尾后元素的指针
  3. 显式传递一个表示数组大小的形参
    第三种管理数组实参的方法是专门定义一个表示数组大小的形参

数组形参和const

  • 当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针。
  • 只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针。

数组引用形参
C++语言允许将变量定义成数组的引用,基于同样的道理,形参也可以是数组的引用。此时,引用形参绑定到对应的实参上,也就是绑定到数组上:

//正确:形参是数组的引用,维度是类型的一部分
void print(int (&arr) [10])
{
	for (auto elem : arr)
		cout << elem << endl;
}

&arr两端的括号必不可少:
f(int &arr[10]) //错误:将arr声明成了引用的数组
f(int (&arr)[10]) //正确:arr是具有10个整数的整型数组的引用

main: 处理命令行选项

有时我们确实需要给main传递实参,一种常见的情况是用户通过设置一组选项来确定函数所要执行的操作。例如,假定main函数位于可执行文件prog之内,我们可以向程序传递下面的选项:

prog -d -o ofile data0

这些命令行选项通过两个(可选的)形参传递给main函数:

int main (int argc,char *argv[]) { ... }

第二个形参argv是一个数组,它的元素是指向C风格字符串的指针;第一个形参argc表示数组中字符串的数量。因为第二个形参是数组,所以main函数也可以定义成:

int main(int argc, char**argv) { ... }

其中arav指向char*。
当实参传给main函数之后,argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参。最后一个指针之后的元素值保证为0。
以上面提供的命令行为例,argc应该等于5,argv应该包含如下的C风格字符串:

argv[o] - "prog" ;//或者argv[0]也可以指向一个空字符串
argv[1] = "-d" ;
argv[2] = "-o" ;
argv[3] = "ofile" ;
argv[4] = "datao" ;
argv[5] = 0;

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

含有可变形参的函数

为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要的方法:

  • 如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型;
  • 如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板

C++还有一种特殊的形参类型(即省略符),可以用它传递可变数量的实参。本节将简要介绍省略符形参,不过需要注意的是,这种功能一般只用于与C函数交互的接口程序。

initializer_list形参
如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用initializer_list类型的形参。initializer_list是一种标准库类型,用于表示某种特定类型的值的数组。initializer_list类型定义在同名的头文件中,它提供的操作如下表所示。
在这里插入图片描述
和vector一样,initializer_list也是一种模板类型。定义initializer_list对象时,必须说明列表中所含元素的类型:

initializer_list<string> ls;              // initializer_list的元素类型是string
initializer_list<int> li;                 // initializer_list的元素类型是int

和vector不一样的是,initializer_list对象中的元素永远是常量值,我们无法改变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" }) ;

在上面的代码中我们调用了同一个函数error_msg,但是两次调用传递的参数数量不同:第一次调用传入了三个值,第二次调用只传入了两个。

省略符形参
省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。通常,省略符形参不应用于其他目的。你的C编译器文档会描述如何使用varargs.

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

省略符形参只能出现在形参列表的最后一个位置,它的形式无外乎以下两种:

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

第一种形式指定了foo函数的部分形参的类型,对应于这些形参的实参将会执行正常的类型检查。省略符形参所对应的实参无须类型检查。在第一种形式中,形参声明后面的逗号是可选的。

返回类型和return语句

无返回值函数(void)

有返回值函数

不要返回局部对象的引用或指针

  • 返回局部对象的引用是错误的;
  • 同样,返回局部对象的指针也是错误的。
  • 一旦函数完成,局部对象被释放,指针将指向一个不存在的对象。

返回类类型的函数和调用运算符
如果函数返回指针、引用或类的对象,我们就能使用函数调用的结果访问结果对象的成员。
例如,我们可以通过如下形式得到较短string对象的长度:

//调用string对象的size成员,该string对象是由shorterstring函数返回的
auto sz = shorterString(s1,s2).size();

引用返回左值
函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值,其他返回类型得到右值。可以像使用其他左值那样来使用返回引用的函数的调用,特别是,我们能为返回类型是非常量引用的函数的结果赋值:

char &get_val (string &str, string::size_type ix){
	return str[ix];        //get_val 假定索引值是有效的
}
int main (){
	string s("a value") ;
	cout << s << endl ;            //输出a value
	get_val(s,0)= 'A';             //将s[0]的值改为A
	cout << s << endl;             //输出A value
	return 0;
}

把函数调用放在赋值语句的左侧可能看起来有点奇怪,但其实这没什么特别的。返回值是引用,因此调用是个左值,和其他左值一样它也能出现在赋值运算符的左侧。

列表初始化返回值
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 } ;
}

返回数组指针

  • 因为数组不能被拷贝,所以函数不能返回数组。
  • 不过,函数可以返回数组的指针或引用。
  • 虽然从语法上来说,要想定义一个返回数组的指针或引用的函数比较烦琐,但是有一些方法可以简化这一任务,其中最直接的方法是使用类型别名:
typedef int arrT[10];             //arrT是一个类型别名,它表示的类型是含有10个整数的数组
using arrT = int [10] ;           //arrT的等价声明
arrT* func (int i) ;              //func返回一个指向含有10个整数的数组的指针

其中 arrT是含有10个整数的数组的别名。因为我们无法返回数组,所以将返回类型定义成数组的指针。因此,func函数接受一个int实参,返回一个指向包含10个整数的数组的指针。

声明一个返回数组指针的函数

要想在声明func时不使用类型别名,我们必须牢记被定义的名字后面数组的维度:

int arr[10];            //arr是一个含有10个整数的数组
int *p1[10] ;           //p1是一个含有10个指针的数组
int (*p2)[10] = &arr;     // p2是一个指针,它指向含有10个整数的数组

和这些声明一样,如果我们想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后。然而,函数的形参列表也跟在函数名字后面且形参列表应该先于数组的维度。因此,返回数组指针的函数形式如下所示:

Type (*function(parameter_list)) [dimension]

类似于其他数组的声明,Type表示元素的类型,dimension表示数组的大小。(*function(parameter_list))两端的括号必须存在,就像我们定义p2时两端必须有括号一样。如果没有这对括号,函数的返回类型将是指针的数组。

函数重载

如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为 重载函数

void print(const char *cp) ;
void print(const int *beg, const int *end) ;
void print(const int ia[], size_t size) ;
  • 对于重载的函数来说,它们应该在形参数量形参类型上有所不同。在上面的代码中,虽然每个函数都只接受一个参数,但是参数的类型不同。
  • 不允许两个函数除了返回类型外其他所有的要素都相同。假设有两个函数,它们的形参列表一样但是返回类型不同,则第二个函-- 数的声明是错误的:
Record lookup (const Account&);
bool lookup(const Account&);          //错误:与上一个函数相比只有返回类型不同

重载和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;
}

这个函数的参数和返回类型都是 const string 的引用。我们可以对两个非常量的string 实参调用这个函数,但返回的结果仍然是const string 的引用。因此我们需要一种新的shorterstring 函数,当它的实参不是常量时,得到的结果是一个普通的引用,使用const_cast可以做到这一点:

string &shorterstring (string &s1, string &s2)
{
	auto &r = shorterstring(const_cast<const string&>(s1),const_cast<const string&>(s2));
	return const_cast<string &>(r);
}

在这个版本的函数中,首先将它的实参强制转换成对const 的引用,然后调用了shorterstring函数的const版本。const版本返回对const string 的引用,这个引用事实上绑定在了某个初始的非常量实参上。因此,我们可以再将其转换回一个普通的string&,这显然是安全的。

特殊用途语言特性

默认实参

我们可以为一个或多个形参定义默认值,不过需要注意的是,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。

为了使得窗口函数既能接纳默认值,也能接受用户指定的值,我们把它定义成如下的形式:

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

函数调用时实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参(靠右侧位置)。例如,要想覆盖backgrnd的默认值,必须为ht和 wid提供实参:

string window ;
window = screen() ;                           //等价于screen (24,80,' ')
window = screen(66);                          //等价于screen (66,80,' ')
window = screen(66,256);                      //screen (66,256,' ')
window = screen(66,256,'#′);                  //screen (66,256,'#’)

window = screen(, , '?') ;                   //错误:只能省略尾部的实参
window = screen ('?');                       //调用screen(' ?’, 80 , ' ')

默认实参声明
换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。假如给定:

//表示高度和宽度的形参没有默认值
string screen(sz, sz, char = ' ');
string screen(sz, sz, char = '*');              //错误:重复声明, 不能修改一个已经存在的默认值
string screen(sz = 24,sz = 80, char);           //正确:添加默认实参

内联函数和constexpr函数

函数调用一般比求等阶表达式的值要慢一些。在大多数机器上,一次函数调用其实包含着一系列工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行。

内联函数可避免函数调用的开销

  • 将函数指定为内联函数(inline),通常就是将它在每个调用点上“内联地”展开;
  • 一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数,而且一个75行的函数也不大可能在调用点内联地展开。

在shorterString函数的返回类型前面加上关键字 inline ,这样就可以将它声明成内联函数了:

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

//调用
cout << shorterstring(s1, s2) << endl;

//将在编译过程中展开成类似于下面的形式:
cout << (s1.size() < s2.size() ? sl : s2) <<endl;

constexpr函数
constexpr函数(constexpr function)是指能用于常量表达式的函数。定义constexpr函数的方法与其他函数类似,不过要遵循几项约定:

  • 函数的返回类型及所有形参的类型都得是字面值类型
  • 而且函数体中必须有且只有一条return语句:
constexpr int new_sz(){ return 42;}
constexpr int foo = new_sz();                   //正确:foo是一个常量表达式

调试帮助

当应用程序编写完成准备发布时,要先屏蔽掉调试代码。这种方法用到两项预处理功能:assert和NDEBUG。

assert 预处理宏

assert是一种预处理宏( preprocessor marco)。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。assert宏使用一个表达式作为它的条件:

assert (expr) ;
  • 首先对expr求值,如果表达式为假(即0),assert 输出信息并终止程序的执行。如果表达式为真(即非0),assert什么也不做。
  • assert 宏定义在cassert头文件中。如我们所知,预处理名字由预处理器而非编译器管理,因此我们可以直接使用预处理名字而无须提供using声明。也就是说,我们应该使用assert而不是std::assert,也不需要为assert 提供using声明。

assert宏常用于检查“不能发生”的条件。例如,一个对输入文本进行操作的程序可能要求所有给定单词的长度都大于某个阙值。此时,程序可以包含一条如下所示的语句:

assert(word.size()> threshold) ;

NDEBUG预处理变量

  • assert 的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert 什么也不做。默认状态下没有定义NDEBUG,此时 assert 将执行运行时检查。
  • 我们可以使用一个#define语句定义NDEBUG,从而关闭调试状态。同时,很多编译器都提供了一个命令行选项使我们可以定义预处理变量:
$ CC -D NDEBUG main.C # use /D with the Microsoft compiler

这条命令的作用等价于在main.c文件的一开始写#define NDEBUG。

  • 定义NDEBUG能避免检查各种条件所需的运行时开销,当然此时根本就不会执行运行时检查。因此,assert应该仅用于验证那些确实不可能发生的事情。我们可以把assert当成调试程序的一种辅助手段,但是不能用它替代真正的运行时逻辑检查,也不能替代程序本身应该包含的错误检查。

函数匹配

调用重载函数时应尽量避免强制类型转换。如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理。

实参类型转换

为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序如下所示:

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

函数指针

函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。例如:

//比较两个string对象的长度
bool lengthCompare (const string &, const string &) ;

该函数的类型是 bool(const string&,const string&)。要想声明一个可以指向该函数的指针,只需要用指针替换函数名即可:

//pf指向一个函数,该函数的参数是两个const string的引用,返回值是bool类型
bool (*pf)(const string &, const string &); //未初始化

从我们声明的名字开始观察,pf前面有个*,因此pf是指针;右侧是形参列表,表示pf指向的是函数;再观察左侧,发现函数的返回类型是布尔值。因此,pf 就是一个指向函数的指针,其中该函数的参数是两个const string 的引用,返回值是bool类型。

pf两端的括号必不可少。如果不写这对括号,则pf是一个返回值为bool指针的函数:
//声明一个名为pf的函数,该函教返回bool

bool *pf (const string &, const string &);