天天看点

《C++面向对象高效编程(第2版)》——4.10 “写时复制”的概念include include include include

本节书摘来自异步社区出版社《c++面向对象高效编程(第2版)》一书中的第4章,第4.10节,作者: 【美】kayshav dattatri,更多章节内容可以访问云栖社区“异步社区”公众号查看。

c++面向对象高效编程(第2版)

通过以上的讨论可知,tstring类相当易懂和易实现。如果经常使用该类的对象作为函数参数和按值返回的值,会出现什么情况?因为tstring类使用了深复制语义,如果tstring

《C++面向对象高效编程(第2版)》——4.10 “写时复制”的概念include include include include

图4-12

类对象中的字符数目很多,将花费很长的时间来复制字符和删除动态分配内存。这也意味着,创建对象和销毁对象的开销很大。我们设计tstring类的初衷,就是希望客户在使用字符串的地方,都能使用tstring类对象。但是,如果创建、复制、赋值和销毁这些对象的开销太大,难免客户避而远之。是否有办法可以优化实现,加快对象的复制速度?

的确,复制tstring类对象时,也要复制对象中的所有字符。但是,这样做太浪费时间。我们可以尝试修改实现,使其在建立多个tstring类对象副本时,让这些副本都共享原始字符串中的字符,并不真正复制它们。我们了解过如何实现这样的共享。重要的是,当某个副本企图修改(或甚至销毁)对象中的字符时,共享机制必须确保该副本(tstring类对象)获得一份自己的字符副本,而不会影响其他仍然共享字符的对象。例如(为理解以下代码,见图4-12)。

class tstring {

 public:

    // 构造函数

  tstring();  // 创建一个空字符串对象

    // 创建一个字符串对象,该对象包含指向字符的s指针。

    // s所指向的字符串必须以null结尾,通过s复制字符。

  tstring(const char* s);  

  tstring(char achar);  // 创建一个包含单个字符achar的字符串

  tstring(const tstring& arg);  // 复制构造函数

  ~tstring();  // 析构函数

   // 赋值操作符

  tstring& operator=(const tstring& arg);

   // 返回指向内部数据的指针,小心。

  const char* c_str() const { return _rp->_str; }

   // 这些方法将修改原始对象,将其他对象的字符附在 *this后。

   // 在字符串中改变字符的情况

  tstring& tolower();  // 将大写字符转换成小写

  tstring& toupper();  // 将小写字符转换成大写

    // 其他成员函数未显示

 private:

  struct stringrep {

   char* _str;  // 实际的字符

   unsigned _refcount;  // 对它引用的数目

   unsigned _length;   // 字符串中的字符数目

 };

 stringrep* _rp;  // 在tstring中唯一的数据成员

};<code>`</code>

// 其他非成员函数未作改动--此处未显示

每个tstring类对象都包含指向stringrep对象的指针。在复制tstring类对象时,只需复制_rp指针,就这么简单。实际上,也可以将strginrep设计成一个带有构造函数和析构函数的真正独立的类。但是在该例中,不用这样做。我们需要的只是一个字符指针和引用计数的占位符。参见图4-14理解以下代码:

tstring::tstring()

{

 _rp = new stringrep;

 _rp-&gt;_refcount = 1;

 _rp-&gt;_length = 0;

 _rp-&gt;_str = 0;

}

tstring::tstring(const char* s)

 _rp-&gt;_refcount = 1;   // 这是使用stringrep的唯一对象

 _rp-&gt;_length = strlen(s);

 _rp-&gt;_str = new char[_rp-&gt;_length + 1];

 strcpy (_rp-&gt;_str, s);

tstring::tstring(char achar)

 _rp-&gt;_length = 1;

 _rp-&gt;_str[0] = achar;

 _rp-&gt;_str[1] = 0;

 _rp-&gt;_refcount = 1;  // 这是使用stringrep的唯一对象

tstring::tstring(const tstring&amp; other)

// 这是最重要的操作之一。

// 我们需要在other中,通过_rp所指向的对象递增引用计数。它又获得一个引用。

 other._rp-&gt;_refcount++;

  // 让它们共享资源

 this-&gt;_rp = other._rp;

tstring&amp; tstring::operator=(const tstring&amp; other)

 if (this == &amp;other)

  return * this;  // 自我赋值

/* 这是另一个重要的操作。我们需要在other中,通过_rp所指向的对象递增引用计数。

同时,需要通过“this”指向的对象递减引用计数。 */

other._rp-&gt;_refcount++;  // 它又获得一个引用

// 递减和测试,是否仍然在使用它?

if (--this-&gt;_rp-&gt;_refcount == 0) {

  delete [] this-&gt;_rp-&gt;_str;

  delete this-&gt;_rp;

this-&gt;_rp = other._rp; // 让它们共享资源

return * this;

// 这是一个重要的成员函数,需要应用“写时复制”方案

tstring&amp; tstring::tolower()

 char* p;

 if (_rp-&gt;_refcount &gt; 1) {

  // 这是最困难的部分。分离tstring 对象并提供它的stringrep对象。

  // 这是“写时复制”操作。

  unsigned len = this-&gt;_rp-&gt;_length; // 保存它

  p = new char[len + 1];

  strcpy(p, this-&gt;_rp-&gt;_str);

  this-&gt;_rp-&gt;_refcount--; // 因为 *this即将离开内存池

  this-&gt;_rp = new stringrep;

  this-&gt;_rp-&gt;_refcount = 1;

  this-&gt;_rp-&gt;_length = len;

  this-&gt;_rp-&gt;_str = p;  // p在前面已创建

// 继续,并改变字符

p = this-&gt;_rp-&gt;_str;

if (p != 0) {

  while (*p) {

   p = tolower(p); ++p;

  }

 return * this;

tstring&amp; tstring::toupper() // 留给读者作为练习

 return *this;

tstring::~tstring()

 if (--_rp-&gt;_refcount == 0) {

   delete [] _rp-&gt;_str;

delete _rp;

}<code>`</code>

// 已省略其他成员函数的实现

下面的代码用于说明赋值操作符如何工作(见图4-15):

tstring x(“1234abcxyz”);

tstring y(x);

tstring z = x;

z.tolower();<code>`</code>

现在,分析一下tstring类的析构函数。当tstring类对象离开作用域后,如果不再使用_rp所指向的内存,必须将其删除。否则,我们只是减少了引用计数的值,并未清理内存就匆忙前进。

线程安全可移植性:

必须记住,在以上讨论的示例中,所有修改_refcount数据成员的地方,都不是多线程安全的操作。在需要多线程安全的情况中,必须保证这样的递增和递减操作是多线程安全的。方法是:使用操作系统特定的同步工具(甚至是在汇编语言例程中);或者,由一个不同的类(将在下一章中介绍)来处理这种针对处理器的操作,而且客户必须使用这个类。重要的是识别线程安全,如何实现它只是细节问题。

思考:

在上面的代码中,很多地方都需要创建、删除和操控stringrep对象。很明显,这并不是最好的方法。尝试修改实现,以便stringrep有自己的构造函数、析构函数以及其他函数。这样,stringrep便可自我管理。另外,完成tstring类的实现。

共享资源是大多数应用程序中十分常见的功能,在需要共享资源(无论是否有“写时复制”)时,使用引用计数是一种整洁的方案。引用计数促使实现更高效、更简洁,而且

《C++面向对象高效编程(第2版)》——4.10 “写时复制”的概念include include include include

图4-15

使应用程序运行得更快。引用计数为客户分担了资源管理的负担,并让其成为实现的一部分(这是正确的处理方法)。

上面使用的引用计数方案有一些与众不同的特点。

tsring类对象负责处理stringrep对象。可以把stringrep对象看成主对象(master object),它拥有存储区和引用计数。实际上,客户并不知道内部如何完成所有的工作,因为“写时复制”方案保证了她不会受到任何影响。客户总会认为自己拥有了tstring类对象副本,其实真正的实现远比这复杂得多。“写时复制”这个概念,在禁止高开销复制、需要更高效复制操作的地方非常有用。

《C++面向对象高效编程(第2版)》——4.10 “写时复制”的概念include include include include

图4-16

在不适合使用“写时复制”方案(因为主对象并不允许复制),但却需要共享的地方,我们将使用无“写时复制”的引用计数语义。在所有情况中,都必须考虑是否允许客户修改主对象。我们将在后面的章节中介绍更多相关的示例。

1这频繁用于操作系统中进程之间的页面共享。mach微处理器将该原则用于虚拟内存系统。unix系统通过调用vfork()也是为了相同的目的。

本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。

继续阅读