五、函数
与C语言相比,C++引入了许多新特性包括内联函数、按引用传递变量、默认的参数值、函数重载(多态)以及模板函数。
(1)内联函数
C++内联函数对函数的调用提供了另一种选择。内联函数的编译代码与其他程序代码“内联”起来了,也就是说,编译器将使用相应函数的代码替换函数的调用,对于内敛代码,函数无需跳入另一个位置处执行代码,再跳转回来;而是直接采用代码替换的方式。
要想使用内联函数这一特性,就必须使用关键字inline
,具体是做法如下:
1、在函数声明前加上关键字
inline
;2、在函数定义前加上关键字
inline
。3、通常的做法是省略函数原型,将整个函数的定义放在本应该提供函数原型的位置。
内联函数和常规函数一样,也是按值进行传递参数的,这里说明一下内联函数和宏的差别:C语言C++中的宏并不是通过传递参数实现的,而是通过文本替换来实现的。但是内联函数虽然是进行代码块的替换,但是内联函数却是通过传递参数来进行实现的。举个例子:
#define SQUARE(X) X * X //定义一个宏
SQUARE(3 + 4); //结果为3 + 4 * 3 + 4 = 19, 宏是通过替换来实现的
inline int square(x) {return x * x};
square(3 + 4); //结果为 (3+4)*(3+4) = 49, 内联函数是通过参数传递来实现的
(2)引用变量
C++新增了一种复合类型——引用变量,引用是已定义的变量的别名。引用变量的主要作用是用作函数的形参,函数将引用变量用作参数,函数将使用原始数据,而不是数据的副本。这样,除指针外,引用变量也为函数处理大型的结构体提供了一种非常方便的途径。引用的作用就是给一个变量创建一个别名,两者指向同一内存空间。
创建引用变量:数据类型 &别名 = 原名
。C语言和C++使用&
符号来指示变量的地址。C++使用符号&
来声明一个引用。创建引用的通用做法是必须在创建引用时就给该引用赋值,而不能先创建后赋值:
typeName somevalue; //创建一个变量
typeName& somerefer = somvalue; //创建该变量的引用,通用写法
例如:
int a = 10;
int& b = a; //创建了一个对a变量的引用b变量
C++新增了右值引用,这种引用可以指向右值,使用&&
来进行声明。例如:
double j = 15.0;
double&& jref = j * 2.0 + 15.0; //j*2.0 + 15.0是一个右值,这里创建了对其的引用
引用的注意事项:1、引用必须进行初始化;2、引用在初始化后就不能发生改变了。
引用作函数的参数:引用经常被用作函数的参数,使得函数中的变量名称为调用程序中的变量的别名,这种传递参数的方式称为按引用传递。按引用传递允许被调用的函数能够访问调用函数中的变量。这项新增特性是对C语言的一种超越,C语言只能够按值进行传递,按值传递导致被调用函数使用调用程序的值的拷贝,当然,C语言也允许避开按值传递的限制,采用按指针传递的方式(本质上也是一种按值传递)。
使用示例:
#include <iostream>
void swapr(int& a, int& b); //按引用传递
void swapp(int* p, int* q); //按指针传递
void swapv(int a, int b); //按值传递
int main(void)
{
using namespace std;
int wallet1 = 300;
int wallet2 = 350;
cout << "wallet1 = $ " << wallet1;
cout << " wallet2 = $ " << wallet2 << endl;
cout << "Using reference to swap contens:\n";
swapr(wallet1, wallet2);
cout << "wallet1 = $ " << wallet1;
cout << " wallet2 = $ " << wallet2 << endl;
cout << "Using pointers to swap contens again:\n";
swapp(&wallet1, &wallet2);
cout << "wallet1 = $ " << wallet1;
cout << " wallet2 = $ " << wallet2 << endl;
cout << "Trying to use passing by value:\n";
swapv(wallet1, wallet2);
cout << "wallet1 = $ " << wallet1;
cout << " wallet2 = $ " << wallet2 << endl;
return 0;
}
void swapr(int& a, int& b)
{
a = a ^ b;
b = a ^ b;
a = a ^ b;
}
void swapp(int* p, int* q)
{
*p = *p ^ *q;
*q = *p ^ *q;
*p = *p ^ *q;
}
void swapv(int a, int b)
{
a = a ^ b;
b = a ^ b;
a = a ^ b;
}
引用变量的特性:传递引用变量的限制很严格,当引用变量名是另一个变量的别名的时候,则实参是该变量,不能使用该变量的表达式来进行参数传递,也就是说,当引用形参的实参是一个右值表达式的时候,这时引用是非法的。举个例子:
double cube(double& ra); //函数原型
double x = 3.0;
cube(x + 3.0); //这种调用是非法的,因为表达式x+3并不是变量,不能被引用
能否被引用就涉及到左值和右值的问题:左值是什么呢?左值参数是可以被引用的数据对象,例如,变量、数组元素、结构体成员、引用和解除引用的指针都是左值。非左值包括字面常量(用引号括起来的字符串除外,它们由其地址表示)和包含多项的表达式。
临时变量、引用参数和const
:const
修饰引用称为常量引用,主要用途是防止使用引用对原数据进行修改。在参数被const
修饰的时候,如果实参和引用参数不匹配,C++将自动生成临时变量空间,并且将引用指向这一临时空间,操作引用对象时实际上是在操作这一临时空间。C++在以下两种情况下才会生成临时变量
1、实参的类型正确,但不是左值。
2、实参的类型不正确,但可以转换为正确的类型。
例如:
double cube(const double& ra)
{
return ra * ra * ra;
}
double side = 3.0;
long edge = 5L;
double c1 = cube(side); //正常的传入引用参数
double c2 = cube(side + 3.0); //实参的类型正确,但是side+3.0并不是左值,因此自动创建临时对象
double c3 = cube(edge); //实参的类型不正确,但是可以转化为正确的类型,自动创建临时对象
引用参数作为函数的返回值:需要注意一点,不要让引用参数返回对局部变量的引用,因为这样在函数运行结束的时候,局部变量已经销毁,此时将不存在对内存单元的引用。为了避免这种问题,最好的做法是返回对函数参数列表的引用,因为这些参数列表是对调用函数所使用数据的引用,因此返回的引用也将指向这些数据。1、不要返回对局部变量的引用;2、函数的调用可用作为左值。示例:
int& test(void)
{
int a;
return a;
}
int& ref = test(); //这样做是非法的,1、不能返回局部变量的引用,因为局部变量位于栈区会自动销毁
int& test(void)
{
static int a;
return a
}
test() = 1000; //2、引用作为返回值时函数的调用可用用作左值,这里就相当于将存放于全局区的静态变量a赋值为1000
引用的本质:引用在C++内部是通过指针常量来实现的,这就解释了为什么引用的指向是不可更改的。
(3)默认参数
C++支持设置默认参数——默认参数是值当函数调用中省略了实参时自动使用一个值。设置默认参数必须通过函数原型,这是因为编译器会通过函数原型来查看程序所需要的参数数目,因此函数原型也必须将可能的默认参数告诉程序。请注意,如果函数声明有默认参数,那么函数定义则不能有默认参数,只需要向函数原型提供默认参数,而函数的定义与没有默认参数的时候完全相同。如果没有指定函数的原型而是直接进行函数的定义,那么在没有函数原型的时候直接在函数定义中添加默认参数。函数声明和函数定义只能有一个有默认参数。
对于带参数的列表的函数,必须从右往左添加默认参数,也就是说,要为某个参数设置默认参数,则必须为它的右边的所有参数都提供默认参数。例如:
char* left(const char* str, int n = 1, int m = 2); //函数原型,并且提供了两个默认参数
char* left(const char* str, int n, int m) //函数定义无需提供默认参数
{
...
}
char* left(const char* str, int n = 1, int m = 2) //在没有函数原型的时候直接在函数定义里添加默认参数
{
...
}
(4)占位参数
函数占位参数,占位参数只有参数类型声明,而没有参数名声明,一般情况下,在函数体内部无法使用占位参数。占位参数可以指定默认值。示例:
void func1(int a, int) //该函数的第二个参数即为占位参数
{
std::cout << "Hello World!" << std::endl;
}
void func2(int a, int = 10) //该函数的第二个参数即为占位参数,并且指定了默认值
{
std::cout << "Hello World!" << std::endl;
}
使用占位参数的目的是为了让函数的参数列表不同,使之满足函数重载的条件。示例:
//在重载前置++和后置++的过程中使用int
MyInteger& operator++() //重载前置++
{
...
}
//int是一个占位参数,可以用于区分前置和后置递增,并且只能用int
MyInteger& operator++(int) //重载后置++
{
...
}
(5)函数重载
函数多态是C++在C语言的基础上的新增的功能。默认参数能够使用不同数目的参数列表调用同一个函数,而函数多态(函数重载)能够使用多个同名的函数。
函数重载的关键在于函数的参数列表——也称为函数特征标(function signature)。如果在同一作用域下,两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量是允许不同的。C++允许定义名称相同的函数,前提是它们的特征标不同。在使用同名的函数的时候,编译器将会根据函数的参数列表来选取对应的函数进行执行。例如:
void print(const char* str, int width); //函数1
void print(const long l, int width); //函数2
void print(const char* str); //函数3,这些都是函数重载的示例
当引用作为重载的条件:需要注意的是,编译器在检查函数特征标的时候,将类型引用和类型本身视作同一个特征标。例如:
double cube(double x); //函数1
double cube(double& x); //函数2,这种函数重载是错误的
需要牢记是特征标,而不是函数类型使得可以对函数进行重载。例如:
long gronk(int n, float m); //函数1
double gronk(int n, float m); //函数2,这种重载是错误的,因为重载的时候允许返回类型不同,但是函数的特征标也必须不同
当函数重载遇到默认参数:函数的重载没有问题,但函数的调用有可能会出现二义性的情况,示例:
void func (int a, int b = 10)
{
...
}
void func (int a) //函数的重载没问题,因为函数的参数列表不同
{
...
}
func(10); //但是这样调用函数的时候,两个函数定义均满足,所有会出现二义性的问题
func(10, 20); //这样调用不会出现二义性
到底什么时候使用函数重载?仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应该采用函数重载。简而言之函数重载需要满足三个条件:
1、在同一个作用域下;
2、函数名称相同;
3、函数参数类型不同,或个数不同,或顺序不同。
(6)函数模板
在C++添加关键字typename
之前,C++使用关键字class
来创建模板,也就是说模板还可以这样定义:
template <class AnyType>
void Swap(AnyType &a, AnyType &b) //定义了一个交换两对象值的函数
{
AnType temp;
temp = a;
a = b;
b = temp;
}
一个完整的示例:
#include <iostream>
template <typename T> //模板和函数原型写在一起
void Swap(T& a, T& b);
int main(void)
{
using namespace std;
int i = 10;
int j = 20;
cout << "i, j = " << i << ", " << j << ".\n";
cout << "Using complier-generated int swapper:\n";
Swap(i, j);
cout << "Now i, j = " << i << ", " << j << ".\n";
double x = 24.5;
double y = 81.7;
cout << "x, y = " << x << ", " << y << ".\n";
cout << "Using complier-generated double swapper:\n";
Swap(x, y);
cout << "Now x, y = " << x << ", " << y << ".\n";
return 0;
}
template <typename T> //用模板来完成函数的定义
void Swap(T& a, T& b)
{
T temp;
temp = a;
a = b;
b = temp;
}
常用的做法是将模板放置在头文件中,并在需要使用模板的文件中包含该头文件。
(7)显示具体化
使用模板能够极大的精简我们的代码量,尤其是在容器类或者一些通用函数中。但是,模板不能帮助我们解决所有问题。有时候我们需要针对特定的场景提供一个特殊的函数(或者类)定义,这个就是具体化。要注意实例化的具体化的区别,可以从编译器的角度来看这两者之间的区别,实例化是指编译器生成特定函数(或者类)定义;而具体化可以看做是一个特殊版本的用来生成函数(或者类)定义的方案,它只是一个方案,因此编译器不会生成定义。
显示具体化机制(explicit sepcialization)提供一个具体化的函数定义,当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。总的来说,如果有多个原型,则编译器在选择原型时,非模板版本优先于显示具体化版本和模板版本;而显示具体化版本又优先于模板生成的版本。优先级:普通函数>显式具体化>显式实例化>普通模版。
C++采用的显示具体化方法的做法:
1、对于给定的函数名,可用有非模板函数、模板函数和显示具体化模板函数以及它们的重载版本。
2、显式具体化的原型和定义应该以
template <>
打头,并通过名称来指出类型。3、具体化优先于常规模板,而非模板函数优先于具体化和常规模板。
具体化的写法示例:
struct ST;
/* 显示具体化的原型和定义应以template <> 开头 */
template <> void Swap<ST>(ST & st1, ST & st2); //一个具体化的函数原型,这个函数设计用于ST结构体
(8)实例化和具体化
在代码中包含函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板的实例(instantiation)。
编译器自动使用模板生成函数定义称为隐式实例化;
当我们手动直接命令编译器创建特定的实例称为显式实例化;
对于某些特殊类型,可能不适合模板实现,需要重新定义实现,此时就是使用显示具体化的场景这种用途称为显示具体化。
隐式实例化是编译器自动使用模板生成函数的实例,所有隐式实例化无需特定的语法。
template <typename T>
void Swap(T& a, T&b); //隐式实例化自动生成函数定义
显式实例化其语法是声明所需的种类——用==<>
符号指示类型,并且声明前加上关键字template
==
template void Swamp<int> (int& a, int& b); //显示实例化,生成了指定特定参数的函数定义,这里为int类型
显式具体化其语法是声明所需的种类——用==<>
符号指示类型,并且声明前加上关键字template <>
==
struct job;
template <> void Swamp <job>(job& a, job& b); //显式具体化,为专门的类别job显示进行函数定义
小结:从含义上,显示具体化告诉编译器不要使用Swap()的函数模板来生成函数定义,而使用专门的job类型显式地定义函数定义 是为了提供一个具体化的函数定义,其中包括所需的代码,当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板,而显示实例化则是生成了一个指出特定参数类型的实例函数