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

    xiaoxiao2024-05-31  107

    本节书摘来自异步社区出版社《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观众需要为观看(订阅)固定数量的节目支付费用。这些节目包括最新的电影、体育节目甚至是直播,或者音乐会。本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。

    相关资源:你必须知道的495个C语言问题.[美]Steve Summit(带详细书签).pdf 压缩版
    最新回复(0)