zl程序教程

您现在的位置是:首页 >  后端

当前栏目

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

C++控制编程 高效 为什么 需要 面向对象 副本
2023-09-11 14:17:37 时间

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


【C++要笑着学】面向对象总结 | 瞎编的C++小故事 | 再次理解封装 | 再次理解面向对象 我是柠檬叶子C。本篇将对之前讲的面向对象的内容进行一个梳理,通过举一些例子去感受C和C++之间的区别和联系。举了一个比较有意思的胡编乱造的故事(bushi)。文章的最后会再次理解一些概念,强调封装的意义,加深对 面向对象 的理解。如果觉得文章不错,可以 一键三连 支持一下博主!你们的关注就是我更新的最大动力!
C++面向对象课程设计报告_快递系统 面向对象程序课程设计 题目:快件管理系统 该系统为两种角色的用户提供服务,一种是代收点服务人员,一种是收件人。代收点服务人员根据账号、密码登录系统。收件人无需登录即可使用系统。代收点服务人员可将快件信息录入系统,快件信息包括快递单号、快递公司、收件人、收件人联系电话、收件人地址、邮编、寄件人、寄件人联系电话、寄件...
从c++到Java,关于Java面向对象基础的学习(二) 继续补充完成java面向基础对象学习的第二部分,本章内容主要包括构造器和this以及javabean和封装等思想,感觉仍然需要更多练习才能帮助熟练java面向对象基础部分
从c++到Java,关于Java面向对象基础的学习(一) 一、设计对象并使用 1、学习获取已有对并使用学 2、学习如何自己设计对象并使用 3、定义类的补充注意事项 二、面向对象内存机制 1、多个对象内存图 2、两个变量指向同一个对象内存图 3、补充(垃圾回收)
C++面向对象友元,全局函数、类、成员函数做友元 在程序里,有些私有属性 也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术 友元的目的就是让一个函数或者类 访问另一个类中私有成员 友元的关键字为 ==friend== 友元的三种实现 • 全局函数做友元 • 类做友元 • 成员函数做友元 全局函数做友元
异步社区 异步社区(www.epubit.com)是人民邮电出版社旗下IT专业图书旗舰社区,也是国内领先的IT专业图书社区,致力于优质学习内容的出版和分享,实现了纸书电子书的同步上架,于2015年8月上线运营。公众号【异步图书】,每日赠送异步新书。