有关 C++ 对象创建时的返回值的一些探讨
我一直有这样一种观点,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= 来复制这个返回值,这样就让效率降低了。
事实上,一般情况下,如果满足以下条件:
- 编译器能够判断代码是否返回的总是一个特定的对象;
- 用来“被赋予”返回值的对象是否是在初始化的时候被赋值。因为如果在初始化的时候使用等号运算符,并不会触发重载的 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 .
