C++扩展了C语言的函数功能。通过将inline关键字用于函数定义,并在首次调用该函数前提供其函数定义,可以使得C++编译器将该函数视为内联函数。也就是说,编译器不是让程序跳到独立的代码段以执行函数,而是用相应的代码替换函数调用。只有在函数很短时才能采用内联方式。
引用变量是一种伪装指针,它允许为变量创建别名。引用变量主要被用作处理结构和类对象的函数的参数。通常,被声明为特定类型引用的标识符只能指向这种类型的数据;然而,如果一个类是从另一个类派生来的,则基类引用可以指向派生类对象。
C++原型让您能够定义参数的默认值。如果函数调用省略了相应的参数,则程序将使用默认值;如果函数调用提供了参数值,则程序将使用这个值。只能在参数列表中从右到左提供默认参数。因此,如果为某个参数提供了默认值,则必须为该参数右边所有的参数提供默认值。
函数的特征是其参数列表。程序员可以定义两个同名函数,只要其特征不同。这被称为函数多态或函数重载。通常,通过重载函数来为不同的数据类型提供相同的服务。
函数模板自动完成重载函数的过程。只需要使用泛型和具体算法来定义函数,编译器将为程序中使用的特定参数类型生成正确的函数定义。
1. 关于C++内联函数
内联函数的编译与其它常规函数不同,编译器会使用函数代码替换函数的调用,使得程序不需要来回跳转,省去了程序跳转的开销,但付出的代价是内存的开销,特别是内联函数如果体量较大,且使用较多时。所以内联一般用于函数很短,且调用频繁时。
使用方法是:将整个定义放在提供函数原型的地方(头文件或源文件开始),并在函数前加上关键字inline
。例如:
#include <iostream>
inline double square(double x) {return x * x;} //内联函数
int main()
{
...
}
Inline是C++新增的特性,是从C的宏定义#define发展来的,但宏定义仅是简单的文本替换,内联却拥有函数的一切特性,例如类型转换、参数的按值传递。例如,同样以返回平方功能为例:
#define SQUARE(X) X * X
a = SQUARE(5.0); //OK
b = SQUARE(2.1 + 3.4); //false: b = 2.1 + 3.4 * 2.1 + 3.4
c = SQUARE(c++); //false: c = c++ * c++;
2. 关于引用变量
C++新增了一种复合类型——引用变量,引用是已定义变量的别名。引用变量的主要用途在于作为函数的形参。通过将引用变量用作参数,函数将使用原始数据,而不是其副本。这样除指针外,引用也为处理大型结构提供了一种方便途径。
- 引用的声明方法为:int & 变量名 = 某个变量;(必须在声明时给引用变量赋值,且此后该引用将不能再作为其它变量的引用了,他一生将忠于初心)
int rat = 100;
int & rat2 = rat; //rat2是rat的引用,至此之后它们二者指向相同的地址,可以相互交换。
int * const pr = &rat; //引用rat2扮演的其实就是*pr的角色。
- 引用的原理如下:
- 以交换两个参数的值为例来解释引用的具体用法及其与传值的区别:
#include <iostream>
void swapr(int & a, int & b);
void swapv(int a, int b);
int main()
{
using namespace std;
int wallet1 = 100;
int wallet2 = 200;
swapr(wallet1, wallet2); //函数调用使用实参初始化形参,即函数的引用参数被初始化为传递来的实参。
cout << "wallet1 = $" << wallet1;
cout << "wallet2 = $" << wallet2 << endl;
swapv(wallet1, wallet2);
cout << "wallet1 = $" << wallet1;
cout << " wallet2 = $" << wallet2 << endl;
}
void swapr(int & a, int & b) //
{
int temp;
temp = a; //a其实就是wallet1的别名,b就是wallet2的别名。
a = b;
b = temp;
}
void swapr(int a, int b)
{
int temp;
temp = a; //a其实就是与wallet1有相同值的另一个副本,a的值得改变不影响wallet1。
a = b;
b = temp;
}
--------------------程序输出结果----------------------------------
wallet1 = $200 wallet2 = $100 //交换成功
wallet1 = $200 wallet2 = $100 //交换失败
-
当函数参数为结构或类等比较大的数据的时候,引用将非常有用。
-
如果引用参数是const,当函数调用的参数不是左值(常量或某个表达式)或与相应的const引用参数类型不匹配,则C++将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数来引用该变量。如果引用参数不是const,那么这些情况会导致编译错误。
-
C++新增了另一种右值引用。它使用&&声明。它的主要目的是让库设计人员能够提供有些操作的更有效实现。
double && rref = std::sqrt(36.00);
double i = 15.0;
double && jref = 2.0 * j + 18.2;
2.1 关于引用结构体
- 使用结构引用参数的方式与使用基本变量引用相同,只需要在声明结构参数时使用引用运算符&即可。
struct free_throws
{
std::string name;
int made;
int attempts;
float percent;
};
void set_pc(free_throws & ft); //为结构体使用引用。
void set_pc(const free_throws & ft); //如果不想改变结构体内容,可以这样声明
- 函数的返回类型也可以设置为引用。一般来说return语句将后面的值先暂存到一个临时内存,然后再返回到调用函数,但如果函数返回的是结构体,则需要比较大的内存来存储它。因此,通过定义被调用函数的返回类型为引用的办法,来避免创建、复制大的结构体内容到一个临时变量,而是将原来的结构体内容直接返回。
- 需要注意的一点是要避免返回的引用本体为局部变量,因为它们在函数返回后消逝。所以要么返回引用指向的是作为参数传递给函数的引用。要么是用new分配的新的存储空间,但这又涉及到如何释放的问题。
2.2 关于引用类对象
- 当使用类型为const的引用的形参时,如果实参的类型与形参不匹配,但可以被转换为引用类型,程序将创建一个引用类型的临时变量并使用转换后的实参值来初始化它,然后传递一个指向该临时变量的引用。所以,当形参为
const string &
时,实参可以是字符串常量、char数组或指向char的指针。 - 任何情况下函数都不能返回一个指向局部变量的引用,因为局部变量在函数调用结束后就被释放了。
//错误示例:
const string & func(string & s1, const string & s2)
{
string temp;
temp = s2 + s1 + s1;
return temp; //因为函数返回类型为引用,所以当函数调用结束,其引用的主体也被释放了
}
- 因为ifstream和ofstream分别是istream和ostream类的继承类,而继承类有着基类所有属性和方法,所以在引用基类作为函数参数的时候,可以传递基类的继承类对象作为实参。例如以下函数原型:
void file_it (ostream & os);
既可以将cout传递给他,也可将一个ofstream对象(如fout)传递给他。
2.3 何时使用引用参数、按值传递、使用指针?
对于使用传递的值而不作修改的函数:
- 如果数据对象很大,则使用const指针或const引用。因为节省了复制整个数据对象所需的时间和空间,进而提高程序运行速度。
- 如果数据对象很小(基本数据类型或小型结构),建议使用按值传递;
- 如果数据对象是数组,则使用指针,并将指针声明为指向const的指针。
- 如果数据对象是类对象(string等),则使用const引用。因为传递类对象参数的标准方式就是按引用传递。
对于修改调用函数中数据的函数:
- 如果数据对象是内置数据类型,则使用指针。
- 如果数据对象是数组,则只能使用指针;
- 如果数据对象是结构,则使用指针或引用。
- 如果数据对象是类对象,则使用引用。
3. 关于默认参数
-
默认参数指的是当函数调用中省略了实参时自动使用的一个值。通过使用默认参数,在设计类时可以减少要定义的析构函数、方法及方法重载的数量。
-
默认参数的设置必须通过函数原型,因为编译器是通过查看函数原型了解函数使用的参数数目及类型的。例如,left()的原型如下:
char * left(const char * str, int n = 1);
上面的原型将n初始化为1,如果省略参数n,则它的值默认为1,否则为传递给它的值.
- 对于带参数列表的函数,必须从右向左添加默认值。即若要为某参数设置默认值,则必须为其右边的所有参数设置默认值。
4. 关于函数重载
C++允许定义名称相同的函数,条件是它们的参数列表各异。编译器将根据调用函数采取的不同被调用函数的参数列表来选择特定的多态函数。如果使用的参数列表类型不符合多态函数(被调用函数)任一类型的话,编译器将对个别参数尝试进行强制类型转换(但如果有多种转换方式使之满足多态函数的多个原型的话,编译器将拒绝调用任何一个并报错)。
仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应该使用函数重载。
注意:
- 由于引用的特殊性,变量及其引用被视为同一种参数类型。因为变量引用其实就是变量自己。
- 当参数使用const修饰时,可以不区分const和非const变量。因为将非const变量赋给const变量是合法的,但反之则非法。
- 如果函数参数列表相同,即使函数返回类型不同,也不构成重载。即函数是否重载决定于参数列表(特征标)。
编译器通过对不同特征标的重载函数名进行名称修饰(name decoration)进行区别。这种对参数数目和类型进行编码后用于对名称修饰的方法随着编译器的不同而各异。
5.关于函数模板
如果需要多个将同一种算法应用于不同类型的函数时可以使用函数模板。如果不考虑向后兼容的问题,请使用typename(typename在C++98之后才出现的,之前使用class)。
template <typename AnyType> //此处的AnyType可换为你自己喜欢的任何名称
void Swap (Anytype &a, Anytype &b)
{
Anytype temp;
temp = a;
a = b;
b = temp;
}
- 上述模板并不创建任何函数,而只是告诉编译器如何定义函数,需要交换两个int数时,编译器将按照模板模式创建这样的函数,并用int代替Anytype。同理,需要交换两个double数时,编译器将按照模板模式创建这样的函数,并用double代替Anytype。
- 一般将模板定义放在头文件中,并在需要使用模板的文件中包含头文件。
- 只需要在程序中直接调用带参数的模板函数,编译器会自动检查参数类型,并生成相应的函数。
- 函数模板并不能缩短可执行程序,最终的代码不包含任何模板,而只包含了为程序生成的实际函数。
5.1 模板函数的重载
需要对多个不同类型变量使用同一种算法时,可使用函数模板.但并非所有的类型都使用相同的算法,为解决该问题,可以对模板函数使用重载。
template <typename T> //早期版本使用class代替typename
void Swap (T &a, T &b)
{
T temp;
temp = a;
a = b;
b = temp;
}
template <typename T>
void Swap (T a[], T b[], int n)
{
T temp;
for (int i = 0; i<n; i++)
{
temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}
编译器若发现Swap()有两个参数时,使用第一个模板,若发现有两个数组加一个整数作为参数的Swap()调用时,将调用第二个模板。
5.2 模板函数重载的局限性
由于编写的模板可能无法处理某些特定类型,例如数组、结构体等。此时就需要提供具体化函数定义——显示具体化:
5.2.1 ISO/ANSI C++标准的“显示具体化函数模板”
-
对于函数,可以有非模板函数、模板函数、显示具体化模板函数以及它们的重载版本。
-
显示具体化的原型和定义应以
template <>
打头,并通过名称来指出类型。 -
具体化优先于常规模板,而非模板函数优先于具体化和常规模板。
-
下面以交换job结构函数为例,其非模板函数、模板函数、显示具体化模板函数的原型如下:
struct job { char name[40]; double salary; int floor; } //非模板函数原型 void Swap(job &, job &); //模板函数原型 template <typename T> //早期版本使用class代替typename void Swap(T &, T &); //显示具体化模板函数原型 template <> void Swap<job>(job &, job &); //<job>也可以省略,因为其参数列表已指明了参数类型为job
5.2.2 显示实例化函数模板
在定义了函数模板后,编译器只在代码调用了带参数的模板函数后才隐式实例化模板函数,除此之外,C++还允许显示的实例化模板函数,其语法是,声明所需的种类——用
<>
符号指示类型,并在声明前加上关键字template
。template void Swap<int>(int, int); //实例化一个两个int参数的Swap函数
在同一文件中使用同一种类型的显示实例和显示具体化将出错。
5.2.3 具体化
隐式实例化、显示实例化、显示具体化统称为具体化(specialization),它们相同之处在于表示的都是使用具体类型的函数定义,而非通用描述。在声明中使用前缀template
和template <>
分别区分显示实例化和显示具体化。
template <class T>
void Swap (T &, T &); //模板原型
template <> void Swap<job>(job &, job &); //显示具体化(job类型)
int main(void)
{
template void Swap<char>(char &, char &); //显示实例化(char类型)
short a,b;
...
Swap(a, b); //隐式实例化(short类型)
job m, n;
...
Swap(m, n); //显示具体化(job类型)
char g,h;
..
Swap(g, h); //显示实例化(char类型)
...
}
5.3 编译器如何选择使用哪个函数版本
对于函数重载、函数模板、函数模板重载,C++需要一个良好的策略来决定为函数调用哪个版本的函数定义,尤其是有多个参数时。此过程也称为重载解析。
- 创建候选函数列表。包含与被调用函数名称相同的函数和模板函数。
- 使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与形参类型完全匹配的情况。例如float参数的函数调用可以将该参数转换为double,从而与double形参匹配,而模板可以为float生成一个实例。
- 确定是否有最佳的可行函数。若有则用,否则报错。
参数转换以达到匹配时的优先级为:
- 完全匹配,但常规函数优先于模板函数;
- 提升转换(例如:char和short自动转换为int,float自动转换为double)
- 标准转换(例如:int转换为char,long转换为double)。
- 用户定义的转换(类声明中定义的转换)
- 以下情况均属于完全匹配
从实参 | 到形参 |
---|---|
Type | Type & |
Type & | Type |
Type [] | * Type |
Type (参数列表) | Type (*)(参数列表) |
Type | const Type |
Type | volatile Type |
Type * | const Type |
Type * | volatile Type * |
注意:在都属于完全匹配的情况下,有如下优先级
1. 即使两个函数都完全匹配,但指向非const的指针或引用仍然优先于指向const的指针或引用。但const与否的区别仅限于指针和引用,对于其它变量是没区别的。例如:
int a = 10;
...
recycle(a);
//在这种情况下,下面的原型都是完全匹配的
void recycle(int); //#1
void recycle(const int); //#2
void recycle(int &); //#3
void recycle(const int &); //#4
以上代码,如果只定义了#3和#4,则将选择#3;如果只定义了#1和#2,那就会因为二义性(ambiguous)而报错。
-
非模板函数优先于模板函数(包括显示具体化);
-
如果都是模板函数,则较具体的模板函数优先,即显示具体化优先于隐式实例化。
-
最具体优先。例如
template <class Type> void recycle (Type t); //#1 template <class Type> void recycle (Type * t); //#2 ... int a = 10; recycle(&a); //此时编译器选择#2,因为它比起#1更为具体,虽然#1也是完全匹配的。
-
用户自己选择。通过显示具体化来调用模板函数
#include <iostream>
template <class T>
T lesser(T a, T b) //#1
{
return a<b ? a : b;
}
int lesser(int a, int b) //#2
{
a = a<0 ? -a : a;
b = b<0 ? -b : b;
return a<b ? a : b;
}
int main()
{
using namespace std;
int m = 20;
int n = -30;
double x = 15.5;
double y = 25.9;
cout << lesser(m, n) << endl; //use #2
cout << lesser(x, y) << endl; //use #1 with double
cout << lesser<>(m, n) << endl; //use #1 with int
cout << lesser<int>(x, y) << endl; //use #1 with int
return 0;
}
程序输出:
20
15.5
-30
15
5.4 关键字decltype
先考虑如下模板函数定义:
template <class T1, class T2>
void ft(T1 x, T2 y)
{
...
?type? xpy =x + y; //xpy应该是什么类型?
...
}
在上述代码中,xpy有可能是T1类型,也有可能是T2类型。但我们该如何表示呢?为此,先来看下关键字——decltype
。其语法如下(声明一个变量var,其类型为expression表达式的类型):
decltype(expression) var;
- 当expression为单个变量时(不是用括号括起来的表达式),var的类型与该变量相同,包括const等限定符。
- 当expression为函数调用时(并不会实际调用函数,而是查看函数原型来获知函数返回类型),则var的类型与函数返回类型相同;
- 当expression为一个左值(为了和第一步区分开来,expression必须是用括号括起来的变量)时,则var为指向其类型的引用。
double xx = 4.4;
decltype((xx)) r1 = xx; //r1 is double &
decltype(xx) r2 = xx; //r2 is double
- 若前面的条件都不满足,则var的类型与expression类型相同。
int i = 3;
int &k = i;
int &n = i;
decltype(100L) i1; //i1 type long
decltype(k+n) i2; //i2 type int
decltype(k) 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];
...
}
5.5 利用关键字auto,使用后置返回类型
先看一个问题:
template <class T1, class T2>
?type? ft(T1 x, T2 y) //函数返回类型是什么?
{
...
return x + y;
}
貌似可以将函数返回类型定为decltype(x+y),但是函数还未开始运行时还未声明参数x和y,它们不在作用域内。必须在声明参数后使用decltype。
此时可以先用关键字auto
占个位,然后使用->
来定义一个后置返回类型。所以以上代码可以写为:
template <class T1, class T2>
auto ft(T1 x, T2 y) -> decltype(x+y)
{
...
return x + y;
}
现在,decltype在参数声明后面,因此x和y位于左右域内,可以使用它们。