为了定义拷贝构造函数和拷贝赋值运算符,我们首先必须确认此类型对象的拷贝语义。通常可以定义拷贝操作,使类的行为看起来像一个值或者像一个指针(即所谓的深拷贝和浅拷贝)
类的行为像一个值,意味着它应该也有自己的状态。当我们拷贝一个像值的对象时,副本和原对象是完全独立的。改变副本不会对原对象有任何影响,反之亦然
行为像指针的类则共享状态。当我们拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。改变副本也会改变原对象,反之亦然
在我们使用过的标准库类中,标准库容器和 string 类的行为像一个值。shared_ptr 提供类似指针的行为。IO 类型和 unique_ptr 不允许拷贝或赋值,因此它们的行为既不像值也不像指针
行为像值的类:
1 #include <iostream> 2 using namespace std; 3 4 class HasPtr{ 5 public: 6 HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0) {} 7 HasPtr(const HasPtr &p) : ps(new std::string(*p.ps)), i(p.i) {} 8 HasPtr& operator=(const HasPtr&); 9 ~HasPtr(){ 10 delete ps; 11 } 12 13 ostream& print(ostream &os){ 14 os << i << " " << *ps << " " << ps; 15 return os; 16 } 17 18 private: 19 std::string *ps; 20 int i; 21 }; 22 23 HasPtr& HasPtr::operator=(const HasPtr &rhs){ 24 auto newp = new string(*rhs.ps);//为了避免两个对象中ps指针相同时出错先将rhs.ps中的内容存放到一个新开辟的空间newp中 25 delete ps;//释放旧内存 26 ps = newp; 27 i = rhs.i; 28 return *this; 29 } 30 31 int main(void){ 32 HasPtr s1("hello"); 33 HasPtr s2 = s1; 34 HasPtr s3; 35 s3 = s1; 36 37 s1.print(cout) << endl; 38 s2.print(cout) << endl; 39 s3.print(cout) << endl; 40 41 // 输出: 42 // 0 hello 0x2a811d8 43 // 0 hello 0x2a811a8 44 // 0 hello 0x2a81198 45 46 return 0; 47 }
注意:在赋值运算符应该要防范自赋值的情况:
1 HasPtr& HasPtr::operator=(const HasPtr &rhs){ 2 delete ps;//如果rhs和本对象是同一个对象,则rhs.ps将成为一个空悬指针 3 ps = new string(*rhs.ps);//错误,我们试图解引用一个空悬指针 4 i = rhs.i; 5 return *this; 6 }
行为像指针的类:
1 #include <iostream> 2 using namespace std; 3 4 class HasPtr{ 5 public: 6 HasPtr(const std::string &s = std::string()) : 7 ps(new std::string(s)), i(0), use(new std::size_t(1)) {}//直接初始化时引用计数为1 8 HasPtr(const HasPtr &p) : ps(p.ps), i(p.i), use(p.use) { 9 ++*use;//引用计数加一 10 } 11 HasPtr& operator=(const HasPtr&); 12 ~HasPtr(); 13 14 ostream& print(ostream &os){ 15 os << *use << " " << i << " " << *ps << " " << ps; 16 return os; 17 } 18 19 private: 20 std::string *ps; 21 int i; 22 std::size_t *use;//记录当前有多少个对象共享*ps成员 23 }; 24 25 HasPtr::~HasPtr() { 26 if(--*use == 0){//每析构一个HasPtr对象引用计数减一 27 delete ps;//如果引用计数为0,释放ps和use所指的内存 28 delete use; 29 } 30 } 31 32 HasPtr& HasPtr::operator=(const HasPtr &rhs){ 33 ++*rhs.use;//递增右侧运算对象的引用计数 34 if(--*use == 0){//然后递减本对象的引用计数 35 delete ps;//如果没有其它用户 36 delete use;//释放本对象分配的内存 37 } 38 ps = rhs.ps; 39 i = rhs.i; 40 use = rhs.use; 41 return *this; 42 } 43 44 int main(void){ 45 HasPtr s1("hello"); 46 HasPtr s2 = s1; 47 HasPtr s3; 48 s3 = s1; 49 HasPtr s4("word"); 50 51 s1.print(cout) << endl; 52 s2.print(cout) << endl; 53 s3.print(cout) << endl; 54 s4.print(cout) << endl; 55 56 // 输出: 57 // 3 0 hello 0x2d31218 58 // 3 0 hello 0x2d31218 59 // 3 0 hello 0x2d31218 60 // 1 0 word 0x2b610c8 61 62 return 0; 63 }
注意:为了实现类似于 shared_ptr 的引用计数功能,我们可以将计数器保持到动态内存中,指向相同 ps 对象的 HasPtr 也指向相同的 use 对象。 这里我们不能使用 static 来实现引用计数,因为它是属于类本身的,这意味着所有 HasPtr 类的对象中 use 值都是相等的,并且我们将无法做到给赋值运算符右侧对象 use 加一,左侧对象 use 减一:
1 #include <iostream> 2 using namespace std; 3 4 class HasPtr{ 5 public: 6 HasPtr(const std::string &s = std::string()) : 7 ps(new std::string(s)), i(0) {}//直接初始化时引用计数为1 8 HasPtr(const HasPtr &p) : ps(p.ps), i(p.i) { 9 ++use;//引用计数加一 10 } 11 HasPtr& operator=(const HasPtr&); 12 ~HasPtr(); 13 14 ostream& print(ostream &os){ 15 os << use << " " << i << " " << *ps << " " << ps; 16 return os; 17 } 18 19 private: 20 std::string *ps; 21 int i; 22 static std::size_t use;//记录当前有多少个对象共享*ps成员 23 }; 24 25 HasPtr::~HasPtr() { 26 if(--use == 0){//每析构一个HasPtr对象引用计数减一 27 delete ps;//如果引用计数为0,释放ps所指的内存 28 } 29 } 30 31 HasPtr& HasPtr::operator=(const HasPtr &rhs){ 32 ++rhs.use;//递增右侧运算对象的引用计数 33 if(--use == 0){//然后递减本对象的引用计数 34 delete ps;//如果没有其它用户,释放本对象分配的内存 35 } 36 ps = rhs.ps; 37 i = rhs.i; 38 use = rhs.use; 39 return *this; 40 } 41 42 size_t HasPtr::use = 1; 43 44 int main(void){ 45 HasPtr s1("hello"); 46 HasPtr s2 = s1; 47 HasPtr s3; 48 s3 = s1; 49 HasPtr s4("word"); 50 51 s1.print(cout) << endl; 52 s2.print(cout) << endl; 53 s3.print(cout) << endl; 54 s4.print(cout) << endl; 55 56 // 输出: 57 // 2 0 hello 0x2be1248 58 // 2 0 hello 0x2be1248 59 // 2 0 hello 0x2be1248 60 // 2 0 word 0x2be10b8 61 62 return 0; 63 }
交换操作:
库函数 swap 的实现依赖于类的拷贝构造函数和赋值运算符。如,对值语义的 HasPtr 类对象使用库函数 swap:
1 #include <iostream> 2 using namespace std; 3 4 class HasPtr{ 5 public: 6 HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0) {} 7 HasPtr(const HasPtr &p) : ps(new std::string(*p.ps)), i(p.i) {} 8 HasPtr& operator=(const HasPtr&); 9 ~HasPtr(){ 10 delete ps; 11 } 12 13 ostream& print(ostream &os){ 14 os << i << " " << *ps << " " << ps; 15 return os; 16 } 17 18 private: 19 std::string *ps; 20 int i; 21 }; 22 23 HasPtr& HasPtr::operator=(const HasPtr &rhs){ 24 auto newp = new string(*rhs.ps);//为了避免两个对象中ps指针相同时出错先将rhs.ps中的内容存放到一个新开辟的空间newp中 25 delete ps;//释放旧内存 26 ps = newp; 27 i = rhs.i; 28 return *this; 29 } 30 31 int main(void){ 32 HasPtr s1("hello"); 33 HasPtr s2("word"); 34 35 s1.print(cout) << endl; 36 s2.print(cout) << endl; 37 38 swap(s1, s2); 39 // HasPtr cmp = s1; 40 // s1 = s2; 41 // s2 = cmp; 42 43 s1.print(cout) << endl; 44 s2.print(cout) << endl; 45 46 // 输出: 47 // 0 hello 0x28411c8 48 // 0 word 0x2841238 49 // 0 word 0x28410d8 50 // 0 hello 0x28410e8 51 52 return 0; 53 }
显然,swap(s1, s2);的实现流程是:
HasPtr cmp = s1;
s1 = s2;
s2 = cmp;
这个过程中分配了 3 次内存,效率及其低下。理论上这些内存分配都是不必要的。我们可以只交换指针而不需要分配 string 的新副本。
因此,除了定义拷贝控制成员,管理资源的类通常还需要定义一个名为 swap 的函数。尤其对于那些与重排元素顺序的算法一起使用的类,定义 swap 是非常重要的。这类算法在需要交换两个元素时会调用 swap。
给值语义的 HasPtr 编写 swap 函数:
1 #include <iostream> 2 using namespace std; 3 4 class HasPtr{ 5 friend void swap(HasPtr&, HasPtr&); 6 7 public: 8 HasPtr(const std::string &s = std::string(), int a = 0) : ps(new std::string(s)), i(a) {} 9 HasPtr(const HasPtr &p) : ps(new std::string(*p.ps)), i(p.i) {} 10 HasPtr& operator=(const HasPtr&); 11 ~HasPtr(){ 12 delete ps; 13 } 14 15 ostream& print(ostream &os){ 16 os << i << " " << *ps << " " << ps; 17 return os; 18 } 19 20 private: 21 std::string *ps; 22 int i; 23 }; 24 25 HasPtr& HasPtr::operator=(const HasPtr &rhs){ 26 auto newp = new string(*rhs.ps);//为了避免两个对象中ps指针相同时出错先将rhs.ps中的内容存放到一个新开辟的空间newp中 27 delete ps;//释放旧内存 28 ps = newp; 29 i = rhs.i; 30 return *this; 31 } 32 33 inline 34 void swap(HasPtr &lhs, HasPtr &rhs){ 35 swap(lhs.ps, rhs.ps); 36 swap(lhs.i, rhs.i); 37 } 38 39 int main(void){ 40 HasPtr s1("hello"); 41 HasPtr s2("word", 1); 42 43 s1.print(cout) << endl; 44 s2.print(cout) << endl; 45 46 swap(s1, s2); 47 // auto cmp = s1.ps; 48 // s1.ps = s2.ps; 49 // s2.ps = cmp; 50 // auto cnt = s1.i; 51 // s1.i = s2.i; 52 // s2.i = cnt; 53 54 s1.print(cout) << endl; 55 s2.print(cout) << endl; 56 57 // 输出: 58 // 0 hello 0x2bb1208 59 // 1 word 0x2bb1128 60 // 1 word 0x2bb1128 61 // 0 hello 0x2bb1208 62 63 return 0; 64 }
注意:如果存在类型特定的 swap 版本,其匹配程度会优于 std 中定义的版本。如果不存在类型特定的版本,则会使用 std 中的版本(假定作用域中有 using 声明)
类指针的 HasPtr 版本并不能从 swap 函数受益
在赋值运算符中使用 swap:
定义了 swap 的类中通常用 swap 来定义它们的赋值运算符。这些运算符使用了一种名为 拷贝并交换(copy and swap) 的技术。这种技术将左侧运算对象与右侧运算对象的一个副本进行交换:
1 #include <iostream> 2 using namespace std; 3 4 class HasPtr{ 5 friend void swap(HasPtr&, HasPtr&); 6 7 public: 8 HasPtr(const std::string &s = std::string(), int a = 0) : ps(new std::string(s)), i(a) {} 9 HasPtr(const HasPtr &p) : ps(new std::string(*p.ps)), i(p.i) {} 10 HasPtr& operator=(HasPtr); 11 ~HasPtr(){ 12 delete ps; 13 } 14 15 ostream& print(ostream &os){ 16 os << i << " " << *ps << " " << ps; 17 return os; 18 } 19 20 private: 21 std::string *ps; 22 int i; 23 }; 24 25 HasPtr& HasPtr::operator=(HasPtr rhs){//注意这里不能是引用 26 swap(*this, rhs);//交换后rhs指向本对象曾经使用的内存 27 return *this;//作用域结束,rhs被销毁,从而delete了rhs种的指针 28 } 29 30 inline 31 void swap(HasPtr &lhs, HasPtr &rhs){ 32 swap(lhs.ps, rhs.ps); 33 swap(lhs.i, rhs.i); 34 } 35 36 int main(void){ 37 HasPtr s1("hello"); 38 HasPtr s2("word", 1); 39 40 s1.print(cout) << endl; 41 s2.print(cout) << endl; 42 43 swap(s1, s2); 44 45 s1.print(cout) << endl; 46 s2.print(cout) << endl; 47 48 // 输出: 49 // 0 hello 0x2ef1128 50 // 1 word 0x2ef1088 51 // 1 word 0x2ef1088 52 // 0 hello 0x2ef1128 53 54 return 0; 55 }
注意:这个版本赋值运算符中,参数并不能是引用
使用拷贝和交换的赋值运算符自动就是异常安全的,且能正确处理自赋值
动态内存管理:
编写一个功能类似于 vector 的管理 string 的类 StrVec:
StrVec.h:
1 #pragma once 2 3 #include <iostream> 4 #include <memory> 5 #include <utility> 6 #include <initializer_list> 7 8 class StrVec{ 9 public: 10 //默认构造函数 11 StrVec() : elements(nullptr), first_free(nullptr), cap(nullptr) {}//allocator成员进行默认初始化 12 StrVec(const std::initializer_list<std::string>&); 13 StrVec(const StrVec&);//拷贝构造函数 14 StrVec& operator=(const StrVec&);//拷贝赋值运算符 15 ~StrVec();//析构函数 16 17 void push_back(const std::string&);//拷贝元素 18 19 size_t size() const{ 20 return first_free - elements; 21 } 22 23 size_t capacity() const{ 24 return cap - elements; 25 } 26 27 std::string* begin() const{ 28 return elements; 29 } 30 31 std::string* end() const{ 32 return first_free; 33 } 34 35 void reserve(const size_t&);//分配指定大小的空间并将原来的元素拷贝到新空间 36 void resize(const size_t&, const std::string &s = "");//使得容器为指定大小但不减小容量 37 38 private: 39 static std::allocator<std::string> alloc;//分配元素 40 41 void chk_n_alloc(){//被添加元素的函数所使用 42 if(size() == capacity()) reallocate(); 43 } 44 45 //工具函数,被拷贝构造函数、赋值运算符和析构函数所使用 46 std::pair<std::string*, std::string*> alloc_n_copy(const std::string*, const std::string*); 47 48 void free();//销毁元素并释放内存 49 void reallocate();//获得更多内存并拷贝已有元素 50 void reallocate(const size_t&); 51 52 std::string *elements;//指向数组首元素的指针 53 std::string *first_free;//指向数组第一个空闲元素的指针 54 std::string *cap;//指向数组尾后位置的指针 55 56 };
StrVec.cpp:
1 #include "StrVec.h" 2 #include <iostream> 3 using namespace std; 4 5 allocator<std::string> StrVec:: alloc; 6 7 void StrVec::push_back(const string &s){ 8 chk_n_alloc();//确保有空间容纳新元素 9 alloc.construct(first_free++, s);//在原先first_free位置构造一个值为s的新元素 10 } 11 12 pair<string*, string*> StrVec::alloc_n_copy(const string *b, const string *e){ 13 auto data = alloc.allocate(e - b);//分配大小等于给定范围元素数目 14 //data指向分配的内存的开始位置 15 return {data, uninitialized_copy(b, e, data)};//uninitialzed_copy返回最后一个构造元素之后的位置 16 } 17 18 void StrVec::free(){ 19 if(elements){//不能传递一个空指针给deallocate 20 for(auto p = first_free; p != elements;){ 21 alloc.destroy(--p);//销毁对象 22 } 23 alloc.deallocate(elements, cap - elements);//释放内存 24 } 25 } 26 27 //拷贝构造函数 28 StrVec::StrVec(const StrVec &s){ 29 auto newdata = alloc_n_copy(s.begin(), s.end()); 30 elements = newdata.first; 31 first_free = cap = newdata.second; 32 } 33 34 StrVec::StrVec(const std::initializer_list<std::string> &il){ 35 auto newdata = alloc_n_copy(il.begin(), il.end()); 36 elements = newdata.first; 37 first_free = cap = newdata.second; 38 } 39 40 //析构函数 41 StrVec::~StrVec(){ 42 free();//释放资源 43 //隐式析构成员 44 } 45 46 StrVec& StrVec::operator=(const StrVec &rhs){ 47 auto data = alloc_n_copy(rhs.begin(), rhs.end());//为了避免自赋值时出错先开辟内存并拷贝rhs 48 free();//释放原有内存 49 elements = data.first; 50 first_free = cap = data.second; 51 return *this; 52 } 53 54 void StrVec::reallocate(){ 55 auto newcapacity = size() ? 2 * size() : 1; 56 auto newdata = alloc.allocate(newcapacity);//分配新内存 57 58 //将旧的数据移动到新内存中 59 auto dest = newdata;//指向新数组中下一个空闲位置 60 auto elem = elements;//z指向旧数组中下一个位置 61 for(size_t i = 0; i !=size(); ++i){ 62 alloc.construct(dest++, std::move(*elem++));//移动而非构造一个新的string 63 } 64 free();//释放旧内存 65 elements = newdata; 66 first_free = dest; 67 cap = elements + newcapacity; 68 } 69 70 void StrVec::reallocate(const size_t &newcapacity){ 71 auto newdata = alloc.allocate(newcapacity);//分配新内存 72 73 //将旧的数据移动到新内存中 74 auto dest = newdata;//指向新数组中下一个空闲位置 75 auto elem = elements;//z指向旧数组中下一个位置 76 for(size_t i = 0; i !=size(); ++i){ 77 alloc.construct(dest++, std::move(*elem++));//移动而非构造一个新的string 78 } 79 free();//释放旧内存 80 elements = newdata; 81 first_free = dest; 82 cap = elements + newcapacity; 83 } 84 85 void StrVec::reserve(const size_t &newcapacity){//分配不小于newcapacity的空间 86 if(newcapacity > size()) reallocate(newcapacity); 87 } 88 89 //使得容器为指定大小但不减小容量 90 void StrVec::resize(const size_t &newcapacity, const std::string &s){ 91 if(newcapacity > size()){ 92 for(int i = size(); i < newcapacity; i++){ 93 push_back(s); 94 } 95 } 96 while(newcapacity < size()){ 97 --first_free; 98 } 99 }
main.cpp:
1 #include <iostream> 2 #include "StrVec.h" 3 using namespace std; 4 5 int main(void){ 6 StrVec s({"gg", "yy"}); 7 s.push_back("hello"); 8 s.push_back("word"); 9 for(const auto &indx : s){ 10 cout << indx << " "; 11 } 12 cout << endl; 13 14 s.reserve(100);//给s分配能容纳100个元素的空间 15 s.resize(10, "jf"); 16 17 for(const auto &indx : s){ 18 cout << indx << " "; 19 } 20 cout << endl; 21 22 s.resize(2); 23 for(const auto &indx : s){ 24 cout << indx << " "; 25 } 26 cout << endl; 27 28 // 输出: 29 // gg yy hello word 30 // gg yy hello word jf jf jf jf jf jf 31 // gg yy 32 33 return 0; 34 }
注意:vector 中分配内存和构造元素是可以分离的,所以我们使用 allocator 类来管理资源
为了提高性能,我们在 reallocate 成员中用库函数 move 的返回值来做 construct 的第二个参数,即令 constrcut 使用 string 的移动构造函数以避免拷贝 string 管理的内存——我们构造的 string 直接从 elem 指向的 string 那里接管内存的所有权