C++Primer学习(6.5 特殊用途语言特性)

6.5 特殊用途语言特性
本节我们介绍三种函数相关的语言特性,这些特性对大多数程序都有用,它们分别是:默认实参、内联函数和constexpr函数,以及在程序调试过程中常用的一些功能。
6.5.1 默认实参
某些函数有这样一种形参,在函数的很多次调用中它们都被赋予一个相同的值,此时我们把这个反复出现的值称为函数的默认实参(default argument)。调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。
例如,我们使用 string对象表示窗口的内容。一般情况下,我们希望该窗口的高、宽和背景字符都使用默认值。但是同时我们也应该允许用户为这几个参数自由指定与默认值不同的数值。为了使得窗口函数既能接纳默认值,也能接受用户指定的值,我们把它定义成如下的形式:

typedef string::size_type sz;//关于typedef参见2.5.1节(第60页)
string screen(sz ht=24,sz wid=80,char backgrnd='');

其中我们为每一个形参都提供了默认实参,默认实参作为形参的初始值出现在形参列表中。我们可以为一个或多个形参定义默认值,不过需要注意的是,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
使用默认实参调用函数
如果我们想使用默认实参,只要在调用函数的时候省略该实参就可以了。例如,screen函数为它的所有形参都提供了默认实参,所以我们可以使用0、2或3个实参调用该函数:

string window;
window =screen();//等价于screen(24,80,' ' )
window =screen(66);//等价于screen(66,80,' ')
window =screen(66256);//screen(66,256,' ')
windowscreen(66256,“#');//screen(66,256,'#')

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

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

需要注意,第二个调用传递一个字符值,是合法的调用。然而尽管如此,它的实际效果却与书写的意图不符。该调用之所以合法是因为"?"是个char,而函数最左侧形参的类型string::sizt_type是一种无符号整数类型,所以char类型可以转换成(参见4.11节,第141页)函数最左侧形参的类型。当该调用发生时,char类型的实参隐式地转换成string::size_type,然后作为height 的值传递给函数。在我们的机器上,‘?‘对应的十六进制数是 0x3F,也就是十进制数的63,所以该调用把值63传给了形参height。当设计含有默认实参的函数时,其中一项任务是合理设置形参的顺序,尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面。
默认实参声明
对于函数的声明来说,通常的习惯是将其放在头文件中,并且一个函数只声明一次,但是多次声明同一个函数也是合法的。不过有一点需要注意,在给定的作用域中一个形参只能被赋予一次默认实参。换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。假如给定
//表示高度和宽度的形参没有默认值
string screen(sz,sz,char=’ ‘);
我们不能修改一个已经存在的默认值:
string screen(sz,sz,char=’*’);//错误:重复声明
但是可以按照如下形式添加默认实参:
string screen(sz=24,s=80,char);//正确:添加默认实参
BestPraetices:通常,应该在函数声明中指定默认实参,并将该声明放在合适的头文件中
默认实参初始值
局部变量不能作为默认实参。除此之外,只要表达式的类型能转换成形参所需的类型该表达式就能作为默认实参:

//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的值,所以对screen的调用将会传递这个更新过的值。另一方面,虽然我们的函数还声明了一个局部变量用于隐藏外层的wd,但是该局部变量与传递给screen的默认实参没有任何关系。
6.5.2 内联函数和constexpr 函数
在6.3.2节(第201页)中我们编写了一个小函数,它的功能是比较两个string形参的长度并返回长度较小的string的引用。把这种规模较小的操作定义成函数有很多好处,主要包括:
1)阅读和理解shorterstring函数的调用要比读懂等价的条件表达式容易得多.
2)函数可以确保行为的统一,每次相关操作都能保证按照同样的方式进行。
3)如果我们需要修改计算过程,显然修改函数要比先找到等价表达式所有出现的地方再逐一修改更容易。
4)函数可以被其他应用重复利用,省去了程序员重新编写的代价。
然而,使用shorterstring函数也存在一个潜在的缺点:调用函数一般比求等价表达式的值要慢一些。在大多数机器上,一次函数调用其实包含着一系列工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参:程序转向一个新的位置续执行。
内联函数可避免函数调用的开销
将函数指定为内联函数(inline),通常就是将它在每个调用点上“内联地”展开。假设我们把shorterstring函数定义成内联函数,则如下调用

cout<<shorterString(s1,s2)<<endl;

将在编译过程中展开成类似于下面的形式:

cout<<(s1.size()<s2.size()?s1:s2)<< endl;

从而消除了 shorterstring 函数的运行时开销。在 shorterstring 函数的返回类型前面加上关键字 inline,这样就可以将它声明成内联函数了:

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

内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数,而且一个75 行的函数也不大可能在调用点内联地展开。
constexpr函数
constexpr函数(constexpr function)是指能用于常量表达式的函数。定义constexpr函数的方法与其他函数类似,不过要遵循几项约定:函数的返回类型及所有形参的类型都得是字面值类型(参见2.4.4节,第59页),而且函数体中必须有且只有一条retur语句:

constexpr int new_sz(){return 42;}
constexpr int foo=new_sz();//正确:foo是一个常量表达式

我们把new_sz定义成无参数的constexpr函数。因为编译器能在程序编译时验证new_sz函数返回的是常量表达式,所以可以用new_sz函数初始化constexpr 类型的变量 foo。
执行该初始化任务时,编译器把对constexpr函数的调用替换成其结果值。为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数。
constexpr函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行。例如,constexpr函数中可以有空语句、类型别名(参见2.5.1节,第60页)以及 using 声明。
我们允许 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)不是常量表达式

如上例所示,当我们给 scale函数传入一个形如字面值2的常量表达式时,它的返回类型也是常量表达式。此时,编译器用相应的结果值替换对scale函数的调用。如果我们用一个非常量表达式调用scale 函数,比如int 类型的对象i,则返回值是一个非常量表达式。当把scale函数用在需要常量表达式的上下文中时,由编译器负责检查函数的结果是否符合要求。如果结果恰好不是常量表达式,编译器将发出错误信息。
Note:constexpr函数不一定返回常量表达式
把内联函数和 constexpr 函数放在头文件内
和其他函数不一样,内联函数和constexpr函数可以在程序中多次定义。毕竟,编译器要想展开函数仅有函数声明是不够的,还需要函数的定义。不过,对于某个给定的内联函数或者constexpr函数来说,它的多个定义必须完全一致。基于这个原因,内联函数和constexpr函数通常定义在头文件中。
6.5.3 调试帮助
C++程序员有时会用到一种类似于头文件保护(参见2.63节,第67页)的技术,以便有选择地执行调试代码。基本思想是,程序可以包含一些用于调试的代码,但是这些代码只在开发程序时使用。当应用程序编写完成准备发布时,要先屏蔽掉调试代码。这种方法用到两项预处理功能:assert和NDEBUG。
assert 预处理宏
assert是一种预处理宏(preprocessor marco)。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。assert宏使用一个表达式作为它的条件:assert(expr);
首先对 expr 求值,如果表达式为假(即0),assert输出信息并终止程序的执行。如果表达式为真(即非0),assert什么也不做。
assert 宏定义在 cassert 头文件中。如我们所知,预处理名字由预处理器而非编译器管理(参见2.3.2节,第49页),因此我们可以直接使用预处理名字而无须提供using声明。也就是说,我们应该使用assert而不是std::assert,也不需要为assert提供using 声明。
和预处理变量一样,宏名字在程序内必须唯一。含有cassert头文件的程序不能再定义名为 assert的变量、函数或者其他实体。在实际编程过程中,即使我们没有包含cassert买又件,也最对不要为了共他目的便用 assert。cassert,这就意味着即使你没有直接包含cassert,它也很有可能通过其他途径包含在你的程序中。
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当成调试程序的一种辅助手段,但是不能用它替代真正的运行时逻辑检查,也不能替代程序本身应该包含的错误检查。除了用于assert外,也可以使用 NDEBUG编写自己的条件调试代码。如果NDEBUG未定义,将执行#ifndef和#endif之间的代码:如果定义了NDEBUG,这些代码将被忽略掉:

void print(const int ia[],size_t size)
{
	#ifndef NDEBUG
	//__func_是编译器定义的一个局部静态变量,用于存放函数的名字
	cerr<<__func__<<":array size is"<< size << endl;
	#endif
}
//...

在这段代码中,我们使用变量func输出当前调试的函数的名字。编译器为每个函数都定义了__func__,它是const char的一个静态数组,用于存放函数的名字。除了C++编译器定义的__func__之外,预处理器还定义了另外4个对于程序调试很有用的名字:

__FILE__存放文件名的字符串字面值。
__LINE__存放当前行号的整型字面值。
__TIME__存放文件编译时间的字符串字面值。
_DATE__存放文件编译日期的字符串字面值。

可以使用这些常量在错误消息中提供更多信息:

if(word.size()< threshold)
cerr << Error:"<<__FILE__
<<:in function "<<__func__
<<"at line "<<__LINE__<< endl
<<"     compiled on"<<__DATE__
<<"at  " <<__TIME__<< endl
<<"   Word read was\""<< word
<<"\":Length too short"<< endl;

如果我们给程序提供了一个长度小于threshold的string 对象,将得到下面的错误消
Error:wdebug.cc :in function main at line 27
compiled on Jul 112012 at20:50:03
Word read was “foo”:Length too short

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值