指针 引用和auto_ptr,论接口参数设计的原则

noiile 2005-12-1

首先,我们来分析下面一段程序:

#include <iostream>
#include <string>
using namespace std;

class Obj { public: Obj() {cout << "Default Constructor/n";} Obj(const obj& o) {cout << "Copy Constructor/n";}

string getName() {return name;}

string name; };

void printObjName(Obj o) { cout << o.getName() << endl; }

int main() { Obj myobj; myobj.name = "hello"; printObjName(myobj); return 0; }

以上程序实际上会构建多少个obj对象呢?
1个?错,是两个!
其执行出来的结果是:
Default Constructor
Copy Constructor

我们可以看到,第二个对象是调用copy constructor,但无论如何程序是创建了两个对象,第二个对象是什么时候创建的呢?很显然,是将myobj传进printObjName这个函数时创建的,因为printObjName的参数类型是obj,所以这里编译器调用复制构造函数,构造一个新的对象。
但是,printObjName仅仅是为了打印该对象的名字,为此而创建一个对象,对于C++程序员来说,这简直是件不能忍受的事情,为此,需要浪费构建一个对象的内存和CPU时间。

实际上还不止如此,当调用obj的成员方法getName()的时候,实际上也对name这个字符串对象进行了一次复制。别忘了,name也是个对象。而getName的返回值是string。这种情况更容易让程序员忽略,因为从代码来看只调用getName这个方法而已,并没有任何显式的对象。但编译器会为它隐式的生成一个临时对象,因为getName()的返回值是string。
更要命的是,这个get方法我们可能会频繁的使用。

这种情况很容易避免,只要我们在函数的参数或返回值的类型使用指针或引用。
例如前面那个程序,将函数的参数或返回值改成常量引用。
将程序修改如下:

class obj
{
public:
    obj() {cout << "Default Constructor/n";}
    obj(const obj& o) {cout << "Copy Constructor/n";}

const string& getName() const {return name;}

string name; };

void printObjName(const obj& o) { cout << o.getName() << endl; }

int main() { obj myobj; myobj.name = "hello"; printObjName(myobj); return 0; }

如此一来,程序再不会浪费半点无用的内存或者CPU时间,看起来完美的多了。

但是,似乎换了指针,也能行的通,确实如此。那我们是应该什么时候用指针什么时候用引用呢?

在弄明白这个问题之前,得先了解指针和引用的区别:
引用保证了对象必须存在,不能为空,也不能更换,也不能delete。
指针允许为空,也允许重新指向别的对象,而且有被别人delete的可能。

很显然,如果我们希望必须有对象传进来,那么就用引用,如果我们不打算修改传进来的对象,那么我们就用常量引用,告诉使用接口的人,”hey,我不会修改你的对象,你放心传进来吧”。

如果我们允许有空对象传进来,那当然用指针了。当我们不会修改指针所指的对象时,就应该用常量指针。我们设计接口,必须要清晰的告诉使用接口的人,我们会对他传进来的对象干什么,会修改它吗?还是只是使用它而已,还是会delete它?
但是,仅用指针似乎不能够表达这一切。试想下,假如接口函数因为只有你才知道的原因,需要获得传进来的指针对象的生命控制权,也就是说,你的接口函数有可能delete使用者传进来的指针。怎么能通过接口告诉使用者呢?
或许你会说,既然常量指针,表示我们不能修改对象的内容,那当然也不能delete它了,相反,非常量指针,就表示我们要获得对象的生命控制权啦。

void testGetObj(const Obj* obj)
{
    cout << obj->getName() << endl;
}

void testGetObj2(Obj* obj) { cout << obj->getName() << endl; delete obj; }

但是,还不足够,如果我们的接口函数既要修改对象的内容(不能使用const),但又不需要对对象的生命周期负责,那怎么办?如果用非常量指针,使用者如果按照之前所说的原则判断,认为接口会delete对象,而实际上接口函数没有delete对象,那会照成很头痛的后果——内存泄露!

实际上,我们还应该尽量遵循另一个原则,对象由谁生成(new),就应该由谁销毁(delete)。除非你能很清晰的告诉使用接口的人,你会delete这个对象。

所幸,标准库为我们提供了一个很好的解决方案:auto_ptr。
将一个指针赋给智能指针auto_ptr,那么智能指针将会具有指针对象的拥有权,它会在生命结束之前,自动delete指针所指的对象。

很好,到现在,我们可以解决问题了,我们定义规则如下:
如果我们要拥有对象的生命周期,那么我们的接口应该使用auto_ptr.

void testGetObj(auto_ptr<Obj> obj);

如果我们不需要拥有对象的生命周期,也不需要修改指针所指的对象,那么我们用常量指针。

void testGetObj(const Obj* obj);

如果我们不需要拥有对象的生命周期,但是需要修改指针所指的对象,那么我们用非常量指针。

void testGetObj(Obj* obj);

对于接口函数的返回值,我们同样也能用以上原则。
对于Obj的getName函数,或许会很频繁的被调用,为了避免无谓的对象复制,我们应该用常量引用(因为不希望使用者随便修改名字)

const string& getName() const;

由使用者来决定,是否对该对象进行复制。如果使用者需要对该对象进行复制,那么他可以这样写

const string myObjName = obj.getName();

当然,如果他要修改,则必须进行常量转换const_cast。
更多的使用,使用者不需要对该对象进行复制,那么他可以这样写:

const string& myObjName = obj.getName();

作为接口的设计者,你必须保留使用者的这种权利,而不应该强制使用者对你的返回对象进行复制。

不过,有种情况,很容易让我们犯错误,看下面这个例子:

const string& getName() const
{
    string name = “Obj”;
    return name;
}

int main() { cout << getName << endl; return 0; }

想想这段程序会打印什么出来?天知道!问题出在哪里?因为string name是一个临时变量,它的生命周期仅限于这个函数体内,而函数的返回值是对临时变量的引用,一超出函数的范围,这个对象是无效的,所以那个引用的返回值已经是个无效的对象。

well,到这里已经讲完我要讲的东西,最后来小结一下:

我们设计接口,必须要遵循一定的规则,让使用接口的人能清楚知道该传什么类型的对象进去,而返回的对象又有可能是什么.

根据指针,引用,和智能指针的特点,我们定义行为规范如下:

  1. 以下规则,通常情况下对基础类型无效(因为通常情况下,基础类型不大需要考虑值拷贝的消耗)
  2. 如果没有特殊需要,我们尽量避免接口的参数,返回值是对象类型,以省去对象的复制所带来无谓的消耗.
  3. 当接口的参数一定是存在值的时候,我们应该用引用,如果我们的函数不需要修改传进来的参数, 则应该用常量引用.
  4. 如果接口的参数不一定存在值,有时可以是空对象的时候,而且对传进来的指针参数,函数不会控制它的生命周期,我们应该用指针,同样,如果对该指针对象我们不会修改,则该接口的参数,应该用常量指针.
  5. 如果当函数需要控制参数的指针对象的生命周期的时候,我们应该用auto_ptr。
  6. 通常,我们类的一些get函数,我们更应该用引用。由调用该函数的人来决定是否复制该对象。当然,如果返回的是一个临时对象(比如该对象是函数体内的局部变量),则不能用引用。

你可能感兴趣的:(String,delete,Class,iostream,Constructor,编译器)