【c++】【基础】【primer_plus】【第八章】 引用与模板函数

内联函数

内联函数的编译代码与其他的程序代码内联到一块了,即编译器使用内联函数里的代码来直接替代函数调用,从而不需要像函数调用那样跳来跳去。

所以内联函数是在编译的时候将函数调用使用其实现代码替换的过程。内联函数的运行速度比常规函数快,但需要更多的内存。

在处理函数调用机制所占时间比执行函数代码的时间还长时,使用内联可节约大量的时间,即对代码执行很短,但调用非常频繁的函数,最好使用内联。

使函数变为内联的方法为在函数声明前加关键字inline同时在函数定义前也加inline。但是一般内联函数在声明的同时也直接定义,比较省事。如下代码所示。

1
inline int max(int a, int b) { return a > b; }

除此之外,内联函数与宏定义都是一种代码替换过程,但是有所区别。内联函数按值传递参数,即如果参数为表达式,则函数将传递表达式的值。而宏定义则是通过文本替换来实现的,即使传递的是表达式,也会原封不动地进行文本替换。

默认参数

1
2
3
4
void func(int a, int b = 1, int c = 0);
func(1);
func(1, 2);
func(1, 2, 3);

我们可以在函数原型处给参数指定默认值,但是必须从右向左添加默认值,即要为某个参数设置默认值,则必须为它右边所有参数设置默认值。

调用函数时,实参应从左向右依次赋值给相应的形参,所以有默认值的参数就减少了最少需要实参的数目。

所以通常将最不经常改动的参数放在最右边。

引用

引用是已定义的变量的别名,即另一种名称。但通过将引用作为参数,函数将使用该参数的原始数据,而非其副本。

引用的创建

引用必须在声明的同时初始化,且一旦初始化,便不能修改其指向的对象(类似于const)。

1
2
int a = 5;
int& r_a = a

此时ar_a指向相同的值和内存单元。

引用的使用

引用常用于作为函数的参数,使用实参初始化形参时,引用将创建实参的别名,而非实参的副本,类似于解除引用的指针(*p)。故使用引用将直接修改原数据。

1
2
3
4
5
void swap(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}

如果既想使用引用(可能为了节约内存和时间),又不想使其所指内存被修改,可在引用声明前加const关键字,使其为const型引用

当实参的类型不是左值,或者实参的类型不正确但可转换为正确的类型时,c++将创建临时变量,而引用类型的形参将作为临时变量的别名。前提是引用类型为const类型,这也是显而易见的,非const型的话可能会修改临时变量值,但修改临时变量的值对传进来的参数是无效的。

所以也有人说,应尽量使用const型的引用。

c++11新增了右值引用,即可以指向右值的引用,使用&&进行声明。这将在第18章详细介绍。如下代码所示。

1
double&& rref = 2.0 * x + 5.0;                  // 右值引用`

引用与结构体

将引用用于结构体,与将引用用于基本变量相同。

引用与类对象

基类引用可引用派生类对象,这点与指针类似,即都可支持多态。

如可以定义一个基类引用作为参数的函数,可以将基类对象作为参数,也可以将子类对象作为参数。

引用作为返回类型

引用作为返回类型时,将直接返回某个左值,即返回的值可用于后续的修改。

若不想让其返回左值,但还想使用它,可在返回值前加const关键字。

注意,应避免返回函数终止时不再存在的内存单元,如局部变量等。可以返回一个作为参数传递给函数的引用,也可以返回使用new分配内存变量的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;

struct point {
int x, y;
};

point& add(point& a, const point& b) {
a.x += b.x;
a.y += b.y;
return a;
}

int main() {
point a {1, 2};
point b {3, 4};

add(add(a, b), b);
add(a, b).x = 1; // 意味着函数的返回值为左值

cout << a.x << " " << a.y << endl;

return 0;

}

模板函数

1
2
3
4
5
6
template<typename T>                    // 也可以为 template<class T>
void swap(T& a, T& b) {
T tmp = a;
a = b;
b = tmp;
}

如上代码所示,使用template<typename T>定义模板函数。其中typename关键字表示定义的泛型类型,旧版也用class,但最好使用typename,二者有些区别,可参照笔者另一篇。typename后面的T是用户自定义的,代指泛型类型的名字。

模板函数并不创建任何函数,只是告诉编译器如何定义函数,编译器在编译阶段会用具体的类型去替换T类型,并生成具体的函数。

除此之外,模板函数也可以重载。

模板函数的实例化

模板函数的实例化(instantiation),是指模板函数并不会创建任何函数(即函数实例),只有在该模板收到指令需要生成一个针对具体类型的函数时,才会真正生成该函数,这个生成的函数才是函数实例,故此过程称为实例化。

模板函数的实例化分为两类,一类是隐式实例化,一类是显示实例化。

隐式实例化

当我们调用模板函数的时候,编译器会根据当前的类型自动生成相应的函数定义,得到函数实例,这个过程称为隐式实例化。如下代码。

1
2
int a = 1, b = 2;
swap(a, b);

则编译器会将模板函数的泛型T替换为int,自动生成如下函数实例。

1
2
3
4
5
void swap(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}

显示实例化

我们可以不等到调用模板函数时才让编译器生成函数实例,因为这可能会影响到性能,毕竟编译器需要额外地去推导使用哪个类型的函数。

所以我们可以自发地给模板函数预先声明好想在一开始就生成的函数。我们给函数声明前加一个template关键字,注意不能加<>,如下代码所示。

1
2
3
4
5
6
7
8
9
template<typename T>				
void swap(T& a, T& b) {
T tmp = a;
a = b;
b = tmp;
}

/** 显示实例化 */
template void swap<int>(int&, int&);

注意,显示实例化只需要声明即可,不需要重新定义。这告诉编译器在一开始就生成好对应类型的函数。

除此之外,我们也可以在函数调用时显示指定函数的类型。但是这不能与显示实例化一起出现。

1
2
3
int a = 1, b = 2;
swap<int>(a, b);
// 或 template void swap<int>(a, b); 也可

模板函数的具体化

模板函数的具体化(specialization),是指对于某些特定的类型,其实现与当前模板函数有所区别,我们需要重新定义对于该特定类型的函数实例(听起来与函数重载类似)。

1
2
3
4
struct foo {
int id;
float value;
}

假如我们有如上结构体,我们当然无法直接使用swap()函数来交互两个这种类型的值。所以我们要定义针对foo类型的swap()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<>
void swap<foo>(foo& a, foo& b) {
float temp = a.value;
a.value = b.value;
b.value = temp;
}

/** 也可以是下面格式 */
template<>
void swap(foo& a, foo& b) {
float temp = a.value;
a.value = b.value;
b.value = temp;
}

可以看到,显示具体化使用template<>前缀进行函数声明,并有具体的函数实现。

注意,不能在同同时使用同一种类型的显式具体化和显式实例化。

模板函数的具体化又称模板函数的全特化,当然全特化这个词在模板类中是经常碰到的,后面会有解释。除此之外,还有偏特化一词,但是一般而言函数模板是不用(也不能)偏特化的,因为这种需求大可利用函数重载来实现。

编译器在选择函数时,会依据以下优先级进行筛选。

非模板函数 > 具体化模板函数 > 常规模板函数

函数重载

仅当函数执行相同的任务,但使用不同形式的方法或过程时,才应采用重载。但是在此之前还要考虑是否可用默认参数完成。

函数重载的关键是函数的参数列表,这也是函数的特征标。函数重载的意思是,c++允许定义名称相同的函数,前提是特征标不同,即参数列表不同。

其实质是在调用这些同名函数时,编译器可以轻而易举地匹配到某个函数。

c++将尝试使用强制类型转换进行匹配,但是如果存在二义性,即有多个匹配的函数,则将拒绝这种转换而报错。

编译器将类型引用和类型本身视为同一特征标。对于不同引用类型的重载,编译器的做法是调用最匹配的那个版本。

1
2
3
4
5
6
7
8
9
void fun(int& x);           // 函数1
void fun(const int& x); // 函数2
void fun(const int&& x); // 函数3

int x = 1;
const int y = 2;
fun(x); // 将调用函数1
fun(y); // 将调用函数2
fun(x+y); // 将调用函数3 -- 若没有函数3, 将调用函数2

重载解析决定使用重载、模板、模板重载中的哪个函数定义。其解析步骤大致如下。

创建候选函数表,只要函数名符合便加入表中。

据候选函数表创建可行函数表,此时考虑参数是否符合。

确定最佳可行函数,若没有则函数调用出错。

完全匹配,常规函数优于模板。

提升转换。

标准转换。

强制转换。

根据模板的部分排序规则选出最具体的模板函数定义。

decltype

模板函数中,我们有时候难以表示一些变量的类型,它们很可能是由模板的参数或其组合决定的。如下代码所示。

1
2
3
4
template<typename T1, typename T2>
void fun(T1& a, T1& b) {
some_type apb = a + b;
}

decltype的出现,提供了一种解决这种问题的方法。

decltype的用法如下所示。

1
decltype(expression) var;       // 使变量var与expression有相同的类型

我们使用decltype关键字定义了一个变量var,该变量的类型与decltype后面括号里的表达式的类型相同。

decltype可以从一个已存在的变量或表达式的类型声明与其相同类型的新的变量。

expression是一没有括号括起来的标识符,则var的类型与该标识符相同。

1
2
const int* x;
decltype(x) w; // w的类型为const int*

expression是一个函数调用,则var的类型与该函数的返回值相同,但并不会实际调用该函数。

1
2
long solve(int, int);
decltype(solve(3, 3)) m; // m的类型为long

expression是一个左值,且用括号括起来了,则var的类型为指向其类型的引用。

1
2
3
double x = 3.3;
decltype(x) v = x; // v为double类型
decltype((x)) r = x; // r为double&类型

若前面的条件都不符合,则var的类型与expression的类型相同。

若需要多次声明此类型,可结合typedef使用,毕竟decltype(exp)本就可以当作一个数据类型来用了。

1
2
typedef decltype(expression) some_type;         // 将其定义为另一种类型名
some_type x; // x的类型为some_type, 且与expression一致

则一开始的问题的一种解决方案如下。

1
2
3
4
5
6
7
template<class T1, class T2>
void fun(T1 x, T2 y) {
typedef decltype(x + y) xpytype; // 将其定义为另一种类型名
xpytype xpy = x + y;
xpytype arr[10];
xpytype& rxy = arr[2];
}

后置返回类型

有时候我们连返回值的类型也是无法确定的,如下代码所示。

1
2
3
4
template<class T1, class T2>
some_type fun(T1 x, T2 y) { // 不知道返回值类型
return x + y;
}

如果我们像上面一样,将some_type写成decltype(x+y),也是不对的,因为此时xy还尚未声明。

好在c++引入了一个新的语法,即后置返回类型。

1
2
3
double solve(int x, float y) {
return x + y + 0.5;
}

如上代码可使用下述语法表示。

1
2
3
4
5
auto solve(int x, int y) -> double {
return x + y + 0.5;
}

cout << solve(1, 2) << endl;

所以我们可以使用decltype结合后置返回类型来解决上面的问题。最终的代码如下。

1
2
3
4
template<class T1, class T2>
auto fun(T1 x, T2 y) -> decltype(x + y) { // 此时x和y都已声明
return x + y;
}