欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 教育 > 锐评 > C++ Primer 特殊用途语言特性

C++ Primer 特殊用途语言特性

2026/5/3 1:16:21 来源:https://blog.csdn.net/m0_56279421/article/details/145662975  浏览:    关键词:C++ Primer 特殊用途语言特性

欢迎阅读我的 【C++Primer】专栏

专栏简介:本专栏主要面向C++初学者,解释C++的一些基本概念和基础语言特性,涉及C++标准库的用法,面向对象特性,泛型特性高级用法。通过使用标准库中定义的抽象设施,使你更加适应高级程序设计技术。希望对读者有帮助!

在这里插入图片描述
在这里插入图片描述

目录

  • 6.5 特殊用途语言特性
    • 默认实参
    • 使用默认实参调用函数
    • 默认实参声明
    • 默认实参初始值
    • 内联函数和constexpr函数
    • 内联函数可避免函数调用的开销
    • constexpr函数
    • 把内联函数和constexpr函数放在头文件内
    • 调试帮助
      • assert预处理宏
    • NDEBUG预处理变量

6.5 特殊用途语言特性

本节我们介绍三种函数相关的语言特性,这些特性对大多数程序都有用,它们分别是:默认实参、内联函数和constexpr函数,以及在程序调试过程中常用的一些功能。

默认实参

某些函数有这样一种形参,在函数的很多次调用中它们都被赋予一个相同的值,此时,我们把这个反复出现的值称为函数的默认实参(default argument)。调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。

例如,我们使用string对象表示窗口的内容。一般情况下,我们希望该窗口的高、宽和背景字符都使用默认值。但是同时我们也应该允许用户为这几个参数自由指定与默认值不同的数值。为了使得窗口函数既能接纳默认值,也能接受用户指定的值,我们把它定义成如下的形式:

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

其中我们为每一个形参都提供了默认实参,默认实参作为形参的初始值出现在形参列表中。我们可以为一个或多个形参定义默认值,不过需要注意的是,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。

使用默认实参调用函数

如果我们想使用默认实参,只要在调用函数的时候省略该实参就可以了。例如,screen函数为它的所有形参都提供了默认实参,所以我们可以使用0、1、2或3个实参调用该函数:

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,'#')

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

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

需要注意,第二个调用传递一个字符值,是合法的调用。然而尽管如此,它的实际效果却与书写的意图不符。该调用之所以合法是因为“?“是个char,而函数最左侧形参的类型string::size_type是一种无符号整数类型,所以char类型可以转换成函数最左侧形参的类型。当该调用发生时,char类型的实参隐式地转换成string::size_type,然后作为height的值传递给函数。在我们的机器上,“2“对应的十六进制数是0x3F,也就是十进制数的63,所以该调用把值63传给了形参height。

当设计含有默认实参的函数时,其中一项任务是合理设置形参的顺序,尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面。

默认实参声明

对于函数的声明来说,通常的习惯是将其放在头文件中,并且一个函数只声明一次,但是多次声明同一个函数也是合法的。不过有一点需要注意,在给定的作用域中一个形参只能被赋予一次默认实参。换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。假如给定

//表示高度和宽度的形参没有默认值
string screen(sz,sz,char = 71);

我们不能修改一个已经存在的默认值:

string screen(sz,sz,char);//错误:重复声明
//但是可以按照如下形式添加默认实参:
stringscreen(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的值,所以对screen的调用将会传递这个更新过的值。另一方面,虽然我们的函数还声明了一个局部变量用于隐藏外层的wd,但是该局部变量与传递给screen的默认实参没有任何关系。

内联函数和constexpr函数

把规模较小的操作定义成函数有很多好处,主要包括:

  • 阅读和理解shorterString函数的调用要比读懂等价的条件表达式容易得多。
  • 使用函数可以确保行为的统一,每次相关操作都能保证按照同样的方式进行。
  • 如果我们需要修改计算过程,显然修改函数要比先找到等价表达式所有出现的地方再逐一修改更容易。
  • 函数可以被其他应用重复利用,省去了程序员重新编写的代价。

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

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

将函数指定为内联函数(inline),通常就是将它在每个调用点上“内联地“展开。假设我们把shorterString函数定义成内联函数,则如下调用

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

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

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

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

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

内联说明只是向编译器发出的一个请求,编评器可以选择忽略这个请求。

一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数,而且一个75行的函数也不大可能在调用点内联地展开。

constexpr函数

constexpr函数(constexpr function)是指能用于常量表达式的函数。定义constexpr函数的方法与其他函数类似,不过要遵循几项约定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句:

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函数中可以有宇语句、类型别名以及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类型的对象;,则返回值是一个非常量表达式。当把scale函数用在需要常量表达式的上下文中时,由编译器负责检查函数的结果是否符合要求。如果结果恰好不是常量表达式,编译器将发出错误信息。

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

把内联函数和constexpr函数放在头文件内

和其他函数不一样,内联函数和constexpr函数可以在程序中多次定义。毕竟,编详器要想展开函数仅有函数声明是不够的,还需要函数的定义。不过,对于多个给定的内联函数或者constexpr函数来说,它的多个定义必须完全一致。基于这个原因,内联函数和constexpr函数通常定义在头文件中。

调试帮助

C++程序员有时会用到一种类似于头文件保护的技术,以便有选择地执行调试代码。基本思想是,程序可以包含一些用于调试的代码,但是这些代码只在开发程序时使用。当应用程序编写完成准备发布时,要先屏蔽掉调试代码。这种方法用到两项预处理功能assert和NDEBUG。

assert预处理宏

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

assert(expr);

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

assert宏定义在cassert头文件中。如我们所知,预处理名字由预处理器而非编译器管理,因此我们可以直接使用预处理名字而无须提供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 -DNDEBUG 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 as " << word
<<":Length too short" <<endl;

如果我们给程序提供了一个长度小于threshold的string对象,将得到下面的错误消息:

error:wdebug.cc:in function main at 1ine 27
Compiled on Jul 11 2012 at 20:50:03
Word read was "foo": length too short

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词