天天看点

《C++面向对象高效编程(第2版)》——4.8 为什么需要副本控制

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

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

在讨论了对象的复制和赋值后,现在来学习为什么需要副本控制。你可能形成这样的一种观点,即每个类都应该提供public复制构造函数和赋值操作符函数。

但是,实际并非如此。很多情况都存在禁止复制对象的语义;另外某些情况下,复制可能仅对一组选定的客户有意义;甚至还有些情况,只允许限定数量的对象副本。所有这些情况都要求有正确且高效的副本控制。在接下来的内容中,我们将举例说明副本控制的必要性。一旦了解这些示例,你将体会到,c++基于每个类提供的副本控制机制如此地灵活。控制创建对象和复制对象的一般技巧将在后面章节中介绍。

假设有一个tsemaphore类。信号量(semaphore)用于过程(或线程)间的同步,以确保安全共享资源。当一个过程需要使用一个共享资源时,该过程需要靠获得守护资源的信号量来确保互斥。这可以通过tsemaphore类提供的acquire方法完成。如果所需资源已被其他任务获得,则acquire调用发生阻塞,而且调用的任务将等待,直到其他任务放弃(relinquish)资源。有可能出现多个任务同时等待相同资源的情况。

一旦任务获得资源,它便完全拥有该资源的所有权,直至通过调用release成员函数放弃资源。鉴于此,复制信号量对象是否正确?更确切地说,复制信号量对象的语义是什么?如果允许复制,那么是否意味着有两个都已获得相同资源的独立信号量对象?这在逻辑上不正确,因为任何时候一个进程只能获得一个资源(或在已统计信号量的情况下有限数量的进程)。或者,这意味着两个信号量对象共享相同的资源?共享状态可能是一个较好的解决方案,但是,这使得信号量的实现和使用复杂化。信号量被看做是经常使用的“轻量级”对象,使其实现复杂化并不合理。在支持任何复制操作之前,还需要澄清一个问题:也许更好的解决方案应该是禁止任何复制。这意味着一旦创建信号量,任何人都不能复制它。

以下是tsemaphore类的接口。

class tautosemaphore {

    public:

      tautosemaphore(tsemaphore& sem)

        : _semaphore(sem)

      { _semaphore.acquire(); }

      ~tautosemaphore() { _semaphore.release(); }

    private:

      tsemaphore& _semaphore;

};

利用这个类,f()中的代码可以简化为:

void x::f() // x的成员函数

{

   // 创建tautosemaphore类对象,同时也获得信号量。

   tautosemaphore autosem(_sem);

   // 希望完成的任务

   if (/ 某些条件 /) { / 一些代码 / return; }

   else { / 其他代码 / }

   // autosem的析构函数在退出f()时,自动释放_sem信号量  

}<code>`</code>

tautosemaphore类的构造函数期望传入一个信号量对象,并将信号量作为构造函数的一部分。tautosemaphore类的析构函数负责释放所获得的信号量。因此,一旦在某作用域内创建了tautosemaphore类的对象,它的析构函数将会确保释放已获得的信号量,程序员无需为此担心。至少现在看来,需要我们管理的事务又少了一件。

这样的类在c++程序中非常普遍。另一个类似的类是ttracer,它用于跟踪进入函数和从函数退出。

class tlicensetoken; // 前置声明

class tlicenseserver {

 public:

  // 构造函数 – 创建一个有maxusers个许可证的新许可证服务

  tlicenseserver(unsigned maxusers);

  ~tlicenseserver();

  // 授予新许可证或返回0。主调函数采用已返回的对象。

  // 不再使用令牌时,应将其销毁 – 见下文

  tlicensetoken* createnewlicense();

 private:

  // 对象不能被复制或赋值

  tlicenseserver(const tlicenseserver&amp; other);

  tlicenseserver&amp; operator=(const tlicenseserver&amp; other);

  unsigned _numissued;

  unsigned _maxtokens;

  // 省略若干细节

class tlicensetoken {

  public:

   tlicensetoken();

   ~tlicensetoken();

  tlicensetoken(const tlicensetoken&amp; other);

  tlicensetoken&amp; operator=(const tlicensetoken&amp; other);

};<code>`</code>

既然tlicensetoken是由tlicenseserver以用户为单位而发出的,那么确保用户无法复制返回的令牌非常重要。否则,许可证服务器将无法控制用户的数量。每当新用户希望使用由许可证服务器控制的应用程序时,他请求tlicenseserver生成一个新的tlicensetoken类对象。如果可以生成新令牌,则返回一个指向新tlicensetoken的指针。该令牌由调用者所拥有,用户不再需要使用应用程序时,必须销毁它。当许可证令牌被销毁时,它将与许可证服务器通信,以减少未归还许可证令牌数目。注意,许可证服务器和令牌都不能被复制,用户不可以复制令牌。许可证令牌可包含许多信息,如任务标识号、机器名、用户名、产生令牌的日期等。因为许可证服务器和令牌的复制构造函数和赋值操作符都为私有,所以不可能复制令牌,这便消除了使用欺骗手段的可能性。

要求用户销毁令牌是件麻烦事。我们可以完成这样的实现,即在令牌追踪软件使用的同时,如果软件在预定时间内未被使用,该实现保证能自动地销毁许可证令牌。实际上,这样的实现十分常见。

账单管理是该实现的一个应用,可根据客户所使用的服务来收费。这广泛应用于有线电视的按次计费的程序中2。

你可能觉得不允许复制令牌的限制过于严格。但是,如果允许这样做应该考虑创建一个新令牌,并通知许可证服务器进行复制。可以完成这样的实现,这仍然需要副本控制。

4.8.3 字符串类示例

各种语言的程序员都使用字符串来显示错误消息、用户提示等,我们也经常使用和操控这样的字符串数组。字符串数组的主要问题是存储区管理和缺少可以操控它们的操作。在c和c++中,字符串数组不能按值传递,只能传递指向数组中第1个字符的指针。这很难实现安全数组。为克服这个障碍,我们应该实现一个tstring类提供所有必须的功能。tstring类对象管理自己的内存,而且它会在需要时分配更多的内存,我们无需为此担心。

注意:

c++标准库包含一个功能强大的string类,也用于处理多字节字符。由于string类易于理解,同时能清楚地说明概念,因此在下面的示例中将用到它。

以下是类tstring的声明:

tstring::tstring()

  _str = 0;

  _length = 0;

}

tstring::tstring(const char* arg)

  if (arg &amp;&amp; *arg) {  // 指针不为0,且指向有效字符。

    _length = strlen(arg);

    _str = new char[_length + 1];

    strcpy(_str, arg);

  }

  else {

   _str = 0;

   _length = 0;

tstring::tstring(char achar)

  if (achar) {

    _str = new char[2];

    _str[0] = achar;

    _str[1] = ‘0’;

    _length = 1;

    _str = 0; _length = 0;

 }

tstring::~tstring() { if (_str != 0) delete [] _str; }

// 复制构造函数,执行深复制。为字符分配内存,然后将其复制给this。

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

  if (arg._str!= 0) {

    this-&gt;_str = new char[strlen(arg._str) + 1];

    strcpy(this-&gt;_str, arg._str);

    _length = arg._length;

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

  if (this == &amp;arg)

   return *this;

  if (this-&gt;_length &gt;= arg._length) {// *this足够大

    if (arg._str != 0)

      strcpy(this-&gt;_str, arg._str);

    else

      this-&gt;_str = 0;

    this-&gt;_length = arg._length;

    return *this;

  // *this没有足够的空间,_arg更大.

  delete [] _str; // 安全

  this-&gt;_length = arg.size();

  if (_length) {

    strcpy(_str, arg._str);

  else _str = 0;

  return *this;  // 总是这样做

tstring&amp; tstring::operator=(const char* s)

  if (s == 0 || *s == 0) { // 源数组为空,让“this”也为空。

    delete [] _str;

    _length = 0; _str = 0;

    _str = 0;

 int slength = strlen(s);

 if (this-&gt;_length &gt;= slength) { //*this足够大

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

  this-&gt;_length = slength;

  return *this;

 // *this没有足够的空间,_arg更大。

 delete [] _str; // 安全

 this-&gt;_length = slength;

 _str = new char[_length + 1];

 strcpy(_str, s);

 return *this;

tstring&amp; tstring::operator=(char chartoassign)

 char s[2];

 s[0] = chartoassign;

 s[1] = ‘0’;

 // 使用其他赋值操作符

 return (*this = s);

int tstring::size() const { return _length; }

tstring&amp; tstring::operator+=(const tstring&amp; arg)

  if (arg.size()) {  // 成员函数可调用其他成员函数

    _length = arg.size() + this-&gt;size();

    char *newstr = new char[_length + 1];

    if (this-&gt;size())  // 如果原始值不是null字符串

      strcpy(newstr, _str);

      *newstr = ‘0’;

    strcat(newstr, arg._str); // 附上参数字符串

    delete [] _str;  // 丢弃原始的内存

    _str = newstr;   // 这是创建的新字符串

tstring operator+(const tstring&amp; first, const tstring&amp; second)

  tstring result = first;

  result += second; // 调用operator+=成员函数

  return result;

bool operator==(const tstring&amp; first, const tstring&amp; second)

  const char* fp = first.c_str(); // 调用成员函数

  const char* sp = second.c_str();

  if (fp == 0 &amp;&amp; sp == 0) return 1;

  if (fp == 0 &amp;&amp; sp) return -1;

  if (fp &amp;&amp; sp == 0) return 1;

  return ( strcmp(fp, sp) == 0); // strcmp是一个库函数

bool operator!=(const tstring&amp; first, const tstring&amp; second)

{ return !(first == second); }  // 复用operator==

// 其他比较操作符的实现类似operator== ,

// 为了简洁代码,未在此处显示它们。

char tstring::operator()(unsigned n) const

  if (n &lt; this-&gt;size())

    return this-&gt;_str[n]; // 返回下标为n的字符

  return 0;

const char&amp; tstring::operator[](unsigned n)const

  cout &lt;&lt; “invalid subscript: ” &lt;&lt; n &lt;&lt; endl;

 exit(-1);  // 应该在此处抛出异常

  return _str[0];  // 为编译器减轻负担(从不执行此行代码)

// 将每个字符变成小写

tstring&amp; tstring::tolower()

  // 使用tolower库函数

  if (_str &amp;&amp; *_str) {

    char *p = _str;

    while (*p) {

      p = tolower(p);

      ++p;

    }

   }

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

tstring tstring::operator()(unsigned posn, unsigned len) const

  int sz = size(); // 源的大小

  if (posn &gt; sz) return “ ”; // 空字符串

  if (posn + len &gt; sz) len = sz – posn;

 tstring result;

  if (len) {

    result._str = new char[len+1];

    strncpy(result._str, _str + posn, len);

    result._length = len;

    result._str[len] = ‘0’;

ostream&amp; operator&lt;&lt;(ostream&amp; o, const tstring&amp; s)

  if (s.c_str())

    o &lt;&lt; s.c_str();

   return o;

istream&amp; operator&gt;&gt;(istream&amp; stream, tstring&amp; s)

  char c;

  s = “ ”;

  while (stream.get(c) &amp;&amp; isspace(c))

    ;// 什么也不做

  if (stream) { // stream正常的话,

    // 读取字符直至遇到空白

    do {

    s += c;

    } while (stream.get(c) &amp;&amp; !isspace(c));

    if (stream)  // 未读取额外字符

      stream.putback(c);

  return stream;

1译者注:服务器指一个管理资源并为用户提供服务的计算机软件,另外,运行这样软件的计算机或计算机系统也被称为服务器。这里,作者为区分两种服务器,用server machine表示运行服务器的计算机,用license server表示许可证服务器。

2观众需要为观看(订阅)固定数量的节目支付费用。这些节目包括最新的电影、体育节目甚至是直播,或者音乐会。

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

继续阅读