C++Primer第五版 第十三章習題答案(41~50)
41:就是前置先加後用,後置先用後加,first_free指向的是一個空位置,前置的話會跳過一個空位置。
42:本章所實現的StrVec類屬於簡化版本的容器類,只適用於string,執行時可動態分配記憶體的大小
43:使用for_each和lambda表示式可能會更好一點,無需迴圈,語義更加明顯
void free() { if (elements) { // for (auto p = first_free; p != elements; ) // { // alloc.destroy(--p); // } for_each(elements, first_free, [this](std::string &rhs){ alloc.destroy(&rhs); }); alloc.deallocate(elements,cap-elements); } }
44:知識點1:const_cast<type_id> (expression)
該運算子用來修改型別的const或volatile屬性。除了const 或volatile修飾之外, type_id和expression的型別是一樣的。
一、常量指標被轉化成非常量的指標,並且仍然指向原來的物件;(本例中用到)
二、常量引用被轉換成非常量的引用,並且仍然指向原來的物件;
三、const_cast一般用於修改底指標。如const char *p形式。
知識點2:別怕煩,動手寫!
#ifndef STRVEC_H #define STRVEC_H #include <string> #include <algorithm> #include <memory> using namespace std; class String { public: String();//預設建構函式 String(const char*s)//接受c風格字串引數的建構函式,s為指向字串的指標(首位置) { auto s1 = const_cast<char*>(s);//轉化為非常量的指標 while(*s1) { ++s1;//使其指向最後一個位置的尾部 } alloc_n_copy(s,s1);//進行拷貝 } String(const String&);//拷貝建構函式 String& operator=(const String&);//拷貝賦值運算子 ~String()//解構函式 { free(); } void free()//釋放記憶體 { if (elements) { for_each(elements,end,[this](char &rhs){alloc.destroy(&rhs);}); alloc.deallocate(elements,end-elements); } } private: allocator<char> alloc;//分配記憶體的方法 char *elements;//首尾指標 char *end; std::pair<char*, char*> alloc_n_copy(const char*a, const char*b)//拷貝賦值函式 { auto s1 = alloc.allocate(b-a);//allocate引數為分配記憶體的大小 auto s2 = uninitialized_copy(a,b,s1);//拷貝賦值,將a到b之間的元素拷貝至s1,返回的是最後一個構造元素之後的位置 return make_pair(s1,s2);//返回首尾指標 } void range_initializer(const char*c, const char*d)//初始化 { auto p = alloc_n_copy(d,c);//拷貝並初始化新的string elements = p.first; end = p.second;//將新的string的首尾指標賦值 } }; #endif STRVEC_H
45:知識點1:新標準的特性:可以移動而非拷貝物件,可直接呼叫std::move(),移動而非拷貝會大幅度的提高效能:不必要進行舊記憶體到新記憶體的拷貝,當物件很大時省下很多的記憶體,另一個原因是像IO類和unique_ptr這樣的類不可以進行拷貝,但是我們可以使用移動來共享其中的資源
知識點2:右值引用:必須繫結到右值的引用通過兩個取址符號&&來獲得右值引用:只繫結到一個將要銷燬的物件,因此,我們可以自由的將右值引用的資源“移動”到另一個物件中
知識點3:左值和右值的區別:P121頁
知識點4:一個右值引用本質上也只是一個物件的另外一個名字而已。對於常規的引用:稱之為左值引用,我們不能將其繫結到所要轉換的表示式,而右值引用可以繫結,見下例:
int a = 42;
int &p1 = a;//正確,是a的引用
int &&p2 = a;//錯誤,不能將一個右值繫結到一個左值上
int &p3 = a *42;//錯誤,a*42為右值,&iii為左值引用
const int &p4 = a*42;//正確,可以將一個右值繫結到一個const的引用上
int &&p5 = a*42;//正確,右值繫結
知識點5:由上例可知,左值持久,右值短暫,左值具有持久的狀態,右值要麼是字面值常量,要麼是在表示式求值的過程中建立的臨時物件(沒有其他使用者進行繫結且要被銷燬):使用右值引用的程式碼可以自由的接管所引用的物件的資源
知識點6:變數是左值
int &&p6 = 42;//正確,字面值是右值
int &&p7 = p6;//錯誤,不能將一個右值引用直接繫結到一個變數上,即使這個變數是右值引用型別
答案見知識點
46:
(a):f()為函式的返回值,臨時值,屬於右值,&&
(b):vi[0]為變數,屬於左值,&
(c):r1為變數,屬於左值,&
(d):右側為表示式,屬於右值,&&
47:知識點:可以通過一個名為move()的函式來獲得繫結到左值上的右值引用,標頭檔案為utility,move告訴編譯器,我有一個左值,但是我想像一個右值一樣處理它:除了賦值或者銷燬,不能再使用它,使用move時應為std::move
String(const String& rhs)//拷貝建構函式
{
range_initializer(rhs.elements, rhs.end);
cout << "拷貝建構函式" << endl;
}
String& operator=(const String& rhs)//拷貝賦值運算子
{
auto newstr = alloc_n_copy(rhs.elements, rhs.end);
free();
elements = newstr.first;
end = newstr.second;
cout << "拷貝賦值運算子" << endl;
return *this;
}
48:見47
#include <iostream>
#include <vector>
#include "StrVec.h"
using namespace std;
int main(int argc, char**argv)
{
vector<String> vec;
String s1("hello");
String s2 = s1;
vec.push_back(s1);
vec.push_back(s2);
return 0;
}
49:知識點1:類的移動建構函式:引數為該類型別的右值引用,任何額外引數須有預設引數,一旦資源被移動,源物件對移動之後的資源已經不再有控制權,最後源物件會被銷燬。注意,移動建構函式是“竊取”資源,並不會分配資源。
知識點2:移動操作不會報出任何異常,因為其不分配資源,所以我們需要告知標準庫,以方便一起處理時多做一些額外的工作——加上noexpect(C++11新標準),在類的宣告和定義是都需要指出noexpect
知識點3:不丟擲異常的移動建構函式和移動賦值運算子必須標記為noexpect——解釋見書本P474頁
知識點4:移動後的源物件可能會在移動後被銷燬,所以必須進入可析構的狀態——將源物件的指標成員置為nullptr來實現
知識點5:當一個類為其自身定義了拷貝建構函式和拷貝賦值運算子或者解構函式,且其所有資料成員皆可移動構造時,那麼編譯器將不會再為該類合成移動建構函式和移動賦值運算子
知識點6:移動右值、拷貝左值、沒有移動建構函式時,右值也會被拷貝
知識點7:所有五個拷貝控制成員應該在類中一起出現!
String& String::operator=(String&& rhs) NOEXCEPT
{
if (this != &rhs) {
free();
elements = rhs.elements;
end = rhs.end;
rhs.elements = rhs.end = nullptr;//將源物件置為可析構的狀態
}
return *this;
}
String::String(String&& s) NOEXCEPT : elements(s.elements), end(s.end)
{
s.elements = s.end = nullptr;//將源物件置為可析構的狀態
}
50:
String baz()
{
String ret("world");
return ret; // 返回值會避免拷貝
}
String s5 = baz(); // 右值會避免拷貝