文章目錄
-
- 一、拷貝、指派與銷毀
-
- 1、構造函數、預設構造函數和拷貝構造函數
- (1)構造函數
- (2)預設構造函數
- (3)拷貝構造函數
- (4)合成拷貝構造函數
- (5)拷貝初始化和直接初始化
- (6)參數和傳回值
- 2、深拷貝與淺拷貝
- 3、拷貝指派運算符
- 3、析構函數
- 4、一個demo:
- 5、三/五法則、使用=default 、阻止拷貝:
- (1)三/五法則
- (2)使用=default
- (3)阻止拷貝
- 6、本節的demo:
- 二、拷貝控制和資源管理、交換操作
-
- 1、行為像值的類
- 2、行為像指針的類
- 3、交換操作
- 4、本節的demo:
- 三、拷貝控制示例
- 四、動态記憶體管理類
- 五、對象移動
-
- 1、左值、右值、右值引用詳解
- (1)左值、右值
- (2)右值、将亡值
- (3)左值引用、右值引用
- 2、移動構造函數和移動指派運算符
- (1)移動語義詳解
- (2)移動構造函數
- (3)移動指派操作符
- (4)合成的移動操作
- (5)指派運算符實作拷貝指派和移動指派兩種功能
- 3、右值和左值引用成員函數
拷貝控制操作包括拷貝構造函數、拷貝指派運算符、移動構造函數、移動指派運算符和析構函數。如果一個類沒有定義所有這些拷貝控制成員,編譯器會為它定義缺失的操作(盡量明确定義對象拷貝、指派、移動或者銷毀時做什麼)。
一、拷貝、指派與銷毀
1、構造函數、預設構造函數和拷貝構造函數
(1)構造函數
構造函數與其他函數不同:構造函數和類同名,沒有傳回類型。
構造函數與其他函數相同:構造函數也有形參表(可為void)和函數體。 (參數表為void的構造函數為預設構造函數)
構造函數構造類對象的順序是:
1、記憶體配置設定,構造函數調用的時候 隐士\顯示的初始化各資料。
2、執行構造函數的運作。
(2)預設構造函數
合成的預設構造函數:當類中沒有定義構造函數(注意是構造函數)的時候,編譯器自動生成的函數。但是我們不能過分依賴編譯器,如果我們的類中有複合類型或者自定義類型成員,我們需要自己定義構造函數。
自定義的預設構造函數:
A(): a(0) {}
A(int i = 1): a(i) {}
可能疑問的是第二個構造函數也是預設構造函數麼?是的,因為參數中帶有預設值。
(3)拷貝構造函數
拷貝構造函數:如果一個構造函數的第一個參數是自身類類型的引用,且任何額外參數都有預設值,此構造函數是拷貝構造函數。
public:
Foo(); //預設構造函數
Foo(const Foo&); //拷貝構造函數
};
拷貝構造函數的第一個參數是必須是自身類類型的引用類型,如果不是引用會無限遞歸。構造函數是特殊的成員函數,隻要建立類類型的新對象,都要執行構造函數。構造函數的工作就是保證每個對象的資料成員具有合适的初始值。
(4)合成拷貝構造函數
合成拷貝構造函數:我們沒有為類定義拷貝構造函數,編譯器會幫我們定義一個。編譯器從給定對象中依次将每個非static成員拷貝到正在建立的對象中。如果數組元素是類類型,則使用元素的拷貝構造函數來進行拷貝。
使用
(5)拷貝初始化和直接初始化
直接初始化:編譯器使用普通的函數比對,來選擇與我們提供的參數最比對的構造函數。
拷貝初始化:将右側的對象拷貝到正在建立的對象中,通常使用拷貝構造函數來完成,如果需要的話還要進行類型轉換。
調用拷貝構造函數 除了=還有:
- 将一個對象作為實參傳遞給一個非引用類型的形參。
- 從一個傳回類型為非引用的函數傳回一個對象。
-
用花括号清單初始化一個數組中的元素或一個聚合類中的成員。
拷貝構造函數不應該是explicit的。每個成員的類型決定了它如何拷貝:對類類型的成員,會使用其拷貝構造函數來拷貝;内置類型的成員則直接拷貝。雖然我們不能直接拷貝一個數組,但合成拷貝狗雜函數會逐元素地拷貝一個數組類型的成員。當初化标準庫容器或是調用其insert或push成員時,容器會對其元素進行拷貝初始化,相對的,用emplace成員建立的元素都進行直接初始化。
(6)參數和傳回值
拷貝構造函數的參數必須是引用:
為了調用拷貝構造函數,我們必須拷貝實參,但為了拷貝實參,我們又需要調用拷貝構造函數,如此循環,是以構造函數被用來初始化非引用類類型參數,解釋了為什麼拷貝構造函數自己的參數一定是引用類型。
傳回值被用來初始化調用方的結果。
2、深拷貝與淺拷貝
淺拷貝: obj1.memberPtr和obj2.memberPtr可能指向同一塊堆記憶體, 這樣obj1如果修改了這塊記憶體, 那麼obj2的資料也會被改變(特别是析構的時候)。例如:
深拷貝: 當進行對象拷貝的時候, 不是進行簡單的對象指派, 而是把堆記憶體中的資料也一一單獨再劃一塊地方指派存儲, 實作對象之間兩不影響的效果, 就叫做深拷貝。
當類中有指針或者動态配置設定的記憶體時,尤其會産生這類問題。例子如下:
#include <bits/stdc++.h>
using namespace std;
class Array
{
public:
Array(int count)
{
m_iCount = count;
m_pArr = new int[m_iCount];//申請動态記憶體。
for (int i = 0; i < m_iCount; ++i)
m_pArr[i] = i;
cout << "Array" << endl;
}
Array(const Array &arr)
{
m_iCount = arr.m_iCount;
//淺拷貝:
//m_pArr = arr.m_pArr。m_pArr和arr.m_pArr指向相同的記憶體位址。
//深拷貝:
m_pArr = new int[m_iCount];//重新申請一段記憶體。
for (int i = 0; i < m_iCount; ++i)
m_pArr[i] = arr.m_pArr[i];//這時m_pArr和arr.m_pArr已經指向了不同的記憶體位址。
cout << "Array &" << endl;
}
~Array()
{
delete [] m_pArr;
m_pArr = nullptr;
cout << "~Array" << endl;
}
inline void setCount(int count) { m_iCount = count; }
inline int getCount(){ return m_iCount; }
inline printAddr() { cout << "m_pArr : " << m_pArr << endl; }
private:
int m_iCount;
int *m_pArr;
};
int main(int argc, char const *argv[])
{
Array arr1(5);
Array arr2(arr1);
arr1.printAddr();
arr2.printAddr();
system("pause");
return 0;
}
3、拷貝指派運算符
HasPtr& operator=(const HasPtr& t)
{
i = t.i;
ps = new string;
*ps = *(t.ps);
}
HasPtr a,b;
a = b; //使用Sales_data的拷貝指派運算符
和拷貝構造函數一樣,如果類未定義拷貝指派運算符,編譯器會合成一個。指派構造函數必須注意它的函數原型,參數通常是引用類型,傳回值通常也是引用類型,否則在傳參和傳回的時候都會再次調用一次拷貝構造函數。
3、析構函數
析構函數執行的與構造函數執行的順序相反,析構函數釋放對象所使用的資源,并銷毀對象的非static資料成員。
【Note】:
1)析構函數不接受參數,不能被重載,對于一個類隻有唯一一個析構函數。
2)隐式銷毀一個内置指針類型的成員不會delete它所指向的對象。
3)當指向一個對象的引用或者指針離開作用域時,析構函數不會被執行
在以下幾種情況下會調用析構函數:
- 變量在離開其作用域的時候;
- 當一個對象被銷毀時,其成員被銷毀;
- 容器被銷毀時;
- 當對指向它的指針應用delete運算符時被銷毀;
- 對于臨時對象,當建立它的完整表達式結束時被銷毀。
4、一個demo:
#include<iostream>
#include<string>
using namespace std;
class Str{
public:
Str() = default;
Str(string s) :str(s),pstr(new string()){ cout << "構造函數" << endl; }
Str(const Str& s){
str = s.str;
pstr = new string(*s.pstr);
cout << "拷貝構造函數" << endl;
}
Str& operator=(const Str& s){
str = s.str;
pstr = new string(*s.pstr);
cout << "拷貝指派運算符" << endl;
return *this;
}
~Str(){
delete pstr;
cout << "析構函數" << endl;
}
private:
string str;
string * pstr;
};
Str func(Str s){
return s;
}
int main(){
Str str1("aa"); //直接初始化
Str str2 = str1; //拷貝初始化
Str str3(str1); //拷貝初始化
Str str4;
str4 = str1; //指派初始化
func(str1);//見拷貝初始化,調用2次拷貝構造函數和2次析構函數
system("pause");
return 0;
}
5、三/五法則、使用=default 、阻止拷貝:
(1)三/五法則
如果一個類需要自定義析構函數,幾乎可以肯定它也需要自定義拷貝指派運算符和拷貝構造函數。
如果一個類需要一個拷貝構造函數,幾乎可以肯定的它也需要一個拷貝指派運算符,反之亦然。
無論是需要拷貝構造函數還是需要拷貝運算符都不意味着也需要析構函數。
(2)使用=default
我們可以通過将拷貝控制成員定義為=default來顯示地讓編譯器來為我們生成預設版本。
【Note】:
1)我們隻能對具有合成版本的成員函數使用=default(即,預設構造函數或拷貝控制成員)。
(3)阻止拷貝
新标準下,我們可以通過将拷貝構造函數和指派運算符定義為删除的函數來阻止拷貝和指派。
【Note】:
1)=delete必須出現在函數第一次聲明的時候。
2)析構函數不能是删除的成員。
3)如果一個類有資料成員不能預設構造、拷貝、複制或銷毀、則對應的成員函數
将定義為删除的。
删除一個類的析構函數或者類的某個成員的類型删除了析構函數,我們都不能定義該類的變量或臨時對象。但可以動态配置設定這種類型,不能釋放。
struct NoDtor{
NoDtor() = default; //使用合成預設構造函數
~NoDtor() = delete; //我們不能銷毀NoDtor類型的對象
};
//NoDtor nd;//錯誤:NoDtor的析構函數時删除的
NoDtor *p = new NoDtor(); //正确:但不能delete p
//delete p; //錯誤:NoDtor的析構函數是删除的
6、本節的demo:
#include <iostream>
#include <string>
#include <fstream>
#include <list>
#include <vector>
#include <map>
#include <set>
#include <cctype>
#include <algorithm>
#include <utility>//儲存pair的頭檔案。
#include <memory>
using namespace std;
//具體操作時将類的聲明置于頭檔案中。
class Employee
{
public:
Employee();//預設構造函數。
Employee(string &s);//接受一個string的構造函數。
Employee(const Employee &) = delete;//不需要拷貝構造函數,雇員号不可能一樣。将其聲明為 =delete。
Employee &operator=(const Employee &) = delete;
int number(){return _number;}
private:
string employee;
int _number;
static int O_number;//static靜态成員資料在類内聲明,但隻可以在類外定義,在類外定義時可不加static。
};
int Employee::O_number = 0;
Employee::Employee()//預設構造函數。
{
_number = O_number++;
}
Employee::Employee(string& s)//接受一個string的構造函數。
{
employee = s;
_number = O_number++;
}
void show(Employee a)
{
cout<<a.number()<<endl;
}
int main(int argc, char**argv)
{
Employee a, b, c;
show(a);//調用函數時需要拷貝一次。
show(b);//發生錯誤,不允許拷貝和指派。
show(c);
return 0;
}
二、拷貝控制和資源管理、交換操作
通常管理類外資源的類必須定義拷貝控制成員。為了定義這些成員,首先必須确定此類型對象的拷貝語義。一般來說有兩種選擇:
可以定義拷貝操作,使類看起來像一個值或像一個指針。值和指針的差別是,值由自己的狀态,拷貝一個像值的對象,副本和原對象完全獨立,而指針則共享狀态。
當用标準庫時,容器和string類的行為像一個值。而不出意外的,shared_ptr類提供類似指針的行為,像StrBlob類一樣。IO類型和unique_ptr不允許拷貝和指派,是以它們的行為既不像值也不像指針。
1、行為像值的類
對于類資源的管理,每個對象都有一份自己的拷貝。
【Note】:
1)如果将一個對象賦予它自身,指派運算符必須能正常工作。
2)大多數指派運算符組合了析構函數和拷貝構造函數的工作。
3)先将右側運算對象拷貝到一個局部臨時對象中,拷貝完後,左側運算對象的現有成員就是安全的了。
#include <iostream>
#include <string>
using namespace std;
class HasPtr{
friend ostream& print(std::ostream &os, const HasPtr&);
public:
HasPtr(const string& s = string()) :ps(new std::string(s)), i(0) { cout << "構造函數" << endl; }
HasPtr(const HasPtr& p) :ps(new string(*p.ps)), i(p.i){ cout << "拷貝構造函數" << endl; }
HasPtr& operator=(const HasPtr&);
~HasPtr(){
delete ps;
cout << "析構函數" << endl;
}
private:
string* ps;
int i;
};
HasPtr& HasPtr::operator = (const HasPtr &p){
auto newp = new string(*p.ps);//考給臨時變量,萬一=左值是自己就釋放了。
delete ps;
ps = newp;
i = p.i;
cout << "拷貝指派運算符" << endl;
return *this;
}
ostream& print(std::ostream &os, const HasPtr& p){
std::cout << "string:" << *p.ps << " int:" << p.i << std::endl;
return os;
}
int main(){
HasPtr p1;
HasPtr p2("hehe");
print(cout, p1);
print(cout, p2);
p1 = p2;
print(cout, p1);
system("pause");
return 0;
}
2、行為像指針的類
引用計數需要确定在哪裡存放引用計數。計數器不能作為HasPtr對象的成員。一種方法是将計數器儲存在動态記憶體中。當建立一個對象時,我們配置設定一個新的計數器。當拷貝或指派對象時,拷貝指向計數器的指針。這種方式,副本和原對象都會指向相同的計數器。
#include <iostream>
#include <string>
using namespace std;
class HasPtr{
friend ostream& print(std::ostream &os, const HasPtr&);
public:
HasPtr(const string& s = string()) :ps(new std::string(s)), i(0), use(new size_t(1)){ cout << "構造函數" << endl; }
HasPtr(const HasPtr& p) :ps(p.ps), i(p.i), use(p.use){
++*use;
cout << "拷貝構造函數" << endl;
}
HasPtr& operator=(const HasPtr&);
~HasPtr();
private:
string* ps;
int i;
size_t* use;
};
HasPtr& HasPtr::operator = (const HasPtr &rhs){
++*rhs.use; //遞增右側運算對象的引用計數
if (--*use == 0){ //遞減本對象的引用計數
delete ps;
delete use;
}
ps = rhs.ps; //将資料從rhs拷貝到本對象
i = rhs.i;
use = rhs.use;
return *this;
}
HasPtr::~HasPtr(){
if (--*use == 0){ //如果引用計數變為0
delete ps; //釋放string記憶體
delete use; //釋放計時器記憶體
}
cout << "析構函數" << endl;
}
ostream& print(std::ostream &os, const HasPtr& p){
std::cout << "string:" << *p.ps << " int:" << p.i << " use:" << *p.use << std::endl;
return os;
}
int main(){
HasPtr p1;
HasPtr p2("hehe");
print(cout, p1);
print(cout, p2);
p1 = p2;
print(cout, p1);
system("pause");
return 0;
}
3、交換操作
與拷貝控制成員不同,swap并不是必要的,但對于配置設定了資源的類swap是個優化手段。
swap函數應該調用swap而不是std::swap。
标準庫swap對HasPtr管理的string進行了不必要的拷貝。如果一個類的成員有自己類型特定的swap函數,調用std:swap就是錯誤的。如果存在類型特定的swap版本,其比對程度會優于std中定義的版本。拷貝指派運算符通常執行拷貝構造函數和析構函數中也要做的工作。這時,公共的工作應該放在private工具函數中完成。
對于那些與重排元素順序的算法一起使用的類,定義swap是非常重要的。這類算法在需要交換兩個元素時會調用swap。
4、本節的demo:
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
class Hasptr1
{
friend void swap(Hasptr1 &,Hasptr1 &);
friend bool operator<(const Hasptr1 &s1,const Hasptr1 &s2);
friend void show(vector<Hasptr1> &vec);
public:
//構造函數,初始化相關成員
Hasptr1(const string &s = string()):ps(new string(s)),i(0),use(new size_t(1)){}
//拷貝構造函數,将引用計數也拷貝過來,并且遞增引用計數
Hasptr1(const Hasptr1 &p):ps(p.ps),i(p.i),use(p.use){++*use;}
//拷貝指派運算符
Hasptr1 &operator= (const Hasptr1 &p1)
{
++*p1.use;//首先遞增右側運算符對象的引用計數
if (--*use == 0)//遞減本對象的引用計數,若沒有其他使用者,則釋放本對象的成員
{
delete ps;
delete use;
}
ps = p1.ps;//進行拷貝
use = p1.use;
i = p1.i;
return *this;
}
//析構函數
~Hasptr1()
{
if (*use == 0)//引用計數變為0,說明已經沒有對象再需要這塊記憶體,進行釋放記憶體操作
{
delete ps;
delete use;
}
}
private:
//定義為指針,是我們想将該string對象儲存在動态記憶體中
string *ps;
size_t *use;//将計數器的引用儲存
int i;
};
inline void swap(Hasptr1 &a,Hasptr1 &b)
{
using std::swap;
swap(a.ps,b.ps);
std::swap(a.i,b.i);
cout<<"123";
}
bool operator< (const Hasptr1 &s1,const Hasptr1 &s2)
{
cout<<"定義的 Operator< 被調用"<<endl;
return *s1.ps < *s2.ps;
}
void show(vector<Hasptr1> &vec)
{
vector<Hasptr1>::iterator it1 = vec.begin();
for (it1; it1 != vec.end(); ++it1)
{
cout<<*(it1->ps)<<endl;
}
}
int main(int argc, char**argv)
{
vector<Hasptr1> vec1;
Hasptr1 a("l");
Hasptr1 b("llll");
Hasptr1 c("lll");
vec1.push_back(a);
vec1.push_back(b);
vec1.push_back(c);
vector<Hasptr1>::iterator it1 = vec1.begin();
sort(vec1.begin(),vec1.end());
show(vec1);
system("pause");
return 0;
}
三、拷貝控制示例
#include <bits/stdc++.h>
using namespace std;
class Message;
class Folder
{
public:
Folder() = default;
Folder(const Folder &);
Folder &operator=(const Folder &);
~Folder() = default;
inline void AddMsg(Message* msg){ messages.insert(msg); }
inline void RemMsg(Message* msg){ messages.erase(msg); }
private:
set<Message*> messages;
};
class Message
{
friend class Folder;
public:
explicit Message(const string &str = ""):contents(str){}
Message & operator=(const Message &);//拷貝指派運算符。
Message(const Message &);//拷貝構造函數。
//移動構造函數。移動contents。
Message(Message &&m): contents(std::move(m.contents)){ move_folders(&m); }
Message & operator=(Message &&rhs);
~Message(){ remove_from_Folders(); }//析構函數。
void save(Folder &);
void remove(Folder &);//對Folder類的操作函數。
void swap(Message &m1,Message &m2)。
void move_folders(Message *m);
private:
string contents;
set<Folder*> folders;
void add_to_Folders(const Message&);
void remove_from_Folders();//從folders中删除本message。
};
void Message::move_folders(Message *m)
{
folders = std::move(m->folders);//使用set的移動指派。
for (auto f : folders)
{
f->RemMsg(m);//删除舊元素。
f->AddMsg(this);//添加新元素。
}
m->folders.clear();//確定銷毀是無害的。
}
void Message::save(Folder &f)
{
folders.insert(&f);//将給定folder加入到我們的folders中。
f.AddMsg(this);//将本message添加到給定folder中。
}
void Message::remove(Folder &f)
{
folders.erase(&f);//将給定folder在我們的folders中删除。
f.RemMsg(this);//将本message在給定folder中删除。
}
void Message::add_to_Folders(const Message &m)
{
for (auto f : m.folders)
{
f->AddMsg(this);
}
}
void Message::remove_from_Folders()
{
for (auto f : folders)
{
f->RemMsg(this);//所有包含此message的folder進行删除操作。
}
}
Message & Message::operator=(const Message &m)//拷貝指派運算符。
{
remove_from_Folders();//删除自身。
contents = m.contents;
folders = m.folders;
add_to_Folders(m);//将本message傳入folder中。
return *this;
}
Message & operator=(Message &&rhs)
{
if(this != &rhs)
{
remove_from_Folders();
contents = std::move(rhs.contents);
move_folders(&rhs);
}
return *this;
}
void Message::swap(Message &m1,Message &m2)
{
using std::swap;
//先将每個message從其folders中删除。
for(auto f : m1.folders)
{
f->RemMsg(this);//所有包含此message的folder進行删除操作。
}
for(auto f : m2.folders)
{
f->RemMsg(this);//所有包含此message的folder進行删除操作。
}
swap(m1.folders,m2.folders);
swap(m1.contents,m2.contents);
for (auto f : m1.folders)
{
f->AddMsg(this);//再進行添加操作。
}
for (auto f : m2.folders)
{
f->AddMsg(this);
}
}
int main(int argc, char const *argv[])
{
Message m("ABC");
return 0;
}
四、動态記憶體管理類
StrVec的設計:vector的每個添加元素的成員函數會檢查是否有空間容納更多的元素。如果有,成員函數會在下一個可用位置構造一個對象。如果沒有可用空間,vector就會重新配置設定空間:它獲得新的空間,将已有元素移動到新空間,釋放舊空間,并添加新元素。free成員,一旦元素被銷毀,就調用deallocate來釋放StrVec對象配置設定的記憶體空間,我們傳遞給deallocate的指針必須是之前某次allocate調用所傳回的指針,是以在調用deallocate之前我們首先檢查elements是否為空。
在重新配置設定記憶體的過程中移動而不是拷貝元素。當拷貝一個string時,新string和原string是互相獨立的。由于string的行為類似值,每個string對構成它的所有字元都會儲存自己的一份副本。拷貝一個string必須為這些字元 配置設定記憶體空間。一旦将元素拷貝到新空間,就會立即銷毀原string。是以,拷貝這些string中的資料是多餘的。move标準庫函數定義在utility中。兩個要點,當reallocate在新記憶體中構造string時,它必須調用move來表示希望使用string的移動構造函數。如果漏掉move操作,将使用string的拷貝構造函數。另外,不為move提供using聲明。
#include <bits/stdc++.h>
using namespace std;
class StrVec
{
public:
StrVec():elements(nullptr), first_free(nullptr), cap(nullptr){}
//拷貝構造函數。
StrVec(const StrVec &);
//移動構造函數。
StrVec(StrVec &&s) noexcept : elements(s.elements),first_free(s.first_free),cap(s.cap)
{
s.elements = s.first_free = s.cap = nullptr;
}
//拷貝指派運算符。
StrVec &operator=(const StrVec &);
//移動指派運算符。
StrVec &operator=(StrVec &&rhs) noexcept;
//為三個指針進行初始化,并将成員進行指派。
StrVec(initializer_list<string> lst)
{
auto newdata = alloc_n_copy(lst.begin(), lst.end());
elements = newdata.first;
first_free = cap = newdata.second;
}
~StrVec(){ free(); }
void push_back(const string &);//拷貝元素。
void push_back(string &&);//移動元素。
size_t size() const{ return first_free - elements; }
size_t capacity() const{ return cap - elements; }
string *begin() const{ return elements; }
string *end() const{ return first_free; }
private:
static allocator<string> alloc;//聲明但是未定義,靜态成員必須在函數外定義。
//沒有空間容納新元素,使用reallocate重新配置設定記憶體。
void chk_n_alloc()
{
if(size() == capacity())
reallocate();
}
pair<string*,string*> alloc_n_copy(const string*, const string*);
void free();//銷毀元素并釋放記憶體。
void reallocate();//獲得更多記憶體并拷貝已有元素。
string *elements;//指向數組首元素的指針。
string *first_free;//指向數組第一個空閑元素的指針。
string *cap;//指向數組尾後位置的指針。
};
//通過實參是右值還是左值确認調用那個版本。
void StrVec::push_back(const string &s)
{
chk_n_alloc();//確定有空間容納新元素。
alloc.construct(first_free++, s);//使用未加1之前的值,遞增構造對象。
}
void StrVec::push_back(string &&s)
{
chk_n_alloc();
alloc.construct(first_free++, std::move(s));
}
pair<string*,string*>
alloc_n_copy(const string *b, const string *e)//将元素拷貝到新的記憶體中。
{
auto data = alloc.allocate(e - b);
return {data,uninitialized_copy(b,e,data)};//使用清單初始化傳回。
}
void StrVec::free()
{
if(elements)
{
//銷毀舊元素。
for_each(elements, first_free, [this](string &rhs){ alloc.destroy(&rhs); });
alloc.deallocate(elements, cap-elements);
}
}
//拷貝構造函數。
StrVec::StrVec(const StrVec &s)
{
auto newdata = alloc_n_copy(s.begin(),s.end());
elements = newdata.first;
first_free = cap = newdata.second;
}
//拷貝指派運算符。
StrVec &StrVec::operator=(const StrVec &rhs)
{
//調用alloc_n_copy配置設定記憶體。
auto data = alloc_n_copy(rhs.begin(),rhs.end());
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}
//移動指派運算符。
StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
//檢查自指派。
if(this != &rhs)
{
free();//釋放舊資源。
//從rhs接管資源。
elements = rhs.elements;
first_free = rhs.first_free;
cap = rhs.cap;
//将rhs置于可析構狀态。
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
void StrVec::reallocate()
{
//配置設定目前大小兩倍的空間。
auto newcapacity = size() ? 2 * size() : 1;
//配置設定新記憶體。
auto first = alloc.allocate(newcapacity);
//将資料從舊記憶體移動到新記憶體,使用移動疊代器。
auto last = uninitialized_copy(
make_move_iterator(begin()),
make_move_iterator(end()),
first);
free();//釋放舊記憶體。
//更新資料。
elements = first;
first_free = last;
cap = elements + newcapacity;
}
int main(int argc, char const *argv[])
{
StrVec sv;
string s = "ABC";
sv.push_back(s);
sv.push_back("edf");
cout << sv.size() << " " << sv.capacity() << endl;
system("pause");
return 0;
}
五、對象移動
新标準的移動對象的能力。在重新配置設定記憶體的過程中,從舊記憶體将元素拷貝到新記憶體是不必要的,更好的方式是移動元素。這一特定源于IO類或unique_ptr這些類。它們包括被共享的資源,是以,這些類的對象不能拷貝隻能移動。
1、左值、右值、右值引用詳解
(1)左值、右值
在C++11中所有的值必屬于左值、右值兩者之一,右值又可以細分為純右值、将亡值。在C++11中可以取位址的、有名字的就是左值,反之,不能取位址的、沒有名字的就是右值(将亡值或純右值)。舉個例子,int a = b+c, a 就是左值,其有變量名為a,通過&a可以擷取該變量的位址;表達式b+c、函數int func()的傳回值是右值,在其被指派給某一變量前,我們不能通過變量名找到它,&(b+c)這樣的操作則不會通過編譯。
(2)右值、将亡值
在了解C++11的右值前,先看看C++98中右值的概念:C++98中右值是純右值,純右值指的是臨時變量值、不跟對象關聯的字面量值。臨時變量指的是非引用傳回的函數傳回值、表達式等,例如函數int func()的傳回值,表達式a+b;不跟對象關聯的字面量值,例如true,2,”C”等。
C++11對C++98中的右值進行了擴充。在C++11中右值又分為純右值(prvalue,Pure Rvalue)和将亡值(xvalue,eXpiring Value)。其中純右值的概念等同于我們在C++98标準中右值的概念,指的是臨時變量和不跟對象關聯的字面量值;将亡值則是C++11新增的跟右值引用相關的表達式,這樣表達式通常是将要被移動的對象(移為他用),比如傳回右值引用T&&的函數傳回值、std::move的傳回值,或者轉換為T&&的類型轉換函數的傳回值。
将亡值可以了解為通過“盜取”其他變量記憶體空間的方式擷取到的值。在確定其他變量不再被使用、或即将被銷毀時,通過“盜取”的方式可以避免記憶體空間的釋放和配置設定,能夠延長變量值的生命期。
(3)左值引用、右值引用
左值引用就是對一個左值進行引用的類型。右值引用就是對一個右值進行引用的類型,事實上,由于右值通常不具有名字,我們也隻能通過引用的方式找到它的存在。
右值引用和左值引用都是屬于引用類型。無論是聲明一個左值引用還是右值引用,都必須立即進行初始化。而其原因可以了解為是引用類型本身自己并不擁有所綁定對象的記憶體,隻是該對象的一個别名。左值引用是具名變量值的别名,而右值引用則是不具名(匿名)變量的别名。
左值引用通常也不能綁定到右值,但常量左值引用是個“萬能”的引用類型。它可以接受非常量左值、常量左值、右值對其進行初始化。不過常量左值所引用的右值在它的“餘生”中隻能是隻讀的。相對地,非常量左值隻能接受非常量左值對其進行初始化。
int &a = 2; # 左值引用綁定到右值,編譯失敗
int b = 2; # 非常量左值
const int &c = b; # 常量左值引用綁定到非常量左值,編譯通過
const int d = 2; # 常量左值
const int &e = c; # 常量左值引用綁定到常量左值,編譯通過
const int &b =2; # 常量左值引用綁定到右值,程式設計通過
右值引用通常不能綁定到任何的左值,要想綁定一個左值到右值引用,通常需要std::move()将左值強制轉換為右值,例如:
int a;
int &&r1 = c; # 編譯失敗
int &&r2 = std::move(a); # 編譯通過
下表列出了在C++11中各種引用類型可以引用的值的類型。值得注意的是,隻要能夠綁定右值的引用類型,都能夠延長右值的生命期。
【Note】:
1)我們可以将一個右值引用綁定到表達式、字面值常量或者傳回右值的表達式,但是不能将右值引用綁定到左值上。
2)右值引用指向将要被銷毀的對象。
3)變量是左值,是以我們不能把右值引用直接綁定到變量上。
4)我們可以銷毀一個移後源對象,也可以賦予其新值,但是不能使用一個移後源對象的值。
5)應該使用std::move而不是move,這樣可以避免潛在的名字沖突。
2、移動構造函數和移動指派運算符
(1)移動語義詳解
移動語義,簡單來說解決的是各種情形下對象的資源所有權轉移的問題。而在C++11之前,移動語義的缺失是C++飽受诟病的問題之一。舉個栗子:
問題一:如何将大象放入冰箱?答案是衆所周知的。首先你需要有一台特殊的冰箱,這台冰箱是為了裝下大象而制造的。你打開冰箱門,将大象放入冰箱,然後關上冰箱門。
問題二:如何将大象從一台冰箱轉移到另一台冰箱?普通解答:打開冰箱門,取出大象,關上冰箱門,打開另一台冰箱門,放進大象,關上冰箱門。2B解答:在第二個冰箱中啟動量子複制系統,克隆一隻完全相同的大象,然後啟動高能雷射将第一個冰箱内的大象氣化消失。等等,這個2B解答聽起來很耳熟,這不就是C++中要移動一個對象時所做的事情嗎?
“移動”,這是一個三歲小孩都明白的概念。将大象(資源)從一台冰箱(對象)移動到另一台冰箱,這個行為是如此自然,沒有任何人會采用先複制大象,再銷毀大象這樣匪夷所思的方法。C++通過拷貝構造函數和拷貝指派操作符為類設計了拷貝/複制的概念,但為了實作對資源的移動操作,調用者必須使用先複制、再析構的方式。否則,就需要自己實作移動資源的接口。
為了實作移動語義,首先需要解決的問題是,如何辨別對象的資源是可以被移動的呢?這種機制必須以一種最低開銷的方式實作,并且對所有的類都有效。C++的設計者們注意到,大多數情況下,右值所包含的對象都是可以安全的被移動的。右值(相對應的還有左值)是從C語言設計時就有的概念,但因為其如此基礎,也是一個最常被忽略的概念。不嚴格的來說,左值對應變量的存儲位置,而右值對應變量的值本身。C++中右值可以被指派給左值或者綁定到引用。類的右值是一個臨時對象,如果沒有被綁定到引用,在表達式結束時就會被廢棄。于是我們可以在右值被廢棄之前,移走它的資源進行廢物利用,進而避免無意義的複制。被移走資源的右值在廢棄時已經成為空殼,析構的開銷也會降低。右值中的資料可以被安全移走這一特性使得右值被用來表達移動語義。以同類型的右值構造對象時,需要以引用形式傳入參數。右值引用顧名思義專門用來引用右值,左值引用和右值引用可以被分别重載,這樣確定左值和右值分别調用到拷貝和移動的兩種語義實作。對于左值,如果我們明确放棄對其資源的所有權,則可以通std::move()來将其轉為右值引用。std::move()實際上是
static_cast<T&&>()
的簡單封裝。
(2)移動構造函數
移動構造函數類似于拷貝構造函數,第一參數是該類類型的一個引用,但這個引用參數在移動構造函數中是一個右值引用。
與拷貝構造函數不同,移動構造函數不配置設定新記憶體;它接管記憶體并把對象中的指針都置為nullptr。最終,移後源對象會被銷毀,意味着将在其上運作析構函數。
A(A && h) : a(h.a)
{
h.a = nullptr; //還記得nullptr?
}
可以看到,這個構造函數的參數不同,有兩個&操作符, 移動構造函數接收的是“右值引用”的參數。
移動構造函數何時觸發? 那就是臨時對象(右值)。用到臨時對象的時候就會執行移動語義。這裡要注意的是,異常發生的情況,要盡量保證移動構造函數 不發生異常,可以通過noexcept關鍵字,這裡可以保證移動構造函數中抛出來的異常會直接調用terminate終止程式。
(3)移動指派操作符
原理跟移動構造函數相同,這裡不再多說:
A & operator = (A&& h)
{
assert(this != &h);
a = nullptr;
a = move(h.a);
h.a = nullptr;
return *this;
}
【Note】:
1)不抛出異常的移動構造函數和移動指派函數運算符必須标記為noexcept。
2)移後源對象必須可析構。
(4)合成的移動操作
隻有一個類沒有定義任何拷貝控制成員(拷貝構造函數,拷貝指派運算符,析構函數)時, 且類的所有非static成員都是可移動的, 此時編譯器才會給該類合成移動構造函數和移動指派運算符。
當既有拷貝操作也有移動操作時,使用哪個?
一條原則:移動右值,拷貝左值。即當右邊是一個右值時, 就優先使用移動操作(也可以使用拷貝)。當右邊是一個左值時, 隻能使用拷貝操作(除非使用std::move将其轉換)。
必要時候, 右值也能被拷貝構造函數和拷貝指派運算符拷貝。但是拷貝構造函數和拷貝指派運算符的參數必須是const類型的引用才行, 如果不是就會出錯(可以自己試試看看到底是什麼情況)。
(5)指派運算符實作拷貝指派和移動指派兩種功能
如果指派運算符的形參是傳值調用, 那麼用實參初始化形參就需要調用拷貝構造函數(實參是左值)或移動構造函數(實參是右值)。那麼可以用下面方式實作等價的拷貝指派和移動指派。(注意:下面的程式=操作符内使用的是自定義的swap, 因為标準庫的swap需要類支援=操作符, 但是=操作符我們還沒定義)。
3、右值和左值引用成員函數
在類的成員函數後面加上& 或&& 可以限定該成員函數隻能接受左值或右值的參數。同樣可以避免對右值對象使用=指派等操作。
參考:http://blog.csdn.net/ruan875417/article/details/44854189
http://blog.csdn.net/refuil/article/details/51547815
http://blog.csdn.net/hyman_yx/article/details/52044632
https://www.zhihu.com/question/22111546
http://blog.csdn.net/Jofranks/article/details/17438955
http://blog.csdn.net/u013480600/article/details/44151643