【C++ primer】第6章 函数 (2)


Part I: The Basics
Chapter 6. Functions


6.4 函数重载

重载(overloaded):在同一作用域内,函数有相同的名字,但有不同的形参列表。

main 函数不能重载。

定义重载函数

Record lookup(const Account&);  // find by Account
Record lookup(const Phone&);    // find by Phone
Record lookup(const Name&);     // find by Name

Account acct;
Phone phone;
Record r1 = lookup(acct);  // call version that takes an Account
Record r2 = lookup(phone); // call version that takes a Phone

如果两个函数只有返回类型不同,那么这是错误的。

Record lookup(const Account&); 
bool lookup(const Account&);   // error: only the return type is different

判断两个形参的类型是否相同

有时两个形参列表看起来不同,但实际上是一样的:

// each pair declares the same function
Record lookup(const Account &acct);
Record lookup(const Account&); // parameter names are ignored

typedef Phone Telno;
Record lookup(const Phone&);
Record lookup(const Telno&); // Telno and Phone are the same type

重载和 const 形参

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

Record lookup(Phone); 
Record lookup(const Phone);   // redeclares Record lookup(Phone) 

Record lookup(Phone*); 
Record lookup(Phone* const);  // redeclares Record lookup(Phone*)

如果形参是某种类型的指针或引用,则区分其指向的是 const 对象还是 非const 对象可以实现函数重载,这样的 const 是底层的。

// functions taking const and nonconst references or pointers have different parameters 
// declarations for four independent, overloaded functions 
Record lookup(Account&);       // function that takes a reference to Account 
Record lookup(const Account&); // new function that takes a const reference

Record lookup(Account*);       // new function, takes a pointer to Account 
Record lookup(const Account*); // new function, takes a pointer to const

在上述例子中,编译器可以通过实参是否是常量来推断应该调用哪个函数。
因为 const 不能转换为 非const,所以只能把 const 对象(或指向 const 的指针)传递给 const 形参。
因为 非const 能转换为 const,所以上面的 4 个函数都能作用于非const 对象或指向 非const 的指针。

const_cast 和重载

// return a reference to the shorter of two strings 
const string &shorterString(const string &s1, const string &s2) {
	return s1.size() <= s2.size() ? s1 : s2;
}

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

调用重载函数

函数匹配(function matching):是将某个函数调用与一组重载函数中的特定函数相关联的过程,也称为重载解析(overloaded resolution)。
编译器通过比较调用函数的实参与重载集合中每个函数的形参,来确定要调用的函数。

调用重载函数时,有 3 种结果:

  • 编译器找到一个与实参最佳匹配(best match)的函数,并生成调用该函数的代码。
  • 没有函数具有与调用的实参匹配的形参,在这种情况下,编译器会发出一条无匹配(no match)的错误信息。
  • 有多个函数可以匹配,且没有一个匹配是明显最佳的。这种情况是错误的,是二义性调用(ambiguous call)。

重载与作用域

string read(); 
void print(const string &);
void print(double);   // overloads the print function 
void fooBar(int ival) {
	bool read = false; // new scope: hides the outer declaration of read
	string s = read(); // error: read is a bool variable, not a function
	// bad practice: usually it's a bad idea to declare functions at local scope
	void print(int);  // new scope: hides previous instances of print
	print("Value: "); // error: print(const string &) is hidden
	print(ival);      // ok: print(int) is visible
	print(3.14);      // ok: calls print(int); print(double) is hidden
}

在 C++语言中,名字查找发生在类型检查之前

void print(const string &);
void print(double); // overloads the print function
void print(int);    // another overloaded instance
void fooBar2(int ival) {
	print("Value: "); // calls print(const string &)
	print(ival);      // calls print(int)
	print(3.14);      // calls print(double)
}

6.5 特殊用途语言特性

默认实参

调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。

typedef string::size_type sz;  // typedef see § 2.5.1 (p. 67) 
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' '); 

默认实参被指定为形参列表中形参的初始值。
可以为一个或多个形参定义默认值。但是,如果一个形参具有默认实参,则其后的所有形参也必须具有默认实参。

调用含有默认实参的函数

string window; 
window = screen();  // equivalent to screen(24,80,' ')
window = screen(66);// equivalent to screen(66,80,' ') 
window = screen(66, 256);      // screen(66,256,' ') 
window = screen(66, 256, '#'); // screen(66,256,'#')

调用的实参按位置解析。 默认参数用于调用的尾部的(最右边)实参。
例如,要覆盖 backgrnd 的默认值,必须提供 htwid 的实参:

window = screen(, , '?'); // error: can omit only trailing arguments 
window = screen('?');     // calls screen('?',80,' '); '?' has the hexadecimal value 0x3F, which is decimal 63

设计具有默认实参的函数时,其中一部分任务是设计形参的顺序,以使那些不太可能使用默认值的形参出现在前面,而那些最可能使用默认值的形参出现在后面。

默认实参声明

在给定的作用域中,一个形参只能被赋予一次默认实参。因此,任何后续声明都只能为先前未指定默认值的形参添加默认值。当然,只有在右侧的所有形参都已经具有默认值的情况下,才可以指定默认值。

string screen(sz, sz, char = ' ');	// no default for the height or width parameters 
string screen(sz, sz, char = '*');	// error: redeclaration
string screen(sz = 24, sz = 80, char);	// ok: adds default arguments

通常,应该在合适的头文件中的函数声明中,指定默认实参。

默认实参初始值

局部变量不能作为默认实参。

// the declarations of wd, def, and ht must appear outside a function 
sz wd = 80;
char def = ' '; 
sz ht();
string screen(sz = ht(), sz = wd, char = def);
string window = screen(); // calls screen(ht(), 80, ' ')

在函数声明的范围内解析用作默认参数的名称。这些名称代表的值在调用时进行计算:

void f2() {
	def =   '*';	// changes the value of a default argument
	sz wd = 100;	// hides the outer definition of wd but does not change the default
	window = screen();	// calls screen(ht(), 80, '*')
}

内联函数和 constexpr 函数

inline 函数避免函数调用的开销

在每次调用时,通常将指定为 inline 的函数“内联”展开。

// inline version: find the shorter of two strings 
inline const string & shorterString(const string &s1, const string &s2) {
	return s1.size() <= s2.size() ? s1 : s2;
}

shorterString 定义为 inline,调用

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

(很可能)在编译过程中展开成为类似下面的这种形式:

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

从而消除了 shorterString 函数的运行时开销。

注意:inline 规范只是向编译器发出的一个请求。编译器可以选择忽略这个请求。

通常,inline 机制旨在优化规模较小、流程直接、调用频繁的函数。许多编译器不支持内联递归函数。一个 75 行的函数几乎肯定不会内联扩展。

constexpr 函数

constexpr 函数是可用于常量表达式的函数。
constexpr 函数的定义与其他函数类似,但必须满足某些限制:return 类型和每个形参的类型必须是字面值类型,且函数主体必须有且只有一条 return 语句。

constexpr int new_sz() { return 42; }
constexpr int foo = new_sz();  // ok: foo is a constant expression

编译器可以在编译时确认 new_sz() 调用返回一个常量表达式,所以可以使用 new_sz() 初始化 constexpr 变量 foo。当可以初始化时,编译器将对 constexpr 函数的调用替换成其结果值。为了能够立即展开该函数,constexpr 函数是隐式内联的

constexpr 函数主体可以包含其他语句,只要这些语句在运行时不执行任何动作。例如,空语句,类型别名,using 声明。

允许 constexpr 函数的返回值不是一个常量

// scale(arg) is a constant expression if arg is a constant expression 
constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }

如果 scale 函数的实参是一个常量表达式,函数返回一个常量表达式,反之则不然:

int arr[scale(2)]; // ok: scale(2) is a constant expression 
int i = 2;         // i is not a constant expression 
int a2[scale(i)];  // error: scale(i) is not a constant expression

把 inline 函数和 constexpr 函数放在头文件中
与其他函数不同,inline 函数和 constexpr 函数可以在程序中定义多次。毕竟,为了展开代码,编译器需要定义,而不仅仅是声明。但是,给定的 inline 函数或 constexpr 函数的所有定义必须完全匹配。因此,inline 函数和 constexpr 函数通常定义在头文件中

调试帮助

思想:程序将包含仅在程序开发时才执行的调试代码。当应用程序完成并准备好交付时,调试代码将关闭。这种方法使用两种预处理器工具:assertNDEBUG

assert 预处理宏

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

assert(expr);

对 expr 求值,如果表达式为 false,那么 assert 输出信息并终止程序。如果表达式为 true,那么 assert 什么也不做。

assert 宏定义在 cassert 头文件中。预处理名字由预处理器管理,而不是编译器管理。

和预处理变量一样,宏的名字在程序中必须唯一。含有 cassert 头文件中的程序不能再定义名为 assert 的变量、函数或其他实体。即使没有包含 cassert 头文件,最好也不要使用 assert 名字。

assert 宏通常用于检查“不能发生”的条件。它可作为调试程序的辅助手段,但不能代替程序的运行时逻辑检查或程序应包含的错误检查。
例,程序要求 word 的长度总是大于 threshold

assert(word.size() > threshold);

NDEBUG 预处理变量

assert 的行为取决于 NDEBUG 预处理变量的状态。若定义了 NDEBUG,则 assert 什么也不做。
默认情况下没有定义 NDEBUG,所以,默认 assert 执行运行时检查。

很多编译器提供了一个命令行选项来定义预处理变量:

$ CC -D NDEBUG main.C  # use /D with the Microsoft compiler

上条命令等价于在 main.C 文件的开头写

#define NDEBUG

表示关闭调试状态。

还可以使用 NDEBUG 编写自己的条件调试代码:

void print(const int ia[], size_t size) {
#ifndef NDEBUG
	// _ _func_ _ is a local static defined by the compiler that holds the function's name 
	cerr << _ _func_ _ << ": array size is " << size << endl; 
#endif 
// ...

预处理器还定义了 5 个对于调试很有用的名字:

  • __func__:存放当前函数名的字符串字面值
  • __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;

6.6 函数匹配

以下面这组函数及其调用为例:

void f(); 
void f(int); 
void f(int, int);
void f(double, double = 3.14);
f(5.6);  // calls void f(double, double) 

确定候选函数和可行函数

候选函数 (candidate function):与调用函数同名的函数,且其声明在调用点可见。在这个例子中,有 4 个名为 f 的候选函数。
可行函数 (viable function):从候选函数中选出的函数,其形参的数目与调用的实参数目相等,每个实参的类型必须匹配或者可以转换为对应形参的类型。在这个例子中,f(int)f(double, double=3.14) 是可行的。
如果没找到可行函数,编译器将报告无匹配函数。

如果有,找到最佳匹配

基本思想:实参类型与形参类型越接近,匹配越好。
精确匹配比需要类型转换的匹配更好。因此,编译器将调用 f(5.6) 解析为函数 f(double, double=3.14) 的调用。

含有多个形参的函数匹配

分析调用:f(42, 2.56);
此例中,可行函数是 f(int, int)f(double, double)

如果有且仅有一个函数满足以下条件,则匹配成功:
①每个实参的匹配都不劣于其他可行函数要求的匹配;
②至少有一个实参的匹配优于其他可行函数提供的匹配。
如果检查了每个实参,没有哪个函数最优,那么调用是错误的。编译器报告调用是二义性的。

编译器拒绝调用 f(42, 2.56);,因为它是二义性的:每个可行函数在调用的一个实参上匹配比其他的匹配更优。

在设计良好的系统中,不需要对实参进行强制类型转换。
调用重载函数应该不需要强制类型转换。需要强制转换表明形参集合设计不佳。

实参类型转换

为了确定最佳匹配,编译器对每个实参类型到其对应形参类型的转换进行排名:

  1. 精确匹配,包括:
    实参类型和形参类型相同。
    实参从数组类型或函数类型转换成对应的指针类型。
    向实参添加顶层 const 或从实参中删除顶层 const。
  2. 通过 const 转换实现的匹配。
  3. 通过类型提升实现的匹配。
  4. 通过算术类型转换或指针转换实现的匹配。
  5. 通过类类型转换实现的匹配。

需要类型提升或算术类型转换的匹配

小整型总是提升到 int 类型或更大的整数类型。

void ff(int);
void ff(short);
ff('a');   // char promotes to int; calls f(int) 

所有算术类型转换的级别都一样。

void manip(long);
void manip(float);
manip(3.14); // 3.14 is a double; error: ambiguous call

函数匹配和 const 实参

Record lookup(Account&);       // function that takes a reference to Account
Record lookup(const Account&); // new function that takes a const reference 
const Account a;
Account b;
lookup(a);   // calls lookup(const Account&)
lookup(b);   // calls lookup(Account&)

在第一个调用中,可行函数只有 lookup(const Account&)
在第二个调用中,两个函数都是可行的。然而,非常量对象初始化对常量的引用需要转换,具有非常量形参的函数与 b 精准匹配。

指针形参与之类似。


6.7 函数指针

函数指针指向的是函数而不是对象。
函数的类型由它的返回类型和形参类型决定,与函数名无关。

// compares lengths of two strings 
bool lengthCompare(const string &, const string &);

该函数的类型是 bool(const string&, const string&)。声明指向函数的指针,用指针替换函数名:

// pf points to a function returning bool that takes two const string references 
bool (*pf)(const string &, const string &);  // uninitialized

使用函数指针

当使用函数名作为一个值时,函数自动转换为指针。

pf = lengthCompare;  // pf now points to the function named lengthCompare 
pf = &lengthCompare; // equivalent assignment: address-of operator is optional 

还可以直接使用指向函数的指针调用该函数,无需解引用指针:

bool b1 = pf("hello", "goodbye");    // calls lengthCompare 
bool b2 = (*pf)("hello", "goodbye"); // equivalent call 
bool b3 = lengthCompare("hello", "goodbye"); // equivalent call 

在指向不同函数类型的指针之间不存在转换。可以给函数指针赋值 nullptr 或值为 0 的整数常量表达式,表示该指针不指向任何函数。

string::size_type sumLength(const string&, const string&); 
bool cstringCompare(const char*, const char*); 
pf = 0;              // ok: pf points to no function 
pf = sumLength;      // error: return type differs 
pf = cstringCompare; // error: parameter types differ 
pf = lengthCompare;  // ok: function and pointer types match exactly

指向重载函数的指针

void ff(int*); 
void ff(unsigned int); 
void (*pf1)(unsigned int) = ff;  // pf1 points to ff(unsigned)

编译器使用指针类型确定使用的是哪个重载函数。指针类型必须与重载函数中的某一个精确匹配:

void (*pf2)(int) = ff;    // error: no ff with a matching parameter list 
double (*pf3)(int*) = ff; // error: return type of ff and pf3 don't match

函数指针形参

不能定义函数类型形参,但形参可以是指向函数的指针。此时,形参看起来是函数类型,实际上是当成指针使用:

// third parameter is a function type and is automatically treated as a pointer to function 
void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &)); 
// equivalent declaration: explicitly define the parameter as a pointer to function 
void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &));

当传入函数作为实参时,它将自动转换为指针:

// automatically converts the function lengthCompare to a pointer to function 
useBigger(s1, s2, lengthCompare); 

类型别名和 decltype 可以简化使用函数指针的代码

// Func and Func2 have function type 
typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2; // equivalent type 

// FuncP and FuncP2 have pointer to function type
typedef bool(*FuncP)(const string&, const string&);
typedef decltype(lengthCompare) *FuncP2;  // equivalent type

注意,decltype 返回函数类型,不会自动转换成指针。要想返回指针,必须加上 *

// equivalent declarations of useBigger using type aliases 
void useBigger(const string&, const string&, Func); 
void useBigger(const string&, const string&, FuncP2);

返回指向函数的指针

不能返回函数类型,但可以返回指向函数类型的指针。声明一个返回指向函数的指针的函数,最简单的方式时使用类型别名:

using F = int(int*, int);     // F is a function type, not a pointer 
using PF = int(*)(int*, int); // PF is a pointer type

注意:与函数类型的形参不同,返回类型不会自动转换为指针。必须显式地将返回类型指定为指针类型:

PF f1(int); // ok: PF is a pointer to function; f1 returns a pointer to function 
F f1(int);  // error: F is a function type; f1 can't return a function 
F *f1(int); // ok: explicitly specify that the return type is a pointer to function

直接声明 f1

int (*f1(int))(int*, int);

使用尾置返回的方式简化返回指向函数的指针的函数的声明:

auto f1(int) -> int (*)(int*, int);

将 decltype 和 auto 用于函数指针类型

string::size_type sumLength(const string&, const string&);
string::size_type largerLength(const string&, const string&);

// depending on the value of its string parameter,
// getFcn returns a pointer to sumLength or to largerLength 
decltype(sumLength) *getFcn(const string &);

【C++ primer】目录

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值