不知道這份答案是否有人看,看某幾章的答案有一點閱讀量,如果有任何錯誤或意見可以評論哦~
13章內存管理這部分看了兩周多,好慢好慢,不是很好理解~ 有沒有也正在看這本書的小伙伴呀,歡迎一起討論呢
13.01 拷貝構造函數是什么?什么時候使用它?
如果一個構造函數的第一個參數是自身類型的引用,且任何額外參數都有默認值,則此構造函數是拷貝構造函數。
拷貝函數通常再發生拷貝初始化的時候使用,而拷貝初始化發生的條件如下:
- 使用“=”定義變量時。
- 將一個對象作為實參傳遞給一個非引用類型的形參。
- 從一個返回類型為非引用類型的函數返回一個對象。
- 用花括號列表初始化一個數組中的元素或一個聚合類中的成員。
13.02 解釋為什么下面的聲明是非法的:
Sales_data::Sales_data(Sales_data rhs);
拷貝構造函數的第一個參數必須是一個引用類型。
13.03 當我們拷貝一個StrBlob時,會發生什么?拷貝一個StrBlobPtr呢?
void test1303()
{
StrBlob sb1;
cout << "reference count of sb1 is " << sb1.count() << endl; // 1
StrBlob sb2(sb1);
cout << "reference count of sb1 is " << sb1.count() << endl; // 2
StrBlobPtr sbp1(sb1);
cout << "reference count of sbp1 is " << sbp1.count() << endl; // 2
StrBlobPtr sbp2 (sbp1);
cout << "reference count of sbp1 is " << sbp1.count() << endl; // 2
}
拷貝StrBlob時,其shared_ptr成員的引用計數會增加。拷貝StrBlobPtr,unique_ptr成員的引用計數不變,其引用了shared_ptr,但不影響shared_ptr的引用計數。
13.04 假定Point是一個類類型,它有一個public的拷貝構造函數,指出下面程序片段中哪些地方使用了拷貝構造函數。
Point global;
Point foo_bar(Point arg) // 使用了拷貝構造1
{
Point local = arg, *heap = new Point(global); // 使用了拷貝構造2,3
*heap = local;
Point pa[4] = {local, *heap}; // 使用了拷貝構造4,5
return *heap; // 使用了拷貝構造
}
13.05 給定下面的類框架,編寫一個拷貝構造函數,拷貝所有成員。你的構造函數應該動態分配一個新的string,並將對象拷貝到ps指向的位置,而不是ps本身的位置。
class HasPtr {
public:
HasPtr (const std::string &s = std::string()) : ps (new std::string(s)), i(0){}
// 拷貝構造函數
HasPtr(const HasPtr& hp) : ps (new std::string(*hp.ps)), i (hp.i) {}
private:
std::string *ps;
int i;
}
13.06 拷貝賦值運算符是什么?什么時候使用它?合成拷貝賦值運算符完成什么工作?什么時候會生成合成拷貝賦值運算符?
拷貝賦值運算符是重載”=“運算符,即為一個名為operator=的函數,其參數與其所在類的的類型相同。
在發生賦值操作的時候使用。
合成拷貝賦值的工作:將后側運算對象的每個非static成員賦予左側運算對象的對應成員。
當類未定義自己的拷貝賦值運算符,編譯器會生成一個合成拷貝運算符。
13.07 當我們將一個StrBlob賦值給另一個StrBlob時,會發生什么?賦值StrBlobPtr呢?
void test1307()
{
StrBlob sb1;
cout << "reference count of sb1 is " << sb1.count() << endl; // 1
StrBlob sb2 = sb1;
cout << "reference count of sb1 is " << sb1.count() << endl; // 2
StrBlobPtr sbp1(sb1);
cout << "reference count of sbp1 is " << sbp1.count() << endl; // 2
StrBlobPtr sbp2 = sbp1;
cout << "reference count of sbp1 is " << sbp1.count() << endl; // 2
}
// 會發生淺拷貝,所有的指針都指向同一塊內存。與拷貝一樣,賦值StrBlob時,shared_ptr的引用計數加1,賦值StrBlobPtr時,引用計數不變。
13.08 為13.1.1節練習13.5中的HasPtr類編寫賦值運算符。類似拷貝構造函數,你的賦值運算符應該將對象拷貝到ps指向的位置。
HasPtr& operator= (const HasPtr& hp)
{
ps = new std::string (*hp.ps);
i = hp.i;
return *this;
}
13.9 析構函數是什么?合成析構函數完成什么工作?什么時候會生成合成析構函數?
析構函數是類的一個成員函數,名字由波浪號接類名構成。它沒有返回值,也不接受參數。
對於某些類,合成析構函數被用來阻止該類型的對象被銷毀。如果不是這種情況,合成析構函數的函數體就為空。
當一個類未定義自己的析構函數時,編譯器會為它定義一個合成析構函數。
13.10 當一個StrBlob對象銷毀時會發生什么?一個StrBlobPtr對象銷毀時呢?
銷毀StrBlob時,分別會執行vector、shared_ptr、string的析構函數,vector析構函數會銷毀我們添加到vector中的元素,shared_ptr析構函數會遞減StrBlob對象的引用計數,
13.11 為前面練習中的HasPtr類添加一個析構函數。
~HasPtr () { delete ps; }
13.12 在下面的代碼片段中會發生幾次析構函數調用?
bool fcn (const Sales_data *trans, Sales_data accum)
{
Sales_data item1 (*trans), item2 (accum);
return item1.isbn() != item2.isbn();
} // 退出作用域時,對item1和item2調用析構函數。
// 參數accum在函數結束時會調用嗎?
// 當一個對象的引用或指針離開作用域,並不會執行析構。
13.13 理解拷貝控制成員和構造函數的一個好方法時定義一個簡單的類,為該類定義這些成員,每個成員都打印出自己的名字:
struct X {
X() { std::cout << "X()" << std::endl; }
X(const X&) { std::cout << "X(const X&)" << std::endl; }
};
給X添加拷貝賦值運算符和析構函數,並編寫一個程序以不同方式使用X的對象:將它們作為非引用和引用參數傳遞;動態分配它們;將它們存放於容器中;諸如此類。觀察程序的輸出,直到你確認理解了什么時候會使用拷貝控制成員,以及為什么會使用它們。當你觀察程序輸出時,記住編譯器可以略過對拷貝構造函數的調用。
struct X {
X() { std::cout << "X()" << std::endl; }
X(const X&) { std::cout << "X(const X&)" << std::endl; }
X& operator= (const X& x);
~ X() { std::cout << "~X()" << std::endl; }
};
X& X::operator=(const X& x)
{
std::cout << "~X()" << std::endl;
}
void f (X& x1, X* x2)
{
vector<X> xvec;
xvec.push_back(x1);
xvec.push_back(*x2);
cout << "------- destructor in f -----" << endl;
}
void test1313()
{
cout << "------- constructor -----" << endl;
X x1;
X *x2 = new X();
cout << "------- copy constructor -----" << endl;
f (x1, x2);
cout << "------- destructor 1-----" << endl;
delete x2;
cout << "------- destructor 2-----" << endl;
}
// 結果如下
------- constructor -----
X()
X()
------- copy constructor -----
X(const X&) // 調用f函數,為什么發生三次拷貝構造和一次析構???
X(const X&)
X(const X&)
~X()
------- destructor in f -----
~X()
~X()
------- destructor 1-----
~X()
------- destructor 2-----
~X()
13.14 假定numbered是一個類,它有一個默認構造函數,能為每個對象生成一個唯一的序號,保存在名為mysn的數據成員中。假定numbered使用合成的拷貝控制成員,並給定如下函數:
void f (numbered s) { cout << s.mysn << endl; }
則下面代碼輸出什么內容?
numbered a, b = a, c = b;
f(a); f(b); f(c);
會輸出三個相同的序號。因為是合成拷貝,因此a, b, c實際使用同一個mysn結構。
13.15 假定numbered定義了一個拷貝構造函數,能生成一個新的序號。這會改變上一題中調用的輸出結果嗎?如果會改變,為什么?新的輸出結果是什么?
會改變,因為調用f函數傳參時以傳值方式傳遞,需要調用拷貝構造函數,會生成一個新的序號,因此也會輸出三個不同的序號。
13.16 如果f中的參數是const numbered&,將會怎樣?這會改變輸出結果嗎?如果會改變,為什么?新的輸出結果是什么?
不會改變,因為如果傳的是引用,則傳遞時numbered對象不會調用拷貝構造,因此三個輸出應該相同。
13.17 分別編寫前三題中描述的numbered和f,驗證你是否正確預測了輸出結果。
class numbered
{
public:
numbered() { mysn = ++ initail_number; }
numbered (const numbered& n)
{
mysn = ++ initail_number;
std::cout << "use copy constructor." << std::endl;
}
int mysn;
static int initail_number;
};
int numbered::initail_number = 2002;
void f (const numbered& s)
{
cout << s.mysn << endl;
}
void test1317_3()
{
numbered a, b = a, c = b;
cout << "--------" << endl;
f(a);
f(b);
f(c);
}
// 如果沒有拷貝構造函數,則輸出都是2003;
// 定義拷貝構造函數,輸出分別是2003,2004,2005
// 第三種情況輸出如下:
use copy constructor.
use copy constructor.
--------
2003
2004
2005
// 在執行numbered a, b = a, c = b; 也會調用拷貝構造函數,因為定義一個b,c是新的對象,調用拷貝構造
13.18 定義一個Employee類,它包含雇員的姓名和唯一的雇員證號。為這個類定義默認構造函數,以及接受一個表示雇員姓名的string的構造函數。每個構造函數應該通過遞增一個static數據成員來生成一個唯一的證號。
class Employee
{
public:
Employee () = default;
Employee (const string& n) : name (n)
{
employee_id = ++ increment_number;
}
private:
static int increment_number;
int employee_id;
string name;
};
13.19 你的Employee類需要定義它自己的拷貝控制成員嗎?如果需要,為什么?如果不需要,為什么?實現你認為Employee需要的拷貝控制成員。
不需要拷貝控制成員,因為每個雇員的ID都不同,不能有兩個id和name都相同的雇員。
class Employee
{
public:
Employee () = default;
Employee (const string& n) : name (n)
{
employee_id = ++ increment_number;
}
Employee (const Employee& ) = delete;
Employee& operator= (const Employee&) = delete;
private:
static int increment_number;
int employee_id;
string name;
};
13.20 解釋我們拷貝、賦值或銷毀TextQuery和QueryResult類對象時會發生什么。
因為這兩個類中使用的是智能指針,因此在拷貝時,類的所有成員都將被拷貝,在銷毀時所有成員也將被銷毀。
13.21 你認為TextQuery和QueryResult類需要定義它們自己版本的拷貝控制成員嗎?如果需要,為什么?如果不需要,為什么?實現你認為這兩個類需要的拷貝控制操作。
判斷一個類是否需要自己版本的拷貝控制成員,一個基本原則是首先確定這個類是否需要一個析構函數。TextQuery和QueryResult類使用智能指針,可以自動控制釋放內存,因為其不需要自己版本的析構函數,也就不需要自己版本的拷貝控制函數了。
13.22 假定我們希望HasPtr的行為像一個值。即,對於對象所指向的string成員,每個對象都有一份自己的拷貝。我們將在下一節介紹拷貝控制成員的定義。但是,你以及學習了定義這些成員所需要的所有知識。在繼續學習下一節之前,為HasPtr編寫拷貝構造函數和拷貝賦值運算符。
class HasPtr {
public:
HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0){}
HasPtr(const HasPtr& hp) : ps (new std::string(*hp.ps)), i (hp.i) {}
HasPtr& operator= (const HasPtr& hp)
{
auto newp = new std::string(*hp.ps);
delete ps;
ps = newp;
i = hp.i;
return *this;
}
string& getPs()
{
cout << ps << endl;
return *ps;
}
~HasPtr () { delete ps; }
private:
std::string *ps;
int i;
};
13.23 比較上一節練習中你編寫的拷貝控制成員和這一節中的代碼。確定你理解了你的代碼和我們的代碼之間的差異(如果有的話)。
13.24 如果本節中的HasPtr版本未定義析構函數,將會發生什么?如果未定義拷貝構造函數,將會發生什么?
若未定義析構函數,則每次類銷毀時都不會釋放ps指向內存,造成內存泄漏。
如果未定義拷貝構造函數,則如果使用另外一個HasPtr類對象構造新的HasPtr類對象,則兩個對象的ps指向同一塊內存。如果要銷毀這兩個對象,就會造成同一塊內存被釋放兩次。
13.25 假定希望定義StrBlob的類值版本,而且希望繼續使用shared_ptr,這樣我們的StrBlobPtr類就仍能使用指向vector的weak_ptr了。你修改后的類將需要一個拷貝構造函數和一個拷貝賦值運算符,但不需要析構函數。解釋拷貝構造函數的拷貝賦值運算符必須要做什么。解釋為什么不需要析構函數。
拷貝構造函數和拷貝賦值函數的作用是:保證類的對象在拷貝時可以自動分配內存,而不是指向右值的內存。
不需要析構函數的原因:StrBlob類中使用的是shared_ptr,可以自動管理內存,在離開作用域時自動銷毀。
13.26 對上一題描述中的StrBlob類,編寫你自己的版本。
在StrBlob類中加:
StrBlob (const StrBlob& sb)
{
data = make_shared<vector<string>>(*sb.data);
}
StrBlob& operator= (const StrBlob& sb)
{
data = make_shared<std::vector<string>>(*sb.data);
return *this;
}
13.27 定義你自己的使用引用計數版本的HasPtr。
class HasPtr {
public:
HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0), use_count(new std::size_t(1)){}
HasPtr (const HasPtr& hp) : ps(hp.ps), i(hp.i), use_count(hp.use_count)
{
++ *use_count;
}
HasPtr& operator= (const HasPtr& hp);
string& getPs()
{
cout << ps << endl;
return *ps;
}
~HasPtr ()
{
if (0 == *--use_count) {
delete ps;
delete use_count;
}
}
private:
std::string *ps;
int i;
std::size_t *use_count;
};
HasPtr& HasPtr::operator=(const HasPtr& hp)
{
++ *hp.use_count; // 遞增右側運算對象的引用計數
if (0 == --*use_count) {
delete ps;
delete use_count;
}
ps = hp.ps;
i = hp.i;
use_count = hp.use_count;
return *this;
}
13.28 給定下面的類,為其實現一個默認構造函數和必要的拷貝控制成員。
// .h文件
class TreeNode
{
public:
TreeNode () : value(std::string()), count (new int(1)), left(nullptr), right(nullptr) {}
TreeNode (const TreeNode& tn) : value (tn.value), count (tn.count), left (tn.left), right(tn.right) { ++ *count; }
TreeNode& operator= (const TreeNode& tn);
~ TreeNode()
{
if (0 == -- *count) {
delete left;
delete right;
delete count;
}
}
private:
std::string value;
int *count;
TreeNode *left;
TreeNode *right;
};
class BinStrTree
{
public:
BinStrTree() : root (new TreeNode()) {}
BinStrTree(const BinStrTree& bst) : root (bst.root) {}
BinStrTree& operator= (const BinStrTree& bst);
~ BinStrTree () { delete root; }
private:
TreeNode *root;
};
// .cpp 文件
TreeNode& TreeNode::operator= (const TreeNode& tn)
{
++ *tn.count;
if (0 == *--count) {
delete left;
delete right;
delete count;
}
value = tn.value;
count = tn.count;
left = tn.left;
right = tn.right;
}
BinStrTree& BinStrTree::operator= (const BinStrTree& bst)
{
TreeNode *new_root = new TreeNode(*bst.root);
delete root;
root = new_root;
return *this;
}
13.29 解釋swap(HasPtr&, HasPtr&)中對swap的調用不會導致遞歸循環。
因為swap(HasPtr&, HasPtr&)函數中,swap (lhs.ps, rhs.ps);調用了std::swap(string, string),swap (lhs.i, rhs.i);調用了std::swap(int, int)。調用了不同的swap函數,因此不會導致遞歸循環。
13.30 為你的類值版本的HasPtr編寫swap函數,並測試它。為你的swap函數添加一個打印語句,指出函數什么時候執行。
// .h文件
class HasPtr {
friend void swap(HasPtr&, HasPtr&);
public:
HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0){}
HasPtr(const HasPtr& hp) : ps (new std::string(*hp.ps)), i (hp.i) {}
HasPtr& operator= (const HasPtr& hp)
{
auto newp = new std::string(*hp.ps);
delete ps;
ps = newp;
i = hp.i;
return *this;
}
std::string& getPs()
{
return *ps;
}
~HasPtr () { delete ps; }
private:
std::string *ps;
int i;
};
inline void swap(HasPtr& lhs, HasPtr& rhs)
{
using std::swap;
swap (lhs.ps, rhs.ps);
swap (lhs.i, rhs.i);
std::cout << "call the swap function." << std::endl;
}
// 測試 .cpp
int main()
{
HasPtr hp1 ("hello");
HasPtr hp2 ("world");
swap(hp1, hp2);
cout << hp1.getPs() << endl;
cout << hp2.getPs() << endl;
return 0;
}
13.31 為你的HasPtr類定義一個<運算符,並定義一個HasPtr的vector。為這個vector添加一些元素,並對它執行sort。注意何時會調用swap。
// .h文件,注意要重寫operator=、swap函數;新增operator<函數。
class HasPtr {
friend void swap(HasPtr&, HasPtr&);
friend bool operator< (HasPtr& lhs, HasPtr& rhs);
public:
HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0){}
HasPtr(const HasPtr& hp) : ps (new std::string(*hp.ps)), i (hp.i) {}
HasPtr& operator= (HasPtr hp)
{
this->swap(hp);
std::cout << "call operator=(HasPtr &rhs)" << std::endl;
return *this;
}
void swap(HasPtr &rhs)
{
using std::swap;
swap(ps, rhs.ps);
swap(i, rhs.i);
std::cout << "call swap(HasPtr &rhs)" << std::endl;
}
std::string& getPs()
{
return *ps;
}
~HasPtr () { delete ps; }
private:
std::string *ps;
int i;
};
void swap(HasPtr& lhs, HasPtr& rhs)
{
lhs.swap(rhs);
}
bool operator< (HasPtr& lhs, HasPtr& rhs)
{
return *lhs.ps < *rhs.ps;
}
// .cpp文件
void test1331()
{
HasPtr hp1 ("hello");
HasPtr hp2 ("world");
HasPtr hp3 ("Anna");
vector<HasPtr> hvec;
hvec.push_back(hp1);
hvec.push_back(hp2);
hvec.push_back(hp3);
std::sort (hvec.begin(), hvec.end());
for (auto i : hvec) {
cout << i.getPs() << endl;
}
}
// 輸出結果:
------------
call swap(HasPtr &rhs)
call operator=(HasPtr &rhs)
call swap(HasPtr &rhs)
call operator=(HasPtr &rhs)
call swap(HasPtr &rhs)
call operator=(HasPtr &rhs)
call swap(HasPtr &rhs)
call operator=(HasPtr &rhs)
------------
Anna
hello
world
13.32 類指針的HasPtr版本會從swap函數受益嗎?如果會,得到了什么益處?如果不是,為什么?
類指針的版本,使用std::swap函數就可以實現指針交換,節省內存空間,不需要重新自定義swap函數。因此類指針的版本不會從swap函數收益。
13.33 為什么Message的成員save和remove的參數是一個Folder&?為什么我們不將參數定義為Folder或是const Folder&?
不定義為Folder的原因是:如果是值傳遞,則會在傳參的時候傳遞參數的副本,執行修改參數的操作,修改的是參數的副本。而且要為副本重新分配內存,浪費空間。
不定義為const Folder&的原因是:在save和remove中都要改變參數,因此不能定義為const類型。
13.34 編寫本節所描述的Message。
// .h 文件
class Folder;
class Message
{
friend class Folder;
friend void swap (Message& lhs, Message& rhs);
public:
explicit Message (const std::string& str = "") : contents(str) {}
Message (const Message&);
Message& operator= (const Message&);
~ Message ();
void save (Folder &f);
void remove (Folder &f);
private:
std::string contents; // 實際消息文本
std::set<Folder*> folders; // 包含本Message的Folder
void add_to_Folders (const Message&);
void remove_from_Folders ();
};
void swap(Message& lhs, Message& rhs)
{
using std::swap;
for (auto f : lhs.folders) {
f->remMsg(&lhs);
}
for (auto f : rhs.folders) {
f->remMsg(&rhs);
}
swap (lhs.folders, rhs.folders);
swap (lhs.contents, rhs.contents);
for (auto f : lhs.folders) {
f->addMsg (&lhs);
}
for (auto f : rhs.folders) {
f->addMsg(&rhs);
}
}
// .cpp文件
void Message::save (Folder &f)
{
folders.insert (&f);
f.addMsg(this);
}
void Message::remove (Folder &f)
{
folders.erase(&f);
f.remMsg(this);
}
void Message::add_to_Folders (const Message& m)
{
for (auto f : m.folders) {
f->addMsg(this);
}
}
Message::Message (const Message& m) : contents (m.contents), folders(m.folders)
{
add_to_Folders (m);
}
void Message::remove_from_Folders ()
{
for (auto f : folders) {
f->remMsg(this);
}
}
Message::~Message()
{
remove_from_Folders();
}
Message& Message::operator= (const Message& rhs)
{
this->remove_from_Folders(); // 先從當前文件中移除
this->contents = rhs.contents;
this->folders = rhs.folders;
add_to_Folders(rhs);
return *this;
}
13.35 如果Message使用合成的拷貝控制成員,將會發生什么?
如果使用合成的拷貝控制成員,則當拷貝一個Message對象時,不會向folder中添加新的Message,folder將不會同步更新。
13.36 設計並實現對應的Folder類。此類應該保存一個指向Folder中包含的Message的set。
class Folder
{
public:
Folder ();
Folder (const Folder&);
Folder& operator= (const Folder&);
~ Folder();
void addMsg (Message* m)
{
messages.insert(m);
}
void remMsg (Message* m)
{
messages.erase(m);
}
private:
std::set<Message*> messages;
};
13.37 為Message類添加成員,實現folders添加或刪除一個給定的Folder*。這兩個成員類似Folder類的addMsg和remMsg操作。
// 在Message類中加:
void addFolder (Folder* f)
{
folders.insert(f);
}
void deleteFolder (Folder* f)
{
folders.erase(f);
}
13.38 我們並未使用拷貝和交換方式來設計Message的賦值運算符。你認為其原因是什么?
當涉及到動態分配時,使用拷貝和交換方式來實現賦值運算符是一個很好的方式(因為又共同的delete操作)。但Message類並未涉及到動態分配,此時如果使用拷貝和交換的方式就沒有意義。
13.39 編寫你自己版本的StrVec,包括自己版本的reserve、capacity和resize。
// .h文件
#include <string>
class StrVec
{
public:
StrVec() : elements(nullptr), first_free(nullptr), cap(nullptr) {}
StrVec(const StrVec&);
StrVec& operator= (const StrVec&);
~ StrVec();
void push_back(const std::string&);
size_t size() const { return first_free-elements; }
size_t capacity() const { return cap-elements; }
std::string *begin() const { return elements; }
std::string *end() const { return first_free; }
void reserve(size_t n);
void resize (size_t n);
void resize (size_t, const std::string& s);
private:
void chk_n_alloc()
{
if (size() == capacity()) {
reallocate();
}
}
std::pair<std::string*, std::string*> alloc_n_copy
(const std::string* element, const std::string* cap); // begin pointer and after tail pointer.
void alloc_n_move (size_t capacity);
void free(); // destroy the elements and free memory
void reallocate(); //
private:
std::allocator<std::string> alloc;
std::string* elements; // point to the first element of array
std::string* first_free; // point to the first free element of array
std::string* cap; // point to after the last element of array
};
// .cpp文件
void StrVec::reallocate()
{
auto newcapacity = size() ? 2 * size() : 1;
auto newdata = alloc.allocate (newcapacity);
auto dest = newdata; // point to the first free position of new array
auto elem = elements; // elements point to origin elements
for (size_t i = 0; i != size(); ++ i) {
alloc.construct (dest++, std::move(*elem++));
}
free();
elements = newdata;
first_free = dest;
cap = elements + newcapacity;
}
StrVec::StrVec(const StrVec& s)
{
auto new_data = alloc_n_copy(s.begin(), s.end());
elements = new_data.first;
first_free = cap = new_data.second;
}
StrVec& StrVec::operator=(const StrVec& s)
{
auto new_data = alloc_n_copy (s.begin(), s.end());
this->free ();
this->elements = new_data.first;
this->first_free = this->cap = new_data.second;
return *this;
}
StrVec::~StrVec()
{
free();
}
void StrVec::free()
{
if (elements) {
for (auto p = first_free; p != elements; ) {
alloc.destroy (--p);
}
alloc.deallocate (elements, cap-elements);
}
}
void StrVec::push_back(const std::string& s)
{
chk_n_alloc();
alloc.construct (first_free++, s);
}
pair<string*, string*> StrVec::alloc_n_copy(const std::string* element, const std::string* cap)
{
auto data = alloc.allocate (cap-element);
return {data, uninitialized_copy(element, cap, data)};
}
void StrVec::alloc_n_move(size_t capacity)
{
auto newdata = alloc.allocate (capacity);
auto dest = newdata;
auto elem = elements;
for (size_t i = 0; i != size(); ++i) {
alloc.construct (dest++, std::move(*elem++));
}
elements = newdata;
first_free = dest;
cap = elements + capacity;
}
void StrVec::reserve(size_t n)
{
if (n <= capacity()) return;
alloc_n_move (n);
}
void StrVec::resize(size_t n)
{
resize (n, "");
}
void StrVec::resize (size_t n, const std::string& s)
{
if (n > size()) {
if (n > capacity()) {
reserve (n);
}
for (size_t i = size(); i < n; ++i) {
alloc.construct (first_free++, s);
}
}
else if (n < size()) {
while ((elements+n) != first_free) {
alloc.destroy (--first_free);
}
}
}
13.40 為你的StrVec類添加一個構造函數,它接受一個initializer_list
// .h文件中添加:
public:
StrVec(std::initializer_list<std::string>& il);
private:
void range_initial(const std::string* first, const std::string* last);
// .cpp文件中添加:
void StrVec::range_initial(const string* first, const string* last)
{
auto newdata = alloc_n_copy (first, last);
elements = newdata.first;
first_free = cap = newdata.second;
}
StrVec::StrVec(std::initializer_list<string>& il)
{
range_initial (il.begin(), il.end());
}
13.41 在push_back中,我們為什么在construct調用中使用前置遞增運算?如果使用后置遞增運算的話,會發生什么?(題目有誤,construct中使用的是后置遞增運算)
first_free指向第一個空位置,使用后置遞增,在插入元素時插入當前指向的空位置,然后遞增移向后移。
如果使用前置遞增,則first_free先向后移動,再插入,此時first_free指向第二個空位置。
13.42 在你的TextQuery和QueryResult類中用你的StrVec類代替vector
//在StrVec.h中增加at方法的實現:
std::string& at (size_t pos) { return *(elements+pos); }
const std::string& at(size_t pos) const { return *(elements + pos); }
#include "StrVec.h"
class QueryResult;
class TextQuery
{
public:
// using line_no = StrVec::size_type();
TextQuery(std::ifstream& ifs);
QueryResult query (const std::string& word) const;
private:
std::shared_ptr<StrVec> sp_text;
// 每個單詞到它所出現的行號的映射
std::map<std::string, std::shared_ptr<std::set<size_t>>> sp_dictionary;
};
// textQuery.h文件實現,將vector<string>都改為StrVec,原來的line_no用size_t替換。
class QueryResult
{
public:
friend std::ostream& print (std::ostream&, const QueryResult&);
public:
QueryResult(const std::string& s,
std::shared_ptr<std::set<size_t>> sp_set,
std::shared_ptr<StrVec> sp_vec):
sought (s), lines (sp_set), file (sp_vec) {}
private:
std::string sought; // 要查找的單詞
std::shared_ptr<std::set<size_t>> lines; // 出現的行號
std::shared_ptr<StrVec> file; // 輸入文件
// vector<string> occur_line;
};
std::ostream& print (std::ostream&, const QueryResult&);
// textQuery.cpp的實現
#include "textQuery.h"
#include "StrVec.cpp"
// 我在windows下實現,這里要加include "StrVec.cpp",否則在鏈接時找不到StrVec類中方法的實現,就會報鏈接錯誤。在linux下應該可以把StrVec編譯為目標文件,然后與textQuery一起編譯。
// undefined reference to `StrVec::~StrVec()'
using namespace std;
TextQuery::TextQuery(std::ifstream& ifs) : sp_text (new StrVec())
{
string text;
while (getline(ifs, text)) {
sp_text->push_back(text);
int n = sp_text->size() - 1; // 當前行號
istringstream line (text);
string word;
while (line >> word) {
auto &lines = sp_dictionary[word];
if (!lines) {
lines.reset (new set<size_t>);
}
lines->insert(n);
}
}
}
QueryResult TextQuery::query(const std::string& word) const
{
// 如果未找到sought, 返回一個指向此set的指針。
static shared_ptr<set<size_t>> nodata (new set<size_t>);
auto loc = sp_dictionary.find(word);
if (loc == sp_dictionary.end()) {
return QueryResult(word, nodata, sp_text);
}
else {
return QueryResult(word, loc->second, sp_text);
}
}
ostream& print(ostream& os, const QueryResult& qr)
{
os << qr.sought << " occurs " << qr.lines->size() << " times." << endl;
for (auto i : *qr.lines) {
os << "\t(line " << i+1 << qr.file->at(i) << endl;
}
return os;
}
void runQueries (ifstream &infile)
{
TextQuery tq (infile);
while (true) {
cout << "Enter word to look for, or q to quit: ";
string s;
if (!(cin >> s) || s == "q") break;
print (cout, tq.query(s)) << endl;
}
}
int main()
{
ifstream ifs ("C:/Users/tutu/Documents/code/cpp_primer/ch12/textQuery.cpp");
runQueries(ifs);
return 0;
}
13.43 重寫free成員,用for_each和lambda來代替for循環destroy元素。你更傾向於哪種實現,為什么?
for_each(elements, first_free, [this](string& p){ alloc.destroy(&p); });
// for_each和lambda一起使用更簡潔,但是for循環更易讀。
13.44 編寫標准庫string類的簡化版本,命名為String。你的類應該至少有一個默認構造函數和一個接受C風格字符串指針參數的構造函數。使用allocator為你的String類分配所需內存。
// .h文件
#include <memory>
class String
{
public:
String() : String("") {}
String (const char* s);
String (const String& s);
String& operator= (String& s);
~ String();
void free();
const char* c_str() { return begin; }
size_t size() { return end - begin; }
size_t length() { return end - begin - 1; }
private:
std::pair<char*, char*> alloc_n_copy (const char* beg, const char* end);
void range_initial(const char* first, const char* last);
private:
char *begin;
char *end;
std::allocator<char> alloc;
};
// .cpp 文件
#include "String.h"
#include <string.h>
using namespace std;
pair<char*, char*> String::alloc_n_copy (const char* beg, const char* end)
{
auto str = alloc.allocate (end - beg);
return {str, uninitialized_copy(beg, end, str)};
}
void String::range_initial (const char* first, const char* last)
{
auto newString = alloc_n_copy (first, last);
begin = newString.first;
end = newString.second;
}
String::String(const char* s)
{
range_initial (s, s+sizeof(s));
}
String::String (const String& s)
{
range_initial (s.begin, s.end);
cout << "copy constructor." << endl;
}
void String::free()
{
if (begin) {
for (auto i = begin; i != end; ++ i) {
alloc.destroy (i);
}
alloc.deallocate (begin, end - begin);
}
}
String& String::operator= (String& s)
{
auto new_s = alloc_n_copy (s.begin, s.end);
free();
begin = new_s.first;
end = new_s.second;
cout << "copy assign constructor." << endl;
return *this;
}
13.45 解釋右值引用和左值引用的區別。
左值引用,也就是常規引用,返回左值的函數有賦值、下標、解引用和前置遞增/遞減運算符。
右值引用就是必須綁定到右值上的引用,返回右值的函數包括算術、關系、位及后置遞增/遞減運算符。
13.46 什么類型的引用可以綁定到下面的初始化器上?
int f();
vector<int> vi(100);
int? r1 = f(); // int &&r = f(); f()的返回值相當於一個常量,只能做右值引用或const引用。
int? r2 = vi[0]; // 下標運算返回左值,所以應該用int &r = vi[0]
int? r3 = r1; // r1此時相當與變量,int &r3 = r1;
int? r4 = vi[0] * f(); // 算術運算產生右值,int &&r4 = vi[0] * f();
13.47 對你在練習13.44中定義的String類,為它的拷貝構造函數和拷貝賦值運算符添加一條語句,在每次函數執行時打印一條信息。
同13.44題。
13.48 定義一個vector
void foo(String x)
{
}
void bar(const String& x)
{
}
String baz()
{
String ret("world");
return ret;
}
int main()
{
String s0;
String s1("hello");
String s2(s0);
String s3 = s1;
s2 = s1;
foo(s1);
bar(s1);
foo("temporary");
bar("temporary");
String s4 = baz();
std::vector<String> svec;
cout << " ---------- " << endl;
svec.push_back(s0);
svec.push_back(s1);
cout << " ---------- " << endl;
svec.push_back(baz());
svec.push_back("good job");
}
// print result:
copy constructor.
copy constructor.
copy assign constructor.
copy constructor.
----------
copy constructor.
copy constructor.
copy constructor. // push_back s0和s1,卻調用3次拷貝構造,因為vector最初capacity為1,push_back(s1)時,capacity需要增長至2,原來的元素都要拷貝到新的內存中。
----------
copy constructor. // capacity增長至4,拷貝之前的兩個元素到新內存。
copy constructor.
copy constructor.
copy constructor. // push_back("good job");調用一次拷貝構造。
// 每次push_back都會調用一次拷貝構造。
13.49 為你的StrVec、String和Message類添加一個移動構造函數和一個移動賦值運算符。
// Str.h文件中加:
StrVec (StrVec&& rhs) noexcept;
StrVec& operator= (StrVec&& rhs) noexcept;
// StrVec.cpp中加:
StrVec::StrVec(StrVec&& rhs) noexcept : elements (rhs.elements), first_free (rhs.first_free), cap (rhs.cap)
{
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
StrVec& StrVec::operator=(StrVec&& rhs) noexcept
{
if (this != &rhs) {
free ();
elements = rhs.elements;
first_free = rhs.first_free;
cap = rhs.cap;
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
//String.h
String (String&& s) noexcept;
String& operator= (String&& s) noexcept;
// String.cpp中加:
String::String(String&& s) noexcept : begin (s.begin), end (s.end)
{
s.begin = s.end = nullptr;
}
String& String::operator= (String&& s) noexcept
{
if (this != &s) {
free();
begin = s.begin;
end = s.end;
s.begin = s.end = nullptr;
}
return *this;
}
// Message.h
Message (Message&& m);
Message& operator= (Message&& m);
void moveFolders (Message* m);
// Message.cpp
void Message::moveFolders (Message* m)
{
folders = std::move(m->folders);
for (auto f : folders) {
f->remMsg(m);
f->addMsg(this);
}
m->folders.clear();
}
Message::Message (Message&& m) : contents (std::move(m.contents))
{
moveFolders(&m);
}
Message& Message::operator=(Message&& m)
{
if (this != &m) {
remove_from_Folders();
contents (std::move(m.contents));
moveFolders(&m);
}
return *this;
}
13.50 在你的String類的移動操作中添加打印語句,並重新運行13.6.1節的練習13.48中的程序,它使用了一個vector
int main()
{
String s0;
String s1("hello");
String s2(s0);
String s3 = s1;
s2 = s1;
cout << "1 ---------- " << endl;
foo(s1);
bar(s1);
foo("temporary");
bar("temporary");
String s4 = baz();
std::vector<String> svec;
cout << "2 ---------- " << endl;
svec.push_back(s0);
svec.push_back(s1);
cout << "3 ---------- " << endl;
svec.push_back(baz());
svec.push_back("good job");
}
// print result
String (const char* s)
construct
String (const char* s)
copy constructor.
copy constructor.
copy assign constructor.
1 ----------
copy constructor.
String (const char* s)
String (const char* s)
String (const char* s)
2 ----------
copy constructor.
copy constructor.
move construct. // 內存增長到2時,舊元素移動到新內存中。
3 ----------
String (const char* s)
move construct. // 有移動構造函數,push_back時會調用移動構造
move construct. // 內存增長至4,舊有的2個元素移到新內存中
move construct.
String (const char* s)
move construct.
13.51 雖然unique_ptr不能拷貝,但我們在12.1.5節中編寫了一個clone函數,它以值方式返回一個unique_ptr。解釋為什么函數是合法的,以及為什么它能正確工作。
不能拷貝unique_ptr的規則有一個例外:我們可以拷貝或賦值一個將要被銷毀的unique_ptr,編譯器知道要返回的對象將要被銷毀,因此會執行一種特殊的"拷貝" -- 移動。最常見的就是從函數返回unique_ptr。
13.52 詳細解釋第478頁中的HasPtr對象的賦值發生了什么?特別是,一步一步描述hp、hp2以及HasPtr的賦值運算符中的參數rhs的值發生了什么變化。
1、hp = hp2,hp2是一個左值,rhs將使用拷貝構造函數來初始化,拷貝構造函數將分配一個新的string,並拷貝hp2指向的string。
2、hp = std::move(hp2),我們調用std::move將一個右值引用綁定到hp2上。在此情況下,拷貝構造函數和移動構造函數都是可行的。但是,由於實參是一個右值引用,移動構造函數是精確匹配的。移動構造函數從hp2拷貝指針,而不會分配內存。
13.53 從底層效率的角度看,HasPtr的賦值運算符並不理想,解釋為什么。為HasPtr實現一個拷貝賦值運算符和一個移動賦值運算符,並比較你的新的移動賦值運算符中執行的操作與拷貝並交換版本中執行的操作。
賦值運算效率低是因為調用的swap操作每次交換時,都會創建臨時空間,用來存放臨時變量的值。
// HasPtr.h
#include <iostream>
class HasPtr {
friend void swap(HasPtr&, HasPtr&);
friend bool operator< (HasPtr& lhs, HasPtr& rhs);
public:
HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0){}
HasPtr(const HasPtr& hp) : ps (new std::string(*hp.ps)), i (hp.i) {}
HasPtr& operator= (HasPtr hp)
{
this->swap(hp);
std::cout << "call operator=(HasPtr &rhs)" << std::endl;
return *this;
}
HasPtr (HasPtr&& hp) noexcept;
// HasPtr& operator= (HasPtr&& hp) noexcept;
void swap(HasPtr &rhs)
{
using std::swap;
swap(ps, rhs.ps);
swap(i, rhs.i);
std::cout << "call swap(HasPtr &rhs)" << std::endl;
}
std::string& getPs()
{
return *ps;
}
~HasPtr () { delete ps; }
private:
std::string *ps;
int i;
};
void swap(HasPtr& lhs, HasPtr& rhs)
{
lhs.swap(rhs);
}
// HasPtr.cpp
using namespace std;
HasPtr::HasPtr(HasPtr&& hp) noexcept : ps (hp.ps), i (hp.i)
{
hp.ps = nullptr;
cout << "call move constructor." << endl;
}
int main()
{
HasPtr hp1("hello"), hp2("world");
//hp1 = hp2;
hp1 = std::move (hp2);
cout << hp1.getPs() << endl;
return 0;
}
13.54 如果我們為HasPtr定義了移動賦值運算符,但未改變拷貝並交換運算符,會發生什么?編寫代碼驗證你的答案。
// 如果定義移動賦值運算符,未改變拷貝並交換運算符,則會出現如下報錯:
error: ambiguous overload for 'operator=' (operand types are 'HasPtr' and 'std::remove_reference<HasPtr&>::type {aka HasPtr}')|
13.55 為你的StrBlob添加一個右值引用版本的push_back。
void push_back (string&& t) { data->push_back(std::move(t)); }
13.56 如果sorted定義如下,會發生什么:
Foo Foo::sorted() const& {
Foo ret(*this);
return ret.sorted();
}
因為ret是一個左值,因此調用ret.sorted()時不會匹配到Foo sorted() &&右值版本,而是繼續調用自身Foo sorted() const&,因此程序會陷入無限遞歸中,並且不停復制this,造成棧內存泄漏。
13.57 如果sorted定義如下,會發生什么:
Foo Foo::sorted() const & { return Foo(*this).sorted(); }
因為Foo(*this)返回的是一個右值,因此可以調用sorted的右值版本,返回排序后的Foo。
13.58 編寫新版本的Foo類,其sorted函數中有打印語句,測試這個類,來驗證你對前兩題的答案是否正確。
// .h文件
class Foo
{
public:
Foo (std::vector<int>& ivec) : data(ivec) {}
Foo sorted() &&;
Foo sorted() const &;
std::vector<int>& getData() { return data; }
private:
std::vector<int> data;
};
.cpp文件
Foo Foo::sorted() &&
{
sort(data.begin(), data.end());
cout << "right value sorted." << endl;
return *this;
}
Foo Foo::sorted() const &
{
cout << "left value sorted." << endl;
Foo ret (*this);
// return ret.sorted();
sort (ret.data.begin(), ret.data.end());
return ret;
// return Foo(*this).sorted();
}
int main()
{
vector<int> ivec = {1, 2, 5, 4, 7};
Foo f1(ivec);
Foo f2 = f1.sorted();
for (auto i : f2.getData()) {
cout << i << " ";
}
cout << endl;
return 0;
}
