8、函数探幽
本章介绍的C++在C语言基础上新增的特性,比前面各章都多,这是进入C++领域的重要一步。
8.1 内联函数
是为提高程序运行速度所做的一项改进;
常规函数与内联函数的主要区别,不在于编写方式,而在于C++编译器如何将它们组合到程序中。
编译过程的最终产物--由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机内存中,因此每条指令都有特定的内存地址。计算机随后将逐步执行这些指令。
有时,将跳过一些指令,向前或向后跳到特定地址。常规函数调用也使程序跳到另一个地址,并在函数结束时返回。
下面更详细地介绍这一过程的典型实现:
执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈,跳到标记函数起点的内存单元,执行函数代码,然后跳回到地址被保存的指令处。来回跳跃记录跳跃位置意味着以前使用函数时,需要一定的开销。
C++内联函数提供了另一种选择,内联函数的编译代码与其他程序代码“内联”起来了。也就是说,编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数块,但代价是占用更多内存。如果程序在10个地方调用同一个内联函数,则该程序将包含该函数代码的10个副本。
应有选择的使用内联函数。如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间将只占整个过程的很小一部分。如果代码执行时间很短,则内联调用就可以节省非内联调用使用的大部分时间。
另一方面,由于这个过程相当快,因此尽管节省了该过程的大部分时间,但节省的时间绝对值并不大。除非该函数经常被调用。
* 函数声明前加上关键字inline;
* 函数定义前加上关键字inline;
通常做法:省略原型,将整个定义放在本应提供原型的地方。
编译器不一定会满足将函数作为内联函数的需求:
* 它可能认为函数过大或注意到函数调用了自己(内联函数不能递归),因此不能将其作为内联函数;
* 有些编译器没有启用或实现这种特性;
#include<iostream>
#include<string>
inline double sqart(double x) {
return x * x;
}
int main() {
using namespace std;
double a = sqart(2);
double b = sqart(2 + 2);
cout << a << " " << b<< endl;
return 0;
}
8.2 引用变量
引用是已定义的变量的别名(另一个名称)。主要用途用作函数的形参。通过将引用变量用作参数,函数将使用原始数据,而不是其副本。
这样,除指针之外,引用也为函数处理大型结构提供了非常方便的途径。
8.2.1 创建引用变量
前面讲,使用&符号来指示变量的地址。
C++给&赋予了另一个含义,将其用来声明引用。
int rats;
int & rodents = rats;
此处,&不是地址运算符,而是类型标识符的一部分。int & 指的是指向int的引用。
上述声明允许将rats和rodents互换---它们指向相同的值和内存单元。
#include<iostream>
#include<string>
int main() {
using namespace std;
int rats = 10;
int & rodents = rats;
cout << "rats: " << rats << endl;
cout << "rodents: " << rodents << endl;
rodents++;
cout << "rats: " << rats << endl;
cout << "rodents: " << rodents << endl;
cout << "rats address: " << &rats << endl;
cout << "rodents address: " << &rodents << endl;
return 0;
}
对于C语言用户,首次解除引用会有些困惑,因此会想到指针,但它们之间还是有区别的。
int rats = 101;
int & rodents = rats;
int * prats = &rats;
这点看,引用看上去很像伪装表示的指针。
实际上,引用还是不同于指针的。比如:必须在声明引用时将其初始化,而不能像指针那样,先声明,再赋值;
引用更接近const指针,必须在创建时进行初始化,一旦与某个变量关联起来,就将一直效忠于它。
也就是说:
int & rodents = rats;
实际上是下述代码的伪装表示:
int * const pr = &rats;
其中,引用rodents扮演的角色与表达式*pr相同。
注意:可以通过初始化声明来设置引用,但不能通过赋值来设置。
8.2.2 将引用用作函数参数
引用经常被用做函数参数,使得函数中的变量名称为调用程序中的变量的别名。这种传递参数的方法称为按引用传递。按引用传递允许被调用函数能够访问调用函数中的变量。
C语言只能按值传递,当然,C语言也允许避开按值传递的限制,采用按指针传递的方式。
案例:交换两个变量的值,
* 按值传递
* 引用
* 指针
#include<iostream>
#include<string>
void swapr(int & a, int & b);
void swapp(int * a, int * b);
void swapv(int a, int b);
int main() {
using namespace std;
int wallet1 = 100;
int wallet2 = 200;
cout << "wallet1: " << wallet1 << endl;
cout << "wallet2: " << wallet2 << endl;
swapr(wallet1, wallet2);
cout << "wallet1: " << wallet1 << endl;
cout << "wallet2: " << wallet2 << endl;
swapp(&wallet1, &wallet2);
cout << "wallet1: " << wallet1 << endl;
cout << "wallet2: " << wallet2 << endl;
swapv(wallet1, wallet2);
cout << "wallet1: " << wallet1 << endl;
cout << "wallet2: " << wallet2 << endl;
return 0;
}
void swapr(int & a, int & b) {
int temp;
temp = b;
b = a;
a = temp;
}
void swapp(int * a, int * b) {
int temp;
temp = *a;
*a = *b;
*b = temp;
}
void swapv(int a, int b) {
int temp;
temp = b;
b = a;
a = temp;
}
其中,引用和指针都成功交换了两个钱夹的内容,而按值传递的方法没能完成这项任务。
swapr(wallet1, wallet2);
swapp(&wallet1, &wallet2);
swapv(wallet1, wallet2);
按引用传递和按值传递看起来相同。只能通过原型或函数定义才能知道swapr()是按引用传递的。
按引用传递和按值传递的代码:
外在区别:声明函数参数的方式不同;
内在区别:按引用传递,变量a和b是wallet1和wallet2的别名,所以交换a和b的值相当于交换wallet1和wallet2的值;
按值传递,变量a和b是复制了wallet1和wallet2的值得新变量,因此,交换a和b的值不会影响wallet1和wallet2的值;
按引用传递和指针传递的代码:
声明函数参数的方式不同;
指针版本需要在函数使用a和b的整个过程中使用*;
8.2.3 引用的属性和特别之处
示例:
#include<iostream>
#include<string>
double cube(double a);
double refcube(double & a);
int main() {
using namespace std;
double a = 10;
cout << cube(a) << ":" << endl;
cout << a << endl;
cout << refcube(a) << ":" << endl;
cout << a << endl;
return 0;
}
double cube(double a) {
a *= a * a;
return a;
}
double refcube(double & a) {
a *= a * a;
return a;
}
refcube修改了a的值,而cube没有,这提醒我们为何通常按值传递。
若程序员的意图是让函数使用传递给它的信息,而不是对这些信息进行修改,同时又想使用引用,则应使用常量引用。
double refcube(const double &a)
另外,若要编写类似于上述示例的函数(即使用基本数值类型),应按值传递,而非引用传递;当数据比较大时(如结构和类),引用参数将很有用。
按值传递的函数,可使用多种类型的实参。
cube(x+2.0);
cube(8.0);
int k = 10;
cube(k);
如果将与上面类似的参数传递给接受引用参数的函数,将会发现,传递引用的限制更严格,毕竟,如果a是一个变量的别名,则实参应是该变量。下面的代码不合理,因为x+2.0并不是变量;
如果试图使用像refcube(x+3.0)这样的函数的调用,将发生什么情况呢?
现代C++中,这是错误的,大多数编译器都将指出这一点;而有些较老的编译器将发出这样的警告:
warning:Temporary used for parameter 'a' in call to refuce(doulbe &)
之所以有这样比较温和的反应由于早期C++确实允许将表达式传递给引用变量。
这样做的结果是:程序将创建一个临时的无名变量,并将其初始化为表达式x+2.0的值,然后,a将成为该临时变量的引用。
下面讨论这种临时变量,看看什么时候创建它们,什么时候不创建。
临时变量:引用参数和const
如果实参与引用参数不匹配,C++将生成临时变量。
当前,仅当参数为const引用时,C++才允许这样做,但以前不是这样。
下面看:
* 何种情况下,C++将生成临时变量;
* 为何对const引用的限制是合理的;
一、何时创建临时变量?如果引用参数是csont,则编译器在以下生成临时变量:
* 实参的类型正确,但不是左值;
* 实参的类型不正确,但可以转换成正确的类型
左值是什么呢?
左值是可以被引用的数据对象。非左值包括字面常量和包含多项的表达式。
C语言中,左值最初指可以出现在赋值语句左边的实体,但这是引入关键字const之前的情况;
现在,常规变量和const变量都可以视为左值,因为可以通过地址访问它们,但常规变量属于可修改的左值,而const变量属于不可修改的左值。
#include<iostream>
#include<string>
double refcube(const double & a);
int main() {
using namespace std;
double side = 10;
double * ps = &side;
double & rs = side;
long edge = 5L;
double len[4] = {2.0, 5.0, 10.0, 12.0};
double c1 = refcube(side);
double c2 = refcube(*ps);
double c3 = refcube(rs);
double c4 = refcube(len[2]);
double c5 = refcube(7.0);
double c6 = refcube(side + 10.0);
return 0;
}
double refcube( const double & a) {
return a * a * a;
}
参数side、len[2]、*ps、rs都有有名称、double类型的数据对象,因此可以创建引用,而不需要临时变量。
然后,edge随是变量,类型却不正确,double引用不能指向long。
另一方面,参数7.0和side+10.0 的类型都正确,但没有名称,在这些情况下,编译器都将生成一个临时匿名变量,并让a指向它。这些临时变量只在函数调用期间存在,此后编译器可以随意删除。
二、为什么对于常量引用,这种行为可行,其他情况不行呢?
void swapr(int & a, int & b) {
int temp;
temp = b;
b = a;
a = temp;
}
若在早期C++比较宽松的情况下,执行下面的操作将发生什么呢?
long a=3,b=5;
swapr(a,b);
这里类型不匹配,因此编译器将创建两个临时int变量,并将它们初始化为3和5,然后交换临时变量的内容,而a和b保持不变。
简而言之,如果接受引用参数的函数的意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现。
解决办法,禁止创建临时变量,现在的C++标准正式如此。
现在来看refcube函数,该函数目的仅使用传递的值,而不是修改它们,因此临时变量不会造成任何不利的影响,反而会使函数在可处理的参数种类方面更通用。
因此,若声明将引用指定为const,C++将在必要时生成临时变量。
实际上,对于形参为const引用的C++函数,若实参不匹配,则其行为类似于按值传递,为确保原始数据不被修改,将使用临时变量来存储值。
应尽可能使用const,理由:
* 使用const避免无意中修改数据的编程错误;
* cosnt使函数能处理const和非const实参,否则只能接受非const数据;
* const引用使函数能够正确生成并使用临时变量;
8.2.4 将引用用于结构
1、程序说明
#include<iostream>
#include<string>
struct free_throws{
std::string name;
int made;
int attempt;
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() {
using namespace std;
free_throws one = {"one", 13, 14};
free_throws two = {"two", 2, 3};
free_throws three = {"three", 4, 5};
free_throws four = {"four", 6, 7};
free_throws five = {"five", 8, 9};
free_throws team = {"team", 10, 11};
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 << "Display team: " << endl;
display(team);
std::cout << "Display dup: " << endl;
display(dup);
set_pc(four);
accumulate(dup, five) = four;
cout << "Display dup: " << endl;
display(dup);
return 0;
}
void display(const free_throws & ft) {
using namespace std;
cout << "name: " << ft.name << endl;
cout << "made: " << ft.made << endl;
cout << "attempt: " << ft.attempt << endl;
cout << "percent: " << ft.percent << endl;
}
void set_pc(free_throws & ft){
if (ft.attempt != 0)
ft.percent = 100.0f * float(ft.made)/float(ft.attempt);
else
ft.percent = 0;
}
free_throws & accumulate(free_throws & target, const free_throws & source) {
target.attempt += source.attempt;
target.made += source.made;
set_pc(target);
return target;
}
初始化了多个结构对象。若指定的初始值比成员少,余下的成员将被设置为0;
display(one);
此处函数使用const引用参数,不修改它。
就这个函数而言,使用按值传递也可,但与复制原始结构的拷贝相比,使用引用可节省时间和内存。
注意此处代码:display(accumulate(team, two));
* 将结构对象team作为第一个参数传给了accumulate,accumulate返回指向它的引用;
* 接下来,将accumulate返回值作为参数传递给display,意味着team传递给了display,display的参数为引用,因此ft指向的是team;
程序以独特的方式使用了accumulate:
accumulate(dup, five) = four;
这条语句将值赋给函数调用,这是可行的,因为函数的返回值是一个引用。
如果accumulate按值返回,这条语句不能通过编译。
2、为何要返回引用
返回值若传统按值传递,这个值被复制到一个临时位置,而调用程序将使用这个值。
dup = accumulate(team, five);
* 若返回一个结构,而不是结构的引用,将把整个结构复制到一个临时位置,再将这个拷贝赋值给dup;
* 但在引用时,将直接把team复制到dup,其效率更高;
3、返回引用时需要注意的问题
返回引用时最重要的一点:避免返回函数终止时不再存在的内存单元引用。
避免编写下面这样的代码:
const free_throws & clone2(free_throws & ft) {
free_throws newguy;
newguy = ft;
return newguy;
}
该函数返回一个指向临时变量的引用,函数运行完毕后不再存在。
为避免这个问题:最简单的办法,返回一个作为参数传递给函数的引用。作为参数的引用将指向调用函数使用的数据,因此返回的引用也将指向这些数据。
另一个办法:使用new来分配新的存储空间。前面见过,使用new为字符串分配内存空间,并返回指向该内存空间的指针。
const free_throws & clone(free_throws & ft) {
free_throws * pt;
*pt = ft;// 创建一个无名的free_throws结构,并让指针pt指向该结构,因此*pt就是该结构
return *pt;// 似乎会返回该结构,但函数声明表明,该函数实际上将返回这个结构的引用。
}
free_throws & jolly = clone(three);
这样jolly称为新结构的引用。
存在的问题:在不再需要new分配内存时,应使用delete来释放它们。调用clone隐藏了对new的调用,这使得以后很容易忘记使用delete来释放内存。
第16张的auto_ptr模块以及C++11的unique_ptr可以帮助程序员自动完成释放工作。
4、为何将const用于引用返回类型
accumulate(dup, five) = four;
效果如下:首先将five的数据添加到dup中,再使用four的内容覆盖dup的内容。
(1)在赋值语句中,左边必须是可修改的左值。意味着,在赋值表达式中,左边的子表达式必须标识一个可修改的内存块。在这里,函数返回指向dup的引用,它缺失标识这样的一个内存块。因此合法。
(2)另一方面,常规返回类型是右值--不能通过地址访问。这种表达式可出现在赋值语句的右边,但不能出现在左边。其他右值包括字面值和表达式。
显然,获取字面值得地址没有任何意义,但为何常规函数返回值是右值呢?因为这种返回值位于临时内存单元中,运行到下一条语句时,它们可能不再存在。
假设您要使用返回值,但又不允许指向像accumulate赋值这样的操作,只需将返回类型声明为const引用。
const free_throws & accumulate(free_throws & target, const free_throws & source)
这样,下面的赋值语句就不合法了。
accumulate(dup, five) = four;
通过省略const,可以编写代码简短,但其含义也更模糊。
8.2.5 将引用用于类对象
#include<iostream>
#include<string>
using namespace std;
string version1(const string & s1, const string & s2);
const string & version2(string & s1, const string & s2);
const string & version3(string & s1, const string & s2);
int main() {
string input;
string copy;
string result;
cout << "Enter the string: " << endl;
getline(cin, input);
copy = input;
cout << "Your string has entered: " << input << endl;
result = version1(input, "***");
cout << "Your string has enhanced: " << result << endl;
cout << "Your origal string: " << input <<endl;
result = version2(input, "***");
cout << "Your string has enhanced: " << result << endl;
cout << "Your origal string: " << input <<endl;
cout << "Reset origal string:" << endl;
input = copy;
result = version3(input, "***");
cout << "Your string has enhanced: " << result << endl;
cout << "Your origal string: " << input <<endl;
return 0;
}
string version1(const string & s1, const string & s2) {
string temp;
temp = s1 + s2 + s1;
return temp;
}
const string & version2(string & s1, const string & s2) {
s1 = s1 + s2 + s1;
return s1;
}
const string & version3(string & s1, const string & s2) {
string temp;
temp = s1 + s2 + s1;
return temp;
}
运行该程序,会崩溃;(程序崩溃的原因:返回一个指向version3中声明的变量的引用。这个函数能够通过编译,但执行时将崩溃)
将C-风格字符串用作string对象引用参数:
实参input和“***”的类型分别是string和const char*。程序怎么能够接受将char指针赋给string引用呢?
一、string类定义了一种char*到string的转换功能,使得可以使用C-风格字符串来初始化string对象。
二、const引用的形参的一个属性。若实参的类型与引用参数类型不匹配,但可被转换为引用类型,将创建一个临时变量,使用转换后的实参值来初始化它,然后传递一个指向该临时变量的引用。
这种属性的结果是:若形参类型为const string &,在调用函数时,使用的实参可以是string对象或C-风格字符串,如用引号扩起来的字符串字面量、以空字符结尾的char数组或指向char的指针变量。
8.2.6 对象、继承和引用
ofstream对象可以使用ostream类的方法,使得能够将特性从一个类传递给另一个类的语言特性称为继承。
ostream是基类,ofstream是派生类。派生类继承了基类的方法。
继承的另一个特性:基类引用可以指向派生类对象,而无需进行强制类型转换。
程序示例:
#include<iostream>
#include<string>
#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 = "a.log";
fout.open(fn);
if (!fout.is_open()) {
cout << "open file fail!" << endl;
exit(EXIT_FAILURE);
}
double objective;
cout << "enter the focal length of your telescope objective in mm:" << endl;
cin >> objective;
double eps[LIMIT];
cout << "enter the focal length of" << LIMIT
<< "eyepieces: " << endl;
for (int i = 0; i < LIMIT; i++) {
cin >> eps[i];
}
file_it(fout, objective, eps, LIMIT);
file_it(cout, objective, eps, LIMIT);
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(2);
os << "f.l. eyepiecs";
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);
}
方法self能够设置各种格式化状态。例如:
* os.setf(ios_base::fixed);将对象置于使用定点表示法的模式;
* os.setf(ios::showpoint);将对象置于显示小数点的模式,即使小数部分未零。
* precision指定显示多少位小数(假定对象除于定点模式下)
所有这些设置将一直保持不变,直到再次调用相应的方法重置它们。
方法width设置下一次输出操作使用的字段宽度,这种设置只在显示下一个值时有效,然后恢复到默认设置。默认宽度为零。
另外:
ios_base::fmtflags initial;
initial = os.setf(ios_base::fixed);
......
os.setf(initial);
方法self返回调用它之前有效的所有格式化设置。ios_base::fmtflags是存储这种信息所需的数据类型名称。
因此,将返回值赋给initial将存储调用file_it之前的格式化设置,然后便可以使用变量initial作为参数来调用self,将所有的格式化设置恢复到原来的值。
8.2.7 何时使用引用参数
使用引用参数的两个主要原因:
* 能够修改调用函数中的数据对象;
* 提高程序的运行速度
引用参数实际上是基于指针的代码的另一个接口。那么,什么时候使用引用、指针、按值传递呢?
对于使用传递的值而不作修改的函数:
如果数据对象很小,如内置数据类型或小型结构,则按值传递
如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针。
如果数据对象是较大的结构,则使用const指针或const引用,以提高程序的效率。这样可以节省复制结构的时间和空间。
如果数据对象 是类对象,则使用const引用。类设计的语义常常要求使用引用,这是c++新增这项特性的主要原因。因此,传递类对象参数的标准方式 是引用传递。
对于修改调用函数中数据的函数。
如果数据对象是内置数据类型,则使用指针。如果看到诸如fixit(&x)这样的代码(其中x是int型),则很明显,该函数将修改x.
如果数据对象是数组,则只能使用指针。
如果数据对象是结构,则使用引用或指针。
如果数据对象是类对象,则使用引用。
8.3 默认参数
默认参数指定是当函数调用中省略了实参时自动使用的一个值。
如何设置默认值?通过函数原型,编译器通过查看原型来了解函数所使用的参数数目,因此函数原型也必须将可能的默认参数告知程序。方法是将值赋给原型中的参数。
例如:
char * left(const char * str,int n=1);
* 希望返回一个新的字符串,因此返回类型为char *(指向char的指针);
* 原始字符串不变,因此使用const;
* 希望n的默认值为1;
默认值是初始化值,因此上面的原型将n初始化为1。如果省略n,则n为1,否则,覆盖1。
注意:
* 对于带参数列表的函数,必须从右向左添加默认值。也就是说,要为某个参数设置默认值,则必须为它右边额所有参数提供默认值;
* 只有原型定义了默认值。函数定义与没有默认参数是完全相同;
#include<iostream>
#include<string>
using namespace std;
const int ArrSize = 80;
char * left(const char arr[] ,int n=1);
int main() {
char charr[ArrSize];
cout << "enter the string: " << endl;
cin.getline(charr, ArrSize);
char * ps = left(charr, 4);
cout << ps << endl;
delete[] ps;
return 0;
}
char * left(const char arr[] ,int n) {
if (n < 0)
n = 0;
char * p = new char[n+1];
int i;
for (i = 0; i < n && arr[i]; i++)
p[i] = arr[i];
while(i <= n) {
p[i] = '\0';
i++;
}
return p;
}
8.4 函数重载
术语 “多态”指的是有多种形式,因此函数多态允许函数可以有多种形式;
术语“函数重载”指的是可以有多个同名的函数,因此对名称进行了重载;
这两个术语指的是同一回事,但我们通常使用函数重载。
函数重载的关键是函数的参数列表--也成为函数特征标。
如果两个涵涵素的参数数目和类型相同。同时参数的排列顺序也相同,则它们的特征标相同,而变量名是无关紧要的。
示例:
void print(const char * str, int width)
void print(double d, int width)
void print(long l, int width)
void print(int i, int width)
void print(const char * str)
使用print函数时,编译器将根据所采取的用法使用有相应特征标的原型:
使用被重载的函数时,需要在函数调用中使用正确的参数类型。
例如:
usigned int year = 2001;
print(year,6);
print与哪个原型匹配呢?
它不与任何原型匹配,没有匹配的原型并不会停止使用其中的某个函数,因为C++将尝试使用标准类型转换强制进行匹配。
* 若只有#2是唯一的原型,则函数调用print(year,6)将把year转换为double类型;
* 但实际上,有3个将数字作为第一个参数的原型,因此有3中转换year的方式,这种情况下,视为错误;
一些看起来彼此不同的特征标识不能共存的。eg:
double cube(double x);
double cube(double & x);
从编译器角度考虑,cobe(x)有两个原型都匹配,因此编译器无法确定究竟是应使用哪个原型,为避免这种混乱,编译器检查函数特征标时,将把类型引用和类型本身视为同一个特征标。
匹配函数时,并不区别const和非const变量: