C++初学者指南 第九篇(3)

来源:互联网 发布:网络拓扑结构的概念 编辑:程序博客网 时间:2024/04/29 16:29

必备技能9.3:为函数传递对象作为参数

对象可以像其它类型的数据一样被作为参数传递给函数。对象传递给函数的时候采用的是C++传统的值传递方式。这就意味着是把对象的一个副本,而不是对象本身传递给函数的。因此,在函数内部对参数所做的修改都不会影响到传递到函数中的实参。下面的程序就演示了这一点:

//传递对象作为函数的参数#include <iostream>using namespace std;class MyClass{    int val;public:    MyClass(int i)    {        val = i;    }    int getval()     {        return val;    }    void setval(int i)    {        val = i;    }};void display ( MyClass ob ){    cout << ob.getval() << '\n';}void change(MyClass ob){    ob.setval(100) ; //对传入的实参没有作用    cout << "Value of ob inside change(): ";    display( ob );}int main(){    MyClass a(10);    cout << "Value of before calling change(): ";    display( a );    change( a );    cout <<"Value of a after calling change():";    display( a );    return 0;}

上面程序的输出如下:

Value of before calling change(): 10

Value of ob inside change(): 100

Value of a after calling change():10

正如程序的输出所示,在函数change()里面对ob的修改不会影响到main()函数中的对象a

构造函数,析构函数和传递对象

尽管上述的传递对象到函数中的方法是非常直观的,但是考虑到构造函数和析构函数,这样的传递方式会导致一些意想不到的问题。为了理解为什么会产生这种问题,我们看看下面的代码:

//构造函数,析构函数和传递对象#include <iostream>using namespace std;class MyClass{    int val;public:    MyClass(int i)    {        val = i;        cout << "Inside constructor\n";    }    ~MyClass()    {        cout << "Destructing\n";    }    int getval()    {        return val;    }};void display (MyClass ob){    cout << ob.getval() << '\n';}int main(){    MyClass a(10);    cout << "Before calling display().\n";    display(a);    cout << "After display() returns.\n";    return 0;}

上面程序的输出如下:

Inside constructor

Before calling display().

10

Destructing

After display() returns.

Destructing

注意上面输出的结果中有两个Desctructing

从上面的输出结果中我们可以看出,只有一次调用了构造函数,但是有两次调用了析构函数。为什么会这样呢?

当传递对象作为函数的参数的时候,会生成一个该对象的副本。这个副本就是传递给函数的参数。这就意味着有新的对象生成了。当函数结束的时候,作为参数的副本就要被销毁。这就引入了两个重要的问题:第一,在生成副本的时候,是否会调用构造函数?第二,当销毁副本的时候,是否调用析构函数?这两个问题的答案乍看上去会让我们大吃一惊!

在传递对象作为函数参数的时候执行的是逐位拷贝的操作,这样做的原因很容易理解。由于构造函数是用来对对象的某些方面进行初始化的,所以它不能被用在对一个已经存在对象的复制操作上来。因为这样的操作会修改对象的值。当我们传递一个对象作为参数的时候,我们想要使用的是该对象当前的状态,而不是它的初始状态。

然而,当函数结束的时候,作为实参副本的对象也要被销毁,需要调用析构函数。这样做是非常必要的,因为对象已经超出了其作用域。这样就是上面的程序为什么会两次调用析构函数的原因了。其中第一次就是当函数display()的参数超出其作用域的时候被调用;第二次是在main()中,当程序结束,对象a被销毁的时候。

小结一下:当对象的一个副本被创建出来作为函数的实参的时候,正常的构造函数是不会被调用的,而调用的是缺省的拷贝构造函数来进行逐位拷贝。然而,当该副本被销毁的时候(通常就是在超出其作用域的时候)则会调用析构函数。

传递对象的引用

另外一种传递对象到函数中的方法就是传递引用。此时是把对该对象的引用传递到了函数中,函数直接操作的是作为实参的对象。因此,在函数中对形参所做的修改都会影响到传递到函数中的实参。但是传递对象的引用到函数中并不适用于所有的场合。然而在适合的场合中,这样做有两大好处。第一,由于此时传递的只是对象的地址,而不是整个对象,传递对象的引用比传递对象更快速和更有效。第二,当传递对象的引用的时候,不会生成新的对象,因此也不需要浪费时间来对临时的对象调用构造函数或者析构函数。

下面的程序就演示了传递对象的引用:

//构造函数,析构函数和传递对象的引用#include <iostream>using namespace std;class MyClass{    int val;public:    MyClass(int i)    {        val = i;        cout << "Inside constructor\n";    }    ~MyClass()    {        cout << "Destructing\n";    }    int getval()    {        return val;    }    void setval(int i)    {        val = i;    }};void display (MyClass &ob){    cout << ob.getval() << '\n';}void change (MyClass &ob){    ob.setval(100);}int main(){    MyClass a(10);    cout << "Before calling display().\n";    display(a);    cout << "After display() returns.\n";    change( a );    cout << "After calling change().\n";    display( a );    return 0;}

程序的输出如下:

Inside constructor

Before calling display().

10

After display() returns.

After calling change().

100

Destructing

在上面的这个程序中,函数display()change()都是采用引用参数。因此实参的地址,而不是实参的一个副本,被传递到函数中了。这样函数是直接对实参进行操作。例如,当调用函数change()的时候,传递的是引用。因此,在函数change()中对形参的修改会影响到main()函数中传递给它的对象a。还要注意上面只调用了一次构造函数和析构函数。这是因为只有一个对象被创建和销毁。程序中没有临时的对象。

传递对象时的潜在问题

尽管从理论上来说,正常的值传递方式对于传递对象来说可以起到保护实参的目的。但是这种方式还是有可能对作为参数的对象造成影响,甚至是破坏。例如,当一个对象在生成的时候被分配了一些系统资源,比如内存,在该对象被销毁的时候,这些资源会被释放。这样以来,在函数中它的副本就会在析构函数被调用的时候释放了这些资源。这样就会造成问题。这是因为原始的对象依然还在使用这些资源。这种情况通常会导致原始的对象遭到了破坏。

解决这种问题的一个方法就是传递对象的引用,正如在前面程序中看到的那样。此时,不会生成对象的副本,因此也就不会在函数结束的时候调用析构函数。正如前面解释的那样,传递对象的引用还可以加快函数调用的速度。因此此时只需要传递对象的地址即可。然而,传递对象的引用不是适用于所有的情况。庆幸的是,还有一个更通用的解决方法:我们可以创建自己的拷贝构造函数。这样做使得我们可以自己决定对象的拷贝应该如何进行,这样也就避免了上面描述的问题。然而,在讨论拷贝构造函数之前,让我们先看看另外一个可以从拷贝构造函数获益的情形。