zoukankan      html  css  js  c++  java
  • 拷贝控制2(拷贝控制和资源管理/交换操作/动态内存管理)

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

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

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

    在我们使用过的标准库类中,标准库容器和 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 }
    View Code

    注意:在赋值运算符应该要防范自赋值的情况:

    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 }
    View Code

    行为像指针的类:

     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 }
    View Code

    注意:为了实现类似于 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 }
    View Code

    交换操作:

    库函数 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 }
    View Code

    显然,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 }
    View Code

    注意:如果存在类型特定的 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 }
    View Code

    注意:这个版本赋值运算符中,参数并不能是引用

    使用拷贝和交换的赋值运算符自动就是异常安全的,且能正确处理自赋值

    动态内存管理:

    编写一个功能类似于 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 };
    View Code

    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 }
    View Code

    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 }
    View Code

    注意:vector 中分配内存和构造元素是可以分离的,所以我们使用 allocator 类来管理资源

    为了提高性能,我们在 reallocate 成员中用库函数 move 的返回值来做 construct 的第二个参数,即令 constrcut 使用 string 的移动构造函数以避免拷贝 string 管理的内存——我们构造的 string 直接从 elem 指向的 string 那里接管内存的所有权

  • 相关阅读:
    第十八周作业
    第十七周作业
    第十六周作业
    第十五周作业
    第十四周作业
    第十三周作业
    第十二周作业
    第二阶段考试
    第十周作业
    启航,带着梦想出发!
  • 原文地址:https://www.cnblogs.com/geloutingyu/p/8418835.html
Copyright © 2011-2022 走看看