# C++基础8-函数2

本文是《C++ Primer Plus》的笔记,本文中的案例均自己实践过,如需转发请在转发开头贴上原文地址,谢谢!

# 函数2

# C++内联函数

内联函数是C++为了运行速度做的一项改进,常规函数和内联函数之间的主要区别不在于编写方式,而在于C++编译器如何将它们组合到程序中。C++编译的最终产品是一堆机器指令,也就是汇编语言,每条指令都有特定的内存地址。计算机将随后逐步执行这些指令,而常规函数的函数体一般在主程序之外的内存空间,到执行到函数时,立即存储该函数名的内存地址,将函数参数复制到堆栈,调到函数体的内存单元,执行完毕后再调回调用函数名的主程序中。而内联函数则是将函数体在函数名原处替换,这样再执行函数名的地方便不再跳转到函数体而是直接顺序执行。由此可见使用内联函数会占用更多的内存,但是运行速度会比常规函数块。

内联函数的使用场景,如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间将只占整个过程的很小一部分。如果代码执行时间很短,小于函数调用机制的时间,那么内联函数可以节省大部分时间。使用方法是在函数声明和定义前加上关键字inline。

# 内联和宏

很多人觉得内联函数和宏很相似,有些地方是不一样的。比如宏不是通过传递参数实现的,而是通过文本替换来实现。

#define multiply(x) x*x
a = multiply(1+2); //会被替换为 1+2*1+2,因此最好如下定义宏
#define multiply(x) (x)*(x)
1
2
3

# 默认参数

void calArea(int width,int height=2,int defArea=3) {
    cout << width * height << endl;
}

int main(int argc,char *argv[])
{
    calArea(3,4);
}
1
2
3
4
5
6
7
8
  1. 带参数列表的函数,必须从右向左添加默认值,就是说,要为某个参数设置默认值,必须为它右边的所有形参设置默认值。
  2. 实参从左到右依次被赋给相应形参,不能跳过任何参数。比如calArea(3,,1);

# 函数重载

函数重载的关键是函数的参数列表(形参的个数和类型),也就是函数特征标(function signature)。如果他们的特征标相同而变量名不同这两个函数被认为是一样的,C++允许定义名称相同但是特征标不同的函数,具体实现时编译器会根据调用的实参个数和类型去找相同形参个数和类型的函数,这叫做函数的重载。

  1. 某个形参是引用还是非引用对于特征标来说都是一样的,被视为同一特征标。
  2. 同理,某个形参是const还是非const都被视为同一特征标。
void calArea(int width,int height=2,int defArea=3) {
    cout << width * height << endl;
}

//如果该width的类型是int,则该函数将永远不会被重载。
void calArea(float width, int height = 2) {
    cout << width * height << endl;
}


int main(int argc,char *argv[])
{
    int a = 1, b = 2;
    calArea(a);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 函数模板

函数模板是通用的函数描述,它们使用泛型来定义函数,其中的泛型可用int,float替代成为一种具体类型,通过将类型作为参数传递给模板,可使编译器生成该类型的函数。由于模板允许以泛型(而不是具体类型)的方式编写程序,因此有时也被称为通用编程。由于类型是用参数表示的,因此模板特性有时也被称为参数化类型(parameterized types)。

实例:

template <typename T>
void swapT(T &a, T &b) {
    T temp;
    temp = a;
    a = b;
    b = temp;
}

template <typename T>
void swapT(T &a, T &b,int c) {
    T temp;
    temp = a+2;
    a = b;
    b = temp;
}

int main(int argc,char *argv[])
{
    int a = 1, b = 2;
    swapT(a, b);
    cout << a << ":" << b << endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 模板具体化与实例化

# 什么是模板具体化?

为某一特定的类型重写函数模板,使用独立专门的函数显式的为特定类型生成函数定义。

# 为什么需要模板具体化?

模板并不是万能的,比如T如果是一个结构体或者自定义对象,那么a+2就不对。所以说模板还是有挺大局限性的,那么如何应对这种局限性呢?最直接的是具化模板函数。

template <typename T>
void swapT(T &a, T &b) {
    T temp;
    temp = a;
    a = b;
    b = temp;
}

struct  animal
{
    int age;
    int id;
};

//template <> void swapT(animal& a1, animal& a2);
template <> void swapT(animal& a1, animal& a2) { //没有与指定类型匹配的 函数模板"swapT"实例
    int temp = a1.age;
    a1.age = a2.age;
    a2.age = temp;
}


void swapT(animal& a1, animal& a2) { //没有与指定类型匹配的 函数模板"swapT"实例
    int temp = a1.id;
    a1.id = a2.id;
    a2.id = temp;
}


int main(int argc, char *argv[])
{
    double a = 4;
    double b = 5;
    animal c, d;
    c.id = 1;
    c.age = 10;
    d.id = 2;
    d.age = 11;
    swapT(a, b);
    cout << a << ":" << b << endl;
    swapT<double>(a, b);
    cout << a << ":" << b << endl;
    swapT(c, d);
    cout << c.id << ":" << d.id << endl;
    swapT<>(c,d);
    //<>指出编译器应该选择模板函数,而非模板函数
    cout << c.age << ":" << d.age << endl;
}

上面输出结果为:
5:4
4:5
2:1
11:10

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

第三代具体化:

  1. 每个函数可以有非模板函数,模板函数和显式具体化模板函数以及它们的重载版本。
  2. 显式具体化的原型和定义应该以template <>开头。
  3. 当每种形式的重载版本都存在时,优先级是:非模板函数(普通函数) 》 具体化 》 常规模板。

显式具体化基于函数模板,只不过在函数模板的基础上添加一个专门针对特定类型的,实现方式不同的具体化函数。

# 什么是模板实例化?

编译器使用模板为特定类型生成函数定义时,得到的是模板的实例(instantiation)。

# 什么是隐式实例化?

在调用函数时不指定类型,只有当使用模板时根据实参类型推断出函数实例,称为隐式实例化(implicit instantiation)。比如上面的swapT(a, b)。

用-fno-implicit-templates编译代码,会令隐式的模板实例化失效,他会显示的初始化所需模板。

# 什么是显示实例化?

使用显式的声明来实例化模板,在使用模板之前,编译器根据显式实例化指定的类型生成模板实例。语法是template+函数定义。但是实际使用时提示找不到swapT的函数定义。

# 两种实例化区别

如果不使用显式实例化,每次调用函数模板都会消耗性能去推导使用哪个类型的函数,增加程序运行时的负担;使用显式实例化,则在编译时会函数实例化。试图在同一个文件中使用同一种类型的显示实例化和显式具体化声明,会出错。

# 函数重载解析(overloading resolution)

当存在函数的多种形式时,C++需要一个定义良好的策略,来决定为函数调用使用哪一个函数定义,这个过程称为重载解析。

  1. 创建候选函数列表,都是一堆名称相同的函数和模板函数。
  2. 使用候选函数列表创建可行函数列表,其中包括实参类型与相应的形参类型完全匹配的情况,有隐式转换的情况。
  3. 确定是否有最佳的可行函数,如果有使用它,否则报错。

在确定是否有最佳可行函数时,优先顺序如下:

  1. 完全匹配,常规函数优于模板。
  2. 自动转换(提升转换)。
    1. bool,char,unsigned char,signed char,short,unsigned short自动转换为int。
    2. float自动转换为double。
  3. 标准转换(例如,int转换为char,long转换为double)。
  4. 用户定义的转换,比如类声明中的转换。

完全匹配允许的的无关紧要转换

实参 形参
Type Type &
Type & Type
Type[] *Type
Type(argument-list) Type(*)(argumnet-list)
Type const Type
Type volatile Type
Type * const Type
Type * volatile Type *

# 模板函数的发展

在C++98中,无法声明函数中的中间变量的类型,比如

template<class T1,class T2>
void ft(T1 x,T2 y){
    ...
    ?type? xpy = x+y;
    ...
}
1
2
3
4
5
6

这时无法声明xpy的类型。

在C++11中添加了decltype关键字来解决上面的情况,比如

template<class T1,class T2>
void ft(T1 x,T2 y){
    ...
    decltype(x + y) xpy = x+y;
    ...
}
1
2
3
4
5
6

decltype为了确定类型,编译器必须遍历一个核对表,比如

decltype(expression) var;

其步骤如下:

  1. 如果expression是一个没有用括号括起来的标识符,则var的类型与该标识符的类型相同,包括const限定符。
  2. 如果expression是一个函数调用,则var的类型与函数的返回类型相同。
  3. 如果expression是一个左值,则var为指向其类型的引用,要从进入这一步的话需要用括号括起来标识符。
  4. 如果前面的条件都不满足,则var的类型与expression的类型相同。

但是函数返回值的类型还是无法确定,那么这就需要另一种C++的后置返回类型(trailing return type)。

template <typename T>
auto add(T a, T b)->decltype(a+b)
{
    decltype(a + b) sum;
    sum = a + b;
    return sum;
}

int main(int argc, char *argv[])
{
    int a = 1, b = 2;
    cout << add(a, b)+4.0 << endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13