所有的函数都使用 在程序运行栈中分配的存储区。该存储区一直保持与该函数相关联,直到函数结束为止。那时,存储区将自动释放以便重新使用。该函数的整个存储区称为活动记录。
系统在函数的活动记录中为函数的每个参数都提供了存储区, 参数的存储长度由它的类型来决定。 参数传递是指用函数调用的实参值来初始化函数参数存储区的过程 。
C++中参数传递的缺省初始化方法是把实参的值拷贝到参数的存储区中 这被称为按值传递 (pass-by-value )。
按值传递时 ,函数不会访问当前调用的实参。 函数处理的值是它本地的拷贝, 这些拷贝被存储在运行栈中, 因此改变这些值不会影响实参的值 。一旦函数结束了, 函数的活动记录将从栈中弹出 ,这些局部值也就消失了 。
在按值传递的情况下 ,实参的内容没有被改变。 这意味着程序员在函数调用时无需保存和恢复实参的值 。如果没有按值传递机制, 那么每个没有被声明为 const 的参数就可能会随 每次函数调用而被改变 。按值传递的危害最小, 需要用户做的工作也最少 。毫无疑问 ,按值 传递是参数传递合理的缺省机制。
但是 ,按值传递并不是在所有的情况下都适合 。不适合的情况包括 :
当大型的类对象必须作为参数传递时, 对实际的应用程序而言 ,分配对象并拷贝到 栈中的时间和空间开销往往过大。
当实参的值必须被修改时。 例如, 在函数 swap()中, 用户想改变实参的值 ,但是在 按值传递的情况下无法做到 。
// swap() 没有交换两个实参的值!
void swap( int v1, int v2 )
{
int tmp = v2;
v2 = v1;
v1 = tmp;
}
swap()交换实参的本地拷贝 ,代表 swap()实参的变量并没有被改变 ,这将在下面调用 swap()的程序中可以看出来:
#include <iostream>
void swap( int, int );
int main()
{
int i = 10;
int j = 20;
cout << "Before swap():\ti: "
<< i << "\tj: " << j << endl;
swap( i, j );
cout << "After swap():\ti: "
<< i << "\tj: " << j << endl;
return 0;
}
编译并执行程序产生如下结果
Before swap(): i: 10 j: 20
After swap(): i: 10 j: 20
为了获得期望的行为, 程序员可以使用两种方法, 一种方法是, 参数被声明成指针, 例 如 swap()可重写如下:
// pswap()交换 v1 和 v2 指向的值
void pswap( int *v1, int *v2 )
{
int tmp = *v2;
*v2 = *v1;
*v1 = tmp;
}
我们必须修改 main()来调用 pswap() ,现在程序员必须传递两个对象的地址而不是对象本身 。
pswap( &i, &j );
修改后的程序编译运行后的结果显示了它的正确性
// 使用指针使程序员能够访问当前调用的实参
Before swap(): i: 10 j: 20
After swap(): i: 20 j: 10
第二种方法是把参数声明成引用 例如 swap()可重写如下
// rswap() 交换 v1 和 v2 引用的值
void rswap( int &v1, int &v2 )
{
int tmp = v2;
v2 = v1;
v1 = tmp;
}
main()中 rswap()的调用看起来像原来的 swap()调用
rswap( i, j );
编译并运行这程序会显示 i 和 j 的值已经被正确交换了。
引用参数
把参数声明成引用, 实际上改变了缺省的按值传递参数的传递机制 。在按值传递时 ,函 数操纵的是实参的本地拷贝。 当参数是引用时, 函数接收的是实参的左值而不是值的拷贝, 这意味着函数知道实参在内存中的位置, 因而能够改变它的值或取它的地址。
什么时候将一个参数指定为引用比较合适呢? 像 swap()的情况 它必须将一个参数改变成指针来允许改变实参的值时就比较合适。 引用参数的第二种普遍用法是向主调函数返回额 外的结果, 第三种用法是向函数传递大型类对象 。
作为‘ 通过引用参数向主调函数返回额外结果’ 的函数的一个例子, 我们来定义一个被 称为 look_up()的函数 ,它在整型 vector 中查找一个特定的值。 如果找到了该值 则 look_up() 返回一个指向含有该值的 vector 元素的 iterator 迭代器, 否则 返问一个指向 vector 最后 一个元素下一位置的 iterator, 表明该值不存在。 在多次出现的情况下, 指向第一次出现的 iterator 被返回, 此外 look_up()用引用参数 occurs 返回该值出现的次数。
#include <vector>
// 引用参数 'occurs' 可以含有第二个返回值
vector<int>::const_iterator look_up(
const vector<int> &vec,
int value, // 值在 vector 中吗?
int &occurs ) // 多少次?
{
// res_iter 被初始化为最后一个元素的下一位置
vector<int>::const_iterator res_iter = vec.end();
occurs = 0;
for ( vector<int>::const_iterator iter = vec.begin(); iter != vec.end(); ++iter )
{
if ( *iter == value )
{
if ( res_iter != vec.end() )
res_iter = iter;
++occurs;
}
}
return res_iter;
}
把一个参数声明成引用的第三种情况是在向函数传递一个大型类对象时。 在按值传递情 况下 ,整个对象将随每次调用而被拷贝。 尽管按值传递对内置数据类型的对象和小型类对象 比较满意 ,但是对于大型类对象, 它的效率就太低了。 使用引用参数, 函数可以访问被指定 为实参的类对象 ,而不必在函数的活动记录中拷贝它 。例如 :
class Huge { public: double stuff[1000]; };
extern int calc( const Huge & );
int main() {
Huge table[ 1000 ];
// ... 初始化 table
int sum = 0;
for ( int ix=0; ix < 1000; ++ix )
// 函数 calc() 将指向 Huge 类型的数组元素指定为实参
sum += calc( table[ix] );
// ...
}
有人可能希望用引用参数以避免拷贝用作实参的大型类对象 ,同时, 又希望防止函数修改实参的值。 如果引用参数不希望在被调用的函数内部被修改, 那么把参数声明为 const 型 的引用是个不错的办法。 这种方式能够使编译器防止无意的改变。 例如 ,下列程序段违反了 foo()的参数 xx 的常量性, 因为 foo_bar()的参数不是 const 型的引用 ,所以我们不能保证 foo_bar()不会改变参数 xx 的值。 这违反了 foo()的参数 xx 的常量性 程序被编译器标记为错误 :
class X;
extern int foo_bar( X& );
int foo( const X& xx )
{
// 错误: const 传递给非 const
return foo_bar( xx );
}
为使该程序通过编译, 我们改变 foo_bar()的参数的类型 。以下两种声明都是可以接受的;
extern int foo_bar( const X& );
extern int foo_bar( X ); // 按值传递
或者可以传递一个 xx 的拷贝做实参 允许 foo_bar()改变它 ;
int foo( const X &xx )
{
// ...
X x2 = xx; // 拷贝值
// 当 foo_bar() 改变它的引用参数时, x2 被改变, xx 保持不变
return foo_bar( x2 ); // ok
}
我们可以声明任意内置数据类型的引用参数。 例如 ,如果程序员想修改指针本身, 而不 是指针引用的对象 ,那么他可以声明一个参数 ,该参数是一个指针的引用。 例如, 下面是交 换两个指针的函数 :
void ptrswap( int *&v1, int *&v2 )
{
int *tmp = v2;
v2 = v1;
v1 = tmp;
}
如下声明
int *&v1;
应该从右向左读 v1, 是一个引用, 它引用一个指针, 指针指向 int 型的对象。
引用和指针参数的关系
引用必须被初始化为指向一个对象 一旦初始化了。 它就不能再指向其他对象 。指针可以指向一系列不同的对象,也可以什么都不指向。因为指针可能指向一个对象或没有任何对象 ,所以函数在确定指针实际指向一个有效的对象之前不能安全地解引用 dereference 一个指针。 例如;
class X;
void manip( X *px )
{
// 在解引用指针之前确信它非 0
if ( px != 0 )
// 解引用指针
}
另一方面 对于指针参数 ,函数不需要保证它指向一个对象,引用必须指向一个对象 ,甚至在我们不希望这样时也是如此。
class Type { };
void operate( const Type& p1, const Type& p2 );
int main() {
Type obj1;
// 设置 obj1 为某个值
// 错误: 引用参数的实参不能为 0
Type obj2 = operate( obj1, 0 );
}
如果一个参数可能在函数中指向不同的对象 ,或者这个参数可能不指向任何对象 。则必 须使用指针参数
引用参数的一个重要用法是: 它允许我们在有效地实现重载操作符的同时 ,还能保证用 法的直观性。
数组参数
在 C++中, 数组永远不会按值传递. 它是传递第一个元素 ,准确地说是第 0 个 的指针。
例如, 如下声明
void putValues( int[ 10 ] );
被编译器视为
void putValues( int* );
数组的长度与参数声明无关, 因此 下列三个声明是等价的;
// 三个等价的 putValues()声明
void putValues( int* );
void putValues( int[] );
void putValues( int[ 10 ] );
因为数组被传递为指针, 所以这对程序员有两个含义 ;在被调函数内对参数数组的改变将被应用到数组实参上而不是本地拷贝上 ,当用作 实参的数组必须保持不变时 ,程序员需要保留原始数组的拷贝, 函数可以通过把参 数类型声明为 const 来表明不希望改变数组元素;
void putValues( const int[ 10 ] );
数组长度不是参数类型的一部分, 函数不知道传递给它的数组的实际长度, 编泽器也不知道。 当编译器对实参类型进行参数类型检查时, 并不检查数组的长度 。例如
void putValues( int[ 10 ] ); // 视为 int*
int main()
{
int i, j[ 2 ];
putValues( &i ); // ok: &i 是 int*; 潜在的运行错误
putValues( j ); // ok: j 被转换成第 0 个元素的指针
// 实参类型为 int*: 潜在的运行错误
return 0;
}
参数的类型检查只能保证 putValues()的两次调用都提供了 int*型的实参, 类型检查不能检验实参是一个 10 元素的数组 。