C++函数重载,函数模板,引用
第八章-深入函数
1. 内联函数
内联函数是C++为了提高程序运行速度所作的一项改进。当执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈,跳到标记函数起点的内存单元,执行函数代码,然后跳回到地址被保存的指令处。来回跳跃并记录跳跃位置意味着使用函数时需要一定的开销。
内联函数的编译代码与其他程序代码连接起来了,编译器直接在对应的位置用函数体内部的代码替换,这样便没有调用函数时指令的跳转了。可以加快程序执行的时间,但是需要占用更多的内存。当函数调用花费的时间较多时,可以使用内联函数的方法。
使用inline关键字便可定义。
inline double square (double x) {
return x * x;
}
内联代码的原始实现为# define square(x) x*x
2. 引用变量
引用是已定义变量的别名(另外一个名称)。使用& 进行定义,而且在定义时需要初始化。
# include<iostream>
int main(){
using namespace std;
int a = 101;
int & b = a; // 定义a的别名b
b = b + 1;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
return 0;
}
在函数的参数传递中,如果使用引用变量,将可以允许函数使用原始数据。
声明时void sum(int &a, int &b)
void sum(int &a, int &b){
return a + b;
}
int m = 26;
int n = 25;
sum(m,n);
在sum函数中,a,b是m,n的别名,对a,b 操作就是对m,n操作。
函数的返回值也可以是一个引用。
# include<iostream>
# include<string>
struct free_throws{
std::string name;
int made;
int attempts;
float percent;
};
void set_pc(free_throws & ft);
free_throws & acc(free_throws & target, const free_throws & source);
int main(){
using namespace std;
free_throws one = {"Ifelsa", 13, 14};
free_throws two = {"Andor", 10, 16};
free_throws three = {"Max", 7, 9};
free_throws four = {"Whily", 5, 9};
free_throws five = {"Long", 6, 14};
free_throws team = {"Throwgoods", 0, 0};
acc(team, one) = four;
cout << team.percent;
return 0;
}
void set_pc(free_throws & ft){
if(ft.attempts !=0)
ft.percent = 100.0f * float(ft.made) / float(ft.attempts);
else
ft.percent = 0;
}
free_throws & acc(free_throws & target, const free_throws & source){
target.attempts += source.attempts;
target.made += source.made;
set_pc(target);
return target;
}
在上述代码中,acc(team, one) = four
看上去很奇怪。一般来说,常规(非返回引用)函数得到的结果都会放到等号的右边。传统的返回机制与按值传递函数参数类似:将return后面的表示式结果计算出来,将结果复制到例外一个临时位置。返回引用时,acc(team, one) = four
其实相当于
acc(team, one);
team = four;
在赋值表达式中,左边的子表达式必须标识一个可以修改的内存块。在这里函数返回team的引用,它指向了一个可以修改的内存块。
引用分为左值引用和右值引用,
- 可以取地址的,有名字的,非临时的就是左值;
- 不能取地址的,没有名字的,临时的就是右值;
可见立即数,函数返回的值等都是右值;而非匿名对象(包括变量),函数返回的引用,const对象等都是左值。
类型 && 引用名 = 右值表达式;
int && a = 100;
3. 默认参数
在函数原型中进行定义,便可使用默认参数的特性。比如
char* left(const char *str, int n=1);
对于带参数列表的函数,必须从右向左添加默认值,也就是说,要为某个参数设置默认值,则必须为它右边的所有参数提供默认值。
int chico(int n, int m =6, int i = 2); // 合法
int chico(int m =6, int i = 2, int n ); // 不合法
4. 函数重载
函数重载是指可以有多个同名的函数。函数重载的关键是函数的参数列表,C++允许定义名称相同的函数,条件是它们的特征标不同。
// 合法的重载
void print(double a);
void print(int b);
void print(char *p);
// 不合法的重载
double cube(double x);
double cube(double &x); // 编译器在检查函数特征标时,把类型引用和类型本身视为同一个特征标。
是特征标志不同,而不是函数类型使得可以对函数进行重载,下面的重载是非法的
long gronk(int n, int m);
double gronk(int n, int m);
在类设计和STL经常使用引用参数,因此有必要知道不同引用类型的重载
void sink(double &a1); // 与可修改的左值参数匹配
void sink(const double &a2); // 与可修改的左值参数,const左值参数,右值参数匹配
void sink(double &&a3); // 与右值参数匹配
sink(10.0 + 10.0); // 与右值参数匹配
5. 函数模板
5.1 函数模板的基本概念
函数模板是通用的函数描述,使用泛型来定义函数,其中的泛型可以用具体的类型(int或者long)来替换。通过将类型作为参数传递给模板,可以使编译器生成该类型的函数。下面通过一个例子介绍
template <typename AnyType>
void swap(AnyType &a, AnyType &b){
AnyType temp;
temp = a;
a = b;
b = temp;
}
要建立模板,关键字template
和typename
是必须的(typename 可以用class代替)。
5.2 重载的模板
并非所有的类型都使用相同的算法,为了满足这个需求,可以像重载常规函数一样重载模板定义,比如
template <typename AnyType>
void swap(AnyType &a, AnyType &b); // 原始的模板定义
template <typename AnyType>
void swap(AnyType *a, AnyType *b); // 新的模板定义
有些时候,函数的参数保持不变,但是我们需要做出不同的操作,明显,使用模板的重载无法实现(因为参数没有改变)C++提供了显式具体化(explicit specialization)的方法。
编译器使用函数的优先级为:非模板函数> 显式具体化函数 > 模板函数
# include<iostream>
# include<string>
using namespace std;
struct job{
char name[40];
double salary;
int floor;
};
//模板函数原型
template <typename T>
void print(T a);
// 具体化后的函数原型
template <> void print(job x);
int main(){
job teacher = {"teacher", 10000, 4};
print(9);
print('a'); // 使用模板函数
print(teacher); // 使用显式具体化函数
return 0;
}
// template prototype
template <typename T>
void print(T a){
cout << a << endl;
}
// specialization
template <> void print(job x){
cout << x.name << endl;
cout <<"salary is : "<< x.salary << endl;
}
编译器使用模板为特定类型生成函数定义时,得到的是模板实例(instantiation),为了更好地理解模板,下面来表明三个概念。
- 隐式实例化:通过实际使用的参数来得到模板实例,比如上面的
print(9)
print函数传入一个int类型的参数,因此经过编译器处理后得到参数为int类型的模板实例。 - 显式实例化:在函数后面指定参数类型,比如
template void print<int>(int);
不可以在函数中定义与原来模板函数不一样的操作 - 显式具体化:在template后面有<>,可以在函数中定义与原来模板函数不一样的操作,比如
template <> void print(job x);
在同一个文件中使用同一种类型的显式实例和显式具体化将报错
5.3 decltype关键字
但是,在编写函数时可能会出现无法确定变量的类型的情况,比如
template <class T1, class T2>
void add(T1 x, T2 y){
type??? a = x + y;
// 当要定义一个变量a来接收x + y的值,a应该什么类型的变量
}
C++11新增关键字decltype,使用方法如下所示
int a;
decltype(a) b; // b与a的类型相同
// 上面的例子
decltype(x + y) a = x+y;
5.4 后置返回
看下面一个例子
template <class T1, class T2>
type??? add(T1 x, T2 y){
return x + y;
// 如何确定函数的返回类型
}
decltype(x + y) add(T1 x, T2 y); // 不可行,在声明时还未定义x,y,
使用decltype关键字也无法确定返回值的类型,因为此时还未声明x,y,x,y不在作用域中。
可以使用下面的语法,使用auto关键字。
template<class T1, class T2>
auto add(T1 x, T2 y) -> decltype(x + y);
6. 编译器的重载解析
对于函数重载,函数模板和函数模板重载,C++需要有一个良好的策略,来决定使用哪一个函数定义,这一过程称为重载解析。简化的步骤有三步
- 创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。
- 使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。例如,使用float参数的函数调用可以将该参数转换为double,从而与double形参匹配,而模板可以为float生成一个实例。
- 确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。
参考资料
- C++ primer plus
- C++ 左值引用与右值引用