第08章 函数探幽
<c++ primer plus>第六版
8 函数探幽
8.1 c++内联函数
内联函数是c++为提高程序运行速度所做的改进.
常规函数与内联函数之间的区别, 不在于编写方式, 而在于c++编译器如何将它们组合到程序中.
例: 一般定义内联函数时, 省略函数原型, 直接在原型处定义整个函数.
inline double square(double x) {return x*x;}
c语言中使用#define来提供宏, 可以实在原始的内联代码
#define SQUARE(X) X*X
a = SQUARE(5.0); //编译时替换为 a = 5.0*5.0, 相当于内联函数.
8.2 引用变量
引用变量是c++新增的一种复合类型.
引用是已定义变量的别名.
引用的主要用途是用途函数的形参.
int rats = 100;
int & rodents = rats;
//rodents 是 rats的别名.
//int & 可以认为是指向int的引用.
//&是类型标识运算符的一部分(不是地址运算符),
//rats和rodents的值和地址都相同.
cout << "rats = " << rats << endl;
cout << "rodents = " << rodents << endl;
cout << "addr rats = " << &rats << endl;
cout << "addr rodents = " << &rodents << endl;
int rats = 101;
int & rodents = rats; //引用, rodents相当于rats, &rodents相当于&rats.
int * rodents = &rats;//指针, *parts 相当于rats, prats 相当于&rats.
引用与指针区别:
- 引用在声明时必须初始化, 指针可以先声明再赋值. 可以认为引用接近于const指针.
int rats = 101; //一个int型变量
int & rodents = rats; //rodents是rats的别名, 它的值是101.
int bunnies = 50; //又一个int型变量
rodents = bunnies; //什么效果? 1) [错]rodents变成了bunnies的引用 2) [对]rodents仍然是rats的引用, 仅仅是值变成了50, 相当于rats=bunnies.
8.2.2 将引用用作函数参数
引用用作函数参数, 使得函数中的变量名成为实际参数的别名, 这种方法称为按引用传递.
#include<iostream>
void swap_r(int &a, int &b); //a和b是引用
void swap_p(int *p, int *q); //p和q是指针
void swap_v(int a, int b); //a和b是新变量
int main()
{
using namespace std;
int wallet1 = 300;
int wallet2 = 350;
cout << "wallet1=" << wallet1 << ", wallet2=" << wallet2 << endl;
//使用引用交换内容
swap_r(wallet1, wallet2);
cout << "wallet1=" << wallet1 << ", wallet2=" << wallet2 << endl;
//使用指针交换内容
swap_p(&wallet1, &wallet2);
cout << "wallet1=" << wallet1 << ", wallet2=" << wallet2 << endl;
//使用新变量交换内容
swap_v(wallet1, wallet2); //交换失败
cout << "wallet1=" << wallet1 << ", wallet2=" << wallet2 << endl;
}
void swap_r(int &a, int &b)
{
int tmp = a;
a = b;
b = tmp;
}
void swap_p(int *p, int *q)
{
int tmp = *p;
*p = *q;
*q = tmp;
}
void swap_v(int a, int b)
{
int tmp = a;
a = b;
b = tmp;
}
8.2.3 引用的属性和特别之处
如下函数要求ra是一个引用, 所以现代c++不允许recube(x+3.0)这种调用, 因为x+3.0不是变量, 早期c++会创建一个临时就是, ra成为该临时变量的引用.
double recube(double &ra);
double recube(double &ra)
{
ra *= ra*ra;
return ra;
}
8.2.4 将引用用于结构
引用的主要作用是为了用于结构和类, 而不是基本的内置类型.
struct free_throws
{
std::string name;
int made;
int attempts;
float percent;
};
void set_pc(free_throws & ft); //函数参数是一个引用, 引用指向free_throws
void display(const free_throws & ft); //函数参数是一个常引用, 不允许修改数据
free_throws &accumulate(free_throws & target, const free_throws & source); //返回引用
传统返回: 计算return后面的表达式, 得到一个值复制到临时位置, 调用函数的程序使用这个临时值.
返回引用: 不需要将值复制到临时位置.
注意: 如果返回引用, 则该引用不能指向临时变量.
const free_throws & clone2(free_throws &ft)
{
free_throws new_guy; //临时变量, 在函数终止时会释放
new_guy = ft;
return new_guy; //返回临时变量的引用, 会引发错误.
}
解决上述问题有两个方法:
- 将引用作为参数传递给函数, 然后返回它.
- 使用new来分配新的内存空间, 但后续要记得使用delete来释放该内存.
const free_throws & clone3(free_throws &ft)
{
free_throws * pt; //分配新内存
*pt = ft;
return *pt; //返回对该内存的引用
}
free_throws & jolly = clone3(three); //jolly变成对函数中新分配内存的引用.
8.2.5 将引用用于类对象
给函数传递类对象时, c++通常做法是使用引用.
8.2.6 对象, 继承, 引用
继承的一个特性是: 基类引用可以指向派生类对象, 无需进行强制类型转换.
实际结果: 定义一个函数, 接受基类引用作为参数, 调用函数时可以传给它基类对象, 也可以传给它派生类对象.
#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." << endl;
exit(EXIT_FAILURE);
}
double objective = 1800;
double eps[LIMIT] = {30, 19, 14, 8.8, 7.5};
file_it(fout, objective, eps, LIMIT); //第一个参数是ostream的引用, 可以传给它ofstream的引用
file_it(cout, objective, eps, LIMIT);
}
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" << endl;
os.setf(ios::showpoint); //显示小数点模式, 当前小数部分为0.
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);
}
8.2.7 何时使用引用参数
使用引用参数的主要原因:
- 在函数中能够修改数据对象.
- 传递引用省去了复制数据的时间, 可以提高程序的运行速度.
指导原则: 只传递值而不做修改的函数中:
- 如果数据对象很小(内置数据类型或小型结构), 则按值传递.
- 如果数据对象是数组, 则使用指针传递, 并将指针声明为const指针.
- 如果数据对象是较大的结构, 则使用const指针或const引用, 节省复制结构所需时间和空间.
- 如果数据对象是类对象, 则使用const引用. 类设计的语义常常要求使用引用, 因此传递类对象参数的标准方式是按引用传递.
指导原则: 在函数中修改数据的函数中:
- 如果数据对象是内置数据类型, 则使用指针.
- 如果数据对象是数组, 则只能使用指针.
- 如果数据对象是结构, 则使用指针或引用.
- 如果数据对象是类对象, 则使用引用.
8.3 默认参数
char * left(const char * str, int n=1); //原型中, 指定参数n的默认值为1
char * left(const char * str, int n) //函数定义中, n不指定默认值
{
...
}
8.4 函数重载(函数多态)
c++通过函数参数列表的不同来确定要使用的重载函数版本.
函数的参数列表也称为函数特征标(function signature).
函数的参数个数和参数类型可以作为特征标.
函数的返回值类型不能做为特征标.
double cube(double x)和 double cube(double &x)不能共存, 因为引用和类型本身视为同一特征标.
匹配函数时, 非const可以转为const, 反过来不行, 所以把一个const变量传给函数的非const参数会报错.
#include<iostream>
using namespace std;
void f0(char * bits);
void f0(const char * bits);
void f1(char * bits);
void f2(const char * bits);
int main()
{
char c0[20] = "abcdefg";
const char c1[20] = "hijklmn";
f0(c0); //非const变量, 调用重载的非const参数函数.
f0(c1); //const变量, 调用重载的const参数函数
f1(c0);
//f1(c1); //const变量不能传给非const参数, 报错: invalid conversion from "const char*" to "char*"
f2(c0); //非const变量可以传给const参数
f2(c1);
return 0;
}
void f0( char * bits) { cout << "calling f0-no-const" << endl; }
void f0(const char * bits) { cout << "calling f0-const" << endl; }
void f1( char * bits) { cout << "calling f1-no-const" << endl; }
void f2(const char * bits) { cout << "calling f2-const" << endl; }
8.5 函数模板
函数模板是通用的函数描述, 也就是说他们使用泛型来定义函数, 其中的泛型可以用具体的类型替换.
由于类型是用参数表示的, 因此模板特性有时也称为参数化类型.
模板不能缩短可执行程序, 它只是使函数定义更简单可靠.
#include<iostream>
//新建一个模板, 将类型命名为T(可以任意选择), 其中template和typename是关键字.
template <typename T> //注意这一行没有分号, 这一行与下一行是一句, 不能分开.
void Swap(T &a, T &b)
{
T temp;
temp = a;
a = b;
b = temp;
}
int main()
{
using namespace std;
int i=10, j=20;
Swap(i, j); //传给模板int类型, 编译器自动生成void Swap(int &, int &)
cout << "i=" << i << ", j=" << j << endl;
double x=24.5, y=81.7;
Swap(x, y); //传给模板double类型, 编译器自动生成void Swap(double &, double &)
cout << "x=" << x << ", y=" << y << endl;
return 0;
}
模板重载.
如果需要对多个不同类型使用相同算法的函数, 则使用模板.
如果需要对多个不同类型使用不同算法的函数, 则使用模板重载.
#include<iostream>
//函数模板声明
template <typename T> void Swap(T &a, T &b);
template <typename T> void Swap(T * a, T * b, int n);
int main()
{
using namespace std;
int i=9, j=70;
Swap(i, j);
cout << "i=" << i << ", j=" << j << endl;
double x[] = {1.2, 2.3, 3.4, 4.5, 5.6};
double y[] = {9.8, 8.7, 7.6, 6.5, 5.4};
Swap(x, y, 5);
for (int i=0; i<5; i++)
{
cout << x[i] << ", " << y[i] << endl;
}
}
//函数模板定义
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[n];
for (int i=0; i<n; i++)
{
temp[i] = a[i];
a[i] = b[i];
b[i] = temp[i];
}
}
模板局限性: 模板函数很可能无法处理某些类型. 一种解决方案, 使用重载运算符, 另一种解决方案, 为特定类型提供具体化的模板定义.
显式具体化
#include<iostream>
template <typename T> void Swap(T &a, T &b);
struct job
{
char name[40];
double salary;
int floor;
};
template <> void Swap<job>(job & j1, job & j2); //显式具体化, 交换job, 表示这是对交换job的一个具体工作方式.
void show_job(const job &j1);
int main()
{
using namespace std;
int i=8, j=72;
Swap(i, j);
cout << "i=" << i << ", j=" << j << endl;
job jx = {"namex", 100.1, 5};
job jy = {"namey", 200.1, 6};
Swap(jx, jy);
show_job(jx);
show_job(jy);
}
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 tmp_salary;
tmp_salary = j1.salary;
j1.salary = j2.salary;
j2.salary = tmp_salary;
int tmp_floor;
tmp_floor = j1.floor;
j1.floor = j2.floor;
j2.floor = tmp_floor;
}
void show_job(const job &j1)
{
using namespace std;
cout << "name = " << j1.name << endl;
cout << "salary= " << j1.salary << endl;
cout << "floor = " << j1.floor << endl;
}
隐式实例化, 显式实例化, 显式具体化, 统称为具体化.
它们的相同之处在于, 它们表示的都是使用的具体类型的函数定义, 而不是通用描述.