《C++ Primer》第6章 函数(二)

参考资料:

  • 《C++ Primer》第5版
  • 《C++ Primer 习题集》第5版

6.4 函数重载(P206)

在同一作用域内的几个函数名字相同形参列表不同,称之为重载(overloaded)函数

void print(const char *str);
void print(const int *beg, const int *end);
void print(const int arr[], size_t size);

函数重载可以缓解程序员起名字、记名字的负担。

main 函数不能重载

定义重载函数

不允许两个函数除了返回类型外其他要素都相同:

int func(int);
double func(int);    // 错误

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

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

int func(int i);
int func(int);

using ll = long long;
int func(long long i);
int func(ll i);

重载和const形参

顶层 const 不能重载,底层 const 可以重载:

void func(int *p);
void func(int *const p);    // 错误

void func(int *p);
void func(const int *p);    // 正确

const_cast和重载

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)又称重载确定(overload resolution),指把函数调用和一组重载函数中的某一个关联起来的过程。调用重载函数可能有三种结果:

  • 编译器找到一个最佳匹配函数,并生成调用该函数的代码
  • 找不到任何一个函数可以与实参匹配,编译器发出无匹配的错误信息
  • 有多个函数可以匹配,但每一个都不是明显的最佳选择,产生二义性调用

6.4.1 重载与作用域(P210)

void func(int p) {
	cout << "int";
}
void func(double) {
	cout << "double";
}
void func(int *p){
    cout << "int*";
}
int main() {
	void func(double);
	int i = 0;
	func(i);	// 输出为double,因为内层func屏蔽了外面的func
    int *p = &i;
    func(p);	// 错误
	return 0;
}

在 C++ 中,名字查找发生在类型检查。在上面的代码中,执行 func(p) 首先寻找该函数的声明,找到的是 void func(double) 的局部生命,并自动忽略外层作用域中的同名实体,由于 int* 无法转换成 double ,所以这个调用是错误的。

6.5 特殊用途语言特性(P211)

6.5.1 默认实参(P211)

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

void func(int height = 24, int width = 80, char background = ' ');

使用默认实参调用函数

默认实参负责填补函数调用缺少的尾部实参

func();    // 等价于func(24, 80, ' ')
func(66);    // 等价于func(66, 80, ' ')
func(, , '#');    // 错误,只能省略尾部实参
func('?');    // 等价于func('?', 80, ' ')

在设计含有默认实参的函数时,应该尽可能把默认实参安排在后面。

默认实参声明

在同一个作用域对同一个函数多次声明中,每个形参只能被赋予一次默认实参:

void func(int, int, char = ' ');
void func(int, int, char = '*');    // 错误
void func(int = 24, int, char);    // 正确

默认实参初始值

局部变量不能作为默认实参,除此之外,只要表达式的类型能转换成形参所需类型,该表达式就能作为默认实参:

int ht = 24;
char def = ' ';
int wd();
void func(int = ht, int = wd(), char = def);

用作默认实参的名字在函数声明的作用域内解析,这些名字的求值过程发生在函数调用时

int main(){
    def = '*';
    int ht = 30;    // 屏蔽外层ht
    func();    // 等价于func(24, wd(), '*');
}

6.5.2 内联函数和constexpr函数(P213)

使用内联函数可避免函数调用额外开销

将函数指定为内联函数(inline),就是将它在每个调用点上展开:

inline const string &shorterString(const string &s1, const string &s2){
    return s1.size() <= s2.size() ? s1 : s2;
}

inline 只是想编译器发出一个请求,编译器可以忽略这个请求。一般来说,内联机制用于优化规模较小、流程直接、调用频繁的函数。

constexpr函数

constexpr 函数指能用于常量表达式的函数。定义 constexpr 是需要遵循几项约定:函数的返回值所有形参必须为字面值类型,函数体中必须有且仅有一条 return 语句(一般而言,整个函数体就只有一条语句,即 return 语句):

constexpr int new_sz() { return 42; }
constexpr int foo = new_sz();

在执行 foo 的初始化任务时,编译器把对 new_sz 的函数调用替换为其结果值。constexpr 函数被隐式指定为内联函数

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

我们允许 constexpr 函数的返回值不是常量:

constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }

对于上述 scale 函数,当我们传入一个形如字面值 2 的常量表达式时,它的返回类型也是常量表达式,并且编译器会用相应的结果替换对 scale 的调用;如果我们传入的不是常量表达式,则返回值也不是常量表达式

应当把 constexpr 也当作一种请求。

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

内联函数和 constexpr 函数允许多次定义(因为这两种函数往往在编译器就要进行一些操作,仅有函数声明是不够的),但每次定义必须完全相同,通常把它们定义在头文件中。

6.5.3 调试帮助(P215)

程序可以包含是一些用于调试的代码,这些代码只在开发程序时使用,当程序真正发布时,需要屏蔽调试代码。这种方法需要用到两项预处理功能:assertNDEBUG

assert预处理宏

assert 宏使用一个表达式作为它的条件:

assert(expr);

如果表达式为假,assert 输出信息并终止程序运行;如果表达式为真则什么都不做。

assert 宏定义在 cassert 头文件中,常用于检查“不能发生”的条件:

assert(word.size() > threshold);

NDEBUG预处理变量

assert 的行为依赖于一个名为 NDEBUG 的预处理变量,如果定义了 NDEBUG ( 默认不定义),则 assert 什么也不做。

NDEBUG 也能用于编写自己的条件调试代码:

void print(const int ia[], size_t size){
#ifndef NDEBUG
    cerr << __func__ << ": array size is " << size << endl;
#endif
}

其中 __func__ 是编译器为每个函数定义的静态局部变量,用于存放函数的名字。除此之外,常用的变量还有:

  • __FILE__ 存放文件名的字符串字面值
  • __LINE__ 存放当前行号的整型字面值
  • __TIME__ 存放文件编译时间的字符串字面值
  • __DATE__ 存放文件编译日期的字符串字面值

6.6 函数匹配(P217)

确定候选函数和可行函数

函数匹配的过程:

  1. 选定本次调用对应的重载函数集,集合中的函数称为候选函数(candidate function)。候选函数具备两个特征:与被调用函数同名、其声明在调用点可见
  2. 考察本次函数调用的实参,从候选函数中选出可行函数(viable function)。可行函数具备两个特征:形参与实参数量相等(如果函数含有默认参数的话可能不同)、实参能转换成形参的类型。
  3. 在可行函数中寻找最佳匹配。如果有且仅有一个函数满足下列条件,则匹配成功:
    • 该函数每个实参的匹配都不劣于其他可行函数的匹配。
    • 至少有一个实参的匹配优于其他可行函数。

如果没有找到可行函数,编译器报告无匹配函数的错误;如果没有找到最佳匹配,编译器将报告二义性调用。

6.6.1 实参类型转换(P219)

编译器将实参到形参的类型转换划分成了几个等级:

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

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

假设两个同名函数,一个接受 int 、另一个接受 short ,即使我们在调用函数时提供一个很小的整数,由于它会被提升成 int ,所以实际调用的是接受 int 的函数。

所有算术类型转换的级别都一样,signed 转换成 unsigned 并不比 int 转换成 double 级别高。

函数匹配和const实参

void func(int&);
void func(const int&);
const int a = 0;
int b = 0;

func(a);    // 调用void func(const int&);
func(b);    // 调用void func(int&);

6.7 函数指针(P221)

函数的类型由它的返回类型和形参类型共同决定,想要声明一个指向函数的指针,只需要用指针替换函数名即可:

bool lengthCompare(const string &, const string &);
bool (*pf)(const string &, const string &) = lengthCompare;

使用函数指针

当我们把函数名当作使用时,该函数名自动转换成指针:

pf = lengthCompare;
pf = &lengthCompare;    // 等价的赋值语句,取地址符是可选的

我们也可以直接使用指针调用函数,无需解引用指针:

bool b1 = pf("hello", "goodbye");
bool b2 = (*pf)("hello", "goodbye");    // 等价的调用

指向不同函数类型的指针不存在转换规则(注意,精确匹配中有些情况看似属于类型转换,但实际上不是。)。

重载函数指针

void func(int *const);
void func(unsigned int);

void (*pf1)(unsigned int) = func;    // 指向void func(unsigned int);
void (*pf2)(int) = func;    // 错误
void (*pf3)(int*) = func;    // 指向void func(int *const);

函数指针形参

和数组类似,我们不能定义函数类型的形参,但形参可以是指向函数的指针:

void useBigger(const string&, const string&,
               bool pf(const string &, const string &));
void useBigger(const string&, const string&,
               bool (*pf)(const string &, const string &));    // 等价声明

我们可以直接把函数当作实参使用。

使用类型别名和 decltype 可以简化代码。

返回指向函数的指针

最好使用类型别名、尾置返回类型简化代码。

autodecltype用于函数指针类型

你可能感兴趣的:(《C++,Primer》,c++,开发语言)