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