我一直有这样一种观点,C++ 中的引用返回,只适合返回已经存在了的对象的引用。如果想要返回一个在函数中新建的对象的引用,则是有问题的:因为这个新建对象要么放在堆中,要么放在栈中;若是放在了栈中,函数返回的时候,对象就被销毁了,这样的话,返回的引用也就失效了;若是放在堆中,返回的引用虽然是正确的,但是由于“引用”只是另一种形式的“指针”,所以即使引用的生命周期结束了,对象还是在堆里没有得到释放。

基于以上观点,我就觉得,如果想让函数创建一个对象,那么我们应该让函数返回对象的指针,或者直接返回对象本身。然后问题又来了,如果我们的程序里用到了指针,就不是很好看了。而且我们也不想整天记着去销毁这些对象,一疏忽,内存就泄露了。所以我还是偏好返回对象本身。

但是,返回对象本身就有效率问题了:对象在函数里,需要用到构造函数,在对象的构造和处理之后,返回这个对象,当赋值操作进行的时候,是否需要使用复制构造函数,或者需要等号运算符呢?如果需要的话,那临时变量又要销毁,这样一创建再销毁,不是浪费了吗?编译器能不能让函数直接在这个要赋值的空间上进行操作呢?

带着这么多疑问,我就写了下面一长串代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include <iostream>
#include <cstdlib>

using namespace std;

class CObject {
public:
    CObject(char a_chName) {
        this->m_iUniqueID = GetUniqueID();
        this->m_chName = a_chName;
        OutputName();
        cout << " constructed." << endl;
    }
    CObject(const CObject &foo) {
        this->m_iUniqueID = GetUniqueID();
        this->m_chName = foo.m_chName;
        OutputName();
        cout << " constructed as a copy of ";
        foo.OutputName();
        cout << endl;
    }
    virtual ~CObject() {
        OutputName();
        cout << " destructed." << endl;
    }
public:
    CObject &operator=(const CObject &foo) {
        OutputName();
        cout << " is being set to another object ";
        foo.OutputName();
        cout << endl;
        this->m_chName = foo.m_chName;
        return *this;
    }
public:
    void OutputName() const {
        cout << "Object " << this->m_chName << " (" << this->m_iUniqueID << ")";
    }
private:
    static int GetUniqueID() {
        return ++s_iUniqueID;
    }
public:
    static int s_iUniqueID;
public:
    char m_chName;
    int m_iUniqueID;
};

int CObject::s_iUniqueID = 0;

CObject &ReturnStackReference(char chName) {
    CObject Object(chName);
    return Object;
}

CObject &ReturnHeapReference(char chName) {
    CObject *pObject = new CObject(chName);
    return *pObject;
}

void TestFunction() {
    cout << endl;
    cout << "Entered TestFunction." << endl;
    cout << endl;

    cout << "Calling ReturnStackReference..." << endl;
    CObject &refObjectA = ReturnStackReference('A');
    cout << "ReturnStackReference called." << endl;
    cout << endl;

    cout << "Calling ReturnHeapReference..." << endl;
    CObject &refObjectB = ReturnHeapReference('B');
    cout << "ReturnHeapReference called." << endl;
    cout << endl;

    cout << "TestFunction returning..." << endl;
    cout << endl;
}

int main(int argc, char *argv[]) {
    cout << "Calling TestFunction..." << endl;
    TestFunction();
    cout << "TestFunction called." << endl;
    return EXIT_SUCCESS;
}

是返回堆空间(动态)中的引用还是返回栈空间(静态)中的引用?

这段代码在编译的时候直接就给了一个警告:

test.cpp: In function 'CObject& ReturnStackReference(char)':
test.cpp:53:17: warning: reference to local variable 'Object' returned

首先就否定了返回栈空间中对象引用的方法。以下是执行结果:

Calling TestFunction...

Entered TestFunction.

Calling ReturnStackReference...
Object A (1) constructed.
Object A (1) destructed.
ReturnStackReference called.

Calling ReturnHeapReference...
Object B (2) constructed.
ReturnHeapReference called.

TestFunction returning...

TestFunction called.

可以注意到,对象 B 并没有被析构,所以,这样做还是需要我们手动销毁对象的。综上,若是在函数中创建一个对象并想返回之,是不适合使用引用返回的。这也证明了我之前的观点是正确的,就是说,返回的引用最好是一个已经存在了的对象的引用。

那我觉得指针不好看,难道还要返回对象吗?会影响效率吗?

在函数中创建对象并返回其指针是在 C 里常见的做法。但是如果对指针产生太多依赖,代码的确会让人脑乱。所以我们通常只能返回一个对象。但是对象的返回,理论上需要先构造,经过一定的处理后再返回。返回的过程中,由于用来“被赋予”函数返回值的对象要调用 operator= 来复制这个返回值,这样就让效率降低了。

事实上,一般情况下,如果满足以下条件:

  1. 编译器能够判断代码是否返回的总是一个特定的对象
  2. 用来“被赋予”返回值的对象是否是在初始化的时候被赋值。因为如果在初始化的时候使用等号运算符,并不会触发重载的 operator= , 而是触发了复制构造函数。

这样,编译器就会将那个总是被返回的对象的内存空间直接指向“被赋予”返回值的对象的内存空间。也就是说,省掉了复制构造函数的执行。

我们在原来的代码里加入如下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CObject ReturnObjectCopyX(char chName) {
    cout << "Entered ReturnObjectCopyX." << endl;
    CObject ObjectCopy(chName);
    cout << "The real object was said to be created." << endl;
    CObject ObjectAnother('X');
    cout << "The cheating object was said to be created." << endl;
    if (true) {
        cout << "Returning the real object." << endl;
        return ObjectCopy;
    }
    cout << "Returning the cheating object." << endl;
    return ObjectAnother;
}

CObject ReturnObjectCopyY(char chName) {
    CObject Object(chName);
    CObject ObjectY('Y');
    return Object;
}

再在 TestFunction() 里加入如下代码:

1
2
3
4
5
6
7
8
9
    cout << "Calling ReturnObjectCopyX..." << endl;
    CObject ObjectD = ReturnObjectCopyX('D');
    cout << "ReturnObjectCopyX called." << endl;
    cout << endl;

    cout << "Calling ReturnObjectCopyY..." << endl;
    CObject ObjectE = ReturnObjectCopyY('E');
    cout << "ReturnObjectCopyY called." << endl;
    cout << endl;

大致的输出是这样的:

Calling ReturnObjectCopyX...
Entered ReturnObjectCopyX.
Object D (4) constructed.
The real object was said to be created.
Object X (5) constructed.
The cheating object was said to be created.
Returning the real object.
Object D (6) constructed as a copy of Object D (4)
Object X (5) destructed.
Object D (4) destructed.
ReturnObjectCopyX called.

Calling ReturnObjectCopyY...
Object E (7) constructed.
Object Y (8) constructed.
Object Y (8) destructed.
ReturnObjectCopyY called.

这就说明,如果编译器发现,函数始终返回的是一个对象的话,那么,这个对象实际上就是将来要“被赋值”的对象。

从这里也可以说明,以下第 2 和 3 行是等效的:

1
2
3
CObject ObjectExample('N');
CObject Object = ObjectExample; // You can write this way...
CObject Object(ObjectExample); // Or this way.

就是说在初始化的时候使用等号,相当于使用了复制构造函数,而不是重载的 operator= .


最后我尝试了一下这些代码:

1
CObject *pObjectF = &ReturnObjectCopyY('F');

编译器给了警告:

test.cpp: In function 'void TestFunction()':
test.cpp:112:44: warning: taking address of temporary

这个,大家应该知道是怎么回事。


最后,代码发出来,献丑了:cpp-test-code-returning-object.cpp

下载完后请把扩展名改成 cpp .

原创文章,转载请注明来源:http://euyuil.com/2164/discussion-about-returning-objects-in-cpp/