C++自修入門實境秀、C++ Primer 5版研讀秀 99-2/ ~12.3.1. Design of the Query Program~練習1...



練習12.6 :

寫一個函式回傳一個由int所構成、且是動態配置的vector。將那個vector作為引數傳入另一個函式(也就是這個函式的參數列中必有一個型別為vector<int>的參數),這個函式會以讀取標準輸入(standard input)的方式來設定那個vector裡的元素值。將那個vector當作引數再傳入另一個函式,並用這個函式印出這個vector的元素值。記得在適當的時機對那個vector進行delete運算,以清除不再需要用到的記憶體資源。

寫一個會回傳由int構成的動態配置的vector的函式。將這個vector傳給另一個函式,這個函式會將標準輸入的值指定給vector的各元素。再將這個vector傳給另一個函式,來印出前一個函式讀到的值(即印出voctor中的各元素值)。記得在適當的時機要delete那個vector。

https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/blob/exercise12_06/prog1/prog1.cpp

#include<iostream>

#include<iterator>

#include<vector>

//#include<new>

using namespace std;

vector<int>* returnDynamicallyAllocatedVec() {

return new(vector<int>);

}



void read_give_values_to_the_elements(vector<int>* vp){

istream_iterator<int>in(cin), end;

while (in!=end)

{

vp->push_back(*in++);

}

}

void print_the_values_that_were_read(vector<int>* vp) {

ostream_iterator<int>out(cout, ",");

for (int i :*vp)

{

*out++=i;

}

}



int main() {

vector<int>* vp = returnDynamicallyAllocatedVec();

read_give_values_to_the_elements(vp);

print_the_values_that_were_read(vp);

delete vp;

vp = nullptr;//這一行在此例中應是可有可無

}



練習12.7 :

7:14:36

重做前面的練習,這次使用智慧指標shared_ptr。

使用shared_ptr重做前面的練習。

https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/blob/exercise12_07/prog1/prog1.cpp

#include<iostream>

#include<iterator>

#include<vector>

//#include<memory>

//#include<new>

using namespace std;

shared_ptr<vector<int>> returnDynamicallyAllocatedVec() {

vector<int> vi;

return make_shared<vector<int>>(vi);

}



void read_give_values_to_the_elements(const shared_ptr<vector<int>>& vp){

istream_iterator<int>in(cin), end;

while (in!=end)

{

vp->push_back(*in++);

}

}

void print_the_values_that_were_read(const shared_ptr<vector<int>>& vp) {

ostream_iterator<int>out(cout, ",");

for (int i :*vp)

{

*out++=i;

}

}



int main() {

shared_ptr<vector<int>> vp = returnDynamicallyAllocatedVec();

read_give_values_to_the_elements(vp);

print_the_values_that_were_read(vp);

cout << vp.use_count() << endl;

if (vp.unique())

{

cout << vp.use_count() << endl;

}

//vp = nullptr;

//cout << vp.use_count() << endl;

vp.reset();//和vp = nullptr是一樣的

//A shared_ptr stops owning a resource when it's reassigned or reset. https://docs.microsoft.com/en-us/cpp/standard-library/shared-ptr-class?f1url=https%3A%2F%2Fmsdn.microsoft.com%2Fquery%2Fdev16.query%3FappId%3DDev16IDEF1%26l%3DEN-US%26k%3Dk(MEMORY%2Fstd%3A%3Ashared_ptr%3A%3Areset);k(std%3A%3Ashared_ptr%3A%3Areset);k(reset);k(DevLang-C%2B%2B);k(TargetOS-Windows)%26rd%3Dtrue%26f%3D255%26MSPPError%3D-2147217396&view=vs-2019

cout << vp.use_count() << endl;

}


練習12.8 :

下列函式若有錯誤,請予解釋。

bool b() {

int* p = new int;

// ...

return p;

}

#include<iostream>

using namespace std;

//int* p;

//bool b() {

// p = new int;

// //int* p = new int;

// // ...

// return p;//沒有delete,則成記憶體耗漏、記憶體洩漏、浪費記憶體了;因為出此範疇,則new所配置的

// //動態記憶體就無法被清除了。p是不會變成懸置指標(dangling pointer),因為出此b函式範疇就自動銷毀(自動物件、區域變數)。

// //return是用傳值的方式(即拷貝),回傳的型別也與p型別不合,但有隱含轉型的關係存在

// //然而沒有delete,尤其在此例,既然要回傳指標p,就不能delete p;應該是將local p的回傳給另一個指標,以對p原所指向的動態配置的物件,可以有後續操作,包括將其delete

// //否則,就是將p已成公用物件,而非區域物件

//}

//以上是以公用物件來改寫p

//以下則是另用一個指標來接管p原指向的動態配置的物件(dynamically allocated object)

int* b() {

int* p = new int;

//int* p = new int;

// ...

return p;//沒有delete,則成記憶體耗漏、記憶體洩漏、浪費記憶體了;因為出此範疇,則new所配置的

}

int main() {

int* q = b();

if (q == 0) cout << "p 沒有指向任何物件" << endl;

else {

cout << q << endl;

cout << *q << endl;

}

//if (b() == 0) cout << "p 沒有指向任何物件" << endl;

//else cout << b() << endl;

////delete p;

delete q;

}



指出下列函式可能的錯誤。

bool b()

{

int *p = new int;

// ...

return p;

}



#include<iostream>

using namespace std;



bool b() {

int* p = new int;

// ...

return p;//沒有delete,則成記憶體洩漏、浪費記憶體了;因為出此範疇,則new所配置的

//動態記憶體就無法被清除了。p是不會變成懸置指標(dangling pointer),因為出此b函式範疇就自動銷毀(自動物件、區域變數)。

}

int main() {

if (b() == 0) cout << "p 沒有指向任何物件" << endl;

else cout << b() << endl;

}

練習12.9 :

解釋執行下列程式碼會發生什麼事:

int *q = new int(42), *r = new int(100);

r = q;

auto q2 = make_shared<int>(42) , r2 = make_shared<int>(100);

r2 = q2;

int main() {

int* q = new int(42), * r = new int(100);

r = q;//因為r現在不再指向new出來的int(100),這個int(100)沒有delete掉,就成了記憶體耗漏、記憶體外洩

//int(42)現在則有二個指標指向它,這些指標就有淪為懸置指標(dangling pointer)的危險

auto q2 = make_shared<int>(42), r2 = make_shared<int>(100);

r2 = q2;//r2原來指向的動態配置的「<int>(100)」因為沒有指向它的智慧指標,就隨之銷毀了

//「<int>(42)」這個動態配置物件則會有兩個智慧指標指向它

//q2.use_count=2,r2.use_count=0---印出來卻是2!因為r2現在等於q2了。r2.use_count=2是表示除了r2以外,還有q2也指向同一個動態配置物件

//因此,所謂的參考計數(reference count),應當時屬於物件的屬性,而不是智慧指標的!

//一個動態配置的物件有多少參考計數,就表示它有多少智慧指標指著它。英文版好像也弄錯了

cout << q2.use_count()<< endl;

cout <<r2.use_count() << endl;

}

解釋下列程式碼會做什麼:

int *q = new int(42), *r = new int(100);

r = q;

auto q2 = make_shared<int>(42), r2 = make_shared<int>(100);

r2 = q2;



參考下面練習12.9

#include<iostream>

using namespace std;



int main() {

int* q = new int(42), * r = new int(100);

r = q;//因為r現在不再指向new出來的int(100),這個int(100)沒有delete掉,就成了記憶體外洩

//int(42)現在則有二個指標指向它

auto q2 = make_shared<int>(42), r2 = make_shared<int>(100);

r2 = q2;//r2原來指向的動態配置的「<int>(100)」因為沒有指向它的智慧指標,就隨之銷毀了

//"<int>(42)"這個動態配置物件則會有兩個智慧指標指向它

//q2.use_count=2,r2.use_count=0---印出來卻是2!

cout << q2.use_count()<< endl;

cout <<r2.use_count() << endl;

}



頁464

12.1.3. Using shared_ptrs with new 將智慧指標shared_ptr和new一起搭配使用

智慧指標若未經初始化,那麼它就會是一個null指標。如表12.3 所見,也能以new回傳的指標來初始化一個智慧指標:

shared_ptr<double> p1; // 能夠指向一個 double 的 shared_ptr

shared_ptr<int> p2(new int(42)); //p2 指向一個int,其值為42


智慧指標有一種建構器是接受new回傳的普通指標作為引數的,這種建構器是explicit的(§7.5.4,頁296)。就是因為這個緣故,我們就無法隱含地將一個內建的普通指標直接轉為智慧指標;而且我們必須使用直接形式的初始化(直接初始化(direct initialize)§3.2.1,頁84)來初始化一個智慧指標:

shared_ptr<int> p1=new int(1024); // 錯誤:必須使用直接初始化

shared_ptr<int> p2(new int(1024));//ok:使用直接相始化

p1的初始化企圖將從new回傳的int*普通指標(plain pointer、ordinary pointer)直接(implicitly)建構出/指定為一個shared_ptr智慧指標(smart pointer)。因為智慧指標接受普通指標作為引數的建構器是explicit的,並不接受直接(implicitly)將一個普通指標轉為一個智慧指標,所以這個初始化是錯誤的。也因為這樣,函式在回傳一個智慧指標shared_ptr的時候,也不能在它的回傳述句(return statement)上用到隱含(implicitly)轉型來企圖將一個普通指標直接轉換為要回傳的智慧指標shared_ptr:

shared_ptr<int> clone(int p) {

return new int(p) ;//錯誤:企圖隱含轉換為 shared_ptr<int>【也就是普通指標與智慧指標的型別是嚴格區分的!】

}

我們必須明確地(explicitly)將一個shared_ptr繫結(bind)至我們想要回傳的指標:

shared_ptr<int> clone (int p) {

// ok:明確地創建一個shared_ptr<int> from int*

return shared_ptr<int>(new int(p));

}

因為在沒有指定如何清除所指物件的情況下,智慧指標是會用delete來清除該物件、並釋放其關聯的記憶體,所以預設情況下,我們必須使用指向動態記憶體的指標來初始化一個智慧指標。若我們想要將智慧指標繫結到指向非動態記憶體資源(other kinds of resources)的指標,那麼,就必須提供自訂的運算來取代delete。我們會在§12.1.4(頁468)看到如何寫出自己的清理資源程式碼(deletion code)。

不要將普通指標和智慧指標混著用…

7:29:10

shared_ptr智慧指標也只有在其他同一指向的shared_ptr指標是從自己拷貝出來的(are copies of itself),才能盡到對其所指動態配置的物件,在適當情況下,完成解構與清除的動作(ccoordinate destruction)。確實,這也就是我們一直以來都建議各位盡量(no way to inadvertently)去使用make_shared而不要去用到new來控管動態記憶體的原因所在。因為用make_shared而不假new之手的話,我們就能在使用到動態記憶體配置出來的物件的同時,也一併將一個shared_ptr繫結到該物件上了。因為在C++中並沒有辦法去將獨立創建——created 也就是自行宣告、定義——的各個shared_ptr(more than one independently created shared_ptr)與一個相同的記憶體位址繫結在一塊。若我們無法在創建智慧指標的同時,將其與其所指物件相繫結,那麼我們就不再有機會這麼做了。指標不能與其所指之物繫結,那麼要那指標又有何用呢?該指標又要如何來控管該物件呢?

想像一下下列函式對智慧指標shared_ptr的操作(operates):

process函式的定義

// ptr會在process被呼叫時創建並初始化

void process(shared_ptr<int> ptr)

{

//使用ptr

}// ptr超出範疇,並被摧毁了

頁465

Table 12.3. Other Ways to Define and Change shared_ptrs

表12.3 :定義shared_ptr和變更shared_ptr的其他方式

shared_ptr<T>p(q) p控管普通的內建型別指標q所指的物件;q必須指向由new所配置出來的動態記憶體位址,且其型別必須能轉換為T*(對於T型別的指標)。【真按:指型別轉換(type conversion)】

shared_ptr<T>p(u) p承繼了unique_ptr智慧指標u對其所指動態物件的所有權;經過此運算後,u就成為一個null指標,不再指向任何物件。p assumes ownership from the unique_ptr u; makes u null.

shared_ptr<T>p(q,d) p承繼了內建指標q對其所指物件【未必是動態物件】的所有權。q必須可轉換為T* 這種型別(對T型別的指標,§4.11.2,頁161)。p則使用可呼叫物件d(§10.3.2,頁388)來取代 delete運算子以在適當時機下刪除q指向的物件,而釋放它所佔用的記憶體資源。

p assumes ownership for the object to which the built-in pointer q points. q must be convertible to T* ( p. 161).

p will use the callable object d (p. 388) in place of delete to free q.

shared_ptr<T>p(p2,d) p是shared_ptr p2的一個拷貝,對其所指物件的參考計數就會+1(現在至少有p與p2指向相同的(未必是動態)物件),如表12.2(頁453)中所述,只不過p使用可呼叫物件d來取代delete。

p.reset()

p.reset(q)

p.reset(q,d) 如果shared_ptr p是指向其物件的唯一一個shared_ptr,reset會摧毀那個物件。如果有傳入內建指標型別的選擇性引數q,p就會指向q所指的物件(此時p、q同指一個物件,但q是普通指標(plain pointer、ordinary pointer)而已),否則就讓p變為null。若有提供可呼叫物件(callable object)d,就會用d來釋放p所指向的記憶體資源——只要與p關聯的參考計數為0;否則就會使用delete來釋放p所指向的記憶體資源。If p is the only shared_ptr pointing at its object, reset frees p's existing object. If the optional built-in pointer q is passed,makes p point to q, otherwise makes p null. If d is supplied,will call d to free q otherwise uses delete to free q.

此式其實應該就把q拷貝給p,如果有提供q的話。將q拷貝給p,則應釋放的應是p之物件佔用的記憶體,怎麼會是q呢?疑英文版已誤矣。是也,英文版誤矣!中文版也跟著誤!

測試英文版錯誤的程式碼如下:真是盡信書不如無書了!唉

shared_ptr<int>pp(new int(3));

void process()

{

int* r = new int(10);

shared_ptr<int> ptr(r);//此時「10」這個動態物件有 普通指標r、智慧指標ptr兩個指標指向它

int* q = new int(2);

cout << "*ptr=" << *ptr << endl;

cout << "ptr計數" << ptr.use_count() << endl;

cout << "pp計數" << pp.use_count() << endl;

ptr.reset(q);//此時r指向的「10」會被ptr摧毀,r就淪為懸置指標(dangling pointer),一個活死人(活屍)

if (q == nullptr)//q仍指向「2」,不會是空指標

cout << "yes:q == nullptr" << endl;

cout << "*ptr=" << *ptr << endl;

cout << "ptr計數" << ptr.use_count() << endl;

cout << "*q=" <<*q << endl;

r = nullptr;//這樣r就不再是懸置指標了,而是一個空指標

}



int main() {

process();

cout << "*pp="<<*pp << endl;

cout << "pp計數="<<pp.use_count() << endl;

}


因為函式的參數是以值來傳遞的(傳值pass by value),所以給process這個函式的shared_ptr型別的引數會被拷貝成ptr。拷貝一個shared_ ptr會遞增與它相關的參考計數,也就是有多少shared_ptr和它指向同一個動態配置物件。參考計數(reference count)正確來講應該是對某個動態物件的參考計數,即有多少智慧指標指向(參考到refer to)該物件。所以應該翻成指向係數還比較傳神達意!因此,在process這個函式的區塊中,這個參考計數的值至少會是「2」。當process執行完後,ptr會被銷毀,而其關聯的參考計數則隨之遞減,但卻不會降到「0」。因此,當這個區域變數ptr被摧毀時,ptr所指(refer to)的記憶體將不能得到釋放。

因此,要正確地使用這個process函式,而不是讓它用過的記憶體得不到釋放,就應該要以shared_ptr作為引數來導入給它:

shared_ptr<int> p(new int (42)) ; //此時p關聯的參考計數是1

process(p) ;//拷貝p會遞增其計數;在process中,參考計數是2

int i = *p; //ok: 參考計數是1

另外值得注意的是:雖然在這個例子中,我們不能直接用一個內建型別的普通指標當作引數傳給process函式,但是卻可以用一個以內建型別new出來的普通指標來明確建構(explicitly construct)一個暫時性的shared_ptr當作引數傳給process。然而,若真的這麼做,雖然在程式的編譯與執行上或許並不會出現什麼大的錯誤,但這麼一來,對動態記憶體的控管就得冒上很大的風險;可以想像像下面這樣的情形:

int *x(new int(1024)); //危險:x只是一個普通的指標,而非一個智慧指標

process (x) ; // 錯誤:無法將 int* 直接轉換為 shared_ptr<int>

process(shared_ptr<int>(x));// 雖然可以正確編譯,(此時等於有2個指標(一個是x這個普通指標,一個是由x創建出來的智慧指標shared_ptr)同時指向new出來的動態物件;但關聯的參考計數卻只有「1」!)但在實際執行時,一旦離開了process範疇,這個shared_ptr所指向的物件,將會隨著這個shared_ptr的消逝而被刪除!因為這時shared_ptr其關聯的參考計數在process區塊中,只有「1」!離開了區塊,就降為0,它所指的物件,也就隨之刪除。

int j = *x; //對一個已經失效的指標作解參考(dereference)運算是未定義的行為(即沒有意義的操作):此時x淪為一個懸置指標!

在上面呼叫process的程式碼中,我們將一個暫時性的shared_ptr當作引數傳給process。這個暫時性的shared_ptr會在它所在的函式process執行完畢後就被摧毀。當它被摧毀時就會將它關聯的參考計數減1,這麼一來,原本只有「1」的參考計數就會歸「0」。此時,這個暫存的shared_ptr指向的動態物件就會被刪除。

但x還是一個活指標(活著的指標),並沒有因shared_ptr的消逝而死掉,它仍然指向先前它所指向的那個記憶體位址,也就是已經被shared_ptr「掏空」的那個記憶體位址,所以x不能避免地就淪為一個懸置指標了。現在開始,對x的一切操作,都將會是未定義的沒有意義的錯誤。

一旦我們將shared_ptr和普通指標繫結在一塊,我們就是將控管該記憶體的責任交給了那個shared_ptr。當我們把一個普通指標原有的責任(控管(未必是動態)記憶體的責任)移交給了shared_ptr,我們就不應再用那個普通指標(或一個內建指標)來存取現在那個shared_ptr指向的記憶體位址。

頁466

警告:

使用內建指標來存取智慧指標所指的物件,是很危險的,因為我們並不知道那個物件被智慧指標(在它們消失殆盡時,調用delete等效的程序來)摧毀了沒。

...and Don’t Use get to Initialize or Assign Another Smart Pointer

…而且別使用智慧指標提供的成員函式get來初始化或指定另一個智慧指標

各別獨立(即參考計數是獨立計算的)的shared_ptr不要共用一個動態配置的物件(dynamically allocated object),以免在某一個參考計數(reference count)歸零時,就逕行將該物件摧毀了。

智慧指標類別定義了一個名為get的成員函式(已見前表12.1),它會回傳一個內建型別的普通指標非智慧指標指向它所在的智慧指標控管的物件。提供這個get成員函式,主要是要在無法使用智慧指標的情境下來使用(如需要傳入一個普通指標(plain pointer)的引數時)。使用到get回傳值的程式碼,決定不能在回傳的這個普通指標上執行delete的運算!

即使編譯器在編譯時不會出現錯誤訊息,但將智慧指標與由某個智慧指標以get的方式回傳的普通指標繫結起來,就是個錯誤:

shared_ptr<int> p(new int(42)) ; // 此時p所指的動態物件其參考計數是 1

int *q = p.get() ; //將q作為儲存p.get()回傳值的變數是ok的,但決定不要在可能刪除q所指物件的情境下用到這個q(don't use q in any way that might delete its pointer)

{ //新的區塊

//這是未定義的:因為兩個各自定義的shared_ptr卻指向相同的記憶體位址

shared_ptr<int>(q);

}// 當區塊結東,「 shared_ptr<int>(q)」(用q來初始化創建的shared_ptr<int>會被摧毁,當它摧毀時,因為它是獨立於p的,所以參考計數是1,摧毀後成了0,就會調用delete來刪除它所指向的物件,這個物件也是q所指向的,所以q指向的記憶體位址就會被釋放

// block ends, q is destroyed, and the memory to which q points is freed

int foo = *p; //這是未定義的;因為p所指的記憶體已被區塊中的shared_ptr<int>釋放了

//此例當即為p與q與shared_ptr<int>三者共享了int(42)這個記憶體資源

測試的程式碼詳見:https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/blob/p466_Dont_Use_get_with_Another_Smart_Pointer_test/prog1/prog1.cpp

果然在這裡英文版又錯了,但是pdf錯,google 圖書版已更正了,中文版卻是根據pdf錯的來翻譯,更正版如下:

shared_ptr<int> p(new int(42)); // reference count is 1

int *q = p.get(); // ok: but don't use q in any way that might delete its pointer

{ // new block

// undefined: two independent shared_ptrs point to the same memory

auto local = shared_ptr<int>(q);

} // block ends, local is destroyed, and the memory to which p and q points is freed

int foo = *p; // undefined; the memory to which p points was freed

Here, p ,q,and local all point to the same memory. 在這裡,p、q和local共享了記憶體資源,都指向同一個記憶體位址。Because p and local were created independently from one other, each has a reference count of 1.因為p和local是分別宣告而定義的shared_ptr,所以它們各自有其獨立的參考計數(reference count),都是「1」。 When the inner block ends,local is destroyed.一旦區塊執行結束,local就會被摧毀。Because local's reference count is 1,the memory to which it points will be freed. 而因為local的參考計數是1,它被摧毀後,這個參考計數就是歸0,其所指向的記憶體資源也會隨之釋出。That makes p and q into a dangling pointers;這就造成p和q都淪為了懸置指標(dangling pointer)。可見,懸置指標與是不是智慧指標、普通指標無關,只要是指標,指向了一個不復存在的物件或被清空的記憶體位址,那麼,它就是懸置指標。 what happens when we attempt to use p or q is undefined. 當p與q 成了懸置指標,對它們做任何的操作或運算都會是未定義的了。Moreover, when p is destroyed, the pointer to that memory will be deleted a second time.甚至,當智慧指標p將消逝時,與它相關的參考計數也會歸0,當它企圖去刪除它指向的物件,這個物件卻早已不復存在,這樣的刪除delete動作就會造成對該處記憶體位址進行第2次清除的這種錯誤,這就有可能如前所述,有損壞集區(pool)或堆置(堆積)記憶體區的危險。

警告

只將get回傳的普通指標用在那些不會對這個指標進行直接/明確或連帶/隱含的delete運算的程式碼。特別是,絕對不要使用get回傳的指標來對另一個智慧指標進行初始化動作,或將這個指標指定給另一個智慧指標。因為這樣一來,對某個物件,就會有其各自獨立的參考計數,只要有一方參考計數歸零,那麼,這個物件就有被其消滅的危險!

忘了錄的部分就看臉書直播513集。 https://www.facebook.com/100003034306665/videos/2591997694244682




留言

熱門文章