《C++ Primer》 拾遗 第 6 章 函数
Notes Cpp Primer
Lastmod: 2021-06-01 周二 22:40:20

第 6 章 函数

6.1 函数基础

我们可以通过调用运算符 call operator 来执行函数。

形参 parameter

实参 argument

主调函数 calling function

被调函数 called function

空形参列表可以是空的括号,或是 void

void f1() {}
void f2(void) {}

6.1.1 局部对象

C++ 中名字有作用域,对象有声明周期 lifetime

名字的作用域是程序文本的一部分,名字在其中可见。

对象生命周期是程序执行过程中该对象存在的一段时间。

形参和函数体内部定义的变量是局部变量 local variable,它们仅在函数作用域中可见,同时会隐藏 hide 在外层作用域中的同名的其他声明。

自动对象

只存在于块执行期间的对象称为自动对象 automatic object

形参也是自动对象。

局部静态对象

local static object 有些对象的生命周期需要贯穿函数调用及之后的时间。

局部静态变量如果没有显示的初始值,则它会被默认初始化。

6.1.2 函数声明

函数声明也称作函数原型 function prototype

函数声明不需要函数体用分号代替。函数声明可以没有形参名字。

6.2 参数传递

形参是引用类型时,实参被引用传递 passed by reference 或函数被传引用调用 called by reference

实参值被拷贝给形参时,实参被值传递 passed by value 或者函数被传值调用 called by value

6.2.3 const 形参和实参

注意实参初始化形参时会忽略掉顶层 const,因此这时需要注意下列重载是不行的。

void f(const int i);
void f(int i);

6.2.4 数组形参

注意因为数组不能拷贝,因此我们不能以值传递的方式使用数组参数,因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。

尽管不能以值传递的方式传递数组,但我们可以把参数形式写成类似数组的形式,下面三个形式是等价的。

void f(const int*);
void f(const int[]);
void f(const int[10]); // 注意这个 10 只是期望大小,实际并不一定

当我期待更改数组内容时使用下面的形式

void f(int*);
数组引用形参
void f(int (&arr)[10]);

但注意,这种写法数组的维度是被限制死的。

传递多维数组
void f(int (*matrix)[10], int row);
void f(int matrix[][10], int row);

6.2.5 main:处理命令行选项

int main(int argc, char *argv[]) {}

其中 argc 是程序接受到的参数个数

argv[0] 是程序名字或空字符串。

6.2.6 含有可变形参的函数

对于不定数量的参数,参数类型相同可以使用 initializer_list ,否则可以使用变长参数模板。

initializer_list<T> lst;
initializer_list<T> lst{a, b, c};
lst2(lst)
lst2 = lst
lst.size();
lst.begin();
lst.end();

注意 initializer_list 中的元素是常量值。

省略符形参
void foo(parm_list, ...);
void foo(...);

这是为了 C++ 代码便于访问 C 代码设置的,这些代码使用了 varargs 的 C 标准库的功能。

6.3 返回类型和 return 语句

6.3.1 无返回值的函数

返回类型的是 void 的函数也可以使用带有表达式的 return 的语句,这时表达式必须是另一个返回 void 的函数。

void f1() {}

void f2() {
  return f1();
}

int main() {
  f2();
  return 0;
}

6.3.2 有返回值函数

值是如何被返回的

返回一个值的方式和初始化变量或形参的方式一致,返回值用来初始化调用点的一个临时量,这个临时量就是函数调用的结果。

如果函数返回值是引用,则它只是所引用对象的别名。

不要返回局部对象的引用或指针!

引用返回左值

如果返回类型时非常量引用,则我们可以对其进行赋值。

列表初始化返回值

可以使用列表初始化初始化返回值,与其他情况使用列表初始化相同。

主函数 main 的返回值

为了使返回值与机器无关 cstdlib 中定义了

EXIT_FAILURE;
EXIT_SUCCESS;

6.3.3 返回数组指针

typedef int arrT[10];
// 或
using arrT = int[10];
arrT* func(int i);

上述写法会好写一点

也可以用下面的写法

int (*func(int i))[10];
使用尾置返回类型

尾置返回类型 trailing return type

auto func(int i) -> int(*)[10];
使用 decltype

当我们知道函数返回的指针指向哪个数组时可以使用

int odd[] = {1, 3, 5};
int even[] = {2, 4, 6};
decltype(odd) *arrPtr(int i) {
  return (i % 2) ? &odd : &even;
}

6.4 函数重载

同一作用域内几个函数名字相同但形参列表不同,我们称之为重载 overloaded 函数。

main 函数不能重载。

注意有时候形参看起来不一样,但实际是相同的。

typedef A B;
void f(A a);
void f(B b);

注意不能重载参数只有顶层 const 的不同的函数

void f(A a);
void f(const A a);

void f(P*);
void f(P* const);

如下是正确的重载

void f(A&);
void f(const A&);
void f(A*);
void f(const A*);

编译器将通过实参是否是常量来判断调用哪个函数。

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 是将函数调用与一组重载函数中的某个关联起来的过程。

调用时存在三种可能:

找到最佳匹配 best match

找不到匹配,发出无匹配no match 错误信息。

有多于一个匹配,并且没有明显的最佳匹配。此时返回错误信息二义性调用 ambiguous call

6.4.1 重载与作用域

一般来说不会将函数声明置于局部作用域内。

注意内层声明会屏蔽外层所有同名的声明,不存在内层函数对外层重载。

C++ 中名字查找在类型检查之前。

6.5 特殊用途语言特性

6.5.1 默认实参

default argument

默认实参声明

注意多次声明同一函数是合法的,但在某给定作用域中,形参只能被赋予一次默认实参。

void f(int a,int b,int c=10);
// void f(int a,int b,int c=100); // 错误
void f(int a=10,int b=0,int); // 正确

注意局部变量不能作为默认实参,表达式能转换成形参类型则也可以作为默认实参。

6.5.2 内联函数和 constexpr 函数

constexpr 函数

是能用于常量表达式的函数,函数的返回类型和所有形参必须是字面值类型,函数体中必须有且只有一条 return 语句。

constexpr 会在编译时隐式指定为内联函数。

constexpr 函数不一定返回常量表达式。

当 constexpr 函数的参数不是常量时,则它不是一个常量表达式。

内联函数和 constexpr 函数,可以在程序中多次定义,因为只有声明是无法内联展开的,但这些定义应当完全一致,所以通常把内联函数和 constexpr 函数放在头文件。

6.5.3 调试帮助

assert 是一个定义在 cassert 中的预处理宏。

当定义了 NDEBUG 预处理变量时,cassert 什么都不会做,否则会检查表达式是否为真,如果为假则输出信息并终止程序执行。

可以使用

__func__ // 输出当前函数名字
__FILE__ // 当前文件名
__LINE__ // 当前行号
__TIME__ // 文件编译时间
__DATE__ // 文件编译日期

6.6 函数匹配

函数匹配的第一步是确定候选函数 candidate function,候选函数满足与被调函数同名,声明在调用点可见。

第二步考察实参,从候选函数中选出可行函数 viable function,可行函数满足形参数量与实参数量相等,实参类型与形参类型相同,或能转换成形参的类型。

第三步是寻找最佳匹配,当有一个函数满足,每个实参匹配都不劣于其他可行函数的匹配,有至少一个实参匹配优于其他可行函数的匹配,则它是最优匹配。

实参到形参类型转化的级别:

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

6.7 函数指针

函数指针指向某种特定类型。

函数类型由返回值和形参类型共同决定。

不同类型的函数指针之间不能转换。

可以将 nullptr 或 0 赋值给函数指针。

以下形式赋值函数指针是相同的。

void f();
void (*fp)() = f;
void (*fp)() = &f;

使用函数指针和使用函数名是类似的。

函数指针形参

和数组类似,虽然不能定义函数类型的形参,但是可以定义指向函数的指针。

void f(int a,int b,bool pf(int c,int d));
void f(int a,int b,bool (*pf)(int c,int d));

也可以使用 decltype 得到函数类型,但是这时需要注意 decltype 得到的函数类型不会自动转换成指针类型。不过使用上看起来好像没什么问题,如下 4 种写法都是可以通过编译并运行的。

#include <iostream>

void fvoid() { std::cout << "do something\n"; }

void (*pf)() = fvoid;

typedef void Func();
typedef decltype(fvoid) Func2;
typedef void (*FuncP)();
typedef decltype(fvoid) *FuncP2;

void f1(Func f) { f(); }
void f2(Func2 f) { f(); }
void f3(FuncP f) { f(); }
void f4(FuncP2 f) { f(); }

int main() {
  f1(fvoid);
  f2(fvoid);
  f3(fvoid);
  f4(fvoid);

  f1(pf);
  f2(pf);
  f3(pf);
  f4(pf);
  return 0;
}
返回指向函数的指针

这里最好还是使用类型别名或者尾置返回类型。

#include <iostream>

using F = void(void);
using PF = void(*)(void);

void f(){}

PF f1() { return f; }
// F f2() { return f; } // compile error 不能返回函数类型
F *f3() { return f; }

int main() {
  return 0;
}
Prev: 《C++ Primer》 拾遗 第 5 章 语句
Next: 《C++ Primer》 拾遗 第 7 章 类