本節書摘來自異步社群出版社《c++面向對象高效程式設計(第2版)》一書中的第4章,第4.8節,作者: 【美】kayshav dattatri,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。
c++面向對象高效程式設計(第2版)
在讨論了對象的複制和指派後,現在來學習為什麼需要副本控制。你可能形成這樣的一種觀點,即每個類都應該提供public複制構造函數和指派操作符函數。
但是,實際并非如此。很多情況都存在禁止複制對象的語義;另外某些情況下,複制可能僅對一組標明的客戶有意義;甚至還有些情況,隻允許限定數量的對象副本。所有這些情況都要求有正确且高效的副本控制。在接下來的内容中,我們将舉例說明副本控制的必要性。一旦了解這些示例,你将體會到,c++基于每個類提供的副本控制機制如此地靈活。控制建立對象和複制對象的一般技巧将在後面章節中介紹。
假設有一個tsemaphore類。信号量(semaphore)用于過程(或線程)間的同步,以確定安全共享資源。當一個過程需要使用一個共享資源時,該過程需要靠獲得守護資源的信号量來確定互斥。這可以通過tsemaphore類提供的acquire方法完成。如果所需資源已被其他任務獲得,則acquire調用發生阻塞,而且調用的任務将等待,直到其他任務放棄(relinquish)資源。有可能出現多個任務同時等待相同資源的情況。
一旦任務獲得資源,它便完全擁有該資源的所有權,直至通過調用release成員函數放棄資源。鑒于此,複制信号量對象是否正确?更确切地說,複制信号量對象的語義是什麼?如果允許複制,那麼是否意味着有兩個都已獲得相同資源的獨立信号量對象?這在邏輯上不正确,因為任何時候一個程序隻能獲得一個資源(或在已統計信号量的情況下有限數量的程序)。或者,這意味着兩個信号量對象共享相同的資源?共享狀态可能是一個較好的解決方案,但是,這使得信号量的實作和使用複雜化。信号量被看做是經常使用的“輕量級”對象,使其實作複雜化并不合理。在支援任何複制操作之前,還需要澄清一個問題:也許更好的解決方案應該是禁止任何複制。這意味着一旦建立信号量,任何人都不能複制它。
以下是tsemaphore類的接口。
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信号量
}<code>`</code>
tautosemaphore類的構造函數期望傳入一個信号量對象,并将信号量作為構造函數的一部分。tautosemaphore類的析構函數負責釋放所獲得的信号量。是以,一旦在某作用域内建立了tautosemaphore類的對象,它的析構函數将會確定釋放已獲得的信号量,程式員無需為此擔心。至少現在看來,需要我們管理的事務又少了一件。
這樣的類在c++程式中非常普遍。另一個類似的類是ttracer,它用于跟蹤進入函數和從函數退出。
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();
tlicensetoken(const tlicensetoken& other);
tlicensetoken& operator=(const tlicensetoken& other);
};<code>`</code>
既然tlicensetoken是由tlicenseserver以使用者為機關而發出的,那麼確定使用者無法複制傳回的令牌非常重要。否則,許可證伺服器将無法控制使用者的數量。每當新使用者希望使用由許可證伺服器控制的應用程式時,他請求tlicenseserver生成一個新的tlicensetoken類對象。如果可以生成新令牌,則傳回一個指向新tlicensetoken的指針。該令牌由調用者所擁有,使用者不再需要使用應用程式時,必須銷毀它。當許可證令牌被銷毀時,它将與許可證伺服器通信,以減少未歸還許可證令牌數目。注意,許可證伺服器和令牌都不能被複制,使用者不可以複制令牌。許可證令牌可包含許多資訊,如任務辨別号、機器名、使用者名、産生令牌的日期等。因為許可證伺服器和令牌的複制構造函數和指派操作符都為私有,是以不可能複制令牌,這便消除了使用欺騙手段的可能性。
要求使用者銷毀令牌是件麻煩事。我們可以完成這樣的實作,即在令牌追蹤軟體使用的同時,如果軟體在預定時間内未被使用,該實作保證能自動地銷毀許可證令牌。實際上,這樣的實作十分常見。
賬單管理是該實作的一個應用,可根據客戶所使用的服務來收費。這廣泛應用于有線電視的按次計費的程式中2。
你可能覺得不允許複制令牌的限制過于嚴格。但是,如果允許這樣做應該考慮建立一個新令牌,并通知許可證伺服器進行複制。可以完成這樣的實作,這仍然需要副本控制。
4.8.3 字元串類示例
各種語言的程式員都使用字元串來顯示錯誤消息、使用者提示等,我們也經常使用和操控這樣的字元串數組。字元串數組的主要問題是存儲區管理和缺少可以操控它們的操作。在c和c++中,字元串數組不能按值傳遞,隻能傳遞指向數組中第1個字元的指針。這很難實作安全數組。為克服這個障礙,我們應該實作一個tstring類提供所有必須的功能。tstring類對象管理自己的記憶體,而且它會在需要時配置設定更多的記憶體,我們無需為此擔心。
注意:
c++标準庫包含一個功能強大的string類,也用于處理多位元組字元。由于string類易于了解,同時能清楚地說明概念,是以在下面的示例中将用到它。
以下是類tstring的聲明:
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;
_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;
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) {
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;
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);
*newstr = ‘0’;
strcat(newstr, arg._str); // 附上參數字元串
delete [] _str; // 丢棄原始的記憶體
_str = newstr; // 這是建立的新字元串
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
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;
}
}
tstring& tstring::toupper() // 留給讀者作為練習
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’;
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觀衆需要為觀看(訂閱)固定數量的節目支付費用。這些節目包括最新的電影、體育節目甚至是直播,或者音樂會。
本文僅用于學習和交流目的,不代表異步社群觀點。非商業轉載請注明作譯者、出處,并保留本文的原始連結。