《C++ Primer》读书笔记第十三章-2-拷贝控制、交换、动态内存管理类
来源:互联网 发布:bootstrap input控件js 编辑:程序博客网 时间:2024/05/22 01:49
笔记会持续更新,有错误的地方欢迎指正,谢谢!
拷贝控制和资源管理
通过定义不同的拷贝操作,使自定义的类的行为看起来像一个值或指针。
- 类的行为像一个值,意味着它应该也有自己的状态。当我们拷贝一个像值的对象时,副本和原对象是完全独立的,改变副本不会对原对象有任何影响,反之亦然。
- 类的行为像指针(共享状态),当我们拷贝一个这种类的对象时,副本和原对象使用相同的底层数据,改变副本也会改变原对象,反之亦然。
标准库容器和string类的行为像一个值;shared_ptr类的行为像指针;IO和unique_ptr
不允许拷贝或赋值,值和指针都不像。
举例:
实现一个类HasPtr,让它的行为像一个值;然后重新实现它,再使其像一个指针:
我们的HasPtr有两个成员,一个int和一个string指针,对于内置类型int,直接拷贝值,改变它也不符合常理;我们把重心放到string指针,它的行为决定了该类是像值还是像指针。
行为像值的类
为了像值,每个string指针指向的那个string对象,都得有自己的一份拷贝,为了实现这个目的,我们需要做以下三个工作:
- 定义一个拷贝构造函数,完成string的拷贝,而不是拷贝指针
- 定义析构函数来释放string
- 定义一个拷贝赋值运算符来释放对象当前的string,并从右侧运算对象拷贝string
我们来抛代码:
class HasPtr{public: HasPtr(const string &s = string()) : ps(new string(s)), i(0){} //默认实参,列表初始化/*补充:const修饰s为字符串常量(只读),此处s恒为空字符串;&的作用是引用,避免再复制一个string,若是const string s的话还要再复制一次,岂不是很浪费。所以,既然已是只读,为啥不直接用引用。*/ HasPtr(const HasPtr &p) : ps(new string(*p.ps)), i(p.i){} //拷贝构造函数 HasPtr& operator=(const HasPtr &); //赋值运算符声明 ~HasPtr(){delete ps;} //析构函数private: string *ps; int i;};
主要在于拷贝构造函数,它是有副本的,会拷贝string对象,所以析构函数要delete来释放内存。这个类写得很优雅~
类值拷贝赋值运算符
举例:a = b;
类值拷贝赋值:自赋值也是安全的,发生异常后左侧对象状态仍有意义;先拷贝构造右侧对象到临时变量,再销毁左侧成员并赋予新值。
所以,赋值运算符定义如下:
HasPtr& HasPtr::operator=(const HasPtr &rhs){ auto newp = new string(*rhs.ps); //拷贝底层string delete ps; //释放对象指向的string,指针无析构函数,所以指针还在 ps = newp; //从右侧对象拷贝数据到本对象 i = rhs.i; return *this; //返回本对象}
定义行为像指针的类
拷贝指针就行了?没那么简单,还是要释放内存。而且这个释放内存的时机很重要:只有当最后一个指向string的HasPtr销毁时,才能释放内存,所以,我们可以用shared_ptr,但这里,不用智能指针,弄麻烦些,让大家看看底层如何实现引用计数:
class HasPtr{public: HasPtr(const string &s = string()) : ps(new string(s)), i(0), use(size_t(1)){} //默认实参,列表初始化 HasPtr(const HasPtr &p) : ps(p.ps), i(p.i), use(p.use){++(*use)} //拷贝构造函数,要递增计数器 HasPtr& operator=(const HasPtr &); //赋值运算符声明 ~HasPtr() //析构函数 { if(--(*use) == 0) //没人引用了才释放 { delete ps; delete use; } }private: string *ps; int i; size_t *use; //引用计数 //补充:size_t是目标平台能够使用的最大的类型,考虑到了跨平台的效率问题。};
类指针拷贝赋值运算符:
HasPtr& HasPtr::operator=(const HasPtr &rhs){ ++(*rhs.use); //递增右侧运算对象的引用计数 if(--(*use) == 0) //递减左侧对象的引用计数并判断是否要释放内存 { delete ps; delete use; } ps = rhs.ps; //拷贝 i = rhs.i; use = rhs.use; return *this; //返回本对象}
交换操作swap
拷贝并交换:在赋值运算符中,若参数不是引用,此时结合拷贝和交换的赋值运算符是安全的。
比如我们来交换一下前面写的HasPtr(值拷贝版本):
HasPtr temp = v1;v1 = v2;v2 = temp;
这是很自然的一种写法,我们借助中间量temp来交换v1和v2,但是,有时候对象很大,你跑去拷贝交换对象很浪费,不如去交换指针更合算:
string *temp = v1.ps;v1.ps = v2.ps;v2.ps = temp;
我们定义交换指针的swap函数后,就可以利用它写出更简洁的赋值运算符啦~
这样做的好处在哪?
在于你不用管内存拷贝释放等事,把类写好后,保证你调用的方式是最经济有效的。
拷贝控制示例
我们将建立两个类用于邮件处理,两个类命名为Message和Folder,分别表示邮件消息和消息目录。每个Message对象可以出现在多个Folder中,但是任意给定的Message的内容只有一个副本,这样的话,一条Message的内容改变,则我们从任意Folder来浏览它时看到的都是更新的内容。
书上写得挺优雅地,分析和代码请见P460
动态内存管理类
某些类需要在运行时分配可变大小的内存空间,这种类最好在底层使用标准容器库,例如vector。但是,有些类需要自己进行内存分配,它基本就要定义自己的拷贝控制成员来管理所分配的内存。
这一节,要实现标准库vector类的一个简化版,它只能用于string,命名为StrVec。
StrVec类的设计
主要参照vector<string>
源码来实现,vector源码请见我的另一篇博客:
http://blog.csdn.net/billcyj/article/details/78801834
我们将使用allocator来获得原始内存,由于它分配的内存是未构造的,我们将需要在添加新元素时,用allocator的construct成员在原始内存中创建对象;同样的,我们在删除元素时就使用destroy成员来销毁函数。
每个StrVec有三个指针成员指向其元素所使用的内存:
- elements 指向首元素
- first_free 尾后元素
- cap 指向分配的内存末尾的下一个位置
StrVec还有一个名为alloc的静态成员,其类型为allocator<string>
。alloc成员会分配StrVec使用的内存。我们的类还有4个工具函数:
- alloc_n_copy会分配内存,并拷贝一个给定范围内的元素
- free会销毁构造的元素并释放内存
- chk_n_alloc保证StrVec至少有容纳一个新元素的空间,如果空间不够的话,它会调用reallocate来分配更多内存
- reallocate在内存用完时为StrVec分配新内存
分析设计好了之后,我们就可以动手写类了:
class StrVec{public: StrVec() : elements(nullptr), first_free(nullptr), cap(nullptr){} //默认构造函数 StrVec(const StrVec&); //拷贝构造函数声明 StrVec &operator=(const StrVec&); //拷贝赋值运算符声明 ~StrVec(); //析构函数 //一些常用成员函数 void push_back(const 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; //分配元素 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;};
该说明的都已经在注释中说明了。
接下来分别实现已经声明的函数定义:
push_back
void StrVec::push_back(const string& s){ chk_n_alloc(); //确保有空间 alloc.construct(first_free++, s); //调用allocator成员construct来插入 //至于construct怎么搞得咱们就不了解了,有兴趣自己去查吧}
alloc_n_copy
分配足够的内存来保存给定范围的元素,并将这些元素拷贝到新分配的内存中:
pair<string*, string*> StrVec::alloc_n_copy(const string *b, const string *e){ auto data = alloc.allocate(e-b); //分配正好的空间 return {data, uninitialized_copy(b, e, data)};}
在返回语句中完成拷贝工作:
返回语句中对返回值进行了列表初始化,返回的pair中,first指向分配内存的开始位置(因为data作为名字来用就是首元素地址),second是uninitialized_copy的返回值,这个值是一个指向尾后元素的指针。
free
free要干两件事:
- destroy元素,是指确实有内容的元素
- 释放分配的内存空间,包括没有元素的内存
void StrVec::free(){if(elements) //确保不是空指针,就是要有元素{ 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 = newdata.second}StrVec::~StrVec() {free();} //析构函数StrVec &StrVec::operator=(const StrVec &rhs){ auto data = alloc_n_copy(rhs.begin(), rhs.end()); free(); elements = data.first; first_free = cap = data.second; //都等于 return *this;}
reallocate
我们会用到一些之后要学的函数:
void StrVec::reallocate(){ //空就分配一个,不空就变为2倍,好好看看,这个写法很装逼 auto newcapacity = size() ? 2*size() : 1; auto newdata = alloc.allocate(newcapacity); //分配新内存 auto dest = newdata; //指向新数组的下一个空闲位置 auto elem = elemments; //指向旧数组的下一个元素 for(size_t i=0; i != size(); ++i) { alloc.construct(dest++, move(*elem++)); //这里的move函数你就理解为把旧数组元素移动到新数组中,不需要拷贝了 } free(); //移动好元素就释放旧内存空间 //更新数据成员 elements = newdata; first_free = dest; cap = elements + newcapacity;}
- 《C++ Primer》读书笔记第十三章-2-拷贝控制、交换、动态内存管理类
- C++primer学习:拷贝控制(5):动态内存管理类_编写自己的vector
- 《C++ Primer》读书笔记——第十三章_拷贝控制
- C++ Primer : 第十三章 : 动态内存管理类
- C++primer第五版笔记-第十三章拷贝控制
- c++primer第十三章拷贝控制小结-13
- C++ Primer : 第十三章 : 拷贝控制示例
- 《C++Primer》读书笔记——第13章 拷贝控制
- 读书笔记《C++ Primer》第五版——第十三章 拷贝控制
- c++ primer读书笔记-第十三章 复制控制
- 《C++Primer》读书笔记——第12章 动态指针与内存管理
- 《C++primer(第五版)》学习之路-第十三章:拷贝控制
- c++primer(第五版) 第十三章 拷贝控制习题答案
- 《C++ Primer》读书笔记第十三章-1-拷贝、赋值、销毁
- C++ Primer : 第十三章 : 拷贝控制之对象移动
- c++ primer 概念总结第十三章 拷贝控制
- c++ primer(第五版)笔记 第十三章(2) 拷贝控制
- 第十三章 拷贝控制
- Struts拦截器的使用
- Hexo+GitHub Pages 加入文章跟帖功能
- STM32F469I-DISCO移植Linux4.13.12
- spring4 quartz 定时任务亲测
- Spring boot拦截器Interceptor引用外部properties配置(@Value)
- 《C++ Primer》读书笔记第十三章-2-拷贝控制、交换、动态内存管理类
- Springboot 使用 RestTemplate
- codeforce 877D 路径查找
- mac 修改myssql 密码重置
- PAT训练(乙级)—— 1021. 个位数统计 (15)
- CUDA学习笔记(6) 共享内存与全局内存
- TCP/TP基础(二)以太网协议,ARP协议,ICMP协议
- 我们为什么不用c++写网页?
- CkEditor使用技巧