本文记录了C++中与函数相关的容易遗忘的一些知识。
函数综述
为什么需要函数原型?
原型向编译器描述函数接口。也就是说,它告诉编译器该函数的返回值类型(如果有的话),以及函数参数的数量和类型。
| 原因 | 解释 |
|---|---|
| 使编译器在调用前知道函数存在 | 否则调用时报错 |
| 类型检查 | 检查参数数量、类型、返回值 |
| 支持多文件工程 | 头文件机制依赖函数原型 |
| 支持函数重载 | 参数列表匹配需要原型 |
| 限制危险的隐式函数声明 | C++ 禁止隐式声明 |
函数原型让编译器能正确、安全地生成函数调用,不然编译器根本不知道怎么调用。
递归函数
参考递归算法设计与实现
函数指针(Pointers to Functions)
定义
函数指针,即指向函数的指针(地址)。函数和数据项一样,都有地址。函数的地址是存储该函数的机器语言代码开始的内存地址。
编写一个以另一个函数的地址作为参数的函数是可行的。这使得第一个函数能够找到第二个函数并运行它。这种方法比直接让第一个函数调用第二个函数要更麻烦一些,但它保留了在不同时间传递不同函数地址的可能性。这意味着第一个函数可以在不同时间使用不同的函数。
基本使用
-
获取函数地址:函数名即为函数地址。
-
声明一个指向函数的指针:要声明一个指向特定类型函数的指针,可以先为所需类型的常规函数编写一个原型,然后将函数名替换为
(*pf)形式的表达式。在这种情况下,pf就是指向该类型函数的指针。 -
使用指针调用函数:一种表示,
(*pf)与函数名等价。另一种表示,C++也允许像使用函数名一样使用pf。使用第一种形式虽然更难看,但它能提供强烈的视觉提示,让人意识到代码正在使用函数指针。double pam(int); double (*pf)(int); pf = pam; // pf now points to the pam() function double x = pam(4); // call pam() using the function name double y = (*pf)(5); // call pam() using the pointer pf double y = pf(5); // also call pam() using the pointer pf
引用变量
C++为该语言新增了一种复合类型——引用变量。引用是一个作为变量别名的名称。引用变量的主要用途是作为函数的形参。如果你使用引用作为参数,函数操作的是原始数据,而不是副本。对于在函数中处理大型结构来说,引用提供了一种比指针更便捷的替代方式。
引用的本质
引用有点像常量指针;在创建它时就必须对其进行初始化,而且当引用宣誓效忠于某个特定变量后,它就会坚守这个誓言。也就是说,
int & rodents = rats;
本质上是一种伪装的表示法,类似于:
int * const pr = &rats;
在这里,引用rodents所扮演的角色和表达式*pr是一样的。
匿名变量
如果函数调用的实参不是左值,或者与对应的常量引用形参类型不匹配,C++会创建一个类型正确的匿名变量,将函数调用实参的值赋给该匿名变量,并让形参引用这个变量。
这里还有一个注意的点是形参必须是常量引用,不然的话修改的其实是匿名变量的值,而实际传的实参值没有改变,这与不加const的本来的修改实参的意图相悖。所以C++禁止这样做。
函数中的引用变量
返回引用的函数实际上是所引用变量的别名。
int& getValue(int& x) {return x; // 返回 x 的引用,也就是 x 本身
}
int& b = getValue(a); // 这里整个函数表达式相当于是a或x的别名
普通函数返回值的赋值为何经常会出现“两次 copy”
这是一个编译器实现与 ABI(Application Binary Interface)相关的问题。
因为大多数 ABI 的函数返回机制要求:
- 函数返回时先把返回值写入一个临时位置(通常是寄存器或隐藏指针指向的空间)
- 然后在赋值语句中再从这个临时位置 copy 到最终变量
所以就出现了 “函数内部一次 copy → 赋值语句又一次 copy”。
2 次 copy 的根本原因(关键):
- ABI 要求 struct 返回值由调用方分配空间
→ 函数必须将内部的返回对象 copy 到调用方提供的缓冲区。 - 赋值语句又需要从返回缓冲区 copy 到目标变量
→ 出现第二次 copy。
返回引用类型的函数在赋值时通常只发生一次 copy
返回引用不会产生返回值 copy,赋值语句本身才是唯一的 copy 来源。
为什么没有返回值 copy?
因为引用(T&)只是一个“地址”,返回时只需把地址放进寄存器,不构造临时对象,不使用隐藏返回缓冲区。
与此相反,返回值(T)必须经由隐藏返回缓冲区 → 可能两次 copy。
不过,有一个重要的补充:
如果写的是 T y = func();,那么确实只有一次 copy(把 func 返回的对象内容 copy 给 y)。但如果你写的是 给引用本身赋值T& y = func(); 或 持续使用引用,可能根本没有 copy。
函数重载
函数签名(参数列表)而非函数返回值类型决定函数重载。例如,以下两个声明是不兼容的:
long gronk(int n, float m);
double gronk(int n, float m);
所以,C++不允许以这种方式重载 gronk() 函数。可以有不同的返回类型,但前提是签名也不同:
long gronk(int n, float m);
double gronk(float n, float m);
函数模板
一般形式
template <typename T> // or class T
void Swap(T &a, T &b)
{T temp; // temp a variable of type Ttemp = a;a = b;b = temp;
}
注意事项
需要注意的是,函数模板并不会缩短可执行程序的长度。而且最终的代码中不会包含任何模板,只包含为程序生成的实际函数。模板的好处在于,它们使得生成多个函数定义变得更简单、更可靠。更常见的做法是,将模板放在头文件中,然后在使用它们的文件中包含该头文件。
模板特化
非模板函数、模板函数以及显式特化 函数原型如下:
// non template function prototype
void Swap(job &, job &);
// template prototype
template <typename T>
void Swap(T &, T &);
// explicit specialization for the job type
template <> void Swap<job>(job &, job &);
Swap<job> 中的 <job> 是可选的,因为函数参数类型表明这是针对 job 的特化版本。因此,该原型也可以这样写:
template <> void Swap(job &, job &); // 更简洁的形式
实例化与特化
template <class T>void Swap (T &, T &); // template prototype
template <> void Swap<job>(job &, job &); // explicit specialization for job
int main(void)
{template void Swap<char>(char &, char &); // explicit instantiation for charshort a, b;...Swap(a,b); // implicit template instantiation for shortjob n, m;...Swap(n, m); // use explicit specialization for jobchar g, h;...Swap(g, h); // use explicit template instantiation for char...
}
decltype关键字
编译器必须通过一个检查清单来确定类型。假设我们有以下内容:
decltype(expression) var;
下面是该清单的一个略作简化的版本。
阶段1:如果expression是一个不带括号的标识符(即没有额外的括号),那么var与该标识符的类型相同,包括const等限定符:
double x = 5.5;
double y = 7.9;
double &rx = x;
const double * pd;
decltype(x) w; // w is type double
decltype(rx) u = y; // u is type double &
decltype(pd) v; // v is type const double *
阶段2:如果expression是一个函数调用,那么变量具有该函数返回类型:
long indeed(int);
decltype (indeed(3)) m; // m is type int
阶段3:如果expression是左值,那么var就是该表达式类型的引用。这似乎意味着像w这样的早期示例应该是引用类型,因为x是左值。然而,请记住这种情况在阶段1中已经涵盖了。要应用此阶段,expression不能是未加括号的标识符。那么它可以是什么呢?一个明显的可能性是加括号的标识符:
double xx = 4.4;
decltype ((xx)) r2 = xx; // r2 is double &
decltype(xx) w = xx; // w is double (Stage 1 match)
阶段4:如果前面的特殊情况都不适用,则var与expression的类型相同:
int j = 3;
int &k = j
int &n = j;
decltype(j+6) i1; // i1 type int
decltype(100L) i2; // i2 type long
decltype(k+n) i3; // i3 type int;
如果您需要不止一个声明,可以将typedef与decltype一起使用:
template<class T1, class T2>
void ft(T1 x, T2 y)
{...typedef decltype(x + y) xytype;xytype xpy = x + y;xytype arr[10];xytype & rxy = arr[2]; // rxy a reference...
}
尾置返回类型
decltype机制本身留下了另一个相关的问题未解决。考虑这个不完整的模板函数:
template<class T1, class T2>
?type? gt(T1 x, T2 y)
{...return x + y;
}
同样,事先并不知道x加y会得到什么类型的结果。似乎可以使用decltype(x + y)来作为返回类型。但遗憾的是,在代码中的那个位置,参数x和y尚未声明,因此它们不在作用域内(对编译器而言是不可见且无法使用的)。decltype说明符必须出现在参数声明之后。为了实现这一点,C++11允许一种新的函数声明和定义语法。
template<class T1, class T2>
auto gt(T1 x, T2 y) -> decltype(x + y)
{...
return x + y;
}
-> decltype(x + y)是尾置返回类型,auto是为尾置返回类型提供的类型占位符。