C++ Primer 第六章 函数

函数基础

  • 一个典型的函数定义包括:返回类型、函数名字、由零个或多个形参组成的列表以及函数体
  • 我们通过调用运算符来执行函数。调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针;圆括号之内是一个用逗号隔开的实参列表,我们用实参初始化函数的形参。调用表达式的类型就是函数的返回类型。
编写函数

例:

// 求阶乘
int fact(int val) {
int ret = 1;
while (val > 1) {
ret *= val --;
}
return ret;
}
调用函数
  • 要调用 fact 函数,必须提供一个整数值,调用得到的结果也是一个整数
int main() {
int j = fact(5);
cout << "5! is " << j << endl;
return 0;
}
形参和实参
  • 实参是形参的初始值。实参的类型必须与对应的形参类型匹配。
// 在上面的例子中,每次调用的时候都必须提供一个能转换成 int 的实参
fact("hello"); // 错误:实参类型不正确
fact(); // 错误:实参数量不足
fact(42, 10, 0); // 错误:实参数量过多
fact(3.14); // 正确:该实参能转换成 int 类型 等价于 fact(3);
函数的形参列表
  • 函数的形参列表可以为空,但是不能省略
void f1() {/* ... */}; // 隐式地定义空形参列表
void f2(void) {/* ... */}; // 显式地定义空形参列表
  • 形参列表中的形参通常用逗号隔开,其中每个形参都是含有一个声明符的声明
int f3(int v1, v2) {/* ... */}; // 错误
int f4(int v1, int v2) {/* ... */}; // 正确
函数返回类型
  • 大多数类型都能用作函数的返回类型。一种特殊的返回类型是 void,它表示函数不返回任何值。
  • 函数的返回类型不能是数组类型或函数类型。
局部对象
自动对象
  • 只存在于块执行期间的对象成为自动对象。当块的执行结束后,块中创建的自动对象的值就变成未定义的了
局部静态对象
  • 可以将局部变量定义成 static 类型从而获得这样的对象
  • 局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。
size_t count_calls() {
static size_t ctr = 0; // 调用结束后,这个值任然有效
return ++ ctr;
}
int main() {
for (size_t i = 0; i != 10; ++ i) {
cout << count_calls() << endl;
}
return 0;
}

每次执行 count_calls() 函数时,变量 ctr 的值都已经存在并且等于函数上一次退出时 ctr 的值。

  • 如果局部静态变量没有显式的初始值,它将执行值初始化,内置类型的局部静态变量初始化为 0.
函数声明
  • 和其他名字一样,函数的名字也必须在使用之前声明,函数只能定义一次,但是可以声明多次。
分离式编译

例:
Chapter6.h

#pragma once
#ifndef CHAPTER6_H
#define CHAPTER6_H
int fact(int a);
#endif

fact.cc

#include "Chapter6.h"
int fact(int a) {
int res = 1;
for (int i = 1; i <= a; i++) res *= i;
return res;
}

factMain.cc

#include <iostream>
#include "Chapter6.h"
int main() {
int n;
std::cin >> n;
std::cout << fact(n) << std::endl;
return 0;
}

参数传递

  • 当形参是引用类型时,我们说它对应的实参被引用传递或者函数被传引用调用
  • 当实参的值被拷贝给形参时,实参和形参是两个相互独立的对象。我们说这样的实参被值传递或者函数被传值调用
传值参数
  • 当初始化一个非引用类型的变量时,初始值被拷贝给变量。对变量的改动不会影响初始值。
    指针形参
  • 指针的行为和其他非引用类型一样,当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后两个指针是不同的指针,因为指针是我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值。
void reset(int *ip) {
*ip = 0;
ip = 0;
}
int i = 42;
reset(&i);
cout << "i = " << i << endl;

使用指针形参交换两个整数的值

void swap_number(int *a, int *b) {
int t = *a;
*a = *b;
*b = t;
}
int main() {
int a, b;
cin >> a >> b;
swap_number(&a, &b);
cout << a << ' ' << b << endl;
return 0;
}

传引用参数

  • 通过引用参数,允许改变一个或多个实参的值。
void reset(int &i) {
i = 0;
}
int j = 42;
reset(j);
cout << "j = " << j << endl; // 输出 0
使用引用避免拷贝
  • 拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括 IO 类型在内根本不支持拷贝操作)。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。
// 比较两个 string 对象的长度
bool isShorter(const string &s1, const string &s2) {
return s1.size() < s2.size();
}

如果函数无需改变引用形参的值,最好将其声明为常量引用

使用引用形参返回额外信息
  • 给函数传入一个额外的引用实参,来保存信息
string::size_type find_char(const string &s, char c, string::size_type &occurs) {
auto ret = s.size();
occurs = 0;
for (decltype(ret) i = 0; i != s.size(); ++ i) {
if (s[i] == c) {
if (ret == s.size())
ret = i;
++ occurs;
}
}
return ret; // 出现次数通过 occurs 隐式地返回
}

const 形参和实参

顶层 const 作用于对象本身

  • 和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层 const。换句话说,形参的顶层 const 被忽略掉了。当形参有顶层 const 时,传给它常量对象或者非常量对象都是可以的。
void fcn(const int i) {/* fcn 能够读取 i,但是不能向 i 写值 */};
void fcn(int i) {/* 错误:重复定义了 fcn(int) */}; // 忽略掉顶层 const 产生的结果
指针或引用形参与 const
  • 形参的初始化变量方式和变量的初始化方式是一样的
  • 我们可以使用非常量初始化一个底层 const 对象,但是反过来不行;同时一个普通的引用必须用同类型的对象初始化
int i = 42;
const int *cp = &i; // 正确:但是 cp 不能改变 i
const int &r = i; // 正确:但是 r 不能改变 i
const int &r2 = 42; // 正确
int *p = cp; // 错误:p 的类型和 cp 的类型不匹配
int &r3 = r; // 错误:r3 的类型和 r 的类型不匹配
int &r4 = 42; // 错误:不能用字面值初始化一个非常量引用
int i = 0;
const int ci = i;
string::size_type ctr = 0;
reset(&i); // 调用形参类型是 int* 的 reset 函数
reset(&ci); // 错误:不能用指向 const int 对象的指针初始化 int *
reset(i); // 调用形参类型是 int& 的 reset 函数
reset(ci); // 错误:不能把普通引用绑定到 const 对象 ci 上
reset(42); // 错误:不能把普通引用绑定到字面值上
reset(ctr); // 错误:类型不匹配,ctr 是无符号类型
// 正确:find_char 的第一个形参是对常量的引用
find_char("Hello World!", 'o', ctr);
尽量使用常量引用
  • 把函数不会改变的形参定义成(普通的)引用是一种比较常见的错误,这么做带给函数的调用者一种误导,既函数可以修改它的实参的值。此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型。
    6.17
#include <iostream>
#include <string>
#include <vector>
#include <cstring>
#include <cstddef>
#include <iterator>
#include <stdexcept>
#include "Chapter6.h"
using std::string;
using std::cin;
using std::cout;
using std::endl;
using std::vector;
using std::begin;
using std::end;
using std::cerr;
bool check(const string &str) {
for (string::size_type i = 0; i != str.size(); ++ i) {
if (isupper(str[i])) return true;
}
return false;
}
void turn(string &str) {
for (string::size_type i = 0; i != str.size(); ++ i) {
str[i] = tolower(str[i]);
}
}
int main() {
string str;
cin >> str;
if (check(str)) {
turn(str);
}
cout << str << endl;
return 0;
}

数组形参

数组的两个性质

  • 不允许拷贝数组
  • 使用数组时(通常)会将其转换成指针
// 尽管形式不同,但这三个 print 函数是等价的
// 每个函数都有一个 const int* 类型的形参
void print(const int*);
void print(const int[]); // 可以看出来,函数的意图是作用于一个数组
void print(const int[10]); // 这里的维度表示我们期望数组含有多少元素,实际不一定
// 尽管表现形式不同,但上面的三个函数是等价的,每个函数的唯一形参都是 const int* 类型的
int i = 0, j[2] = {0, 1};
print(&i); // 正确:&i 的类型是 int*
print(j); // 正确:j 转换成 int* 并指向 j[0]

如果我们传给 print 函数的是一个数组,则实参自动地转换成指向数组首元素的指针,数组的大小对函数的调用没有影响

  • 和其他使用数组的代码一样,以数组作为形参的函数也必须确保使用数组时不会越界
使用标记指定数组长度
  • 管理数组实参的第一种方法是要求数组本身包含一个结束标记,例如 C 风格字符串
void print(const char *cp) {
if (cp) { // 若不是空指针
while (*cp) { // 只要指针所指的字符不是空字符
cout << *cp ++;
}
}
}
使用标准库规范
  • 要正确地计算指针所指位置,保证代码安全
void print(const int *beg, const int *end) {
while (beg != end) {
cout << *beg ++ << ' ';
}
cout << endl;
}
int main() {
int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
print(begin(a), end(a));
return 0;
}
显式传递一个表示数组大小的形参
void print(const int a[], size_t size) {
for (size_t i = 0; i != size; ++ i) cout << a[i] << ' ';
cout << endl;
}
int main() {
int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
print(a, end(a) - begin(a));
return 0;
}
数组形参和 const
  • 当函数不需要对数组元素执行 写操作的时候,数组形参应该是指向 const 的指针。只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针。
数组引用形参
  • 数组大小是构成数组类型的一部分,所以只要不超过维度,在函数体内就可以放心地使用。但是这一用法也无形中限制了 print 函数的可用性,我们只能将函数作用于大小为 10 的数组。
void print(int (&arr)[10]) {
for (auto elem : arr) {
cout << elem << ' ';
}
}
/*
&arr 两端的括号必不可少
f(int &arr[10]) // 错误:将 arr 声明成了引用的数组
f(int (&arr)[10]) // 正确:arr 是具有 10 个整数的整型数组的引用
*/
int main() {
int i = 0, j[2] = {0, 1};
int k[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
print(&i); // 错误:实参不是含有 10 个整数的数组
print(j); // 错误:实参不是含有 10 个整数的数组
print(k); // 正确:实参是含有 10 个整数的数组
return 0;
}
传递多维数组
  • 将多维数组传递给函数时,真正传递的是指向数组首元素的指针。因为处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针。数组第二维的大小都是数组类型的一部分,不能省略。
void print(int (*matrix)[10], int rowSize) {/* ... */} // matrix 指向含有 10 个整数的数组的指针
/*
括号必不可少
int *matrix[10]; // 10 个指针构成的数组
int (*matrix)[10]; // 指向含有 10 个整数的数组的指针
*/
// 等价定义
void print(int matrix[][10], int rowSize) {/* ... */};

6.22 交换两个 int 指针

void swap_pointer(int *(&pa), int *(&pb)) {
int *pt = pa;
pa = pb;
pb = pt;
}
int main() {
int a = 1, b = 2;
int *pa = &a, *pb = &b;
swap_pointer(pa, pb);
cout << *pa << ' ' << *pb << endl;
return 0;
}

main:处理命令行选项

#include <iostream>
#include <string>
#include <vector>
#include <cstring>
#include <cstddef>
#include <iterator>
#include <stdexcept>
#include "Chapter6.h"
using std::string;
using std::cin;
using std::cout;
using std::endl;
using std::vector;
using std::begin;
using std::end;
using std::cerr;
int main(int argc, char *argv[]) { // 第一个形参 argc 代表数组中字符串的数量
// 第二个形参 argv 是一个数组,元素是指向 C 风格字符串的指针
// 也可以定义成:int main(int argc, char **argv) { /* ... */ };
string str;
for (int i = 0; i != argc; ++ i) str += argv[i];
cout << str << endl;
return 0;
}

含有可变形参的函数

  • initializer_list标准库类型
  • 可变参数模板(16.4介绍)
initializer_list 形参
  • initializer_list是一种标准库类型,用于表示某种特定类型的值的数组。定义在initializer_list头文件中
    提供的操作
initializer_list<T> lst; 默认初始化;T 类型元素的空列表
initializer_list<T> lst{a, b, c, ...}; lst 的元素数量和初始值一样多;
lst 的元素是对应初始值的副本;
列表中的元素是 count
lst2(lst) 拷贝或赋值一个 initializer_list 对象不会拷贝列表中的元素;拷贝后,
lst2 = lst 原始列表和副本共享元素
lst.size() 列表中的元素数量
lst.begin() 返回指向 lst 中首元素的指针
lst.end() 返回指向 lst 中为元素下一位置的指针

定义对象

initializer_list<string> ls; // initializer_list 的元素类型是 string
  • initializer_list对象中的元素永远是常量值,我们无法改变 initializer_list 对象中元素的值
    使用如下形式编写输出错误信息的函数,使其可以作用于可变数量的实参
void error_msg(std::initializer_list<string> li) {
for (auto beg = li.begin(); beg != li.end(); ++ beg) cout << *beg << ' ';
cout << endl;
}

**如果想向initializer_list形参中传递一个值的序列,则必须把序列放在一对花括号内

string expected, actual;
if (expected != actual) {
error_msg({"functionX", expected, actual});
} else {
error_msg({"functionX", "okay"});
}

**含有initializer_list形参的函数也可以同时拥有其他形参

void error_msg(ErrCode e, std::initializer_list<string> li) {
cout << e.msg() << endl;
for (const auto &elem : li) cout << elem << ' ';
cout << endl;
}
if (expected != actual) {
error_msg(ErrCode(42), { "functionX", expected, actual });
} else {
error_msg(ErrCode(0), { "functionX", "okay" });
}
省略符形参
  • 省略符形参只能出现在形参列表的最后一个位置,它的形式无外乎一下两种
void foo(parm_list, ...);
void foo(...);

**第一种形式指定了 foo 函数的部分形参的类型,对应于这些形参的实参将会执行正常的类型检查。省略符形参所对应的实参无须类型检查。在第一种形式中,形参声明后面的逗号是可选的。

返回类型和 return 语句

  • return 语句有两种形式
    return ;
    return expression;

无返回值函数

  • 没有返回值的 return 语句只能用在返回类型是 void 的函数中
  • 一个返回类型是 void 的函数也能使用 return 语句的第二种形式,不过此时 return 语句的 expression 必须是另一个返回 void 的函数。
// 输出的是交换后的值
void f(int a, int b) {
cout << a << ' ' << b << endl;
}
void turn(int &a, int &b) {
int t = a;
a = b;
b = t;
return f(a, b);
}
int main() {
int a, b;
cin >> a >> b;
turn(a, b);
return 0;
}

有返回值函数

  • 只要函数的返回类型不是 void,则该函数内的每条 return 语句必须返回一个值。return 语句返回值的类型必须与函数的返回类型相同,或者能隐式地转换成函数的返回类型。
值是如何被返回的
  • 返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果
  • 如果函数返回引用,则该引用仅是它所引对象的一个别名
const string &shoterString(const string &s1, const string &s2) {
return s1.size() <= s2.size() ? s1 : s2;
}
不要返回局部对象的引用或指针
  • 函数完成后,它所占用的存储空间也随之被释放掉。因此函数终止意味着局部变量的引用将指向不再有效的内存区域
const string &manip() {
string ret;
if (!ret.empty()) return ret; // 错误:返回局部对象的引用
else return "Empty"; // 错误:"Empty" 是一个局部临时量
}
  • 返回局部对象的指针也是错误的。一旦函数完成,局部对象被释放,指针将指向一个不存在的对象
返回类类型的函数和调用运算符
  • 调用运算符的优先级与点运算符和箭头运算符相同,并且也符合左结合律
// 得到较短 string 对象的长度
auto sz = shortString(s1, s2).size();
引用返回左值
  • 调用一个返回引用的函数得到左值,其他返回类型得到右值
char &get_val(string &str, string::size_type ix) {
return str[ix];
}
int main() {
string s("a value");
cout << s << endl; // a value
get_val(s, 0) = 'A';
cout << s << endl; // A value
return 0;
}

如果返回类型是常量引用,我们不能给调用的结果赋值,这一点和我们熟悉的情况是一样的

列表初始化返回值
  • 如果函数返回的是内置类型,则花括号包围的列表最多包含一个值,而且该值所占空间不应该大于目标类型的空间。如果函数返回的是类类型,由类本身定义初始值如何使用。
vector<string> process() {
// ...
// expected 和 actual 是 string 对象
if (expected.empty()) return {}; // 返回一个空 vector 对象
else if (expected == actual) return {"functionX", "okay"}; // 返回列表初始化的 vector 对象
else return {"functionX", expected, actual};
}
主函数 main 的返回值
  • 允许 main 函数没有 return 语句直接结束。如果控制到达了 main 函数的结尾处而且没有 return 语句,编译器将隐式地插入一条返回 0 的 return 语句。
  • main 函数的返回值可以看做是状态指示器。返回 0 表示执行成功,返回其他值表示失败,其中非 0 值的具体含义依机器而定。
  • 为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量,我们可以使用这两个变量分别表示成功与失败
int main() {
// 因为它们是预处理变量,所以既不能在前面加上 std::,也不能在 using 声明中出现
if (some_failure) return EXIT_FAILURE;
else return EXIT_SUCCESS;
}
递归
  • 函数调用自身,(一定有某条路径是不包含递归调用的,否则会递归循环)
int factorial(int val) {
if (val > 1) return factorial(val - 1) * val;
return 1;
}

返回数组指针

  • 因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用
typedef int arrT[10]; // arrT 是一个类型别名,它表示的类型是含有 10 个整数的数组
using arrT = int[10]; // 等价
arrT* func(int i); // func 返回一个指向含有 10 个整数的数组的指针
声明一个返回数组指针的函数

函数形式

Type (*function(parameter_list)) [dimension]
// 举例
int (*func(int i)) [10];
  • func(int i) 表示调用 func 函数时需要一个 int 类型的实参
  • (*func(inr i)) 意味着我们可以对函数调用的结果执行解引用操作
  • (*func(int i)) [10]表示解引用 func 的调用将得到一个大小是 10 的数组
  • int (*func(int i)) [10]表示数组中的元素是 int 类型
使用尾置返回类型
  • 尾置返回类型跟在形参列表后面并以一个 -> 符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个 auto
// func 接受一个 int 类型的实参,返回一个指针,该指针指向含有 10 个整数的数组
auto func(int i) -> int(*)[10];

例:

int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto func() -> int(*)[10] {
return &a;
}
int main() {
int (*b)[10] = func();
for (int i = 0; i != 10; ++ i) cout << *(*b + i) << ' ';
cout << endl;
return 0;
}
使用 decltype
  • 如果我们知道函数返回的指针将指向哪个数组,就可以使用 decltype 关键字声明返回类型
int odd[] = {1, 3, 5, 7, 9};
int even[] = {0, 2, 4, 6, 8};
decltype(odd) *arrPtr(int i) {
return (i % 2) ? &odd : &even;
}
int main() {
int (*b)[5] = arrPtr(3);
for (int i = 0; i != 5; ++ i) cout << *(*b + i) << ' ';
cout << endl;
return 0;
}

decltype 并不负责把数组类型转换成对应的指针,所以 decltype 的结果是个数组,要想表示 arrPtr 返回指针还必须声明时加一个 * 符号
6.37

typedef string s[10];
using s = string[10];
string ss[10];
string (&func()) [10] {}
s &func() {}
auto func() -> string(&)[10] {}
decltype(ss) &func() {}

函数重载

  • 如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载函数
    main函数不能重载!!!
定义重载函数
  • 要在形参数量或形参类型上有所不同,不允许两个函数除了返回类型外其他所有的要素都相同
Record lookup(const Account&);
bool lookup(const Account&); // 错误:与上一个函数相比只有返回类型不同
判断两个形参的类型是否相异
  • 有时候两个形参列表看起来不一样,但实际上时相同的
// 每对声明的是同一个函数
Record lookup(const Account &acct);
Record lookup(const Account&); // 省略了形参的名字
typedef Phone Telno;
Record lookup(const Phone&);
Record lookup(const Telno&); // Telno 和 Phone 的类型相同
重载和 const 形参
  • 顶层 const 不影响传入函数的对象,一个拥有顶层 const 的形参无法和另一个没有顶层 const 的形参区分开来
Record lookup(Phone);
Record lookup(const Phone); // 重复
Record lookup(Phone*);
Record lookup(Phone* const); // 重复
  • 如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的 const 是底层的
Record lookup(Account&); // 函数作用于 Account 的引用
Record lookup(const Account&); // 新函数,作用于常量引用
Record lookup(Account*); // 新函数,作用于指向 Account 的指针
Record lookup(const Account*); // 新函数,作用于指向常量的指针

**因为 const 不能转换成其他类型,所以我们只能把 const 对象(或指向 const 的指针)传递给 const 形参,相反的,因为非常量可以转换成 const,所以上面 4 个函数都能作用于非常量对象或者指向非常量对象的指针。

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);
}
int main() {
const string a, b;
string c, d;
const string &x = shorterString(a, b);
string &y = shorterString(c, d);
return 0;
}

如果不加下面的重载,我们可以对两个非常量的 string 实参调用这个函数,但返回的结果仍然是 const string 的引用

调用重载的函数

函数匹配也叫重载确定,指一个过程,在这个过程中我们把函数调用与一组重载函数中的某一个关联起来
现在需要掌握的是,当调用重载函数时有三种可能的结果

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

重载与作用域

  • 如果我们在内部作用域中声明名字,它将隐藏外层作用域中声明的同名实体。在不同的作用域中无法重载函数名
string read();
void print(const string&);
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) 被隐藏掉了
}

特殊用途语言特性

默认实参

  • 默认实参:在很舒缓的很多次调用中它们都被赋予了一个相同的值
  • 一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
使用默认实参调用函数
  • 如果想使用默认实参,只要在调用函数的时候省略该实参就可以了
string window;
window screen(); // screen(24, 80, ' ');
window screen(66); // screen(66, 80, ' ');
window screen(66, 256); // screen(66, 256, ' ');
window screen(66, 256, '#'); // screen(66, 256, '#');
  • 函数调用时实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参(靠右位置)。
window = screen(, , '?'); // 错误:只能省略尾部的实参
window = screen('?'); // 调用:screen('?', 80, ' ');
默认实参声明

注意:在给定的作用域中一个形参只能被赋予一次默认实参

string screen(sz, sz, char = ' ');
string screen(sz, sz, char = '*'); // 错误:重复声明
string screen(sz = 24, sz = 80, char); // 正确:添加默认实参
默认实参初始值
  • 局部变量不能作为默认实参,除此之外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参
// 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, '*')
}

内联函数和 constexpr 函数

内联函数可避免函数调用的开销
inline const string &shorterString(const string &s1, const string &s2) {
return s1.size() <= s2.size() ? s1 : s2;
}
cout << shorterString(s1, s2) << endl;
// 将在编译过程中展开成类似于下面的形式
cout << (s1.size() <= s2.size() ? s1 : s2) << endl;
constexpr 函数
  • constexpr 函数是指能用于常量表达式的函数
constexpr int new_sz() { return 42; }
constexpr int foo = new_sz(); // 正确:foo 是一个常量表达式

我们允许 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) 不是常量表达式
把内联函数和 constexpr 函数放在头文件内

调试帮助

assert 预处理宏

表达式

assert(expr);

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

NDEBUG 预处理变量
  • 可以使用一个 #define 语句定义 NDEBUG,从而关闭调试状态。
    **除了用于 assert 外,也可以使用 NDEBUG 编写自己的条件调试代码。如果 NDEBUG 未定义,将执行 #ifndef 和 #endif 之间的代码,如果定义了 NDEBUG,这些代码将被忽略
void print(const int ia[], size_t size) {
#ifndef NDEBUG
cerr << __func__ << ": array size is " << size << endl;
#endif
}
__func__ 存放函数的名字
__FILE__ 存放文件名的字符串字面值
__LINE__ 存放当前行号的整型字面值
__TIME__ 存放文件编译时间的字符串字面值
__DATA__ 存放文件编译日期的字符串字面值

函数匹配

确定候选函数和可行函数
  • 本次调用对应的重载函数集中的函数称为候选函数
  • 从候选函数中选出能被这组实参调用的函数成为可行函数
寻找最佳匹配(如果有的话)
  • 实参类型与形参类型越接近,匹配得越好
含有多个形参的函数匹配

如果有且只有一个函数满足下列条件,则匹配成功

  • 该函数每个实参的匹配都不劣于其他可行函数需要的匹配
  • 至少有一个实参的匹配优于其他可行函数提供的匹配
  • 如果在检查了所有实参之后没有任何一个函数脱颖而出,则该调用是错误的。编译器将报告二义性调用的信息

实参类型转换

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

函数指针

  • 函数的指针指向的是函数而非对象
// 比较两个 string 对象的长度
bool lengthCompare(const string &, const string &);

该函数的类型是 bool(const string &, const string &)

  • 要想声明一个可以指向该函数的指针,只需用指针替换函数名即可
// pf 指向一个函数,该函数的参数是两个 const string 的引用,返回值是 bool 类型
bool (*pf)(const string &, const string &);
使用函数指针
  • 当我们把函数名作为一个值使用时,该函数自动地转换成指针。
pf = lengthCompare; // pf 指向名为 lengthCompare 的函数
pf = &lengthCompare; // 等价的赋值语句:取地址符是可选的
  • 我们还能直接使用指向函数的指针调用该函数,无须提前解引用指针
bool b1 = pf("hello", "goodbye"); // 调用 lengthCompare 函数
bool b2 = (*pf)("hello", "goodbye"); // 一个等价的调用
bool b3 = lengthCompare("hello", "goodbye"); // 另一个等价的调用
  • 在指向不同函数类型的指针间不存在转换规则。我们可以为函数指针赋一个 nullptr 或者值为 0 的整型常量表达式,表示该指针没有指向任何一个函数。
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 &));
// 可以直接把函数作为实参使用,此时它会自动转换成指针
useBigger(s1, s2, lengthCompare);
// 简化
// 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; // 等价的类型
// 使用类型别名重新声明
void useBigger(const string&, const string&, Func);
void useBigger(const string&, const string&, FuncP2);
返回指向函数的指针
  • 使用类型别名
using F = int(int*, int); // F 是函数类型,不是指针
using FP = int(*)(int*, int); // FP 是指针类型
PF f1(int); // 正确:PF 是指向函数的指针,f1 返回指向函数的指针
F f1(int); // 错误:F 是函数类型,f1 不能返回一个函数
F *f1(int); // 正确:显式地指定返回类型是指向函数的指针
  • 直接声明
int (*f1(int))(int*, int);
  • 尾置返回类型
auto f1(int) -> int(*)(int*, int);
将 auto 和 decltype 用于函数指针类型
  • 将 decltype 作用于某个函数时,它返回函数类型而非指针类型。因此我们显式地加上 * 以表名我们需要返回指针,而非函数本身
string::size_type sumLength(const string&, const string&);
string::size_type largeLength(const string&, const string&);
decltype(sumLength) *getFcn(const string&);

6.56

//#define NDEBUG
#include <iostream>
#include <string>
#include <vector>
#include <cstring>
#include <cstddef>
#include <iterator>
#include <stdexcept>
#include <initializer_list>
#include <cstdlib>
#include <cassert>
#include "Chapter6.h"
using std::string;
using std::cin;
using std::cout;
using std::endl;
using std::vector;
using std::begin;
using std::end;
using std::cerr;
using FP = int(*)(int, int);
vector<FP> v;
int jia(int a, int b) {
return a + b;
}
int jian(int a, int b) {
return a - b;
}
int cheng(int a, int b) {
return a * b;
}
int chu(int a, int b) {
return a / b;
}
int main() {
v.push_back(&jia);
v.push_back(&jian);
v.push_back(&cheng);
v.push_back(&chu);
int a = 2, b = 3;
for (const FP x : v) {
cout << x(a, b) << endl;
}
return 0;
}
posted @   HuiPuKui  阅读(43)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示