C++自修入門實境秀、C++ Primer 5版研讀秀 103/ ~第13章 拷貝控制13.2.1.表現和值一樣的類別 -中文版重譯~頁511
練習13.13
第103集開始
11:17
理解類別的拷貝控制機制(members)及其建構器的一個好辦法就是定義出一個簡單的類別,有著這些機制,並在這些機制自己的本體中,將它們自己的名稱列印出來,一如下例:
struct X
{
X() { std::cout << "X()" << std::endl; }
X(const X &) { std::cout << "X(const X&)" << std::endl; }
};
請再加入拷貝指定運算子及解構器到這個X類別中,並寫一個程式,以不同的方式來運用X物件,以便觀察這些拷貝控制機制的作用時機及其作用。比如說將X物件分別傳給參考與非參考型別的參數,或者是動態配置這些X物件,或將它們放到容器裡面,諸如此類。仔細研讀這個程式所列印出來的結果,確定您可以很清楚明白:什麼時候會調用到哪些拷貝控制,又為什麼會用到那些拷貝控制機制。在您摸索(解讀)這印出來的結果的同時,也請您要留意,有時編譯器會逕自略過拷貝建構器的調用。
#include<vector>
#include<iostream>
using namespace std;
struct X
{
X(const string& s) :s(s){ std::cout << "X()" << std::endl; }//建構器
X(const X&,const string&s="南無阿彌陀佛"):s(s) { std::cout << "X(const X&)" << std::endl; }//拷貝建構器
X& operator=(const X&x){//拷貝指定運算子
std::cout<<"operator="<<endl;
s=x.s;
return *this;
}
~X() { cout << "~X()" << endl; };//解構器
string s;
};
int main() {
X x("阿彌陀佛"); //普通一般的
const X xc("const南無阿彌陀佛");//常值的
X& xRef{x};//參考型別
X x1(x,x.s);
X x2(xc,xc.s);
X xr(xRef,xRef.s);
X* xp = new X("new孫守真");
//vector<X>vec{*xp,xr,x2,x1,x };
vector<X>vec;
vec.push_back(*xp);
vec.push_back(xr);
vec.push_back(x2);
vec.push_back(x1);
vec.push_back(x);
vec.pop_back();
delete xp;
}
1:15:40
1:59:20
沒有必要將所有的拷貝控制機制都加以實作,有時只要定義其中的幾個就可以了。雖然如此,但一般都是將拷貝控制的所有機制視為一個單元整體(as a unit)。因此,只需要其中的一個,而無須定義全部機制的情況是並不常見的。
Classes That Need Destructors Need Copy and Assignment
需要解構器的類別就必須定義好拷貝與指定運算
1:21:50
有一條經驗法則是值得我們運用的:那就是如果要考慮一個類別是否需要定義它自己的拷貝控制機制,那麼就先想清楚它是否需要解構器。通常對解構器的需求會較對拷貝建構器或指定運算子的需求來得明顯得多了。如果該類別確實要用到解構器,那麼幾乎就可以肯定它也必須要定義好自己專屬的拷貝建構器及拷貝指定運算子了。
在前面練習所見的HasPtr類別,就是一個很好的範例(§13.1.1,頁499)。該類別會在其建構器中動態配置記憶體,而由編譯器合成的解構器並不會對指標的資料成員調用delete運算子來將其所指物件給清除掉。因此,像HasPtr這樣的類別就需要有它自己的解構器來釋放其建構器所動態配置出來的記憶體資源了。
1:29:40
相較於解構器的需求判斷,較不容易察覺的,也是我們的經驗法則給予我們的教訓是:像HasPtr這樣的類別通常也需要有它自己定義的拷貝建構器與拷貝指定運算子。
頁505
2:7:47
想像一下如果HasPtr有它自己的解構器卻是用編譯器湊合成的拷貝建構器與拷貝指定運算子的話,那會發生什麼事:
class HasPtr
{
pbulic : HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0) {}
~HasPtr() { delete ps; }//HasPtr有它自己的解構器
//這是錯的:HasPtr需要自己的拷貝建構器與拷貝指定運算子
//其他的成員一如之前那樣
};
在這樣的版本中,HasPtr的建構器配置出的記憶體資源就會在其物件摧毀時得到清除。不幸的是,若真這樣寫的話,就會有著致命的錯誤!這樣的HasPtr類別是用編譯器湊合出來的拷貝與指定運算,這樣湊合出來的運算會直接拷貝HasPtr的指標成員,而不是其指標所指向的東西,那麼,所有經由這樣拷貝運算後的HasPtr物件都會、也只能指向同一個記憶體位置了。
HasPtr f(HasPtr hp) //HasPtr型別的引數是由傳值(pass by value)的方式傳遞的,所以是以拷貝的方式來進行運算的
{
HasPtr ret = hp; //將引數的HasPtr物件拷貝給ret
//處理ret
return ret;//在此之後ret和hp就會被摧毀了
}
當上述的f函式回傳值後,hp和ret都會消失,也會分別在它們各自身上觸發HasPtr的解構器。而那個解構器會對ret和hp此二物件內的指標成員進行delete運算。只是這兩個物件的指標成員的值是經由拷貝的,它們有著相同的值。因此像上述那樣的程式碼就會在這個指標值上進行2次的delete運算子運算。在前面§12.1.2(頁462)處我們已經學過:對同一個記憶體區進行多於一次的delete運算,就是嚴重的錯誤!怎麼做都將會是未定義的(undefined,沒有意義的)行為。
此外f的呼叫端也可能仍要使用它傳給f用的那個物件(同一個記憶體區域):
HasPtr p("some values");
f(p); //當f執行完畢後,由p的ps成員所指向的記憶體位址就會被清空
這裡英文版(中文版)原文如下:
when f completes, the memory to which p.ps points is freed
HasPtr q(p); //現在p與q內的ps成員都將淪為懸置指標,指向了一個已被清空的記憶體區域
由p和q的ps成員所指向的記憶體區域在經過如此運算後就不再有效了。這個記憶體區域的資源在hp或ret被摧毀時就經由HasPtr的解構器的delete運算而還給系統了。
2:26:24
訣竅/要領:如果一個類別需要自訂的解構器,那麼幾乎可以確定它就需要定義它自己的拷貝指定運算子與拷貝建構器。
1:58:30
Classes That Need Copy Need Assignment, and Vice Versa
需要自訂拷貝的類別就需要自訂指定,反之亦然
【拷貝與指定是互相需要、不可或缺的】
2:27:44 2:51:44
雖然許多類別會需要定義它們自己全部的拷貝控制機制(members),或並不需要全部定義出來;但有些類別是需要在拷貝或指定物件的運算時,進行其他的工作,但卻並不需要解構器。
例如,想像一下有某個類別它會派給它型別的物件各自且獨特的序號。這樣的類別就需要用到拷貝建構器來在其物件建置時,產生一個新的、獨特的序號,以配給該物件。這個建構器是需要從某個此型別的來源物件,將其除了該序號的所有資料成員給複製起來。這樣的類別也需要它自己專用的拷貝指定運算子來避免用編譯器湊合出來的指定運算而誤將其序號成員也給拷貝過來。只是,這個類別是用不上解構器的。
頁506
2:54:50
像上述的實例就已觸及在運用拷貝控制時的第2項經驗法則,那就是:只要類別本身需要拷貝建構器,它八成也就會需要定義它自己的拷貝指定運算子;反之,若類別需要定義它自己的指定運算子,那麼它就會有必要去定義它自己的拷貝建構器。然而雖說是這樣,但不管是需要拷貝建構器或拷貝指定運算子,有著這任一種的需求,都不必然表示該類別就一定需要它自己的解構器。也就是說,拷貝建構器與拷貝指定運算子間的這種相互需求,與解構器的需求之間,並沒有必然的連帶性的。
「儘管如此,需要拷貝建構器或拷貝指定運算子中任一個的類別,就不一定也會需要解構器了。」(中文版)
Nevertheless, needing either the copy constructor or the copy-assignment operator does not (necessarily) indicate the need for a destructor.
3:0:59
練習13.14
假設numbered這樣的類別有一個預設建構器來為每個其型別的物件產生一個獨特的序號,這個序號是用mysn(my serial number)這個資料成員來存放的。如果numbered這個類別用了編譯器湊合成的拷貝控制機制,且若執行以下函式的話:
void f(numbered s) { cout << s.mysn << endl; }
那麼下列程式碼會印出怎樣的訊息?
numbered a, b = a, c = b;
f(a);f(b);f(c);
3:19:44
#include<iostream>
using namespace std;
struct numbered{
numbered():mysn(2*rand()) {};
unsigned mysn;
};
void f(numbered s) { cout << s.mysn << endl; }
int main() {
//f(numbered());
numbered a, b = a, c = b;
f(a);f(b); f(c);
}
練習13.15
假設numbered有了一個可以產生新序號的拷貝建構器。那麼,前一練習所輸出的結果會有所不同嗎?如果會的話,是為什麼?會產生怎樣的輸出結果?
#include<iostream>
using namespace std;
struct numbered{
numbered():mysn(2*rand()) {};
numbered(const numbered&):mysn(2*rand()) {};
unsigned mysn;
};
//「numbered s」還要再調用一次拷貝建構器,所以s與a、b、c的mysn成員值未必一致
void f(numbered s) { cout << s.mysn << endl; }
int main() {
//f(numbered());
numbered a, b = a, c = b;
f(a);f(b); f(c);
}
3:33:27 3:35:10
練習13.16
如果前面練習的f函式其參數型別是const numbered&的話,那結果又會是如何?輸出的結果是否還會一樣?如果不一樣了,是為什麼?其輸出的結果又會是怎樣的?
已詳前一題測試實境秀說明,臉書直播見第533集約4:7:00前 https://www.facebook.com/oscarsun72/videos/2640399309404520/
練習13.17
3:40:22
分別寫出上述3題版本的numbered類別及f函式,來檢測您的推斷是否正確。
同前一題
13.1.5. Using = default
3:43:30
這是c++11的新標準
如果我們想要要求編譯器為我們湊合出拷貝控制成員,可以在定義該成員時,使用「= default」這樣的陳述(§7.1.4,頁264)。
3:45:51 4:2:43
class Sales_data
{
public:
//使用編譯器湊合出來的預設版拷貝控制成員
Sales_data() = default;
Sales_data(const Sales_data &) = default;
Sales_data &operator=(const Sales_data &);
~Sales_data() = default;
//其他的成員一如之前
};
Sales_data& Sales_data::operator=(const Sales_data&)=default;
3:49:36 4:4:27
當我們在類別本體內對拷貝控制機制的宣告指明了「=default」,由編譯器湊合出來的拷貝控制成員函式就會隱含默認是inline的(一如其他的成員函式在類別本體內定義的那樣)。如果我們並不想要讓這些編譯器湊合出來的機制是inline的,那麼在這些機制成員的定義中,我們就可以用「=default」來指明,就如同上例中我們對拷貝指定運算子的定義一樣。
如果我們希望那個合成的成員是一個inline 函式,我們可以在該成員的定義上指定=default,就跟我們在拷貝指定運算子的定義中做的一樣。(中文版這裡翻錯了)
If we do not want the synthesized member to be an inline function, we can specify = default on the member’s definition, as we do in the definition of the copy-assignment operator.
3:55:22
頁507
注意:「=default」只能用在可以由編譯器湊合出來的成員函式身上。比如預設建構器或各種拷貝控制機制成員(copy-control member,⑧找對主詞 ,這裡的member是指類別的member成員,並不是組成拷貝控制的組成成員的意思。所以類別中的成員殆有以下幾類:資料成員、型別成員、成員函式、拷貝控制機制成員(包括建構器、運算子、解構器,這3種均屬特殊的成員函式))。
4:1:12 4:10:22
13.1.6. Preventing Copies 要如何才能防止、避免、禁止拷貝
養成好習慣:大部分的類別都應該要定義好預設建構器、及拷貝建構器、拷貝指定運算子等拷貝控制機制成員,不論是隱含地或明確地定義它們。
4:19:38
雖然大多數的類別都會、也通常會定義好拷貝建構器以及拷貝指定運算子,然而對於有些類別來說,這些操作確實沒有任何的意義。在這種情況下,該類別就必須定義好防止拷貝或指定的機制。比如說,iostream類別就有著防止拷貝的機制以避免同時間有多個物件從同個IO緩衝區中企圖讀取或寫入資料。只是,請別天真地以為只要我們不去定義這些拷貝控制成員就可以停止拷貝與指定工作的進行,因為即使我們自訂的類別不曾定義這些拷貝控制的成員出來,但編譯器一樣會為我們代作,並逕行拷貝或指定的工作。
4:16:56
Defining a Function as Deleted 在新標準下將一個函式指定為為已刪除;如何讓函式作廢;讓這個函式雖然存在在程式中,但並不起任何的作用
4:23:15 4:53:14
在新標準下我們可以藉由將拷貝建構器及拷貝指定運算子指定為報廢的函式(deleted functions)來避免不必要的拷貝操作。一個已報廢的函式就是一個雖然已經宣告,但卻無法使用的函式。只要在一個函式的參數列(parameter list)後加上「=delete」的陳述,就可以宣告那個函式已經作廢:
struct NoCopy
{
Nocopy() = default; //使用編譯器湊合成的預設建構器
NoCopy(const NoCopy &) = delete; //禁止拷貝的宣告,報廢拷貝建構器
NoCopy &operator=(const NoCopy &) = delete; //禁止指定的宣告;報廢指定運算子
~NoCopy() = default; //使用編譯器湊合成的解構器
//以下是其他的成員
};
像上述的「=delete」這樣的宣告就是在讓編譯器以及讀我們程式碼的人知道,我們是故意不要用到這些拷貝控制的成員的。
不像「=default」可以在稍後的定義中才下達,「=delete」必須在一開始的宣告處就要寫明。這樣的差異是符合宣告它們的邏輯的。
「這種差異是這些宣告的意義之邏輯後果。」(中文版)
4:37:10 4:58:40
一個預設(= default)的成員定義只會左右編譯器要如何產生適當的程式碼,因此在編譯器要產生適當的程式碼前,「=default」沒有必要出現。
「一個預設成員只影響編譯器會產生什麼程式碼,因此=default 在編譯器產生程式碼之前都不需要。」(中文版)
可是,編譯器卻必須事先知道哪些函式是要報廢的,以禁斷、阻斷、禁絕、攔截任何想要用到這些函式的呼叫。
也和「=default」只能用在編譯器能合成出來的函式這一屬性不同,「=delete」則是可以用在任何函式上頭。(我們只能在預設建構器、或拷貝控制成員函式這些可由編譯器湊合出來的成員函式上使用「=default」。)雖然函式作廢這樣的機制主要的用途還是在於拷貝控制成員身上,但作廢函式的方法有時候用在引導函式匹配的進程(function-matching process)中也是很有用的。
「已刪除函式在我們想要引導函式匹配(function-matching)程序時,有時也會有用處。」(中文版)
4:51:47 5:3:34
頁508
The Destructor Should Not be a Deleted Member 解構器不應被標記為作廢的
5:24:07 5:6:00
要注意的是,我們雖然報廢了一些函式,但至今為止並沒有將解構器給報廢掉。因為如果將解構器作廢,那麼就沒有辦法將該類別的物件給銷毀了。編譯器也不會讓我們定義一個解構器已報廢類別的變數或建置該型別的暫存物件。甚至,若一個類別的成員其型別具有報廢的解構器,我們也無法根據此類別定義出一個變數或暫存物件。如果成員的型別是有著報廢的解構器,那麼那個成員也就無法被摧毀;而如果成員無法被摧毀,那麼成員所在的整個物件,連帶地也就無法被徹底摧毀了。
雖然我們無法就具有報廢解構器的類別定義出變數或類別成員,但是我們卻仍然可以經由動態配置的方式來建置出具有報廢解構器型別的物件。然而這麼做的話,我們就沒法將其摧毀,並釋放其佔用的資源:
struct NoDtor
{
NoDtor() = default; //使用編譯器合成的預設建構器
~NoDtor() = delete; //作廢解構器,將無法摧毀NoDtor型別的物件
};
NoDtor nd; //這是不可以的,因為NoDtor的解構器已報廢
NoDtor *p = new NoDtor(); //這是可以的,但卻無法對p執行delete的運算
delete p; //這是錯的,因為NoDtor的解構器已經報廢,而delete運算子是要調用其運算元指標所指物件的型別之解構器才能將該物件摧毀
5:21:36 5:29:50
警告:只要物件的型別有著報廢的解構器,我們就不可能去定義出一個這樣的物件,或對一個指向動態配置出的這樣的物件的指標下達delete運算。
The Copy-Control Members May Be Synthesized as Deleted 由編譯器湊合成的拷貝控制成員函式也可能是個報廢的函式
5:31:32
當我們沒有定義自己的拷貝控制成員,那麼編譯器就會為我們定義出一套湊合著用的拷貝控制成員。若一個類別並沒有定義建構器,那麼編譯器就會為此類別合成一個預設建構器(§7.1.4,頁262)。而像下列這些類別,編譯器會將其湊合成的版本設定為報廢的、不可用的函式:
如果類別成員其型別的解構器是報廢的,或是無法調用的,如private的,那麼合成的解構器就會是不可用的(deleted)。
如果類別成員型別的拷貝建構器是報廢的,或無法調用的,那麼編譯器合成出來的拷貝建構器就會是報廢的。如果類別成員型別的解構器是報廢或無法調用的,那麼湊合出來的解構器也就會是報廢型的。
如果類別成員型別有報廢的拷貝指定運算子或其拷貝指定運算子是無法調用的,那麼合成出來的拷貝指定運算子就會是報廢型的。若類別有常值(const)或參考型別的成員,那麼編譯器湊合出來的拷貝指定運算子也會是報廢無用的(deleted)。
只要類別的成員型別有著報廢的解構器或其解構器是無法調用的,那麼合成出來的預設建構器就會是報廢型的。而若類別的成員是參考型別且不存在類別內的初始器(§2.6.1,頁73)的話,那合成出來的預設建構器也會是不能用的(deleted)。或類別的常值成員其型別並沒有明確定義自己的預設建構器,且這個成員又沒有類別內初始器的話,那麼編譯器合成出來的預設建構器也會是不堪用的(deleted)。
頁509
5:48:40
其實,這些規則只是擺明了只要一個類別其資料成員若不能經由預設的方式加以建構、拷貝、指定或摧毀,那麼編譯器所合成出來的建構、拷貝、指定或解構的拷貝控制成員就會是不能用的(deleted function)。
5:52:02 6:16:46
也許類別成員的型別若具有報廢式的解構器或其解構器是無法調用的,那麼編譯器合成出來的預設建構器、拷貝建構器就會是不可用的(deleted),這樣的結果是令人意外的。但這是因為若沒有這項限制,那麼就可能建置出我們無法摧毀的物件出來。
編譯器是無法對一個成員是參考或常值(const)的、且不能以預設的方式建構這些成員出來的類別,湊合出一個有效的預設建構器。至於具有常值成員的類別,也無法合成出一個拷貝指定運算子,這項限制,應該並不令人意外。畢竟,拷貝指定運算子就是打算拷貝以指定物件內的所有成員;而對一個常值而言,我們是無法對其進行指定新值的運算的。
5:59:59 6:20:11
雖然我們煞似可以對一個參考指定新值給它,但其實這樣做也只是動到這個參考所指向的物件值而已,並不是更動到參考本身的值(參考本身是沒有值的,它只是個虛幻的,是一個具體物件的分身、影子而已)。如果對這樣的類別合成出一個拷貝指定的運算子,那麼其指定後儲存結果的左運算元就會繼續去更動到在執行指定運算前所參考的那個物件,如此循環不已,沒完沒了,它終不能去參考到右運算元參考的那個物件。
那麼左手邊的運算元就會持續指涉跟指定之前相同的物件。它不會指涉跟右手邊運算元相同的物件。(中文版)
Although we can assign a new value to a reference, doing so changes the value of the object to which the reference refers. If the copy-assignment operator were synthesized for such classes, the left-hand operand would continue to refer to the same object as it did before the assignment. It would not refer to the same object as the right-hand operand.
就是因為這樣的結果非如預期,所以只要類別的成員是參考型別的時候,合成的拷貝指定運算子就會被標記為是不可用的(deleted)。
6:5:54 6:21:38 6:32:40
在§13.6.2(頁539)、§15.7.2(頁624)、§19.6(頁849)我們還會看到因為類別的其他特性而導致編譯器合成出來的拷貝成員被標記為不可用的(deleted)。
6:7:55 6:33:4
注意:其實,只要對其成員無法進行拷貝、指定或解構的話,那麼編譯器合成出來的相關的拷貝控制成員就會被標記為不可用的。
private Copy Control 私有的private拷貝控制
6:9:44 6:34:44
在新標準實施之前,C++的類別是藉由將拷貝建構器和拷貝指定運算子宣告在類別的private區段來禁止拷貝的:
class PrivateCopy
{
//在沒有存取指定符的標記下,class預設的存取層級就是私有的(private,見§7.2,頁268)
//寫在這裡的拷貝控制成員都是私有的,所以一般的程式碼是無法調用到它們的
PrivateCopy(const PrivateCopy &);
PrivateCopy &operator=(const PrivateCopy &);
//其他的成員
public:
PrivateCopy() = default; //使用編譯器合成的預設建構器
~PrivateCopy(); //有了解構器,就可以建置出此類別的物件出來,但是並不能對它進行拷貝與指定的運算
};
7:1:55
因為PrivateCopy的解構器是公開的(public),就可以定義出PrivateCopy物件出來。可是拷貝建構器與拷貝指定運算子都是私有的(private),因此一般使用到PrivateCopy的程式碼就不能對它們進行呼叫,也就無法對這些建構出來的物件進行拷貝、指定的運算。然而如果是這個類別的朋友(friends)以及這個類別中的成員,仍然可以進行拷貝指定等的運算。故為了要禁絕類別成員與朋友還能進行拷貝指定,我們不但在private區段來宣告它們,且我們只宣告它們而不加以定義。
頁510
6:49:49
除了在§15.2.1(頁594)我們會談到的那個例外,只宣告而不去完整定義成員函式是可以的(§6.1.2,頁206)。若企圖去調用未定義的成員函式,則會導致繫結階段的錯誤。
「連結時期的失誤(link-time failure)」(中文版)
藉由在private區段宣告但不加以定義拷貝建構器的方式,我們就可以禁絕一切對此型別物件進行拷貝的企圖。如果使用這個型別物件的程式碼還會想要拷貝這個型別的物件,在編譯時期就會被標識為一項錯誤;而若是成員函式或該類別的朋友想要進行拷貝,則會導致連結階段的錯誤。
試著製作一個拷貝的使用者程式碼會在編譯時期被標示為錯誤,而在成員函式或friend中進行拷貝,則會導致連結時期的錯誤。(中文版)
6:59:49 7:8:41
養成好習慣:在新標準下,想要禁絕拷貝的類別應該要把它們的拷貝建構器及拷貝指定運算子用「=delete」來定義為報廢的,而不是只是把它們定義在private區段中而已。
練習13.18
7:10:59
忘了錄的部分臉書直播第535集約10分鐘前後 https://www.facebook.com/oscarsun72/videos/2642786965832421/
定義一個名為Employee的類別,它包括了員工的名字和一個唯一的員工識別證號碼。請給這個類別定義一個預設建構器,還有一個建構器是帶了一個string型別的參數用來表示員工的名字。這二個建構器都應該要能夠藉由遞增一個靜態的資料成員來產生一個唯一的識別號碼。【為什麼要用靜態?因為靜態成員的生命週期是在一經配置後就會保留到整個應用程式結束時才會結束;因此只要我們不會去歸零它,它的值就永遠不會歸零;但要注意若在下次啟動應用程式時,一定要有一個讀取最後值的機制來賦予給它,否則就會從零開始遞增,那就必然會有重複值了】
7:55:59
#include<iostream>
using namespace std;
struct Employee {
//public:
Employee() :ID(++myID),employeeName("十方三世佛,共同一法身,一心一智慧,力無畏亦然"){};
Employee(const string& employeeName) :ID(++myID),employeeName(employeeName) {};
const string employeeName;
const unsigned ID;
private:
static unsigned myID;//此類似宣告-配置資源(宣告類別內的靜態成員)
};
unsigned Employee::myID = 0;/*此類似定義-建構實例,初始化已經宣告的類別靜態成員
如何初始化靜態資料成員
https://openhome.cc/Gossip/CppGossip/staticMember.html
static 資料成員屬於類別,而非個別實例,想在類別內初始 static 資料成員的話,必須是個 constexpr,也就是必須是編譯時期常數,若否,必須在類別外指定,例如:
class Math {
public:
static double PI;
};
double Math::PI = 3.14159;
*/
void f(Employee s) { cout << s.employeeName << "'s ID is :"<< s.ID << endl; }
int main() {
Employee a,b("孫守真"),c("阿彌陀佛");
f(a);f(b); f(c);
}
https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/blob/exercise13_18/prog1/prog1.cpp
練習13.19
8:11:56
您這個Employee類別是否需要定義它自己專用的拷貝控制成員?如果是的話,那是為什麼?若不然,又是為什麼?將您認為Employee類別該有的拷貝控制成員給實作出來。
需要!(詳實境秀說明)。複製(拷貝)某個員工(物件)是要做什麼呢?沒有必要嘛。即使真需要複製某個員工(物件),也只要利用編譯器湊合出來的版本就可,因為Employee只有二個資料成員。非也!有3個!然那個靜態的資料成員是屬於類別在管的,不是類別物件,所以在複製(拷貝)類別物件時,應該並不是拷貝到這個靜態成員。
頁508的規則中,我們這個Employee中了這招:
若類別有常值(const)或參考型別的成員,那麼編譯器湊合出來的拷貝指定運算子也會是報廢無用的(deleted)。
function "Employee::operator=(const Employee &)" (declared implicitly) cannot be referenced -- it is a deleted function
//可見static成員因不屬於物件所有,所以在拷貝控制成員操作時,並不會受到影響
練習13.20
8:47:46
請試著解釋一下當我們在對TextQuery、QueryResult物件(§12.3,頁484)作拷貝、指定與解構時會發生什麼事
總之,都注目在資料成員及其型別上思考就是了。
先看我們自己定義的:
https://ithelp.ithome.com.tw/articles/10212716
https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/tree/exercise13_20/prog1
再看課本的:
.cpp
#include<iostream>
#include<memory>
#include <fstream>
#include"TextQuery.h"
using namespace std;
string make_plural(size_t ctr, const string& word,//頁224
const string &ending)
{
return (ctr > 1) ? word + ending : word;
}
ostream& print(ostream& os, const QueryResult& qr)
{
//如果要找的字有找到,就印出它出現的次數及所有找到的內容
//os << qr.sought << " occurs " << qr.lines->size() << " "
// << make_plural(qr.lines->size(), "time", "s") << endl;
os << qr.sought << " occurs " << qr.lines->size() << " "
<< make_plural(qr.lines->size(), "time", "s") << endl;
//印出所有要找的字詞所在行的內容
for (auto num : *qr.lines) //處理set中的每個元素
//避免出現「第0行」這樣的訊息來使人困擾
os
<< "\t(line " << num + 1 << ")"
<< *(qr.file->begin() + num) << endl;
return os;
}
int main() {
string fName, strSearch;
//cout << "請指定要檢索的檔案全名(fullname,含路徑與副檔名)" << endl;
//if (cin >> fName);
////必須檢查檔案存不存在
//else//若沒有指定檔案的話
//{
fName = "V:\\Programming\\C++\\input.txt";
//}
//cin.clear();//cin前面已經移動它的迭代器(iterator)了到讀取失敗的位置,故要歸零清除,
//否則如果這裡讀取失敗,後面的cin >> strSearch判斷就會永遠都是false(讀取失敗)了
//第103集8:58:00//可參考前面談資料流(stream)的部分
ifstream ifs(fName);
TextQuery tq1(ifs);
//TextQuery tq(tq1);//使用編譯器湊合的拷貝建構器,會將TextQuery的二個成員都複製(拷貝)
//TextQuery tq=tq1;//使用編譯器湊合的拷貝指定運算子,一樣會將TextQuery的二個成員都複製(拷貝)
TextQuery* p= new TextQuery(tq1);//使用編譯器湊合的拷貝建構器
TextQuery tq = *p;//使用編譯器湊合的拷貝指定運算子,拷貝一個副本出來存在tq裡,故當p所指的被清除了,tq依然有效,二者是互不相干的獨立物件
delete p;//使用TextQuery自行定義的解構器
while (true) {
cout << "請輸入檢索字串,或輸入「q」離開" << endl;
if (!(cin >> strSearch) || strSearch == "q") break;
QueryResult qr1 = tq.query(strSearch);
//QueryResult qr(qr1);//使用編譯器湊合的拷貝建構器
//QueryResult qr=qr1;//使用編譯器湊合的拷貝指定運算子
QueryResult* p=new QueryResult(qr1);//使用編譯器湊合的拷貝建構器
QueryResult qr = *p;
delete p;//使用QueryResult自訂的解構器
print(cout,qr);
}
}
.h
#ifndef TextQuery_H
#define TextQuery_H
#include<vector>
#include<memory>
#include<iostream>
#include<fstream>
#include<sstream>
#include<string>//要用getline函式,要引入這一行
#include<map>
#include<set>
using namespace std;
class QueryResult; //對於query成員函式的回傳值而言,這個宣告是必須,因為QueryResult就是query函式的回傳型別
class TextQuery//頁487
{
public:
using line_no = vector<string>::size_type;
TextQuery(ifstream&);
QueryResult query(const string&) const;
private:
shared_ptr<vector<string>> file; //指向要被檢索的檔案資料
//
map<string, shared_ptr<set<line_no>>> wm;
};
//讀取要檢索的檔案內容以建構含有其每行內容及箭號的map
TextQuery::TextQuery(ifstream& is) : file(new vector<string>)
{
string text;
while (getline(is, text))
{ //一行行處理要檢索的檔案
file->push_back(text); //將這行的內容存放到vector裡頭
int n = file->size() - 1; //當前這行的行號
istringstream line(text); //讀取此行內的各個詞彙
string word;
while (line >> word)
{ //處理這行內的個個字彙
//如果這個字彙並不在wm這個map裡面,就對wm下標來新增這個字彙進去
auto& lines = wm[word]; //lines這個變數的型別是shared_ptr
if (!lines) //如果這個shared_ptr是個空值的話,那麼這就是第一次加入這個新字彙到wm中
lines.reset(new set<line_no>); //若此字彙是第一次加入到wm中就配置一個新的set給wm作「值」用
lines->insert(n); //將此行的行號,在set中記下來
}
}
}
class QueryResult//頁489
{
friend std::ostream& print(std::ostream&, const QueryResult&);
public:
QueryResult(std::string s,
std::shared_ptr<std::set<TextQuery::line_no>> p,
std::shared_ptr<std::vector<std::string>> f) :
sought(s), lines(p), file(f) {}
private:
std::string sought; //這次要找的字
std::shared_ptr<std::set<TextQuery::line_no>>lines;//要找的字所在的行號
std::shared_ptr<std::vector<std::string>> file; //要被檢索的檔案內容
};
QueryResult TextQuery::query(const string& sought) const
{
//如果要找的字沒找到的話,就回傳這個指向空set的shared_ptr
static shared_ptr<set<line_no>> nodata(new set<line_no>);
//用find來找而不是用下標運算,以避免動到wm中的元素
auto loc = wm.find(sought);
if (loc == wm.end())
return QueryResult(sought, nodata, file);
//沒有找到的話,就回傳這個QueryResult
else
return QueryResult(sought, loc->second, file);
}
#endif // !TextQuery_H
https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/tree/exercise13_20_our_TextQuery_QueryResult/prog1
練習13.21
10:8:17
您認為TextQuery和QueryResult類別有必要定義它們自己專用的拷貝控制成員嗎?如果是的話,那是為什麼?如果不然,但又為何?請將您認為該有的拷貝控制成員給實作出來。
沒有必要!
10:23:30 詳下13.2所述,複習頁504所述,只要有解構器的需求,就需要有拷貝控制成員(拷貝建構器及拷貝指定運算子)
而這裡QueryResult物件在每次檢索後就須被解構,當然就需要解構器了。
13.2. Copy Control and Resource Management拷貝控制與資源管控
10:57:10
通常需要管控不在類別內部配置資源的類別就必須定義它自己的拷貝控制成員。就如§13.1.4(頁504)所見那樣的類別,就需要解構器來釋放其物件所配置出來的資源。而只要類別需要自己的解構器,那幾乎就可以肯定它也必須有它自己的拷貝建構器與拷貝指定運算子了。
想要定義這些拷貝控制的成員,我們就有必要清楚知道,所謂的「拷貝這個類別的物件」是什麼意思。通常我們可以從兩個方面來思考:我們是打算將我們所需的拷貝運算定義得像對一個值的複製,還是對一個指標的拷貝。(即在拷貝時,是要傳值,還是傳址)
如果是要像值的傳遞一樣,那拷貝出來的結果物件,就會與其來源物件毫不相干;雙方將會是各自獨立的個體。對任何一端的作為,都不會影響到另一端。
如果是從指標來思考的話,那麼拷貝的來源與目的這兩端就會是生命的共同體。當我們對這樣的類別物件進行拷貝時,這兩端其實是共用同一個底層物件的。因此對任何一端的作為,都會直接影響到另一端。
頁511
10:37:55 11:2:59
就我們目前所用過的程式庫類別(library classes)而言,容器與string類別在拷貝時就是值的拷貝,而shared_ptr在拷貝時,就是指標類的行為。StrBlob類別也是。(§12.1.1,頁456)IO型別的物件及unique_ptr物件就不允許進行拷貝或指定的操作,當然它們也就沒有所謂的類值或類指標的行為可言了。
要演示類值(傳值)與類指標(傳址)這兩種模式,我們將對前面練習所作過的HasPtr類別添入拷貝控制成員。先讓這個類別看起來有著類值的作為,再讓它表現得像個指標。
10;45:45
在HasPtr類別中有2個資料成員,分別是一個int和一個指向string的普通指標。通常類別對其內建型別的成員(除了指標)的拷貝都是直接拷貝它們,因為這些成員都是值,自然應該表現得像值的行為一樣。所以,只有在我們對那個指標成員被拷貝時、定義了怎麼樣的行為,才會影響這個HasPtr類別物件在拷貝時表現得像個值、還是像個指標。
練習13.22
11:6:03
如果我們想讓HasPtr有著與值相似的行為,那麼各個HasPtr物件就必然會有各自一份那個指標成員指向的string的副本。在下一區段的課文中我們就會將這樣的拷貝控制成員的定義給寫出來。可是現在您已具備實作這些拷貝控制成員一切必要的知識,因此現在就請您試著寫出HasPtr該有的拷貝建構器與拷貝指定運算子。
#include<string>
using namespace std;
class HasPtr
{
public:
HasPtr(const std::string& s = std::string()) :
ps(new std::string(s)), i(0) {}
//HasPtr(const HasPtr& hp):ps(&(*hp.ps)),i(hp.i) {}//拷貝建構器
HasPtr(const HasPtr& hp):ps(&(*hp.ps)),i(hp.i) {}//拷貝建構器
HasPtr& operator=(const HasPtr& hp) {
ps = &(*hp.ps);
i = hp.i;
return *this;
}
//private:
std::string* ps;
int i;
};
int main() {
HasPtr hp("海賢老和尚"), hp1(hp), hp2("孫守真"), hp3("阿彌陀佛");
hp2 = hp1;
hp2 = hp3;
hp2.i = 2;//the hp3.i still is 0;
}
https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/blob/exercise13_22/prog1/prog1.cpp
13.2.1. Classes That Act Like Values 有著類值行為的類別 表現得像值的類別
留言