C++常见问题总结_拷贝控制和资源管理

通常,管理类外资源的类必须定义拷贝控制成员。为了定义这些成员,我们首先必须确定此类型对象的拷贝语义。一般有两种选择:使类的行为看起来像一个值或者像一个指针。

行为像值的类
类的行为像一个值,意味着它应该有自己的状态。当我们拷贝一个像值的对象时,副本和源对象是完全独立的。改变副本不会对原对象有任何影响。如标准库类型中string
1、定义一个类值行为的类

#include<iostream>
#include<string>

using namespace std;

class hasptr
{
public:
    hasptr(const string &s = string()) :
        ps(new string(s)) ,i(0) {}
    hasptr(const hasptr &p) :                       //拷贝构造函数
        ps(new string(*p.ps)), i(p.i) {}
    hasptr& operator=(const hasptr&);               
    hasptr& operator=(const string&);               //拷贝赋值运算符
    string& operator*();                            // 重载解引用
    ~hasptr();
private:
    string *ps;
    int i;
};

hasptr::~hasptr()
{
    delete ps;
}

inline hasptr& hasptr::operator=(const hasptr&rhs)
{
    auto newps = new string(*rhs.ps);
    delete ps;
    ps = newps;
    i = rhs.i;
    return *this;
}

hasptr& hasptr::operator=(const string& rhs)
{
    *ps = rhs;
    return *this;
}
string& hasptr::operator*()
{
    return *ps;
}

int main()
{
    hasptr h("hi mom!");
    hasptr h2(h);
    hasptr h3 = h;                                  //行为类值,指向不同部分,互不影响
    h2 = "hi,dad!";
    h3 = "hi,son!";
    cout << "h: " << *h << endl;           
    cout << "h2: " << *h2 << endl;
    cout << "h3: " << *h3 << endl;
    getchar();
}

这里写图片描述
对于上面例子的说明:

  • 定义拷贝构造函数时,是对string的拷贝,而不是对指针的拷贝。
  • 类值行为的拷贝赋值运算符,通常综合了析构函数和构造函数的操作。赋值操作会销毁左侧运算对象的资源,并且类似于拷贝构造函数,赋值操作会从右侧运算对象拷贝数据。最重要的是:这些操作要以正确的顺序执行,即将一个对象赋予它自身,赋值运算符可以正常工作,自赋正确。如以下就是错误的:
//此时不能将一个对象赋予它自身
inline hasptr& hasptr::operator=(const hasptr&rhs)
{
    delete ps;
    auto newps = new string(*rhs.ps);
    ps = newps;
    i = rhs.i;
    return *this;
}

行为像指针的类
行为像指针的类共享状态,当我们拷贝一个这种类对象时,副本和原对象使用相同的底层数据。改变副本也会改变原对象。例如在动态内存中讲到的shared_ptr类就是。
1、定义一个行为像指针的类

#include<string>
#include<iostream>

using namespace std;

class hasptr {
public:
    hasptr(const string &s = string()) :
        ps(new string(s)), i(0), use(new size_t(1)) {}

    hasptr(const hasptr&p) :
        ps(p.ps), i(p.i), use(p.use) {
        ++*use;
    }
    hasptr& operator=(hasptr&);
    hasptr& operator=(const string&);
    string& operator*();
    ~hasptr();
private:
    string *ps;
    int i;
    size_t *use;                          //引用计数类似shared_ptr,用来记录有多少成员共享*ps的成员
};

hasptr::~hasptr()                        
{
    if (--*use == 0)                     //如果引用计数变为0,就释放对应的内存
    {
        delete ps;
        delete use;
    }
}

hasptr& hasptr::operator=(hasptr& rhs)
{
    ++*rhs.use;//应该放这里,处理自赋值
    if (--*use == 0)
    {
        delete ps;
        delete use;
    }
    ps = rhs.ps;
    i = rhs.i;
    use = rhs.use;
    return *this;
}

hasptr& hasptr::operator=(const string&rhs)
{
    *ps = rhs;
    return *this;
}

string& hasptr::operator*()
{
    return *ps;
}

int main()
{
    hasptr h("hi,mom!");
    hasptr h2 = h;     //未分配新string,h2和h指向相同的string
    h = "hi,dad!";
    cout << "h:" << *h << endl;
    cout << "h2:" << *h2 << endl;
    getchar();
}

这里写图片描述
由上面结果可以看到h和h2指向相同的string。

对于上数例子的说明:

  • 我们定义其拷贝构造函数和拷贝赋值运算符时,拷贝的是指针成员本身,而不是他指向的内容。
  • 我们的析构函数中不能单方面的释放所关联的对象。只有当最后一个指向对象的销毁时才可以释放string,在这里我们学习shared_ptr中,使用一个引用计数来记录有多少个用户指向共享的对象,当引用计数变为0时,才会释放所关联的内存。

  • 引用计数


1、在我们创建对象时,构造函数除了初始化每个对象,还要创建一个引用计数,记录多少个对象来共享内容
2、拷贝构造函数不会分配新的计数器,而是拷贝给定的数据成员,包括计数器,并且会递增计数器的值。
3、析构函数递减计数器,当计数器的值变为0时,析构函数会释放对应的关联内存。
4、拷贝赋值运算符,递减左侧运算对象计数器,递增右侧运算对象计数器,如果左侧运算对象的计数器值变为0,就必须释放对应的内存。

然而引用计数应该存放在哪里?
对于这个问题,假如计数器直接作为hasptr对象的成员:

hasptr p1("hai");
hasptr p2(p1);
hasptr p3(p1);
/*当我们创建p3时我们可以递增p1中的计数器的值,并且将其拷贝到p3中,这时我们没办法更新p2中的计数器*/

我们将计数器的值保存在动态内存中,当创建一个对象时,我们也分配一个新的计数器。当拷贝和赋值对象时,我们拷贝指向计数器的指针,使原对象和副本都会指向相同的计数器。

交换操作
除了定义拷贝控制成员,管理资源的类通常还定义一个名为swap的函数。与拷贝控制成员不同,swap并不是必要的。但是,对于分配了资源的类,定义swap可能是一种重要的优化手段。
例如为上述的类值版本的hasptr 定义一个swap函数:

class hasptr
{
    friend void swap(hasptr&, hasptr&);
    //和上述类似

};
inline void swap(hasptr &lhs, hasptr&rhs)
{
    cout << "交换 " << *lhs.ps << "和" << *rhs.ps << endl;
    using std::swap;
    swap(lhs.ps, rhs.ps);
    swap(lhs.i, rhs.i);
}

int main()
{
    hasptr h("hi mom");
    hasptr h2(h);
    hasptr h3 = h;
    h2 = "hi dad!";
    h3 = "hi son!";
    swap(h2, h3);
    cout << "h: " << *h << endl;
    cout << "h2: " << *h2 << endl;
    cout << "h3: " << *h3 << endl;
    getchar();
}

这里写图片描述
上述例子的说明:
上述的swap函数中又调用了swap函数不会导致递归循环,因为这两个成员是内置类型,因此在函数中的调用会被解析成std::swap,而不是hasptr的特定版本的swap。

猜你喜欢

转载自blog.csdn.net/xc13212777631/article/details/80610812