Chapter 8函数探幽
8.1 C++内联函数
内联函数是在主函数中创建了一个函数副本,节约了函数调用时地址跳转回跳过程的时间,代价是占用的内存更多。
要使用内联函数,必须采用以下措施之一:
- 在函数声明前加关键字inline;
- 在函数定义前加关键字inline。
通常做法是省略原型,将整个定义放在本应提供原型的地方。
内联与宏
#define
可以定义宏。
计算平方的宏:
#define SQUARE(X) X*X
该宏只能计算SQURE(5.0),不能计算SQURE(7.5 + 4.5),该宏可以通过加()计算上述值。
#define SQUARE(X) (X)*(X)
,但是仍然不能计算SQURE(c++)
8.2 引用变量
引用是已定义变量的别名。例如将twain作为clement变量的引用,可以交替使用twain和clement来表示表示该变量。
8.2.1 创建引用变量
&有另一个含义,声明引用,样例如下:
int rats;
int & rodents = rats;
引用与指针类似,但有很多不同之处:
- 必须在声明引用时将其初始化,而指针可以不赋值
- 引用声明后不能修改对象
可以通过初始化声明引用,赋值只会修改引用变量的地址的值。
8.2.2 将引用用作函数参数
引用经常被用作函数参数,使得函数中的变量名成为调用程序中的变量的别名。这种传递参数的方法成为引用传递。按引用传递允许被调用的函数能够方位调用函数中的变量。样例如下:
void swapr(int &x, int &y);
8.2.3 引用的属性和特别之处
希望利用引用传递信息,而不修改引用的值,应使用const限定符。
传递引用的限制比值传递更严格,不能将表达式赋给传递引用。
如果使用引用的目的只是传递值,而不是修改他们,最好声明为const变量,这会使函数可处理的参数种类更多。
将引用参数声明为常量数据的理由:
- 使用const可以避免勿以修改数据的编程错误;
- 能够接受const和非const的实参;
- 使用const能正确生成并使用临时变量
8.2.4 引用用于结构
引用非常适合用于结构和类。
- 返回引用的原因:
返回值时,值先被复制到一个临时位置,在拷贝给dup,而引用返回是直接复制。 - 返回引用时需要注意问题
返回引用时应避免返回临时变量的指针;应该使用new创建分配一个内存空间存储指针。 - 将const用于引用返回类型
可以进制给返回赋值,避免模糊。
8.2.5 将引用用于类对象
将类对象传递给函数时,C++通常的做法是使用引用。
8.2.6 对象、继承和引用
将特性从一个类传递给另一个类的语言特性成为继承。继承的特征:
- 派生类继承了基类的方法;
- 基类引用可以指向派生类对象,而无需进行强制转换。
参数os(类型为ostream &)可以指向ostream对象,也可以指向ofstream对象如fout。
setf()可以设置各种格式化的状态,setf(ios_base::fixed)将对象置于使用定点表示法的模式;setf(ios_base::showpoint)将对象置于显示小数点的模式,即使小数部分为0.
方法precision()指定显示多少位小数(假设对象处于定点模式下)。所有的设置都将一直保持不便,直到再次调用相应方法重新设置他们。
方法width()设置下一次输出操作使用的字段宽度,这种设置只在显示下一个值时有效,然后将恢复到默认设置。默认的字段宽度为零。
8.2.7 何时使用引用参数
使用引用参数的主要原因:
- 程序员需要修改调用函数中的数据对象
- 通过传递引用,可以提高程序的运行速度
选择指导: - 类对象使用引用;
- 数组使用指针;
- 基本类型不需修改用值
8.3 默认参数
带参数列表的函数,必须从右向左添加默认值;
实参按从左向右的顺序依次被赋给相应的形参,而不能跳过任何参数。
默认参数在函数声明式添加,函数定义式不能添加。
8.4 函数重载
函数多态是你能够使用多个同名的函数。术语”多态“指的是有多种形式,因此函数多态允许函数有多种形式。类似,术语函数重载指的是有多个同名的函数,因此对名称进行了重载。我们通常使用函数重载,可以通过函数重载来设计一系列函数——完成相同的工作,但使用不同的参数列表。
重载函数就像是有多种含义的动词,可以根据上下文知道每一种情况下,root的含义是什么。
函数重载的关键是函数的参数列表——函数特征标
参数的数目或参数的类型不同,特征标就不同;
将类型引用和类型本身视为同一个特征标。
是特征标而不是函数类型使得可以对函数进行重载。
重载引用参数
重载可以同时使用三个函数的参数,将调用最匹配的版本。
8.4.1 重载示例
// leftover8_10.cpp -- overloading the left() function
#include<iostream>
#include<cstring>
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 = 0; 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)
{
int digits = 1;
unsigned long result;
result = num;
while(num /= 10)
digits++;
if(digits > ct)
{
ct = digits - ct;
while(ct--)
result /= 10;
}
return result;
}
char * left(const char * str, int n)
{
char * ts;
if(n < 0)
return ts; //n=0;
int i;
n = (n < strlen(str)) ? n : strlen(str); //
ts = new char[n + 1];
for(i = 0; i < n && str[i]; i++)
{
ts[i] = str[i];
}
ts[n] = '\0';
return ts;
}
8.4.2 何时使用函数重载
当函数基本上执行相同的任务,但使用不同形式的数据时才采用函数重载。
用默认参数可以实现同样的目的时,尽量采用默认参数。
名称修饰
名称修饰或者矫正添加的符号随函数特征标而异,因此可以实现函数重载。
8.5 函数模板
函数模板是通用的函数描述。
使用泛型来定义函数,其中的泛型可以用具体的类型替换。
将类型作为参数传递给模板,可以使编译器生成该类型的函数。
模板允许以泛型的方式编写程序,有时被称为通用编程。
注:函数模板不能缩短可执行程序,最终代码不包含任何模板。
使用模板的好处是它使生成多个函数定义更简单、更可靠。
8.5.1 重载的模板
交换数组时,使用上面的模板就不可以,这就涉及模板的重载。
样例:
// twotemps8_12.cpp -- using overload template funtcion
#include<iostream>
template<typename T>
void Swap(T & a, T & b);
template<typename T>
void Swap(T [], T [], int n);
template<typename T>
void Show(const T a[]);
const int Lim = 8;
int main()
{
using namespace std;
int i = 10, j = 20;
cout << "i, j = " << i << ", " << j << ".\n";
cout << "Using compiler-genterated 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;
}
}
template<typename T>
void Show(const T 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;
}
8.5.2 模板的局限性
编写的模板函数不可能处理所有的类型。
例如没有为结构定义+运算符,有两种解决方案:
- 重载运算符+
- 为特定类型提供具体化的模板定义
8.5.3 显示具体化
可以提供一个具体化函数定义——称为显示具体化,其中包含所需的代码。
当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。
- 第三代具体化
实验其他具体化方法,C++98选择下面的方法。
- 对于给定的函数名,可以有非模板函数、模板函数和显示具体化模板函数以及他们的重载版本
- 显示具体化的原型和定义应以template<>打头,并通过名称来指出类型。
- 具体化模板优先于常规模板,而非模板函数优先于具体化和常规模板。
具体化原型:
// explicit specialization for the job type
template<>void Swap<job>(job &, job &);
// simpler form
template<>void Swap(job &, job &)
- 显示具体化示例
// twoswap8_13.cpp -- specialization overrides a template
#include<iostream>
template<typename T>
void Swap(T &, T &);
struct job
{
char name[40];
double salary;
int floor;
};
// explicit specialization
template<>void Swap<job>(job &j1, job &j2);
void show(job &);
int main()
{
using namespace std;
cout.precision(2);
cout.setf(ios_base::fixed, ios::floatfield);
int i = 10, j = 20;
cout << "i, j = " << i << ", " << j << ".\n";
cout << "Using compiler-genterated int swapper:\n";
Swap(i,j); //generate a Swap(int & a, int & b)
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)
{
Swap(j1.salary, j2.salary);
Swap(j1.floor, j2.floor);
}
void show(job &j)
{
using std::cout;
cout << "Name: " << j.name << ", Salary: " << j.salary;
cout << ", Floor: " << j.floor << "\n";
}
8.5.4 实例化和具体化
函数模板本身并不会产生函数定义,只有在实例化之后才会产生函数定义。有两种实例化方法:
- 隐式实例化,函数调用
- 显示实例化,如Swap<int>()
- 具体化直接添加一个定义
注:试图在同一个文件中使用同一种类型的显示实例和显示具体化将出错
8.5.5 编译器选择使用哪个函数版本
对于函数重载、函数模板和函数模板重载,C++有一个定义良好的策略,选择的过程称为重载解析:
- 第一步:创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。
- 第二步:使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况,例如,使用float参数的函数调用可以将该参数转换为double,从而与double形参匹配,而模板可以为float生成一个实例。
- 第三步:确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。
- 完全匹配和最佳匹配
进行完全匹配时,C++允许某些"无关紧要的转换。"
完全匹配允许的无关紧要的转换
从实参 | 到形参 |
---|---|
Type | Type & |
Type & | Type |
Type[] | Type * |
Type(argument-list) | Type(*)(argument-list) |
Type | const Type |
Type | volatile Type |
Type * | const Type |
Type * | volatile Type * |
-
部分排序规则示例
重载解析将寻找最匹配的函数。
如果只存在一个这样的函数,则选择它;
如果存在多个这样的函数,但其中只有一个是非模板函数,则选择该函数;
如果存在多个适合的函数,且它们都为模板函数,但其中一个函数比其他函数更具体,则选择该函数。
如果有多个同样适合的非模板函数或模板函数,但没有一个函数比其他函数更更具体,则函数调用将是不确定的,因此是错误的;
当然,如果不存在匹配的函数,则也是错误。 -
自己选择
在有些情况下,可以通过编写合适的函数调用,引导编译器做出您希望的选择。
8.5.6 模板函数的发展
-
什么是类型
由于无法预先知道x和y的类型,因此没办法声明xpy的类型。 -
关键字decltype(C++11)
修复模板函数ft()
template<class T1, class T2>
void ft(T1 x, T2 y)
{
decltype(x + y) xpy = x + y;
}
如下声明 decltype(expression) var;
将var声明成表达式的类型。
- 另一种函数声明语法(C++后置返回类型)
使用关键字auto,样例如下
template<class T1, class T2>
auto gt(T1 x, T2 y) ->decltype(x + y)
{
...
return + y;
}
8.6 总结
- inline内联函数
- 引用
- 默认参数
- 函数重载
- 模板函数
8.7 复习题
-
哪种函数适合定义为内联函数?
代码短,重复使用的函数。 -
假设song()函数的原型如下:
void song(const char * name, int times);
a. 如何修改原型,使times的默认值为1?
void song(const char * name, int times = 1);
b. 函数定义需要做哪些修改?
void song(const char * name, int times){}
不需要
c. 能否为name提供默认值"O.MY Papa"?
不可以,添加默认值应该从右往左添加,times首先有默认值才可以。 -
编写iquote()的重载版本——显示其用双引号括起的参数。编写三个版本:一个用于int参数,一个用于double参数,另一个用于string参数。(仔细读题)
using namespace std;
void iquote(int n){cout << "\"" << n << "\"";}
void iquote(double x){cout << "\"" << x << "\"";}
void iquote(string s){cout << "\"" << s << "\"";}
{
for(int i = 0; s[i] == '\0'; i++)
{
if(s[i] == "\"")
{
do
{
cout << s[++i];
}while(s[i] != "\"")
}
}
}
- 下面是一个结构模板:
struct box
{
char maker[40];
float height;
float width;
float length;
float volume;
};
a. 请编写一个函数,它将box结构的引用作为形参,并显示每个成员的值.
void show(const box & b)
{
using std::cout;
cout << "Maker: " << b.maker << "\n";
cout << "Height: " << b.height << "\n";
cout << "Width: " << b.width << "\n";
cout << "Length: " << b.length << "\n";
cout << "Volume: " << b.volume << "\n";
}
b. 请编写一个函数,它将box结构的引用作为形参,并将volume成员设置为其他3边的乘积。
void set_volume(box & b)
{
b.volume = b.height * b.length * b.width;
}
-
为让函数fill()和show()使用引用参数,需要将程序清单7.15做哪些修改?
首先修改函数声明;修改完之后修改函数定义;修改完函数定义在修改以下show()函数调用。 -
指出下面每个目标是否可以使用默认函数或者函数重载完成,或者这两种都无法完成,并提供合适的原型。
a. mass(density,volume)返回密度为density、体积为volume的物体的质量,而mass(density)返回密度为density、体积为1.0立方米的物体的质量。这些值的类型都为double。
可以使用默认参数函数完成 double mass(double density, double volume = 1.0);
可以使用函数重载完成 double mass(double density)
b. repeat(10,"I'm OK")将指定的字符串显示10次,而repeat("But you're kind of stupid")将指定的字符串显示5次。
默认参数不可以;
函数重载可以;void repeat(int n, char * ps); void repeat(char * ps);
c. average(3,6)将返回两个int参数的平均值(int类型),而 average(3.0,6.0)将返回两个double参数的平均值(double类型)。
默认参数不可以;
函数重载可以 int average(int a, int b); double average(double a, double b);
d. mangle("I'm glad to meet you")根据是将值赋给char变量还是char*变量,分别返回字符I和指向字符串"I'm glad to meet you"的指针;
char * mangle(char * ps );
都不可以,特征标完全相同
- 编写返回两个参数中较大值的函数模板。
template<typename T>
T More(T &a, T&b)
{
return a > b ? a : b;
}
- 给定复习题7的模板和复习题4的box结构,提供一个模板具体化,它接受两个box参数,并返回体积较大的那一个。
struct box
{
char maker[40];
float height;
float width;
float length;
float volume;
};
template<typename T>
T More(T &a, T&b)
{
return a > b ? a : b;
}
template<>
T<box>More(box &b1, box &b2)
{
if(b1.volume > b2.volume)
return b1;
else if(b1.volume < b2.volume)
return b2;
}
改:
T<box>More(box &b1, box &b2)
{
return b1.volume > b2.volume ? b1 : b2;
}
- 在下述代码(假定这些代码是一个完整程序的一部分)中,v1、v2、v3、v4和v5分别是那种类型?
int g(int x);
...
float m = 5.5f;
flota &rm = m;
decltype(m) v1 = m;
decltype(rm) v2 = m;
decltype((m)) v3 = m;
decltype(g(100)) v4;
decltype(2.0 * m) v5;
v1 float型;v2 float &型;v3 float 型;v4int型;v5double型
v3 float &