七. 函数探幽
7.1 C++内联函数
要使用这项特性,必须采取下述措施之一:
· 在函数声明前加上关键字inline;
· 在函数定义前加上关键字inline;
通常的做法是省略原型,将整个定义(即函数头和所有函数代码)放在本应提供函数原型的地方。另外,内联函数不能递归。
inline.cpp
#include <iostream>
inline double square(double x) { return x * x; }
int main() {
using namespace std;
double a, b;
double c = 13.0;
a = square(5.0);
b = square(4.5 + 7.5);
cout << "a = " << a << ", b = " << b << "\n";
cout << "c = " << c;
cout << ", c squared = " << square(c++) << "\n";
cout << "Now c = " << c << "\n";
return 0;
}
7.2 引用变量
引用是已定义变量的别名,主要用途是用作函数的形参,通过将引用变量用作参数,函数将使用原始数据,而不是其副本。
7.2.1 创建引用变量
C++用&来声明引用:
int rats;
int & rodents = rats;
firstref.cpp
#include <iostream>
int main() {
using namespace std;
int rats = 101;
int& rodents = rats;
cout << "rats = " << rats;
cout << ", rodents = " << rodents << endl;
rodents++;
cout << "rats = " << rats;
cout << ", rodents = " << rodents << endl;
cout << "rats address = " << &rats;
cout << ", rodents address = " << &rodents << endl;
return 0;
}
声明引用时必须将其初始化。
scref.cpp
#include <iostream>
int main() {
using namespace std;
int rats = 101;
int& rodents = rats;
cout << "rats = " << rats;
cout << ", rodents = " << rodents << endl;
cout << "rats address = " << &rats;
cout << ", rodents address = " << &rodents << endl;
int bunnies = 50;
rodents = bunnies;
cout << "bunnies = " << bunnies;
cout << ", rats = " << rats;
cout << ", rodents = " << rodents << endl;
cout << "bunnies address = " << &bunnies;
cout << ", rodents address = " << &rodents << endl;
return 0;
}
可以通过初始化声明来设置引用,但不能通过赋值来设置。
7.2.2 将引用用作函数参数
引用经常被用作函数参数,使得函数中的变量名成为调用程序中的变量的别名。这种传递参数的方法成为按引用传递。按引用传递允许被调用的函数能够访问调用函数中的变量。
swapd.cpp
#include <iostream>
void swapr(int& a, int& b);
void swapp(int* p, int* q);
void swapv(int a, int b);
int main() {
using namespace std;
int wallets1 = 300;
int wallets2 = 350;
cout << "wallets1 = $" << wallets1;
cout << " wallets2 = $" << wallets2 << endl;
cout << "Using references to swap contents:\n";
swapr(wallets1, wallets2);
cout << "wallets1 = $" << wallets1;
cout << " wallets2 = $" << wallets2 << endl;
cout << "Using pointers to swap contents again:\n";
swapp(&wallets1, &wallets2);
cout << "wallets1 = $" << wallets1;
cout << " wallets2 = $" << wallets2 << endl;
cout << "Trying to use passing by value:\n";
swapv(wallets1, wallets2);
cout << "wallets1 = $" << wallets1;
cout << " wallets2 = $" << wallets2 << endl;
return 0;
}
void swapr(int& a, int& b) {
int temp;
temp = a;
a = b;
b = temp;
}
void swapp(int* p, int* q) {
int temp;
temp = *p;
*p = *q;
*q = temp;
}
void swapv(int a, int b) {
int temp;
temp = a;
a = b;
b = temp;
}
7.2.3 引用的属性和特别之处
cubes.cpp
#include <iostream>
double cube(double a);
double refcube(double& ra);
int main() {
using namespace std;
double x = 3.0;
cout << cube(x);
cout << " = cube of " << x << endl;
cout << refcube(x);
cout << " = cube of " << x << endl;
return 0;
}
double cube(double a) {
a *= a * a;
return a;
}
double refcube(double& ra) {
ra *= ra * ra;
return ra;
}
临时变量、引用参数和const
如果实参与引用参数不匹配,C++将生成临时变量。仅当参数为const引用时,C++将生成临时变量,有两种情况:
· 实参的类型正确,但不是左值;
· 实参的类型不正确,但可以转换为正确的类型。
左值参数是可被引用的数据对象,例如:变量、数组元素、结构成员、引用和解除引用的指针都是左值。非左值包括字面常量(用引号引括起的字符串除外,它们由其地址表示和包含多项的表达式。在C语言中,左值最初指的是可出现在赋值语句左边的实体,但这是引入关键字const之前的情况。现在,常规变量都可视为左值,因为可通过地址访问它们。但常规变量属于可修改的左值,而const变量属于不可修改的左值。
如果函数调用的参数不是左值或与相应的const引用参数的类型不匹配,则C++将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数来引用该变量。
应尽可能使用const,因为:
· 使用const可以避免无意中修改数据的编程错误;
· 使用const使函数能够处理const和非const实参,否则只能接受非const数据;
· 使用const引用使函数能够正确生成并使用临时变量。
因此,应尽可能将引用形参声明为const。
7.2.4 将引用用于结构
引用非常适合用于结构和类,引用主要是为了用于这些类型,而不是基本的内置类型。使用结构引用参数的方式与使用基本变量引用相同,只需在声明结构参数时使用引用运算符&即可。
strtref.cpp
#include <iostream>
#include <string>
struct free_throws {
std::string name;
int made;
int attemps;
float percent;
};
void display(const free_throws& ft);
void set_pc(free_throws& ft);
free_throws& accumulate(free_throws& target, const free_throws& source);
int main() {
free_throws one = { "Ifelsa Branch", 13, 14 };
free_throws two = { "Andor Knott", 10, 16 };
free_throws three = { "Minnie Max", 7, 9 };
free_throws four = { "Whily Looper", 5, 9 };
free_throws five = { "Long Long", 6, 14 };
free_throws team = { "Throwgoods", 0, 0 };
free_throws dup;
set_pc(one);
display(one);
accumulate(team, one);
display(team);
display(accumulate(team, two));
accumulate(accumulate(team, three), four);
display(team);
dup = accumulate(team, five);
std::cout << "Displaying team:\n";
display(team);
std::cout << "Displaying dup after assignment:\n";
display(dup);
set_pc(four);
accumulate(dup, five) = four;
std::cout << "Displaying dup after ill-advised assignment:\n";
display(dup);
return 0;
}
void display(const free_throws& ft) {
using std::cout;
cout << "Name: " << ft.name << '\n';
cout << " Made: " << ft.made << '\t';
cout << "Attempts: " << ft.attemps << '\t';
cout << "Percent: " << ft.percent << '\n';
}
void set_pc(free_throws& ft) {
if (ft.attemps != 0)
ft.percent = 100.0f * float(ft.made) / float(ft.attemps);
else
ft.percent = 0;
}
free_throws& accumulate(free_throws& target, const free_throws& source) {
target.attemps += source.attemps;
target.made += source.made;
set_pc(target);
return target;
}
7.2.5 将引用用于类对象
strquote.cpp
#include <iostream>
#include <string>
using namespace std;
string version1(const string& s1, const string& s2);
const string& version2(string& sl, const string& s2);// has side effect
const string& version3(string& sl, const string& s2); // bad design
int main() {
string input;
string copy;
string result;
cout << "Enter a string:";
getline(cin, input);
copy = input;
cout << "Your string as entered:" << input << endl;
result = version1(input, "***");
cout << "Your string enhanced: " << result << endl;
cout << "Your original string: " << input << endl;
result = version2(input, "###");
cout << "Your string enhanced: " << result << endl;
cout << "Your original string: " << input << endl;
cout << "Reseting original string.\n";
input = copy;
result = version3(input, "@@@");
cout << "Your string enhanced: " << result << endl;
cout << "Your original string: " << input << endl;
return 0;
}
string version1(const string& s1, const string& s2) {
string temp;
temp = s2 + s1 + s2;
return temp;
}
const string& version2(string& s1, const string& s2) {
s1 = s2 + s1 + s2;
return s1;
}
const string& version3(string& s1, const string& s2) {
string temp;
temp = s2 + s1 + s2;
return temp;
}
7.2.6 对象、继承和引用
ostream和ofstream类凸显了引用的一个有趣属性,正如fstream 对象可以使用ostream类的方法,这使得文件输入/输出的格式与控制台输入/输出相同。使得能够将特性从一个类传递给另一个
类的语言特性被称为继承。简单地说,ostream 是基类(因为ofstream 是建立在它的基础之上的),而ofstream 是派生类(因为它是从ostream 派生而来的)。派生类继承了基类的方法,这意味着ofstream对象可以使用基类的特性,如格式化方法precision()和setf()。
继承的另一个特征是,基类引用可以指向派生类对象,而无需进行强制类型转换。这种特征的一个实际结果是,可以定义一个接受基类引用作为参数的函数,调用该函数时,可以将基类对象作为参数,也可以将派生类对象作为参数。例如,参数类型为 ostream&的函数可以接受ostream对象(如cout)或声明的ofstream对象作为参数。
filefunc.cpp
#include <iostream>
#include <fstream>
#include <cstdlib>
using namespace std;
void file_it(ostream& os, double fo, const double fe[], int n);
const int LIMIT = 5;
int main() {
ofstream fout;
const char* fn = "ep-data.txt";
fout.open(fn);
if (!fout.is_open()) {
cout << "Can't open" << fn << ". Bye.\n";
exit(EXIT_FAILURE);
}
double objective;
cout << "Enter the focal length of your "
"telephone objective in mm: ";
cin >> objective;
double eps[LIMIT];
cout << "Enter the focal lengths, in mms, of " << LIMIT
<< " eyepieces:\n";
for (int i = 0; i < LIMIT; i++) {
cout << "Eyepiece #" << i + 1 << ": ";
cin >> eps[i];
}
file_it(fout, objective, eps, LIMIT);
file_it(cout, objective, eps, LIMIT);
cout << "Done\n";
return 0;
}
void file_it(ostream& os, double fo, const double fe[], int n) {
ios_base::fmtflags initial;
initial = os.setf(ios_base::fixed);
os.precision(0);
os << "Focal length of objective: " << fo << " mm\n";
os.setf(ios::showpoint);
os.precision(1);
os.width(12);
os << "f.1.eyepiece";
os.width(15);
os << "magnification" << endl;
for (int i = 0; i < n; i++) {
os.width(12);
os << fe[i];
os.width(15);
os << int(fo / fe[i] + 0.5) << endl;
}
os.setf(initial);
}
7.2.7 何时使用引用参数
使用引用参数的主要原因有两个:
· 修改调用函数中的数据对象;
· 通过传递引用而不是整个数据对象,可以提高程序的运行速度。
下面是一些使用引用、指针和按值传递的指导原则:
1. 对于使用传递的值而不做修改的函数
· 如果数据对象很小,如内置数据类型或小型结构,则按值传递;
· 如果数据对象是数组,则使用指针(唯一选择),并将指针声明为指向const的指针;
· 如果数据对象是较大的结构,则使用指针或const引用,以提高程序的效率,这样可以节省复制结构所需的时间和空间;
· 如果数据对象是类对象,则使用const引用。类设计的语义常常要求使用引用,这是C++新增这项特性的主要原因。因此,传递类对象参数的标准方式是按引用传递。
·2. 对于修改调用函数中数据的函数
· 如果数据对象是内置数据类型,则使用指针。如果看到诸如fixit(&x)这样的代码(其中x
是int),则很明显,该函数将修改x;
· 如果数据对象是数组,则只能使用指针;
· 如果数据对象是结构,则使用引用或指针;
· 如果数据对象是类对象,则使用引用。
7.3 默认参数
下面介绍 C++的另一项新内容一默认参数。默认参数指的是当函数调用中省略了实参时自动使用的一个值。例如,如果将void wow (int n)设置成n有默认值为1,则函数调用 wow()相当于 wow (1)。这极大地提高了使用函数的灵活性。假设有一个名为 left()的函数,它将字符串和作为参数,并返回该字符串的前n个字符。更准确地说,该函数返回一个指针,该指针指向由原始字符串中被选中的部分组成的字符串。例如,函数调用left(“theory”,3)将创建新字符串“the”,并返回一个指向该字符串的指针。现在假设第二个参数的默认值被设置为1,则函数调用 left(“theory”, 3)仍像前面讲述的那样工作,3将覆盖默认值但函数调用 left(“heory”)不会出错,它认为第二个参数的值为 1,并返回指向字符串“t”的指针。如果程序经常需要抽取一个字符组成的字符串,而偶尔需要抽取较长的字符串,则这种默认值将很有帮助。
如何设置默认值呢?必须通过函数原型。由于编译器通过查看原型来了解函数所使用的参数数目,因此函数原型也必须将可能的默认参数告知程序。方法是将值赋给原型中的参数。例如,left()的原型如下:
char* left(const char* str, int n = 1);
对于带参数列表的函数,必须从右向左添加默认值,即要为某个参数设置默认值,则必须为他右边所有的参数提供默认值。只有原型指定了默认值,函数定义与没有默认参数时完全相同。
实参按从左到右的顺序依次赋给相应的形参,而不能跳过任何参数。
如果在 C++ 中调用函数时提供的实参类型与第一个形参的类型不匹配,编译器将尝试进行类型转换。如果能成功地将实参类型隐式转换为形参类型,程序将正常编译和运行。如果无法进行这种隐式转换,编译器将报错。
left.cpp
#include <iostream>
const int Arsize = 80;
char* left(const char* str, int n = 1);
int main() {
using namespace std;
char sample[Arsize];
cout << "Enter a string:\n";
cin.get(sample, Arsize);
char* ps = left(sample, 4);
cout << ps << endl;
delete[] ps;
ps = left(sample);
cout << ps << endl;
delete[] ps;
return 0;
}
char* left(const char* str, int n) {
if (n < 0)
n = 0;
char* p = new char[n + 1];
int i;
for (i = 0; i < n && str[i]; i++)
p[i] = str[i];
while (i <= n)
p[i++] = '\0';
return p;
}
7.4 函数重载
函数多态是C++在C语言的基础上新增的功能。默认参数能够使用不同数目的参数调用同一个函数,而函数多态(函数重载)能够使用多个同名的函数。术语“多态”指的是有多种形式,因此函数多态允许函数可以有多种形式。函数重载指的是可以有多个重名的函数,因此对函数进行了重载。这两个术语指的是同一回事,但我们通常使用函数重载。可以通过函数重载来设计一系列函数——它们完成相同的工作,但使用不同的参数列表。
函数重载的关键是函数的参数列表一一也称为函数特征标 (function signature)。如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量名是无关紧要的。C++允许定义名称相同的函数,条件是它们的特征标不同。如果参数数目和/或参数类型不同,则特征标也不同。
使用被重载的函数时,需要在函数调用中使用正确的参数类型。另外,函数在检查函数特征标时,将把类型引用和类型本身视为同一个特征标。匹配函数时,并不区分const和非const变量,编译器将根据实参是否为const来决定使用哪个原型。
7.4.1 重载示例
leftover.cpp
#include <iostream>
unsigned long left(unsigned long num, unsigned ct);
char* left(const char* str, int n = 1);
int main() {
using namespace std;
const char* trip = "Hawaii!!";
unsigned long n = 12345678;
int i;
char* temp;
for (i = 1; i < 10; i++) {
cout << left(n, i) << endl;
temp = left(trip, i);
cout << temp << endl;
delete[] temp;
}
return 0;
}
unsigned long left(unsigned long num, unsigned ct) {
unsigned digits = 1;
unsigned long n = num;
if (ct == 0 || num == 0)
return 0;
while (n /= 10)
digits++;
if (digits > ct) {
ct = digits - ct;
while (ct--)
num /= 10;
return num;
}
else
return num;
}
char* left(const char* str, int n) {
if (n < 0)
n = 0;
char* p = new char[n + 1];
int i;
for (i = 0; i < n && str[i]; i++)
p[i] = str[i];
while (i <= n)
p[i++] = '\0';
return p;
}
7.4.2 何时使用函数重载
仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应采用函数重载。
7.5 函数模板
如果需要多个将同一种算法用于不同类型的函数,应使用模板。如果不考虑向后兼容的问题,并愿意键入较长的单词示,则声明类型参数时,应使用关键字typename而不使用class。
funtemp.cpp
#include <iostream>
template <typename T> //or class T
void Swap(T& a, T& b);
int main() {
using namespace std;
int i = 10;
int j = 20;
cout << "i, j = " << i << ", " << j << ".\n";
cout << "Using compiler_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 compiler_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.5.1 重载的模板
twotemps.cpp
#include <iostream>
template <typename T> //or class T
void Swap(T& a, T& b);
template <typename T>
void Swap(T* a, T* b, int n);
void Show(int a[]);
const int Lim = 8;
int main() {
using namespace std;
int i = 10;
int j = 20;
cout << "i, j = " << i << ", " << j << ".\n";
cout << "Using compiler_generated int swapper:\n";
swap(i, j);
cout << "Now i, j = " << i << ", " << j << ".\n";
int d1[Lim] = { 0,7,0,4,1,7,7,6 };
int d2[Lim] = { 0,7,2,0,1,9,6,9 };
cout << "Original arrays:\n";
Show(d1);
Show(d2);
Swap(d1, d2, Lim);
cout << "Swapped arrays:\n";
Show(d1);
Show(d2);
return 0;
}
//函数模板定义
template <typename T>
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;
}
}
void Show(int a[]) {
using namespace std;
cout << a[0] << a[1] << "/";
cout << a[2] << a[3] << "/";
for (int i = 4; i < Lim; i++) {
cout << a[i];
}
cout << endl;
}
7.5 2 模板的局限性
编写的模板函数可能无法处理某些类型,例如:数组无法执行赋值操作,结构无法进行比较操作。
7.5.3 显式具体化
1. 第三代具体化(ISO/ANSI C++标准)
· 对于给定的函数名,可以有非模板函数、模板函数和显式具体化模板函数以及它们的重载版本;
· 显式具体化的原型和定义应以teplate<>打头,并通过名称来指出类型;
· 具体化优先于常规模板,而非模板函数优先于具体化和常规模板。
下面是用于交换job结构的非模板函数、模板函数和具体化的原型:
//非模板函数原型
void Swap(job& , job&);
//模板函数原型
template <typename T>
void Swap(T&, T&);
//具体化原型
template <> void Swap<job> (job&, job&);
2. 显式具体化示例
twoswap.cpp
#include <iostream>
template <typename T> //or class T
void Swap(T& a, T& b);
struct job {
char name[40];
double salary;
int floor;
};
template <> void Swap<job>(job& j1, job& j2);
void Show(job& j);
int main() {
using namespace std;
cout.precision(2);
cout.setf(ios::fixed, ios::floatfield);
int i = 10, j = 20;
cout << "i, j = " << i << ", " << j << ".\n";
cout << "Using compiler_generated int swapper:\n";
Swap(i, j);
cout << "Now i, j = " << i << ", " << j << ".\n";
job sue = { "Susan Yaffee", 73000.60,7 };
job sidney = { "Sidney Taffee", 78060.72, 9 };
cout << "Before job swapping:\n";
Show(sue);
Show(sidney);
Swap(sue, sidney);
cout << "After job swapping:\n";
Show(sue);
Show(sidney);
return 0;
}
//函数模板定义
template <typename T>
void Swap(T& a, T& b) {
T temp;
temp = a;
a = b;
b = temp;
}
template <>void Swap<job>(job& j1, job& j2) {
double t1;
int t2;
t1 = j1.salary;
j1.salary = j2.salary;
j2.salary = t1;
t2 = j1.floor;
j1.floor = j2.floor;
j2.floor = t2;
}
void Show(job& j) {
using namespace std;
cout << j.name << ": $" << j.salary
<< " on floor " << j.floor << endl;
}
7.5.4 实例化和具体化
在代码中包含函数模板本身并不会生成函数定义,他只是一个用于生成函数定义的方案,编译器使用模板为特定类型生成函数定义时,得到的时模板实例。
最初,编译器只能通过隐式实例化,来使用模板生成函数定义,现在C++允许显式实例化:
template void Swap<int>(int, int);
意思是使用Swap()模板生成int类型的函数定义。
与显式实例化不同的是,显式具体化使用下面两个等价的声明之一:
template <> void Swap<int>(int&, int&);
template <> void Swap(int&, int&);
区别在于,具体化声明的意思是不要使用Swap模板生成一个使用int类型的实例,而应使用专们为int类型显式定义的函数定义。这些原型必须有自己的函数定义。显式具体化声明在关键字template后包含<>,而显式实例化没有。应注意,试图在同一个文件(或转换单元)中使用用一种类型的显式实例和显式具体化将出错。
隐式实例化、显式实例化和显式具体化统称为具体化。它们的相同之处在于,它们表示的都是使用具体类型的函数定义,而不是通用描述。
template <class T>
void Swap(T&, T&); //模板原型
template <> void Swap<job>(job&, job&); //显式具体化
int main() {
template void Swap<char>(char&, char&); //显式实例化
short a, b;
...
Swap(a, b); //模板的隐式实例化
job n, m;
...
Swap(n, m); //调用显式具体化
char g, h;
...
Swap(g, h); //调用显式实例化
return 0;
}
编译器看到char的显式实例化后,将使用模板定义来生成Swap()的char版本。对于其他Swap()调用,编译器根据函数调用中实际使用的参数,生成相应的版本。例如,当编译器看到函数调用Swap(a, b)后,将生成Swap()的short版本,因为两个参数的类型都是short。当编译器看到Swap(n, m)后,将使用为job类型提供的独立定义(显式具体化)。当编译器看到Swap(g, h)后,将使用处理显式实例化时生成的模板具体化。
7.5.5 编译器选择使用哪个函数版本
对于函数重载、函数模板和函数模板重载,C++需要一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时,这个过程称为重载解析。
第1步: 创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。
第2步: 使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。例如,使用 loat 参数的函数调用可以将该参数转换为 double,从而与 double 形参匹配,而模板可以为float 生成一个实例
第3步: 确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。
考虑只有一个函数参数的情况,如下面的调用:
may('B');
首先,编译器将寻找候选者,即名称为 may()的函数和函数模板。然后寻找那些可以用一个参数调用的函数。例如,下面的函数符合要求,因为其名称与被调用的函数相同,且可只给它们传递一个参数:
void may(int); //#1
float may(float, float = 3); //#2
void may(char); //#3
char* may(const char*); //#4
char may(const char&); //#5
template<class T> void may(const T&); //#6
template<class T> void may(T*); //#7
注意,只考虑特征标,而不考虑返回类型。其中的两个候选函数(#4和#7)不可行,因为整数类型不能被隐式地转换(即没有显式强制类型转换)为指针类型,剩余的一个模板可用来生成具体化,其中T被替换为 char 类型。这样剩下5个可行的函数,其中的每一个函数,如果它是声明的唯一一个函数,都可以被使用。
接下来,编译器必须确定哪个可行函数是最佳的。它查看为使函数调用参数与可行的候选函数的参数匹配所需要进行的转换。通常,从最佳到最差的顺序如下所述:
1.完全匹配,但常规函数优先于模板。
2.提升转换(例如,char和shorts自动转换为int,float 自动转换为double)。
3.标准转换(例如,int 转换为char,long转换为 double)。
4.用户定义的转换,如类声明中定义的转换。
例如,函数#1优于函数#2,因为 char 到 int 的转换是提升转换,而char 到 float 的转换是标准转换。函数#3、函数#5和函数#6 都优于函数#1和2因为它们都是完全配的。#3和#5优于#6,因为6 函数是模板。这种分析引出了两个问题。什么是完全匹配?如果两个函数(如#3和#5)都完全匹配,将如何办呢?通常,有两个函数完全匹配是一种错误,但这一规则有两个例外。需要对这一点做更深入的探讨。
1. 完全匹配和最佳匹配
进行完全匹配时,C++允许某些“无关紧要的转换”。下表列出了这些转换——Type 表示任意类型,例如,int 实参与int &形参完全匹配。注意,Type 可以是 char &这样的类型,因此这些规则包括从 char &到const char &的转换。Type (argument-list)意味着用作实参的函数名与用作形参的函数指针只要返回类型和参数列表相同,就是匹配的(之前介绍了函数指针以及为何可以将函数名作为参数传递给接受函数指针的函数)。在后面的章节将介绍关键字volatile。
如果有多个匹配的原型,则编译器将无法完成重载解析的过程。有两种情况例外,首先,指向非const数据的指针和引用优先与非const指针和引用参数匹配,这种区别只适用于指针和引用指向的数据;另一种情况是,其中一个是非模板函数,而另一个不是,这种情况下,非模板函数将优先于模板函数(包括显示具体化),如果两个完全匹配的函数都是模板函数,则较具体的模板函数优先,例如:显式具体化将由于模板隐式生成的具体化。
术语“最具体(most specialized)”并不一定意味着显式具体化,而是指编译器推断使用哪种类型时执行的抓换最少。
2. 部分排序规则示例
temptempover.cpp
#include <iostream>
template <typename T>
void ShowArray(T arr[], int n);
template <typename T>
void ShowArray(T* arr[], int n);
struct debts {
char name[50];
double amount;
};
int main() {
using namespace std;
int things[6] = { 13,31,103,301,310,130 };
struct debts mr_E[3] = {
{"Ima Wolfe", 2400.0},
{"Ura Foxe", 1300.0},
{"Iby Stout", 1800.0}
};
double* pd[3];
for (int i = 0; i < 3; i++)
pd[i] = &mr_E[i].amount;
cout << "Listing Mr.E's counts of things:\n";
ShowArray(things, 6);
cout << "Listing Mr.E's debts:\n";
ShowArray(pd, 3);
return 0;
}
template <typename T>
void ShowArray(T arr[], int n) {
using namespace std;
cout << "template A\n";
for (int i = 0; i < n; i++)
cout << arr[i] << ' ';
cout << endl;
}
template <typename T>
void ShowArray(T* arr[], int n) {
using namespace std;
cout << "template B\n";
for (int i = 0; i < n; i++)
cout << *arr[i] << ' ';
cout << endl;
}
简而言之,重载解析将寻找最匹配的函数。如果只存在一个这样的函数,则选择它;如果存在多个这样的函数,但其中只有一个是非模板函数,则选择该函数:如果存在多个适合的函数,且它们都为模板函数,但其中有一个函数比其他函数更具体,则选择该函数。如果在多个同样合适的非模板函数或模板函数,但没有一个函数比其他函数更具体,则函数调用将是不确定的,因此是错误的;当然,如果不存在匹配的函数,则也是错误。
3. 自己选择
choices.cpp
#include <iostream>
template <typename T>
T lesser(T a, T b)
{
return a < b ? a : b;
}
int lesser(int a, int b) {
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;
cout << lesser(x, y) << endl;
cout << lesser<>(m, n) << endl;
cout << lesser<int>(x, y) << endl;
return 0;
}
7.6 总结
C++扩展了C语言的函数功能,通过将inline关键字用于函数定义,并在首次调用该函数前提供其函数定义,可以使得C++编译器将该函数视为内联函数。也就是说,编译器不是让程序跳到独立的代码段以执行函数,而是用相应的代码替换函数调用。只有在函数很短时才能采用内联函数。
引用变量是一种伪装指针,它允许为变量创建别名。引用变量主要被用作处理结构和类对象的函数的参数。通常,被声明为特定类型引用的标识符只能指向这种类型的数据;然而,如果一个类(如ofstream)是从另一个类(ostream)派生出来的,则基类引用可以指向派生类对象。
C++原型能够定义参数的默认值。如果函数调用省略了相应的参数,则程序将使用默认值;如果函数调用提供了参数值,则程序将使用这个值(而不是默认值)。只能在参数列表中从右向左提供默认参数。因此,如果为某个参数提供了默认值,则必须为该参数右边所有的参数提供默认值。
函数的特征标是其参数列表。程序员可以定义两个同名函数,只要其特征标不同。这被称为函数多态或函数重载。通常,通过重载函数来为不同的数据类型提供相同的服务。
函数模板自动完成重载函数的过程。只需使用泛型和具体算法来定义函数,编译器将为程序中使用的特定参数类型生成正确的函数定义。