拷贝控制2(拷贝控制和资源管理交换操作动态内存管理)

mac2024-04-14  37

为了定义拷贝构造函数和拷贝赋值运算符,我们首先必须确认此类型对象的拷贝语义。通常可以定义拷贝操作,使类的行为看起来像一个值或者像一个指针(即所谓的深拷贝和浅拷贝)

类的行为像一个值,意味着它应该也有自己的状态。当我们拷贝一个像值的对象时,副本和原对象是完全独立的。改变副本不会对原对象有任何影响,反之亦然

行为像指针的类则共享状态。当我们拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。改变副本也会改变原对象,反之亦然

在我们使用过的标准库类中,标准库容器和 string 类的行为像一个值。shared_ptr 提供类似指针的行为。IO 类型和 unique_ptr 不允许拷贝或赋值,因此它们的行为既不像值也不像指针

行为像值

#include<bits/stdc++.h> using namespace std; struct Node{ public: Node(const string &s=string()): ps(new string(s)),i(0){};//使行为像值,就在构造函数里定一个默认的值 Node(const Node&p):ps(new string(*p.ps)),i(p.i){};//拷贝构造函数 Node& operator=(const Node&);//三五法则 ~Node(){ delete ps;}//还要定义自己的析构函数 ostream& print(ostream &os){ os<<i<<" "<<*ps<<" "<<ps<<endl; } private : string *ps; int i; }; Node& Node::operator=(const Node &rhs){//定义自己的赋值运算 auto newps=new string(*rhs.ps);//为了避免两个指针指向同一块内存 delete ps;//释放自己旧的内存 ps=newps; i = rhs.i; return *this; } int main(){ Node s1("hello"); Node s2=s1; Node s3=s2; s1.print(cout); s2.print(cout); s3.print(cout); }

但是我们要注意:在重新定义赋值操作的时候,要防范自赋值的情况

Node& Node::operator=(const Node &rhs){//定义自己的赋值运算 delete rhs.ps;//如果rhs是自身,那么ps就会成为一个空悬指针 ps=rhs.ps;//不指向任何对象。 i = rhs.i; return *this; }

行为像指针的类:

#include<bits/stdc++.h> using namespace std; class Node{ public : Node(const string &s=string())://分配一个新的string ps(new string(s)), i(0), use(new std::size_t(1)) {} // use(new std::size_t(1)){}//直接初始化 Node(const Node &p):ps(p.ps),i(p.i),use(p.use){//拷贝构造函数 ++*use; } Node& operator=(const Node&); ~Node(); ostream& print(ostream &os){ os<< *use <<" "<<i<<" "<< *ps <<" "<<ps<<endl; return os; } private : string *ps;//一个指向string的指针 int i; std::size_t *use;//定义一个计数器 }; Node& Node::operator=(const Node &rhs){ ++*rhs.use; if(--*use==0){//如果左值的引用计数器变为0,那么就讲左边的指针指向的对象给清空 delete ps; delete use; } ps=rhs.ps; i=rhs.i; use=rhs.use; return *this; } Node::~Node(){ if(--*use==0){ delete ps; delete use; } } int main(){ Node s1("Hello"); Node s2=s1; Node s3=s1; Node s4("word"); s1.print(cout); s2.print(cout); s3.print(cout); s4.print(cout); /*3 0 Hello 0xac14d0 3 0 Hello 0xac14d0 3 0 Hello 0xac14d0 1 0 word 0xac18c0*/ }

注意:为了实现类似于 shared_ptr 的引用计数功能,我们可以将计数器保持到动态内存中,指向相同 ps 对象的 HasPtr 也指向相同的 use 对象。 这里我们不能使用 static 来实现引用计数,因为它是属于类本身的,这意味着所有 HasPtr 类的对象中 use 值都是相等的,并且我们将无法做到给赋值运算符右侧对象 use 加一,左侧对象 use 减一:

交换操作:

库函数 swap 的实现依赖于类的拷贝构造函数和赋值运算符。如,对值语义的 HasPtr 类对象使用库函数 swap:

swap(s1, s2);的实现流程是: HasPtr cmp = s1; s1 = s2; s2 = cmp; 这个过程中分配了 3 次内存,效率及其低下。理论上这些内存分配都是不必要的。我们可以只交换指针而不需要分配 string 的新副本(对于管理资源的类来讲)。

因此,除了定义拷贝控制成员,管理资源的类通常还需要定义一个名为 swap 的函数。尤其对于那些与重排元素顺序的算法一起使用的类,定义 swap 是非常重要的。这类算法在需要交换两个元素时会调用 swap。

在行为像值的类中,因为swap的实现依赖拷贝赋值函数,会多次分配内存(因为你自定义的拷贝赋值操作是要重新分配内存的)。所以使用库函数的swap就会降低性能,这是我们就需要自定义swap函数

对于行为像值的类的swap:

#include <iostream> using namespace std; class HasPtr{ friend void swap(HasPtr&, HasPtr&); public: HasPtr(const std::string &s = std::string(), int a = 0) : ps(new std::string(s)), i(a) {} HasPtr(const HasPtr &p) : ps(new std::string(*p.ps)), i(p.i) {} HasPtr& operator=(const HasPtr&); ~HasPtr(){ delete ps; } ostream& print(ostream &os){ os << i << " " << *ps << " " << ps; return os; } private: std::string *ps; int i; }; HasPtr& HasPtr::operator=(const HasPtr &rhs){ auto newp = new string(*rhs.ps);//为了避免两个对象中ps指针相同时出错先将rhs.ps中的内容存放到一个新开辟的空间newp中 delete ps;//释放旧内存 ps = newp; i = rhs.i; return *this; } inline void swap(HasPtr &lhs, HasPtr &rhs){ swap(lhs.ps, rhs.ps); swap(lhs.i, rhs.i); } int main(void){ HasPtr s1("hello"); HasPtr s2("word", 1); s1.print(cout) << endl; s2.print(cout) << endl; swap(s1, s2); // auto cmp = s1.ps; // s1.ps = s2.ps; // s2.ps = cmp; // auto cnt = s1.i; // s1.i = s2.i; // s2.i = cnt; s1.print(cout) << endl; s2.print(cout) << endl; // 输出: // 0 hello 0x2bb1208 // 1 word 0x2bb1128 // 1 word 0x2bb1128 // 0 hello 0x2bb1208 return 0; }

注意:如果存在类型特定的 swap 版本,其匹配程度会优于 std 中定义的版本。如果不存在类型特定的版本,则会使用 std 中的版本(假定作用域中有 using 声明)

对于行为像指针的类来讲,自定义swap并不能优化性能。

在赋值运算符中使用 swap:(行为像值的类的例子)

定义了 swap 的类中通常用 swap 来定义它们的赋值运算符。这些运算符使用了一种名为 拷贝并交换(copy and swap) 的技术。这种技术将左侧运算对象与右侧运算对象的一个副本进行交换:

#include<bits/stdc++.h> using namespace std; class Node{//行为像值的类 friend void swap(Node&,Node&); public : Node(const string s=string(),int a=0): ps(new string(s)),i(a){} Node(const Node &p): ps(new string(*p.ps)),i(p.i){};//拷贝复制函数 Node& operator=(Node);//注意:使用拷贝并交换技术,参数不能是引用 ~Node(){ delete ps;//delete 删除ps所指向的对象。 } ostream& print(ostream& os){ os<<i<<" "<<*ps<<" "<<ps<<endl; return os; } private: string *ps; int i; }; Node& Node::operator=(Node rhs){//注意:使用交换并且赋值的技术 swap(*this,rhs);//这个函数将rhs赋值给this,函数结束的时候,rhs(赋值前的this)被销毁 return *this; } inline void swap(Node &lhs,Node&rhs){ swap(lhs.ps,rhs.ps); swap(lhs.i,rhs.i); } int main(){ Node s1("hello"); Node s2("world",1); s1.print(cout); s2.print(cout); cout<<"----------------"<<endl; swap(s1,s2);//交换的是string的指针,并不是值 s1.print(cout); s2.print(cout); cout<<"---------------"<<endl; s1=s2; s1.print(cout);//string重新分配了一次内存,并不是普通swap的三次、 s2.print(cout); }

注意:这个版本赋值运算符中,参数并不能是引用 使用拷贝和交换的赋值运算符自动就是异常安全的,且能正确处理自赋值

#pragma once//表示在编译过程中,这个文件只会被include一次 #include<iostream> #include<string> #include<memory> #include<utility> class StrVec{ public: StrVec():elements(nullptr),frist_free(nullptrs),cap(nullptr); //三五法则 StrVec(const StrVec&);//拷贝构造函数 StrVec& operator=(const StrVec&);//重载运算符 ~StrVec();//析构函数、 void push_back(const string&)//拷贝元素 size_t size()const {return frist_free-elements;} size_t capacity()const {return cap-elements;}//容量的大小 string *begin()const {return elements;} string *end()const {return frist_free;} private: string *elements;//指向分配的内存中的首元素 string *frist_free;//指向最后一个实际元素之后的位置 string *cap;//指向分配的内存之后的位置 static allocator<string> alloc;//??为什么静态的? void chk_n_alloc(){ if(size()==capacity()reallocate;)}//如果没有空余位置,就分配新的空间 pair<string*,string *> alloc_n_copy(const string *,const string *);//分配内存,并且拷贝范围元素 void free();//释放内存 void reallocate();//重新分配内存 }; void StrVec::push_back(const string&s){ chk_n_alloc();//检查内存是否足够 alloc.construct(frist_free++,s);//分配一个元素,记得递增这个元素 } //分配空间保存给定空间的元素 pair<string*,string*> StrVec::alloc_n_copy(const string *s1,const string *s2){ auto data = a.alloc(s2-s1);// return {data,uninitialized_copy(s1,s2,data)};//返回目的(递增后的)位置迭代器 } //copy和uninitialized_copy的不同,是依次调用拷贝构造函数 void StrVec::free(){//释放空间 if(elements){//不能给destroy一个空的 for(auto p=frist_free;p!=elements;){ alloc.destory(--p);//对p所指向的对象执行析构函数 } alloc.deallocate(elements,cap-elements); elements=frist_free=cap=nullptr; } } //调用调用分配空间复制的函数, StrVec::StrVec(const StrVec&s){//拷贝构造函数, auto newdata=alloc_n_copy(s.begin(),s.end()); elements=newdata.frist; frist_free=cap=newdata.second; } StrVec::~StrVec(){ free();//先释放空间,析构函数再delete指针。 } StrVec& StrVec::operator=(const StrVec& s){//赋值构造函数 auto data=alloc_n_copy(s.begin(),s.end()); free(); elements=data.frist; cap=frist_free=data.second; return *this; } //释放旧的内存 void StrVec::reallocate(){ auto newcapacity()=size()?size()*2:1; auto newdata=alloc.allocate(newcapacity);//构造空间 auto dest=newdata; //指向新内存 auto elem=elements; //指向旧内存 for(size_t i=;i!=size();i++) alloc.construct(dest++,std::move(*elem++));//construct的第二个参数决定了使用哪个构造函数 free(); elements=newdata; frist_free=dest; cap=elements+newcapacity;//空间的末尾 } /* 总结: 资源管理类,有三个指针成员,元素前,元素后,以及内存后 添加元素的函数要每次检查内存是否充足,内存充足就直接添加,不足就重新释放空间,移动所有元素, 要定义自己的析构函数(释放allocate)分类的空间 释放空间函数 拷贝构造函数:通过allocate直接开辟空间,然后进行拷贝。 移动函数:切记要使用std::move而不是move */

StrVec.h:

#pragma once #include <iostream> #include <memory> #include <utility> #include <initializer_list> class StrVec{ public: //默认构造函数 StrVec() : elements(nullptr), first_free(nullptr), cap(nullptr) {}//allocator成员进行默认初始化 StrVec(const std::initializer_list<std::string>&); 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(const size_t&);//分配指定大小的空间并将原来的元素拷贝到新空间 void resize(const size_t&, const std::string &s = "");//使得容器为指定大小但不减小容量 private: static std::allocator<std::string> alloc;//分配元素 void chk_n_alloc(){//被添加元素的函数所使用 if(size() == capacity()) reallocate(); } //工具函数,被拷贝构造函数、赋值运算符和析构函数所使用 std::pair<std::string*, std::string*> alloc_n_copy(const std::string*, const std::string*); void free();//销毁元素并释放内存 void reallocate();//获得更多内存并拷贝已有元素 void reallocate(const size_t&); std::string *elements;//指向数组首元素的指针 std::string *first_free;//指向数组第一个空闲元素的指针 std::string *cap;//指向数组尾后位置的指针 };

StrVec.cpp:

#include "StrVec.h" #include <iostream> using namespace std; allocator<std::string> StrVec:: alloc; void StrVec::push_back(const string &s){ chk_n_alloc();//确保有空间容纳新元素 alloc.construct(first_free++, s);//在原先first_free位置构造一个值为s的新元素 } pair<string*, string*> StrVec::alloc_n_copy(const string *b, const string *e){ auto data = alloc.allocate(e - b);//分配大小等于给定范围元素数目 //data指向分配的内存的开始位置 return {data, uninitialized_copy(b, e, data)};//uninitialzed_copy返回最后一个构造元素之后的位置 } void StrVec::free(){ if(elements){//不能传递一个空指针给deallocate for(auto p = first_free; p != elements;){ alloc.destroy(--p);//销毁对象 } 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(const std::initializer_list<std::string> &il){ auto newdata = alloc_n_copy(il.begin(), il.end()); elements = newdata.first; first_free = cap = newdata.second; } //析构函数 StrVec::~StrVec(){ free();//释放资源 //隐式析构成员 } StrVec& StrVec::operator=(const StrVec &rhs){ auto data = alloc_n_copy(rhs.begin(), rhs.end());//为了避免自赋值时出错先开辟内存并拷贝rhs free();//释放原有内存 elements = data.first; first_free = cap = data.second; return *this; } void StrVec::reallocate(){ auto newcapacity = size() ? 2 * size() : 1; auto newdata = alloc.allocate(newcapacity);//分配新内存 //将旧的数据移动到新内存中 auto dest = newdata;//指向新数组中下一个空闲位置 auto elem = elements;//z指向旧数组中下一个位置 for(size_t i = 0; i !=size(); ++i){ alloc.construct(dest++, std::move(*elem++));//移动而非构造一个新的string } free();//释放旧内存 elements = newdata; first_free = dest; cap = elements + newcapacity; } void StrVec::reallocate(const size_t &newcapacity){ auto newdata = alloc.allocate(newcapacity);//分配新内存 //将旧的数据移动到新内存中 auto dest = newdata;//指向新数组中下一个空闲位置 auto elem = elements;//z指向旧数组中下一个位置 for(size_t i = 0; i !=size(); ++i){ alloc.construct(dest++, std::move(*elem++));//移动而非构造一个新的string } free();//释放旧内存 elements = newdata; first_free = dest; cap = elements + newcapacity; } void StrVec::reserve(const size_t &newcapacity){//分配不小于newcapacity的空间 if(newcapacity > size()) reallocate(newcapacity); } //使得容器为指定大小但不减小容量 void StrVec::resize(const size_t &newcapacity, const std::string &s){ if(newcapacity > size()){ for(int i = size(); i < newcapacity; i++){ push_back(s); } } while(newcapacity < size()){ --first_free; } }

main.cpp

#include <iostream> #include "StrVec.h" using namespace std; int main(void){ StrVec s({"gg", "yy"}); s.push_back("hello"); s.push_back("word"); for(const auto &indx : s){ cout << indx << " "; } cout << endl; s.reserve(100);//给s分配能容纳100个元素的空间 s.resize(10, "jf"); for(const auto &indx : s){ cout << indx << " "; } cout << endl; s.resize(2); for(const auto &indx : s){ cout << indx << " "; } cout << endl; // 输出: // gg yy hello word // gg yy hello word jf jf jf jf jf jf // gg yy return 0; }
最新回复(0)