文章目录
第 4 章 表达式
4.1 基础
4.1.1 基本概念
函数调用也是一种特殊的运算符,它对运算对象的数量没有限制。
(1)左值和右值
简单归纳: 当一个对象被用作右值的时候,用的是对象的值(内容);
当一个对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
在需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是位置)使用。当一个左值被当成右值使用时,实际使用的是它的内容(值)。
(2)decltype 与表达式
如果表达式的求值结果是左值,decltype 作用于该表达式(不是常量)得到一个引用类型。
如果表达式的求值结果是右值,decltype 作用于该表达式得到该右值的类型。
int *p;
decltype (*p) x; // 等价于 int &x
decltype (&p) y; // 等价于 int **y
4.1.2 优先级与结合律(*)
4.1.3 求值顺序
(1)调用同一对象的运算顺序
对于一些没有指定顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义行为。例如,
int i = 0;
cout << i << " " << i++ << endl; // 未定义
编译器可能先求 i++ 的值再求 i 的值,此时输出结果为 1 1;
也可能先求 i 的值再求 i++ 的值,输出结果是 0 1;
甚至编译器还可能做出完全不同的操作,这些都是错误的。
(2)函数调用同一对象
运算对象的求值顺序与优先级和结合律无关。
在形如 f() + g() * h() + j() 的表达式中,他们的调用顺序没有明确规定,不受限制。如果其中某几个函数影响同一对象,则它是一条错误的表达式,将产生未定义行为。
4.2 算术运算符
在除法运算中,如果两个运算对象的符号相同则商为正,否则商为负。C++ 语言的早期版本允许结果为负值的商向上或向下取证,C++11 新标准则规定商一律向 0 取整(直接切除小数部分)。
商和取余运算的法则:
- (- m) / n = m / (- n) = - (m / n)
- m % (- n) = m % n
- (- m) % n = - (m % n)
21 % 6 /* 结果为 3 */ 21 / 6 /* 结果为 3 */
21 % 7 /* 结果为 0 */ 21 / 7 /* 结果为 3 */
-21 % -8 /* 结果为 -5 */ -21 / -8 /* 结果为 2 */
21 % -5 /* 结果为 1 */ 21 / -5 /* 结果为 -4 */
4.3 逻辑和关系运算符(*)
4.4 赋值运算符(*)
4.5 递增和递减运算符
建议: 除非必须,否则不用递增递减运算的后置版本。
前置版本的递增运算符避免了不必要的工作,它把值 +1 后直接返回改变了的运算对象。与之相比,后置版本需要将原始值储存下来以便于返回这个未修改的值。如果我们不需要这个内容,那么就会造成一种不必要的浪费。
4.6 成员访问运算符(*)
4.7 条件(三目)运算符(*)
4.8 位运算符
运 算 符 功 能 用 法 ˜ 位 求 反 expr << 左 移 expr1 << expr2 >> 右 移 expr1 >> expr2 & 按 位 与 expr & expr ˆ 按 位 异 或 expr ˆ expr | 按 位 或 expr | expr \begin{array}{c} \hline \bold{运算符} \qquad&\qquad \bold{功能} \qquad&\qquad \bold{用法}\\ \hline \text{\~{}} \qquad&\qquad \bold{位求反} \qquad&\qquad \text{~expr}\\ \text{<<} \qquad&\qquad \bold{左移} \qquad&\qquad \text{expr1 << expr2}\\ \text{>>} \qquad&\qquad \bold{右移} \qquad&\qquad \text{expr1 >> expr2}\\ \text{\&} \qquad&\qquad \bold{按位与} \qquad&\qquad \text{expr \& expr}\\ \text{\^{}} \qquad&\qquad \bold{按位异或} \qquad&\qquad \text{expr \^{} expr}\\ \text{|} \qquad&\qquad \bold{按位或} \qquad&\qquad \text{expr | expr}\\ \hline \end{array} 运算符˜<<>>&ˆ|功能位求反左移右移按位与按位异或按位或用法 exprexpr1 << expr2expr1 >> expr2expr & exprexpr ˆ exprexpr | expr
4.9 sizeof 运算符
- 对 char 或者类型为 char 的表达式执行 sizeof 运算,结果得 1
- 对引用类型执行 sizeof 运算得到被引用对象所占空间的大小
- 对指针执行 sizeof 运算得到指针本身所占空间的大小
- 对解引用指针执行 sizeof 运算得到指针指向的对象所占空间的大小,指针不需要有效
- sizeof 运算符无须我们提供一个具体的对象就可以计算对象成员的大小
- 对数组执行 sizeof 运算得到整个数组所占空间的大小,等价于对数组所有的元素各执行一次 sizeof 运算并将所得结果求和。sizeof 运算不会把数组转换成指针来处理
- 对 string 对象或 vector 对象执行 sizeof 运算只返回固定类型部分的大小,不会计算对象中的元素占用了多少空间
constexpr size_t sz = sizeof(ia) / size0f(*ia); // sz 为数组 ia 的长度
int arr2[sz]; // 正确
4.10 逗号运算符
逗号运算符首先对左侧的表达式求值,然后将求职结果丢弃掉。真正的结果是右侧表达式的值,如果右侧运算是左值,那么最终的求值结果也是左值。
4.11 类型转换
4.11.1 算数转换(*)
4.11.2 其他隐式类型转换
(1)数组转换成指针
int ia[100];
int *ip = ia; // ia 转换成指向数组首元素的指针
(2)指针的转换
- 0 和 nullptr 转换为任意指针类型
- 指针转换为 void*、const void*
- 子类转换为父类
(3)转换成布尔类型
char *cp = get_string();
if (cp) /* ... */ // 如果指针 cp 不是 0,条件为真
while (*cp) /* ... */ // 如果 *cp 不是空字符,条件为真
(4)转换成常量
int i;
const int &j = i;
const int *p = i;
(5)类类型定义的转换
string s, t = "a value"; // 字符串字面值转换为 string 类型
4.11.3 显式转换
强制类型转换的形式为:
c
a
s
t
-
n
a
m
e
<
t
y
p
e
>
(
e
x
p
r
e
s
s
i
o
n
)
;
cast\text{-}name\text<type\text>\ (expression);
cast-name<type> (expression);
cast-name:static_cast、dynamic_cast、const_cast、reinterpret_cast 中的一种。其中,dynamic_cast 支持运行时类型识别,在 19.2 节中介绍。
expression:要转换的值
type:转换的目标类型
(1)static_cast
任何具有明确定义的类型转换,只要不包含底层 const,都可以使用 static_cast。
作用: 把一个较大的算数类型赋给一个较小的类型。
编译器会知道我们不在乎精度损失,不会发出警告。我们可以通过 static_cast 找出存在于 void* 指针中的值。
void *p = &d; // 正确。任何非常量对象的地址都可以转换成 void*
double *dp = static_cast<double*> (p);
(2)const_cast
作用: 只能改变对象的底层 const,常用于有函数重载的上下文中。
const char *pc;
char *p = const_cast<char*> (pc); // 正确。但是通过 p 写值是未定义的行为
void func(const int& a)//形参为 int,引用指向 const int
{
int& b = const_cast<int&>(a);//去掉 const 限定,因为原本为非常量
b++;
}
可以去掉常量的 const 性质,编译器不会阻止我们对该对象进行写操作。
如果对象本身不是一个常量,使用强制类型转换获得写权限是合法的行为;
如果对象是一个常量,再使用 const_cast 执行写操作就会产生未定义的后果。
(3)reinterpret_cast
作用: 为运算对象的位模式提供较低层次上的重新解释。(不建议使用)
int *ip;
char *pc = reinterpret_cast<char*> (ip);
(4)旧式的强制类型转换
int i = (int) 3.14;
double d = double (3);
建议:避免强制类型转换的出现。
4.12 运算符优先级表(*)
第 5 章 语句
5.1 简单语句(*)
5.2 语句作用域(*)
5.3 条件语句
5.3.1 if 语句
if 和 else 的匹配问题
if (grade % 10 > 3)
if (grade % 10 > 7)
lettergrade += '+';
else
lettergrade += '-'; // 虽然缩进是与外层的 if 同列,但实际与内层的 if 匹配
应尽可能地添加大括号来确定块。
if (grade % 10 > 3)
{
if (grade % 10 > 7)
lettergrade += '+';
}
else
lettergrade += '-'; // 匹配外层的 if
5.3.2 switch 语句
case 标签必须是整型常量表达式。
即使不准备在 default 标签下做任何工作,定义一个 default 标签也是有用的。目的在于告诉程序读者已经考虑到了默认的情况,只是啥也没做。
5.4 迭代语句
5.4.1 while 语句(*)
5.4.2 传统的 for 语句(*)
5.4.3 范围 for 语句
如果要对序列中的元素进行写操作,循环变量必须声明成引用类型。
5.4.4 do while 语句(*)
5.5 跳转语句
5.5.1 break 语句(*)
5.5.2 continue 语句(*)
5.5.3 go to 语句(*)
5.6 try 语句块和异常处理
5.6.1 throw 表达式(*)
5.6.2 try 语句块(*)
5.6.3 标准异常
C++ 标准库定义的异常类分别在 4 个头文件中:
-
< exception >:最通用的异常类 exception。只报告异常的发生,不提供额外的任何信息。
-
< stdexcept >:定义了几种常用的异常类。
Cannot read properties of undefined (reading 'type') -
< new >:定义了 bad_alloc 异常类型。见 12.1.2 节
-
< type_info >:定义了 bad_cast 异常类型。见 19.2 节
对于 exception、bad_alloc 和 type_info 对象,我们只能用默认初始化的方式创建,不允许为这些对象提供初值。其他异常类需要提供一个字符串的初值。
异常类型只定义了一个 what() 成员函数,返回一个提供关于异常文本信息的字符串。
第 6 章 函数
6.1 函数基础
函数的调用完成两项工作:
- 用实参初始化函数对应的形参
- 将控制权转移给被调用的函数
函数结束执行后也完成两项工作:
- 返回 return 中的值(如果有)
- 将控制权从被调函数转移回主调函数
编译器能以任意可行的顺序对实参求值。
为了与 C 语言兼容,可以使用关键字表示函数没有形参:
void f(void) { /* ... */ }
6.1.1 局部对象(*)
6.1.2 函数声明(原型)(*)
6.1.3 分离式编译(*)
6.2 参数传递
6.2.1 传值参数(*)
6.2.2 传引用参数(*)
6.2.3 const 形参和实参(*)
6.2.4 数组形参
(1)管理指针形参的三种常用技术
-
使用标记指定数组长度
void print(const char *cp) { if (cp) while (*cp) cout << *cp++; }
-
使用标准库规范
void print(const int *beg, const int *end) { while (beg != end) cout << *beg++; }
-
显示传递一个表示数组大小的形参
void print(const int ia[], size_t size) { for (size_t i = 0; i < size; ++i) cout << ia[i]; }
(2)数组引用形参
数组引用形参限制了函数的可用性:只能将函数作用于指定大小的数组。
void print(int (&arr)[10])
{
for (auto elem : arr)
cout << elem;
}
// &arr 两端的括号必不可少
f(int &arr[10]); // 将 arr 声明成了引用的数组
(3)传递多维数组
void print(int (*matrix)[10], int rowSize) { /* ... */ }
// 等价定义
void print(int matrix[][10], int rowSize) { /* ... */ }
6.2.5 main:处理命令行选项(*)
6.2.6 含有可变形参的函数
(1)initializer_list 形参
initializer_list 是一种标准库类型,用于表示某种特定类型的值的数组,定义在头文件 < initializer_list > 中。
Cannot read properties of undefined (reading 'type')
void error_msg(ErrCode e, initializer_list<string> il)
{
cout << e.msg() << endl;
for (const auto &elem : il)
cout << elem << " ";
cout << endl;
}
// expected 和 actual 是 string 对象
if (expected != actual)
error_msg(ErrCode(42), {"functionX", expected, actual});
else
error_msg(ErrCode(0), {"functionX", "okay"});
(2)省略符形参
省略符形参是为了便于 C++ 程序访问某些特殊的 C 代码而设置的,这些代码使用了名为 varargs 的 C 标准库功能。省略符形参应该仅用于 C 和 C++ 通用的类型,大多数类型的对象在传递给省略符形参时都无法正确拷贝。
void foo(parm_list, ...);
void foo(...);
第一种形式指定了 foo() 函数的部分形参类型,对应于这些形参的实参将会执行正常的类型检查。省略符形参所对应的实参无需类型检查。
6.3 返回类型和 return 语句
6.3.1 无返回值函数(*)
6.3.2 有返回值函数
(1)列表初始化返回值
C++11 新标准规定,函数可以返回花括号包围的值的列表。此处的列表也用来对表示函数返回的临时量进行初始化。
vector<string> process()
{
// ...
// expected 和 actual 是 string 对象
if (expected.empty())
return {};
else if (expected != actual)
return {"functionX", expected, actual};
else
return {"functionX", "okay"};
}
(2)主函数 main 的返回值
main 函数的返回值可以看做是状态指示器,返回 0 表示执行成功,返回其他值表示执行失败,其中非 0 值的具体含义依机器而定。为了使返回值与机器无关,< cstdlib > 头文件定义了两个预处理变量,我们可以用这两个变量分别表示成功与失败。
int main()
{
if (some_failure)
return EXIT_FAILURE;
else
return EXIT_SUCCESS;
}
6.3.3 返回数组指针
(1)使用类型别名
typedef int arrT[10]; // arrT 是一个类型别名
using arrT = int[10]; // arrT 的等价声明
arrT* func(int i); // func 返回一个指向含有 10 个整数的数组的指针
(2)声明一个返回数组指针的函数
int arr[10];
int *p1[10]; // p1 是含有 10 个指针的数组
int (p2)[10] = &arr; // p2 是一个指针,指向含有 10 个整数的数组
函数形式:
T
y
p
e
(
∗
f
u
n
c
t
i
o
n
(
p
a
r
a
m
e
t
e
r
_
l
i
s
t
)
)
[
d
i
m
e
n
s
i
o
n
]
Type\ \ (*function(parameter\_list))[dimension]
Type (∗function(parameter_list))[dimension]
下面这个 func 函数的声明没有使用类型别名:
int (*func(int i))[10]; // 返回指向含有 10 个整数的数组的指针
(3)使用尾置返回类型
在 C++11 新标准中有一种可以简化上述 func 声明的方法,就是使用尾置返回类型。这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或者数组的引用。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个 auto。
auto func(int i) -> int(*)[10];
(4)使用 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;
}
arrPtr 使用关键字 decltype 表示它的返回类型是个指针,并且该指针所指的对象与 odd 的类型一致。因为 odd 是数组,所以 arrPtr 返回一个指向含有5 个整数的数组的指针。
有一个地方需要注意:decltype 并不负责把数组类型转换成对应的指针,所以 decltype 的结果是个数组,要想表示 arrPtr 返回指针还必须在函数声明时加一个*符号。
6.4 函数重载
(1)重载和 const 形参
顶层 const 不能影像传入函数的对象:
Record lookup(Phone);
Record lookup(const Phone); // 重复声明
Record lookup(Phone*);
Record lookup(Phone* const); // 重复声明
底层 const 对参数类型会加以区分开来:
Record lookup(Account&);
Record lookup(const Account&); // 新函数
Record lookup(Account*);
Record lookup(const Account*); // 新函数
(2)const_cast 和重载
回忆函数 shorterString:
const string &shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size ? s1 : s2;
}
我们可以对两个非常量的 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<const string&> (r);
}
6.4.1 重载与作用域
重载对作用域的一般性质并没有什么改变∶如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。在不同的作用域中无法重载函数名:
string read ();
void print(const string s);
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(double)被隐藏掉了
}
调用 print 函数的过程非常相似。在 fooBar 内声明的 print(int)隐藏了之前两个 print 函数,因此只有一个 print 函数是可用的:该函数以 int 值作为参数。
当我们调用 print 函数时,编译器首先寻找对该函数名的声明,找到的是接受 int值的那个局部声明。一旦在当前作用域中找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体。剩下的工作就是检查函数调用是否有效了。
6.5 特殊用途语言特性
6.5.1 默认实参
我们使用 string 对象表示窗口的内容。一般情况下,我们希望该窗口的高、宽和背景字符都使用默认值。但是同时我们也应该允许用户为这几个参数自由指定与默认值不同的数值。为了使得窗口函数既能接纳默认值,也能接受用户指定的值,我们把它定义成如下的形式∶
typedef string∶∶size_type sz; // 关于typedef 参见2.5.1 节
string screen(sz ht = 24, sz wid = 80, char backgrnd ='');
我们可以为一个或多个形参定义默认值,不过需要注意的是,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
(1)使用默认实参调用函数
string window; // 等价于screen(24,80,'')
window = screen(); // 等价于 screen(66,80,'')
window = screen(66); window = screen(66,256); // screen(66,256,'')
window = screen(66, 256, '#'); // screen(66,256,'#')
函数调用时实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参(靠右侧位置)。例如,要想覆盖 backgrnd 的默认值,必须为 ht 和 wid 提供实参∶
window = screen(, , '?'); // 错误∶只能省略尾部的实参
window = screen('?'); // 调用 screen('?',80,'')
需要注意,第二个调用传递一个字符值,是合法的调用。然而尽管如此,它的实际效果却与书写的意图不符。该调用之所以合法是因为 ‘?’ 是个 char,而函数最左侧形参的类型 string::size type 是一种无符号整数类型,所以 char 类型可以转换成函数最左侧形参的类型。当该调用发生时,char 类型的实参隐式地转换成 string::size type,然后作为 height 的值传递给函数。在我们的机器上,’?’ 对应的十六进制数是 0x3F,也就是十进制数的 63,所以该调用把值 63 传给了形参 height。
当设计含有默认实参的函数时,其中一项任务是合理设置形参的顺序,尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面。
(2)默认实参声明
需要注意,在给定的作用域中一个形参只能被赋予一次默认实参。换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。假如给定
// 表示高度和宽度的形参没有默认值
string screen (sz, sz, char = '');
我们不能修改一个已经存在的默认值:
string screen (sz, sz, char = '*'); // 错误∶重复声明
但是可以按照如下形式添加默认实参:
string screen (Sz = 24, sz = 80, char); // 正确∶添加默认实参
(3)默认实参初始值
局部变量不能作为默认实参。除此之外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参:
// 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.4 节中我们编写了一个小函数 shorterString,它的功能是比较两个 string 形参的长度并返回长度较小的 string 的引用。把这种规模较小的操作定义成函数有很多好处,主要包括:
- 阅读和理解 shorterString 函数的调用要比读懂等价的条件表达式容易得多。
- 使用函数可以确保行为的统一,每次相关操作都能保证按照同样的方式进行。
- 如果我们需要修改计算过程,显然修改函数要比先找到等价表达式所有出现的地方
再逐一修改更容易。 - 函数可以被其他应用重复利用,省去了程序员重新编写的代价。
然而,使用 shorterString 函数也存在一个潜在的缺点:调用函数一般比求等价表达式的值要慢一些。
在大多数机器上,一次函数调用其实包含着一系列工作:
- 调用前要先保存寄存器,并在返回时恢复;
- 可能需要拷贝实参;
- 程序转向一个新的位置继续执行。
(1)内联函数
将函数指定为内联函数(inline),通常就是将它在每个调用点上"内联地"展开。 假设我们把 shorterString 函数定义成内联函数,则如下调用
cout << shorterString(s1, s2) << endl;
将在编译过程中展开成类似于下面的形式
cout << (s1.size() <= s2.size ? s1 : s2;) << endl;
从而消除了函数的运行开销。
在 shorterString 函数的返回类型前面加上关键字 inline,这样就可以将它声明成内联函数了:
// 内联版本∶寻找两个 string 对象中较短的那个 inline const string &
shorterString (const string &s1, const string &s2)
{
return s1.size ()<= s2.size() ? s1 : s2;
}
注意:内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。
(2)constexpr 函数
constexpr 函数遵循的几项约定:
- 函数的返回类型及所有形参的类型都得是字面值类型
- 函数体中必须有且只有一条 return 语句
constexpr int new_sz() { return 42; }
constexpr int foo = new_sz(); // 正确:foo 是一个常量表达式
执行该初始化任务时,编译器把对 constexpr 函数的调用替换成其结果值。为了能在编译过程中随时展开,constexpr 函数被隐式地指定为内联函数。
constexpr 函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行。例如,constexpr 函数中可以有空语句、类型别名以及 using 声明。
constexpr 函数的返回值也可以不是一个常量:
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 函数,比如 int 类型的对象 i,则返回值是一个非常量表达式。当把 scale 函数用在需要常量表达式的上下文中时,由编译器负责检查函数的结果是否符合要求。如果结果恰好不是常量表达式,编译器将发出错误信息。
通常,我们把内联函数和 constexpr 函数放在头文件内。
6.5.3 调试帮助
(1)assert 预处理宏
assert 是一种预处理宏(preprocessor marco)。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。assert 宏使用一个表达式作为它的条件∶
assert(expr);
首先对 expr 求值,如果表达式为假(即 0),assert 输出信息并终止程序的执行。如果表达式为真(即非 0),assert 什么也不做。
assert 宏定义在 < assert > 头文件中。预处理名字由预处理器而非编译器管理,因此我们可以直接使用预处理名字而无须提供 using 声明。
和预处理变量一样,宏名字在程序内必须唯一。含有 cassert 头文件的程序不能再定义名为 assert 的变量、函数或者其他实体。在实际编程过程中,即使我们没有包含 < cassert > 头文件,也最好不要为了其他目的使用 assert。很多头文件都包含了 cassert,这就意味着即使你没有直接包含 cassert,它也很有可能通过其他途径包含在你的程序中。
assert 宏常用于检查“不能发生”的条件。例如,一个对输入文本进行操作的程序可能要求所有给定单词的长度都大干某个阈值。此时,程序可以包含一条如下所示的语句;
assert(word.size() > threshold);
(2)NDEBUG 预处理变量
assert 的行为依赖于一个名为 NDEBUG 的预处理变量的状态。如果定义了NDEBUG,则 assert 什么也不做。默认状态下没有定义 NDEBUG,此时 assert 将执行运行时检查。
定义 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__:存放文件编译日期的字符串字面值。
可以使用这些常量在错误消息中提供更多信息。
6.6 函数匹配
void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(5.6); // 调用 void f(double, double);
f(42, 2.56); // 发生二义性,报错
(1)函数匹配的步骤
-
第一步 —— 选定本次调用对应的重载函数集,集合中的函数称为候选函数。
候选函数具备两个特征:
- 与被调用的函数同名
- 其声明在调用点可见。
-
第二步 —— 从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数。
可行函数也有两个特征:
- 其形参数量与本次调用提供的实参数量相等
- 每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。
-
第三步 —— 从可行函数中选择与本次调用最匹配的函数。
在这一过程中,逐一检查函数调用提供的实参,寻找形参类型与实参类型最匹配的那个可行函数。
在我们的例子中,编译器把 f(5.6) 解析成对含有两个 double 形参的函数的调用,并使用默认值填补我们未提供的第二个实参。
(2)含有多个形参的函数匹配
编译器依次检查每个实参以确定哪个函数是最佳匹配。如果有且只有一个函数满足下列条件,则匹配成功∶
- 该函数每个实参的匹配都不劣于其他可行函数需要的匹配。
- 至少有一个实参的匹配优于其他可行函数提供的匹配。
如果在检查了所有实参之后没有任何一个函数脱颖而出,则该调用是错误的。编译器将报告二义性调用的信息。
6.6.1 实参类型转换
为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序如下所示∶
- 精确匹配,包括以下情况:
- 实参类型和形参类型相同。
- 实参从数组类型或函数类型转换成对应的指针类型
- 向实参添加顶层 const 或者从实参中删除顶层 const。
- 通过 const 转换实现的匹配。
- 通过类型提升实现的匹配。
- 通过算术类型转换或指针转换实现的匹配。
- 通过类类型转换实现的匹配(参见 14.9 节,第 514 页,将详细介绍这种转换)。
6.7 函数指针
bool lengthCompare(const string &, const string &); // 函数类型
bool (*pf)(const string &, const string &); // 函数指针
(1)使用函数指针
函数名被赋予函数指针时,会自动转换成指针。
可以直接使用函数指针,不需要解引操作。
pf = lengthCompare; // pf 指向名为 lengthCompare 的函数
pf = &lengthCompare; // 等价语句
bool b1 = pf("hello", "goodbye"); // 调用 lengthCompare 函数
bool b2 = (*pf)("heelo", "goodbye"); // 等价调用
(2)函数指针的重载
编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配。
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 的返回类型不匹配
(3)函数指针作为形参
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 &)); // 等价声明
使用类型别名和 decltype 简化代码:
// Func 和 Func2 是函数类型
typedef bool Func(const string &, const string &);
typedef decltype (lengthCompare) Func2; //等价的类型
// FuncP 和 FuncP2 是指向函数的指针
typedef bool (*FuncP)(const string &, const string &);
typedef decltype (lengthCompare) *FuncP2; // 等价的类型
// useBigger 的等价声明,其中使用了类型别名
void useBigger(const string &, const string &, Func);
void useBigger(const string &, const string &, FuncP2);
(4)返回函数指针
使用类型别名声明一个返回函数指针的函数:
using F = int(int *, int); // F 是函数类型,不是指针
using PF = int(*)(int *, int); // PF 是指针类型
需要注意的是,和函数类型的形参不一样,返回类型不会自动地转换成指针。我们必须显式地将返回类型指定为指针∶
PF f1(int); // 正确:PF 是指向函数的指针,f1 返回指向函数的指针
F f1(int); // 错误:F是函数类型,f1 不能返回一个函数
F *f1(int); // 正确:显式地指定返回类型是指向函数的指针
当然,我们也能用下面的形式直接声明 f1:
int (*f1(int))(int*,int);
按照由内向外的顺序阅读这条声明语句:我们看到 f1 有形参列表,所以 f1 是个函数;f1 前面有 *,所以 f1 返回一个指针;进一步观察发现,指针的类型本身也包含形参列表,因此指针指向函数,该函数的返回类型是 int。
还可以使用尾置返回类型的方式声明一个返回函数指针的函数∶
auto f1(int) -> int (*)(int *, int);
(5)将 auto 和 decltype 用于函数指针类型
如果我们明确知道返回的函数是哪一个,就能使用 decltype 简化书写函数指针返回类型的过程。
string::size type sumLength(const string &, const string &);
string::size_type largerLength(const string &, const string );
// 根据其形参的取值,getFcn 函数返回指向 sumLength 或者 largerLength 的指针
decltype (sumLength) *getFcn(const string &);
声明 getFcn 唯一需要注意的地方是,牢记当我们将 decltype 作用于某个函数时,它返回函数类型而非指针类型。因此,我们显式地加上 * 以表明我们需要返回指针,而非函数本身。