《Essential C++》读书笔记 之 面向过程编程风格
2014-06-18
在函数swap的参数中使用reference和pointer
2.4 使用局部静态对象(Local Static Objects)
2.7 定义并使用Template Functions (模板函数)
2.8 函数指针(Pointers to Functions)带来更大的弹性
2.2 调用(invoking)一个函数
2.2.1 Pass by Reference语义
reference扮演着外界与对象之间的一个间接号码牌的角色。只要在型别名称和reference名称之间插入&符号,便声明了一个reference:
#include <iostream>
using namespace std; int main()
{
int ival=; //对象,型别为int
int *pi=&ival; //pointer(指针),指向一个int对象
int &rval=ival; //reference(化身),代表一个int对象 //这里不是令rval改为代表jval对象,而是将jval赋值给rval所代表的对象(也就是ival)。
int jval=;
rval=jval; ival =;
pi;
//这里不是令pi指向rval对象,而是将ival(此为rval所代表之对象)的地址赋给pi
pi=&rval; return ;
}
重点是:面对reference的所有操作都像面对“reference所代表的对象”所进行的操作一样。
在函数swap的参数中使用reference和pointer
当我们以reference作为函数参数时,情况是一样的,如下代码所示:
#include <iostream>
void swap(int &, int &); int main()
{
int v1=;
int v2=;
swap(v1,v2); v1;
v2; return ;
} void swap(int &val1, int &val2)
{
int temp=val1;
val1=val2;
val2=temp;
}
运行结果如下图所示:
将参数声明为reference的理由有两个:
- 希望直接对所传入的对象进行修改;
- 为了降低复制大型对象的负担。
如果我们愿意,也可以将参数以pointer形式传递。这和以reference传递的效用相同:传递的是对象地址,而不是整个对象的复制品。唯一的差别是他们的用法不同。如下代码所示:
#include <iostream> void swap(int *,int *); int main()
{
int v1=;
int v2=;
swap(&v1,&v2); v1;
v2; return ;
} void swap(int *val1, int *val2)
{
int temp=*val1;
*val1=*val2;
*val2=temp;
}
但如果swap方法改成如下,变量v1,v2不会调换:
void swap(int *val1, int *val2)
{
int *temp=val1;
val1=val2;
val2=temp;
}
因为上述方法只是更改了指针本身的地址,如下图:
pointer参数和reference参数的差异和用法
pointer可能(也可能不)指向一个实际对象。当我门提领pointer时,一定要先确定其值并非为0。至于reference则必定会代表某个对象。
一般来说,除非你希望在函数内更改参数值,否则建议传递内建型别时,不要使用传址的方式。传址机制主要是作为传递class objects之用。
2.4 使用局部静态对象(Local Static Objects)
fibon_seq()函数是这样一个函数,每次调用时,会计算出Fibonacci数列(元数数目由用户指定),并以一个vector存储计算出来的元素值,然后返回。代码如下:
vector<int> fibon_seq( int size )
{
if ( size <= || size > )
{
cerr << "Warning: fibon_seq(): "
<< size << " not supported -- resetting to 8\n";
size = ;
} vector<int> elems( size ); for ( int ix = ; ix < size; ++ix )
if ( ix == || ix == )
elems[ ix ] = ;
else elems[ ix ] = elems[ix-] + elems[ix-]; return elems;
}
以上代码有一个问题:每次调用时,都要重新计算。
我们希望,保存已经计算出来的元素。上面代码的局部变量肯定不行。如果将vector对象定义于file scope之中,又过于冒险,它会打乱不同函数之间的独立性,使它们难以理解。
本例的另一个解法便是使用局部静态对象:
#include <iostream>
#include <string>
#include<vector>
using namespace std; //定义函数
const vector<int> *fibon_seq( int); int main()
{
fibon_seq( );
fibon_seq( ); //前4个element都已计算,不会重复计算
fibon_seq( ); //只计算还没有计算出来的第6个element return ;
} const vector<int> *fibon_seq( int size )
{
const int max_size = ;
static vector< int > elems; if ( size <= || size > max_size ){
cerr << "fibon_seq(): oops: invalid size: "
<< size << " -- can’t fulfill request.\n";
return ;
} // if size is equal to or greater than elems.size(),
// no calculations are necessary ...
for ( int ix = elems.size(); ix < size; ++ix ){
if ( ix == || ix == )
elems.push_back( );
else elems.push_back( elems[ix-]+elems[ix-] );
} return &elems;
}
2.5 声明一个inline函数
回想一下,fibon_elem()返回一个Fibonacci数列元素,其位置由用户指定。在最初的版本中,每次调用,它都会重新计算每一个数列元素,直到用户指定的位置位置。它会检验用户所指定的位置是否合理。
为了使这个函数更容易理解,我们可以将各个小工作分解为独立函数,以求更简化:
bool is_size_ok(int size)
{
const int max_size=;
if(size<=||size>max_size)
{
cerr<<"Oops: requested size is not supported: "
<<size
<<" --can't fulfill request.\n";
return false;
}
return true;
}
//计算Fibonacci数列中的size个元素
const vector<int>* fibon_seq(int size)
{
static vector<int> elems;
if(!is_size_ok(size))
return ;
for(int ix=elems.size();ix<size;++ix)
{
if(ix==||ix==)
elems.push_back();
else elems.push_back(elems[ix-]+elems[ix-]);
}
return &elems;
}
//返回Fibonaci数列中位置为pos的元素
bool fibon_elem(int pos,int &elem)
{
const vector<int> *pseq=fibon_seq(pos);
if(!pseq)
{
elem=;
return false;
}
elem=(*pseq)[pos-];
return true;
}
但是,先前的做法中,fibon_elem()只须调用一个函数便可完成所有运算,如今必须动用3个函数。这成了它的缺点。这项负担是否很重要呢?这和应用时的形势有关。如果其执行效能不符合理想,只能在将3个函数重新组合成一个。
然而C++还提供了另一个解决方法,就是将这些函数声明为inline。
将函数声明为inline,表示要求编译器在每个函数调用点上,将函数的内容展开。面对一个inline函数,编译器可将该函数的调用操作改为一份函数代码副本取而代之。只要在函数前面加上关键字inline即可:
//ok:现在fibon_elem()成了inline函数
inline bool fibon_elem(int pos,int &elem)
{
/*函数定义与先前版本相同*/
}
注意:将函数指定为inline,只是对编译器提出一种要求而没有强制性,具体分析,请参考7.1.1节
inline函数的定义常常被置于头文件中。由于编译器必须在它被调用的时候加以展开,所有这个时候起定义必须是有效的,2.9节有更深入的讨论。
2.7 定义并使用Template Functions (模板函数)
假设有3个diplay_messaeg()函数,分别用以处理元数型别为int、double、string的3中vectors:
void display_message(const string&, const vector<int>&);
void display_message(const string&, const vector<double>&);
void display_message(const string&, const vector<string>&);
我们在假设他们的函数主体也很相似,唯一的差别仅在于第二个参数的型别。
这样的情况很多,C++提供一种机制,函数模板(function template),将参数表中指定的参数的型别信息抽离出来。
#include <iostream>
#include <string>
#include<vector>
using namespace std; //在mian()之前,就不用声明函数了
template <typename elemType>
void display_message(const string &msg, const vector<elemType> &vec )
{
cout<<msg;
for ( int ix = ; ix < vec.size(); ++ix )
{
elemType t =vec[ix];
cout<<t
<<' ';
}
} int main()
{
string iMsg="show int vector: ";
int iArr[]={,,};
vector<int> iVec(iArr,iArr+);
display_message(iMsg,iVec);
cout<<'\n'; string cMsg="show char vector: ";
char cArr[]={'a','b','c'};
vector<char> cVec(cArr,cArr+);
display_message(cMsg,cVec); return ;
}
2.8 函数指针(Pointers to Functions)带来更大的弹性
假设有一个函数"fibon_elem()"要返回fibon数列指定位置的元素:
bool fibon_elem( int pos, int &elem )
{
const vector<int> *pseq=fibon_seq(pos); //(A) if(!pseq)
{
elem=;
return false;
}
elem=(*pseq)[pos-];
return true;
}
上述代码,调用了函数"fibon_seq()",但除了fibon,还有其它5种数列和相应的"数列_seq()"函数:
const vector<int> *lucas_seq(int size);
const vector<int> *pell_seq(int size);
//...
难道我们要实现其他5种“数列_elem()”?
可以发现在函数"fibon_elem()"中,唯一和数列相关的部分,只有(A)。如果我们可以消除这个关联性,就可以不必提供多个相似函数了。
所谓函数指针,必须指明其所指向之函数的返回值类型及参数表,此外,函数指针必须将*置于某个位置,表示这份定义所表现的是一个指针。当然,最后还必须给于一个名称。
const vector<int>* *seq_ptr(int); //几乎是对的了
但这其实不是我们所要的。上述这行将seq_ptr定义为一个函数,参数表中仅有一个int类型,返回值类型是个指针,这个指针指向另一个指针,后者指向一个const vector,其元素类型为int。为了让seq_ptr被视为一个指针,我们必须以小括号改变运算顺序:
const vector<int>* (*seq_ptr)(int); //ok
现在,seq_ptr可以指向“具有所列之返回值类型及参数表”的任何一个函数。让我们将fibon_elem()重新写国,使它蜕变成更为通用的seq_elem():
bool seq_elem( int pos, int &elem, const vector<int>* (*seq_ptr)(int))
{
//调用seq_ptr所指的函数
const vector<int> *pseq=seq_ptr(pos); //(A) if(!pseq)
{
elem=;
return false;
}
elem=(*pseq)[pos-];
return true;
}
现在的问题是如何取得函数的地址呢?只要给于函数名称就可以了:
//将pell_seq()地址赋给seq_ptr
seq_ptr=pess_seq;
如何想把函数指针放入一个数组,可以这么定义:
//seq_array是数组,内放函数指针
const vector<int* (*seq_array[])(int)=
{fibon_seq, lucas_seq, pell_seq, triang_seq, squqre_seq, pent_seq};
函数指针和指针函数区别
指针函数是指带指针的函数,即本质是一个函数。函数返回类型是某一类型的指针:
//类型标识符 *函数名(参数表)
int *f(x,y);
函数指针是指向函数的指针变量,即本质是一个指针变量。
int (*f) (int x); /* 声明一个函数指针 */
f=func; /* 将func函数的首地址赋给指针f */
2.9 设定头文件(Header Fiels)
调用seq_elem()之前,必须先声明它,以便让程序知道它的存在。如果它被5个程序文件调用,就必须调用5次。C++提供一种简单的方法,把函数声明置于头文件中,并在每个程序文件代码文件中含入(include)这些函数声明。
头文件的扩展名,习惯上是.h。标准程序库例外,它们没有扩展名。我把我们的头文件命名为NumSeq.h,并将于数列处理相关的所有函数的声明都置于此文件中:
//NumSeq.h
bool seq_elem(int pos, int &elem);
const vector<int> *fibon_seq(int size);
const vector<int> *lucas_seq(int size);
const vector<int> *pell_seq(int size);
//...
注意:函数的定义只能有一份,不过声明可以有很多份。我们不能把函数的定义纳入头文件,因为同一个程序的多个代码文件可能都会含入这个头文件。
“只定义一份”的规则有个例外:inlne函数的定义。 为了能扩展inline函数的内容,在每个调用点上,编译器都得取得其定义。这意味着我们必须将inline函数的定义置于头文件中。
另外,const object和inline函数一样,是“一次定义规则”的例外。下面代码显示了把const object seq_cnt的定义加入到头文件NumSeq.h:
const int seq_cnt=;
为什么const object是是“一次定义规则”的例外?
因为const object的定义只要一出文件之外便不可见。这意味着我们可以在多个文件中加以定义,不会导致任何错误。
我们何时将const object加入头文件呢?
当它需要跨文件使用时。
如果想把指针放入头文件NumSeq.h,需要加上关键字extern:
const int seq_cnt=;
//seq_array是指向const object的指针
extern const vector<int>* (*seq_array[seq_cnt])(int);
以下代码是含入头文件iostream和NumSeq.h:
#include <iostream>
#include "NumSeq.h"
为什么头文件iostream用尖括号,而NumerSeq.h用双引号?
如果此文件被认定是标准的、或项目专属的头文件,我们便以尖括号括住:编译器搜索此文件时,会现在某些默认驱动器目录中寻找。
如果文件名由成队双引号括住,此文件 便被认为是一个用户自行提供的头文件:编译器搜索此文件时,会由含入此文件所在的驱动器目录开始找起。