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

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

4.8 为什么需要副本控制

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

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

4.8.1 信号量示例

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

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

以下是TSemaphore类的接口。

class TSemaphore {
   public:
      // 默认构造函数
     TSemaphore();
      // 由客户调用,以获得信号量。
     bool Acquire();
      // 不再需要独占访问资源时,调用此函数。
     void Release();
      // 有多少资源正在等待使用该资源?
     unsigned GetWaiters() const;
   private:
      // TSemaphore对象不能被复制或赋值
     TSemaphore(const TSemaphore& other);
     TSemaphore& operator=(const TSemaphore& other);
     // 细节省略
};
信号量(资源)的自动获得和释放```

程序员在使用TSemaphore这样的类时,必须记住使用Acquire成员函数来获得信号量。更重要的是,在离开函数前必须释放信号量(使用Release)。典型代码如下:
``
class X {
  public:
     // 成员函数
    void f();
  private:
    TSemaphore _sem;
};
void X::f() // X的成员函数
{
     // 获得已锁定的信号量
    _sem.Acquire();
     // 希望完成的任务
    if (/* 某些条件 */) {/* 一些代码 */ _sem.Release();
      return; }
    else { /* 其他代码 */ _sem.Release(); }
}```
必须记住,在每退出f()函数时都要释放信号量。这很容易出错,为避免这样的麻烦,我们可以使用辅助类来自动获得和释放信号量,如下TAutoSemaphore类所示。

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

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

class TTracer {
   public:
      #ifdef DEBUG
      TTracer(const char message [])
        : _msg(message)
      { cout << “>> Enter ” << _msg << endl; }
      ~TTracer() { cout << “<<Exit “ << _msg << endl; }
   private:
      const char* _msg;
      #else
      TTracer(const char message []) { }
      ~TTracer() { }
      #endif
};```
在后面的章节中,可以找到更多这样的例子。
这种类的实现是操作系统(和处理器)特定的,它甚至需要使用汇编语言代码。

##4.8.2 许可证服务器示例
另举一例,假设有一个允许站点注册的软件包。公司可以为固定数量的用户购买站点许可证,而不是购买同一个应用程序的多个独立副本。现在,虽然只有一份软件的副本(因此需要较少存储区),但公司里的每个人(受限于许可证授予的数量)都可以使用。可以在服务器(server machine)上运行许可证服务器(license server)1,为任何想使用此软件的人授权许可证令牌(license token)。只有当未归还许可证令牌(outstanding token)数量少于需要授权的站点数量时,才会发出许可证令牌。TLicenseServer类如下所示。

class TLicenseToken; // 前置声明
class TLicenseServer {
 public:
  // 构造函数 – 创建一个有maxUsers个许可证的新许可证服务
  TLicenseServer(unsigned maxUsers);
  ~TLicenseServer();
  // 授予新许可证或返回0。主调函数采用已返回的对象。
  // 不再使用令牌时,应将其销毁 – 见下文
  TLicenseToken* CreateNewLicense();
 private:
  // 对象不能被复制或赋值
  TLicenseServer(const TLicenseServer& other);
  TLicenseServer& operator=(const TLicenseServer& other);
  unsigned _numIssued;
  unsigned _maxTokens;
  // 省略若干细节
};
class TLicenseToken {
  public:
   TLicenseToken();
   ~TLicenseToken();
 private:
  TLicenseToken(const TLicenseToken& other);
  TLicenseToken& operator=(const TLicenseToken& other);
  // 省略若干细节
};`
既然TLicenseToken是由TLicenseServer以用户为单位而发出的,那么确保用户无法复制返回的令牌非常重要。否则,许可证服务器将无法控制用户的数量。每当新用户希望使用由许可证服务器控制的应用程序时,他请求TLicenseServer生成一个新的TLicenseToken类对象。如果可以生成新令牌,则返回一个指向新TLicenseToken的指针。该令牌由调用者所拥有,用户不再需要使用应用程序时,必须销毁它。当许可证令牌被销毁时,它将与许可证服务器通信,以减少未归还许可证令牌数目。注意,许可证服务器和令牌都不能被复制,用户不可以复制令牌。许可证令牌可包含许多信息,如任务标识号、机器名、用户名、产生令牌的日期等。因为许可证服务器和令牌的复制构造函数和赋值操作符都为私有,所以不可能复制令牌,这便消除了使用欺骗手段的可能性。

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

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

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

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

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

/*
* 一个字符串类的实现,基于ASCII字符集。
* TString类对象可以被复制和赋值。该类实现了深复制。
* 用这个类代替 “ ”字符串。
*/
#include <iostream.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
class TString {
 public:
   // 构造函数,创建一个空字符串对象。
  TString();
   // 创建一个字符串对象,该对象包含指向字符的s指针。
   // s必须以NULL结尾,从s中复制字符。
  TString(const char* s);
   // 创建一个包含单个字符aChar的字符串
  TString(char aChar);
  TString(const TString& arg);  // 复制构造函数
  ~TString();  // 析构函数
   // 赋值操作符
  TString& operator=(const TString& arg);
  TString& operator=(const char* s);
  TString& operator=(char s);
   // 返回对象当前储存的字符个数
  int Size() const;
   // 返回posn中len长度的子字符串
  TString operator()(unsigned posn, unsigned len) const;
   // 返回下标为n的字符
  char operator()(unsigned n) const;
   // 返回对下标为n的字符的引用 
  const char& operator[](unsigned n) const;
   // 返回指向内部数据的指针,当心。
  const char* c_str() const { return _str; }
  // 以下方法将修改原始对象。
   // 把其他对象中的字符附加在 *this后
  TString& operator+=(const TString& other);
   // 在字符串中改动字符的情况
  TString& ToLower();  // 将大写字符转换成小写
  TString& ToUpper();  // 将小写字符转换成大写
 private:
   // length是储存在对象中的字符个数,但是str所指向的内存至少要length+1长度。
  unsigned _length;
  char*  _str;   // 指向字符的指针
};
// 支持TString类的非成员函数。
// 返回一个新的TString类对象,该对象为one和two的级联。
TString operator+(const TString& one, const TString& two);
// 输入/输出操作符,详见第7章。
ostream& operator<<(ostream& o, const TString& s);
istream& operator>>(istream& stream, TString& s);
// 关系操作符,基于ASCII字符集比较。
// 如果两字符串对象包含相同的字符,则两对象相等。
bool operator==(const TString& first, const TString& second);
bool operator!=(const TString& first, const TString& second);
bool operator<(const TString& first, const TString& second);
bool operator>(const TString& first, const TString& second);
bool operator>=(const TString& first, const TString& second);
bool operator<=(const TString& first, const TString& second);```
如下所示,简单的实现将为字符分配内存,而且在需要时为对象进行深复制。这些实现都易于理解和执行。

TString::TString()
{
  _str = 0;
  _length = 0;
}
TString::TString(const char* arg)
{
  if (arg && *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;
  }
  else {
    _str = 0; _length = 0;
 }
}
TString::~TString() { if (_str != 0) delete [] _str; }
// 复制构造函数,执行深复制。为字符分配内存,然后将其复制给this。
TString::TString(const TString& arg)
{
  if (arg._str!= 0) {
    this->_str = new char[strlen(arg._str) + 1];
    strcpy(this->_str, arg._str);
    _length = arg._length;
  }
  else {
    _str = 0; _length = 0;
  }
}
TString& TString::operator=(const TString& arg)
{
  if (this == &arg)
   return *this;
  if (this->_length >= arg._length) {// *this足够大
    if (arg._str != 0)
      strcpy(this->_str, arg._str);
    else
      this->_str = 0;
    this->_length = arg._length;
    return *this;
  }
  // *this没有足够的空间,_arg更大.
  delete [] _str; // 安全
  this->_length = arg.Size();
  if (_length) {
    _str = new char[_length + 1];
    strcpy(_str, arg._str);
  }
  else _str = 0;
  return *this;  // 总是这样做
}
TString& TString::operator=(const char* s)
{
  if (s == 0 || *s == 0) { // 源数组为空,让“this”也为空。
    delete [] _str;
    _length = 0; _str = 0;
    _str = 0;
    return *this;
 }
 int slength = strlen(s);
 if (this->_length >= slength) { //*this足够大
  strcpy(this->_str, s);
  this->_length = slength;
  return *this;
 }
 // *this没有足够的空间,_arg更大。
 delete [] _str; // 安全
 this->_length = slength;
 _str = new char[_length + 1];
 strcpy(_str, s);
 return *this;
}
TString& TString::operator=(char charToAssign)
{
 char s[2];
 s[0] = charToAssign;
 s[1] = ‘0’;
 // 使用其他赋值操作符
 return (*this = s);
}
int TString::Size() const { return _length; }
TString& TString::operator+=(const TString& arg)
{
  if (arg.Size()) {  // 成员函数可调用其他成员函数
    _length = arg.Size() + this->Size();
    char *newstr = new char[_length + 1];
    if (this->Size())  // 如果原始值不是NULL字符串
      strcpy(newstr, _str);
    else
      *newstr = ‘0’;
    strcat(newstr, arg._str); // 附上参数字符串
    delete [] _str;  // 丢弃原始的内存
    _str = newstr;   // 这是创建的新字符串
  }
  return *this;
}
TString operator+(const TString& first, const TString& second)
{
  TString result = first;
  result += second; // 调用operator+=成员函数
  return result;
}
bool operator==(const TString& first, const TString& second)
{
  const char* fp = first.c_str(); // 调用成员函数
  const char* sp = second.c_str();
  if (fp == 0 && sp == 0) return 1;
  if (fp == 0 && sp) return -1;
  if (fp && sp == 0) return 1;
  return ( strcmp(fp, sp) == 0); // strcmp是一个库函数
}
bool operator!=(const TString& first, const TString& second)
{ return !(first == second); }  // 复用operator==
// 其他比较操作符的实现类似operator== ,
// 为了简洁代码,未在此处显示它们。
char TString::operator()(unsigned n) const
{
  if (n < this->Size())
    return this->_str[n]; // 返回下标为n的字符
  return 0;
}
const char& TString::operator[](unsigned n)const
{
  if (n < this->Size())
    return this->_str[n]; // 返回下标为n的字符
  cout << “Invalid subscript: ” << n << endl;
 exit(-1);  // 应该在此处抛出异常
  return _str[0];  // 为编译器减轻负担(从不执行此行代码)
}
// 将每个字符变成小写
TString& TString::ToLower()
{
  // 使用tolower库函数
  if (_str && *_str) {
    char *p = _str;
    while (*p) {
      p = tolower(p);
      ++p;
    }
   }
   return *this;
}
TString& TString::ToUpper() // 留给读者作为练习
{
   return *this;
}
TString TString::operator()(unsigned posn, unsigned len) const
{
  int sz = Size(); // 源的大小
  if (posn > sz) return “ ”; // 空字符串
  if (posn + len > 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’;
  }
  return result;
}
ostream& operator<<(ostream& o, const TString& s)
{
  if (s.c_str())
    o << s.c_str();
   return o;
}
istream& operator>>(istream& stream, TString& s)
{
  char c;
  s = “ ”;
  while (stream.get(c) && isspace(c))
    ;// 什么也不做
  if (stream) { // stream正常的话,
    // 读取字符直至遇到空白
    do {
    s += c;
    } while (stream.get(c) && !isspace(c));
    if (stream)  // 未读取额外字符
      stream.putback(c);
  }
  return stream;
}`
1译者注:服务器指一个管理资源并为用户提供服务的计算机软件,另外,运行这样软件的计算机或计算机系统也被称为服务器。这里,作者为区分两种服务器,用server machine表示运行服务器的计算机,用license server表示许可证服务器。
2观众需要为观看(订阅)固定数量的节目支付费用。这些节目包括最新的电影、体育节目甚至是直播,或者音乐会。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值