本节书摘来自异步社区出版社《C++面向对象高效编程(第2版)》一书中的第4章,第4.8节,作者: 【美】Kayshav Dattatri,更多章节内容可以访问云栖社区“异步社区”公众号查看。
C++面向对象高效编程(第2版)在讨论了对象的复制和赋值后,现在来学习为什么需要副本控制。你可能形成这样的一种观点,即每个类都应该提供public复制构造函数和赋值操作符函数。
但是,实际并非如此。很多情况都存在禁止复制对象的语义;另外某些情况下,复制可能仅对一组选定的客户有意义;甚至还有些情况,只允许限定数量的对象副本。所有这些情况都要求有正确且高效的副本控制。在接下来的内容中,我们将举例说明副本控制的必要性。一旦了解这些示例,你将体会到,C++基于每个类提供的副本控制机制如此地灵活。控制创建对象和复制对象的一般技巧将在后面章节中介绍。
假设有一个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 压缩版