《C++11Primer》阅读随记 -- 六、函数

第六章 函数

返回数组指针

因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用。虽然从语法上来说,要想定义一个返回数组的指针或引用的函数比较繁琐,但是有一些方法可以简化,其中最直接的方法就是使用类型别名

typedef int arrT[10];		// arrT 是一个类型别名,它表示的类型是
							// 含有 10 个整数的数组
using arrT = int[10];		// arrT 的等价声明
arrT* func(int i);			// func 返回一个指向含有 10 个整数的数组的指针

其中 arrT 是含有 10 个整数的数组的别名。因为服务发返回数组,所以将返回类型定义成数组的指针。

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

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

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

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

Type (*function(parameter_list))[dimension]

int (*func(int i))[10];

应该按照如下顺序逐层理解该声明含义:

  • func(int i) 表示调用 func 函数时需要一个 int 类型的实参
  • (*func(int i)) 意为着我们可以对函数调用的结果执行解引用操作
  • (*func(int i))[10] 表示解引用 func 的调用结果将得到一个大小是 10 的数组
  • int (*func(int i))[10] 表示数组中的元素是 int

C++11 新特性 -> 尾置返回类型

尾置返回类型跟在形参列表后面并以一个 -> 符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个 auto

// func 接受一个 int 类型的实参,返回一个指针,该指针指向含有 10 个整数的数组
auto func(int) -> int(*)[10];

使用 decltype

还有一种情况,如果我们知道函数返回的指针将指向哪个数组,就可以使用 decltype 关键字声明返回类型。例如,下面的函数返回一个指针,该指针根据参数 i 的不同指向两个已知数组中的某一个

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 使用关键字 decltyp 表示它的返回类型是个指针,并且该指针所指的对象与 odd 的类型一致。因为 odd 是数组,所以 arrPtr 返回一个指向含有 5 个整数的数组的指针。有一个地方需要注意:decltype 并不负责吧数组类型转换成对应的指针,所以 decltype 的结果是个数组,要想表示 arrPtr 返回指针还必须在函数声明时加一个 * 符号。

重载和 const 形参

顶层 const 不影响传入函数的对象。一个拥有顶层 const 的形参无法和另一个没有顶层 const 形参区分开。

Record lookup(Phone);
Record lookup(const Phone);			// 重复声明了 Record lookup(Phone)

Record lookup(Phone*);
Record lookup(Phone* const);			// 重复声明了 Record lookup(Phone*)

在这两组函数的声明中,每一组的第二个声明和第一个声明是等价的。

// 对于接受引用或指针的函数来说,对象是常量还是非常量对应的形参不同
// 定义了 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;
}

我们可以对两个非常量的 string 实参调用这个函数,但返回的结果仍然是 const string& 的引用。因此需要一种新的 shorterString 函数,当它的实参不是常量时,得到的结果是一个普通的引用。

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

默认实参声明

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

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

// 不能修改一个已经存在的默认值
string screen(sz, sz, char = '*');	// 重复声明

// 但是可以按照如下形式添加默认实参
string screen(sz = 24, sz = 80, char);	// 正确声明:添加默认实参

内联函数和 constexpr 函数

之前有一个比较两个 string 形参的长度并返回长度较小的 string 引用给的函数。把这种规模较小的操作定义成函数有很多好处,主要包括:

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

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

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

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

cout << shorter String(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 函数的方法与其他函数类似,不过要遵循几项规定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条 return 语句

constexpr int new_sz() { return 42; }
constexpr int foo = 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 arr[scale(2)];			// 正确: scale(2) 是常量表达式
int i = 2;					// i 不是常量表达式
int a2[scale(i)];			// 错误,scale(i) 不是常量表达式

assert 预处理宏

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

assert( expr );

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

assert 宏定义在 cassert 头文件中。

NDEBUG 预处理变量

assert 的行为以来u有一个名为 NDEBUG 的预处理变量的状态。如果定义勒 NDEBUGassert 什么也不做。默认状态下没有定义该预处理变量,此时 assert 将执行运行时检查

可以使用一个 #define 语句定义 NDEBUG,从而关闭调试祖杭太。同时很多编译器都提供了一个命令行选项是我们可以定义预处理变量

CC -D NDEBUG main.c

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

定义该变量可以避免检查各种条件所需的运行时开销,当然此时根本就不会执行运行时检查。因此,assert 应该仅用于验证那些确实不可能发生的事情。

除了用于 assert 外,也可以使用 NDEBUG 编写自己的条件调试代码。如果 NDEBUG 未定义,将执行 #ifndef#endif 之间的代码;

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

在这u但代码中,使用变量 __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;
Error:wbdebug.cc : in function main at line 27
	  Compiled on Jul 11 2012 at 20:50:03
	  Word read was "foo": Length too short

函数指针

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

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

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

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

*pf 两端的括号必不可少。如果不写这对括号,则 pf 是一个返回值为 bool 指针的函数

使用函数指针

当我们把函数名为作为一个值使用时,该函数自动地转换成指针。例如,按照如下形式可以将 lengthCompare 的地址赋给 pf

pf = lengthCompare;		// pf 指向名为 lengthCompare 的函数
pf = &lengthCompare;	// 等价的复制语句:取地址符是可选的	

此外,我们还能直接使用指向函数的指针调用该函数,无须提前解引用指针:

bool b1 = pf("hello", "goodbye");			// 调用 lengthCompare 函数
bool b2 = (*pf)("hello", "goodby");			// 等价调用
bool b3 = lengthCompare("hello", "goodby");	// 等价调用

在指向不同函数类型的指针间不存在转换规则。

string::size_type sumLength(const string&, const string&);
bool cstringCompare(const char*, const char*);
pf = 0;					// 正确: pf 不指向任何函数
pf = sumLength;			// 错误: 返回类型不匹配
pf = cstringCompare;	// 错误: 形参类型不匹配
pf = lengthCompare;		// 正确: 精确匹配

重载函数的指针

当使用重载函数时,上下文必须清晰地界定到底选用哪个函数。

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 的返回类型不匹配

函数指针形参

// 第三个形参是函数类型,它会自动地转换成指向函数的指针
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&))

可以直接把函数作为实参使用,此时会自动转换成指针

// 自动将函数 lengthCompare 转换成指向该函数的指针
useBigger(s1, s2, lengthCompare);

正如 useBigger 的声明语句所示,直接使用函数指针类型显得冗长而繁琐。类型别名和 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;	// 等价的类型

我们使用 typedef 定义自己的类型。FuncFunc2 是函数类型,而 FuncPFuncP2 是指针类型。需要注意的是,decltype 返回函数类型,此时不会将函数类型自动转换成指针类型。所以只有在结果前面加上 * 才能得到指针。可以使用如下的形式重新声明 userBigger

// userBigger 的等价声明,其中使用类类型别名
void useBigger(const string&, const string&, Func);
void useBigger(const string&, const string&, FuncP2);

返回指向函数的指针

和数组类似,虽然不能返回一个函数,但是能返回指向函数类型的指针。然而,必须把返回类型写成指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理。要想声明一个返回函数指针的函数,最简单的办法是使用类型别名:

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

将 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 作用于某个函数时,它返回函数类型而非指针类型。因此需要显示地加上 * 以表明需要返回指针,而非函数本身。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Artintel

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值