6.5.1 默认实参
- 将反复出现的数值称为函数的默认实参,调用含有默认实参的时候可以包含该实参也可以不包含
- 比如程序打开页面会有一个默认的宽高,如果用户不喜欢也允许用户自由指定与默认数值不同的数值,具体例子如下图所示
typedef string::size_type sz;
string screen(sz ht = 24,sz wid = 80,char backgrnd = ' ');
- 默认实参作为形参的初始化数值出现在形参列表中,可以为一个或者多个形参定义成默认数值,但是,一旦某个形参被赋予了默认数值,这个形参之后的数值也必须有默认数值。
使用默认实参调用函数
- 如果使用默认的实参,调用函数的时候省略掉该实参就可以了。
- 如上面提出的screen函数,有三个变量,因此可以使用0、1、2、3个实参调用该函数
typedef string::size_type sz;
string screen(sz ht = 24,sz wid = 80,char backgrnd = ' ');
int main(){string window;window = screen();window = screen(1);window = screen(1,2);window = screen(1,2,'&');
}
- window = screen(,,'&');//错误,只可以忽略掉尾部的实参,即左边的必须补齐定义,只有右边的才可以缺省
- 因此,在实际使用的场景中,需要设计形参的顺序,将不怎么使用默认形参的变量放在前面
默认实参声明
- 对于函数的声明来讲,通常是将其放在头文件中,并且每个函数声明一次,但是多次声明也是合法的。
- 但是,在给定的作用域中,一个形参只可以赋予一次默认实参。即,函数的后续声明只能为之前那些没有默认值的形参添加默认的实参,而且这个形参的左侧的所有的形参都有默认值。
默认实参初始值
- 局部变量不可以作为默认的实参,只要表达式的类型可以转化为形参所需要的类型,该表达式就可以作为默认的实参。
typedef string::size_type sz;
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,内部声明的局部变量会隐藏外部的wd,但是该局部变量并不会传递给screen的默认的参数
6.5.2 内联函数和constexpr函数
定义函数的好处
- 使用函数可以确保行为的统一,每次相关的操作都按照同样的方式进行
- 如果需要修改计算的过程,显然修改函数比先找到等价的表达式所有出现地方再逐一修改更加方便
- 函数可以被其他应用重复调用,省去了重新编写开发的代价
坏处
- 调用函数会比使用表达式慢,因为使用函数要涉及到一系列的工作:调用前保存寄存器、并且在返回的是会恢复;需要拷贝实参,程序需要在不同的新的位置上继续执行。
内联函数可以避免函数的调用所带来的开销
- 将函数指定为内联函数,通常是将它在每一个调用点上“内联地”展开,将会减少函数运行使得开销。
- 在函数的前面加上关键字inline就可以将其声明称为内联函数
inline const string &shorterString(const string &s1,const string &s2){return s1.size() < s2.size() ? s1 : s2;
}
- 内联说明只是向编译器发出一个请求,编译器可以选择忽略这个请求
- 内联机制一般用于优化规模较小,流程直接、频繁调用的函数,很多编译器都不支持内联递归函数,如果程序很大的话,也不大可能在调用点内联的展开
constexpr函数
- constexpr是指可以用于常量表达式的函数。
- 其函数的返回类型以及所有的形参的类型都得是字面值类型,而且函数体中必须有且只有一个return语句。
constexpr int new_sz() {return 42;}
constexpr int foo = new_sz(); //正确,foo是一个常量的表达式
- 把new_sz定义成无参数的constexpr函数,因为编译器能在程序编译的时候验证new_sz函数返回的是常量的表达式,所以可以使用new_sz来初始化foo函数。
- 执行该初始化任务的时候,编译器把constexpr函数的调用替换成其结果数值。为了能在编译的过程中随时展开,constexpr会被隐式转变为内联函数。
- constexpr函数体内也可以包含其他语句,只要这些语句在运行的时候不执行任何操作就可以。例如,constexpr函数内可以有空的语句、类型别名以及using声明。
- 允许constexpr函数的返回值并非一个常量。
//如果arg是常量的表达式,则scale(arg)也是常量的表达式
constexpr size_t scale(size_t cnt){return new_sz() * cnt;//当scale的实参是常量表达式的时候,返回的数值也是常量的表达式;反之不然;
}
int main(){int arr[scale(2)];//正确,scale(2)是常量的表达式int i = 2;int a2[scale(i)];//错误,scale(i)不是常量的表达式
}
把内联函数和constexpr函数放在头文件中
- 和其他函数不一样,内联函数和constexpr函数可以在程序中多次定义。毕竟,编译器要想展开函数仅仅有函数的声明是不够的,还需要函数的定义。但是对于某个给定的内联函数或者constexpr函数来说,他的多个定义必须完全一致,因此通常将他们定义在头文件中。
6.5.3 调试帮助
- C++有些时候会用到一些类似于头文件保护的技术,从而可以有选择的执行代码。
- 基本思想是,程序包含一些用于调试的代码,但是这些代码只会在开发程序的时候使用,当程序完准备上线的时候,需要先屏蔽代码,这就用到了两项预先处理的功能:assert和NDEBUG
assert预处理宏
- 预处理宏就是一个预处理变量,其行为类似于内联函数
- assert使用一个表达式作为它的条件 assert(expr); 首先对于expr进行求值,如果表达式为假,assert会输出信息并且终止程序的执行。如果表达式为真,assert什么也不做
- assert的头文件会定义在cassert文件中,因为预处理名字是由预处理器而不是编译管理,因此可以直接使用预处理名字而不需要提供using声明。即直接使用assert,而不是std::assert,也不需要为assert提供using的声明。
- 和预处理的变量一样,宏名字在程序内部必须唯一。但是在实际编程的时候,即使没有包含这个头文件,也不要使用assert这个名字进行任何相关的变量、函数、其他实体等的命名,因为,很多头文件都会包含这个cassert,有可能通过其他途径包含在程序中。
- assert宏常常用于检查“不能发生的条件”。例如,一个对于输入文本进行操作的语句对于输入的长度有很大的要求,必须大于某一个指定的阈值。这个使用,程序可能会包含一个如下所示的语句 assert(word.size() > threshold );
- assert都是一种调试程序的辅助手段,但是他不可以替代运行时候的逻辑检查,也不可以替代程序本身应该包含的错误检查
NDEBUG预处理变量
- assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,那么assert就什么都不做。默认情况下是没有定义这个NDEBUG的,因此assert会执行运行检查。
- 使用#define NDEBUG 或者使用命令行进行程序编译的时候,使用 CC -D NDEBUG main.c 这两者之间是等效的
- 同理,也可以使用NDEBUG来编写自己的条件调试的代码,如果NDEBUG没有定义,那么将会执行#ifdef和#endif之间的代码;如果定义了NDEBUG,那么这些代码将会被忽略掉
- #define NDEBUG
#ifdef NDEBUG// __func__是编译器定义的一个局部静态变量,用于存放函数的名字cerr << __func__ << ": array size is " << size << endl;
#endif
- __func__是一个const char的一个静态数组,用于存放函数的名字