C++ 11的移动语义 - 清晰的示例及浅显的说理
C++ 11引入了移动语义以提高对象“复制”的效率,这种复制效率对于容器而言至关重要。清晰明了地向学生解释移动语义、右值引用并不是一件容易的事,为此,我们设计了一个简单明了的示例,化繁为简地说理,试图解决这一问题。
本文引用自作者编写的下述图书; 本文允许以个人学习、教学等目的引用、讲授或转载,但需要注明原作者"海洋饼干叔
叔";本文不允许以纸质及电子出版为目的进行抄摘或改编。
1.《Python编程基础及应用》,陈波,刘慧君,高等教育出版社。免费授课视频 Python编程基础及应用
2.《Python编程基础及应用实验教程》, 陈波,熊心志,张全和,刘慧君,赵恒军,高等教育出版社Python编程基础及应用实验教程
3. 《简明C及C++语言教程》,陈波,待出版书稿。免费授课视频
19.9 移动语义*
19.9.1 对象复制的编译优化
编译器会穷尽所能进行代码优化,避免不必要的对象复制行为。在下述代码中,我们定义了一个Message类,其中包含一个动态分配的缓冲区buffer用于存储真正的消息文本。此外,Message类还定义了拷贝构造函数以及自定义operator=()操作符函数。理论上,一个Message对象可以十分“巨大”,对其进行复制费时费力。
//Project - MessageCopy
#include <iostream>
#include <cstring>
using namespace std;class Message {char* buffer = nullptr;
public:int id = 0;Message(){ cout << "Constructor, id = " << id << endl; }Message(int id, const char* text){cout << "Constructor, id = " << id << endl;this->id = id;buffer = new char[strlen(text)+1];strcpy(buffer,text);}Message(const Message& r){cout << "Copy Constructor, from " << r.id << " to " << id << endl;id = r.id;if (buffer) delete [] buffer;buffer = new char[strlen(r.buffer)+1];strcpy(buffer,r.buffer);}Message& operator=(const Message& r){cout << "operator=(), from " << r.id << " to " << id << endl;id = r.id;if (buffer) delete [] buffer;buffer = new char[strlen(r.buffer)+1];strcpy(buffer,r.buffer);return *this;}const char* content() const { return buffer; }~Message(){cout << "Destructor, id = " << id << endl;if (buffer) delete[] buffer;}
};Message fetchMessage(){Message m(1,"Washington, this is pearl harbour, we are under japanese attack!");return m;
}int main() {Message s = fetchMessage();cout << "Message " << s.id << ": " << s.content() << endl;return 0;
}
上述代码的执行结果为:
Constructor, id = 1
Message 1: Washington, this is pearl harbour, we are under japanese attack!
Destructor, id = 1
第44 ~ 47行:fetchMessage()函数构造“局部”对象m,然后返回。
第50行:main()函数的“局部”对象s接收fetchMessage()的返回对象。
逻辑上,上述代码至少存在两个Message对象,分别是main()函数内的s以及fetchMessage()函数里的m。多数读者会推导出如下的代码执行序列:m被构造并返回;返回的m作为参数参与s的拷贝构造;m被析构;s在main()函数返回时被析构。但作者计算机上的执行结果不支持上述推导,整个程序的生命周期内,只有编号为1的对象被构造及析构,整个程序事实上只生成了一个Message对象!
显然,这是编译器代码优化的结果。编译器认为先构造一个临时对象m再复制给s是没有必要的,它选择绕过中间对象的m,直接构造s:在fetchMessage()函数内对对象m进行的操作,事实上发生在外部的s对象上。从程序结果上看,编译器做得很好,省时省力且没有“误解”程序员的本意。
但编译器还没有厉害到可以完美地避免一切不必要的对象复制的程度。将上述代码的main()函数稍作调整:在第50行先构造对象s,然后再用s接受fetchMessage()返回对象的赋值。
int main() {Message s;s = fetchMessage();cout << "Message " << s.id << ": " << s.content() << endl;return 0;
}
调整后的代码在作者的计算机上获得了如下的执行结果:
Constructor, id = 0
Constructor, id = 1
operator=(), from 1 to 0
Destructor, id = 1
Message 1: Washington, this is pearl harbour, we are under japanese attack!
Destructor, id = 1
根据程序执行结果,我们可以逐行反推代码的执行序列:1).编号为0的对象s被构造;2).编号为1的对象m被构造;3).由于s已存在,返回对象m通过s的operator=()操作符函数复制给s;4).对象m析构;5).打印s的内容;6).对象s析构,由于s由m复制而来,所以执行结果第6行显示的编号为1。
“不必要”的对象复制发生在第3步。在operator=()操作符函数里,s对象分配了新的缓冲区buffer,然后一个字节又一个字节地从m对象复制缓冲区内容。考虑到临时对象m将很快被销毁,如果直接将m对象的缓冲区“偷”走,直接“挪”给s对象,将显著提高程序的执行效率。【C++ 11】引入了移动语义(move semantics)来解决这个问题。
19.9.2 右值引用
int a = 69;
a = a + 3;
C++标准引入了术语左值(lvalue)和右值(rvalue)来区分两种不同类型的对象。上述代码中的a具有确定的内存地址,它可以被赋值,我们称a为一个左值对象。在机器语言层面,表达式a+3的计算通常是借助于CPU寄存器来完成的,然后再从寄存器复制到对象a的内存。这个位于寄存器的临时对象没有确定的内存地址且“用完即弃”,我们称该对象为一个右值对象。
C++ 11的移动语义 - 清晰的示例及浅显的说理相关推荐
最新文章
热门文章 |
---|