ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C++中的Primer拷贝控制和资源管理详解

2026/10/4 14:48:05 拓冰建站 浏览量
C++中的Primer拷贝控制和资源管理详解 贝控制和资源管理通常管理类外资源的类必须定义拷贝控制成员这种类需要通过析构函数来释放对象所分配的资源。一旦一个类需要析构函数那么它儿乎肯定也需要一个拷贝构造函数和一个拷贝赋值运算符。为了定义这些成员我们首先必须确定此类型对象的拷贝语义。一般来说有两种选择可以定义拷贝操作使类的行为看起来像一个值或者像一个指针。类的行为像一个值意味着它应该也有自己的状态。当我们拷贝一个类值的对象时副本和原对象是完全独立的。改变副本不会对原对象有任何影响反之亦然。行为像指针的类则共享状态。当我们拷贝一个这种类的对象时副本和原对象使用相同的底层数据。改变副本也会改变原对象反之亦然。在我们使用过的标准库类中标准库容器和string类的行为像一个值。而不出意外的shared_ptr类提供类似指针的行为就像我们的StrBlob类一样IO类型和unique_ptr不允许拷贝或赋值因此它们的行为既不像值也不像指针。为了说明这两种方式我们会为练习中的HasPtr类定义拷贝控制成员。首先我们将令类的行为像一个值;然后重新实现类使它的行为像一个指针。我们的HasPtr类有两个成员一个int和一个string指针。通常类直接拷贝内置类型(不包括指针)成员;这些成员本身就是值因此通常应该让它们的行为像值一样。我们如何拷贝指针成员决定了像HasPtr这样的类是具有类值行为还是类指针行为。行为像值的类为了提供类值的行为对于类管理的资源每个对象都应该拥有一份自己的拷贝。这意味着对于ps指向的string每个HasPtr对象都必须有自己的拷贝。为了实现类值行为HasPtr需要定义一个拷贝构造函数完成string的拷贝而不是拷贝指针定义一个析构函数来释放string定义一个拷贝赋值运算符来释放对象当前的string并从右侧运算对象拷贝string类值版本的HasPtr如下所示1234567891011121314classHasPtr{public:HasPtr(conststd::string sstd::string()):ps(newstd::string(s)1),i(0){}//对ps指向的string,每个BasPtr对象都有自己的拷贝HasPt(constHasPtrp):ps(newstd::string(*p.ps)),i(p.i){}HasPtr operator(constHasPtr);~HasPtr(){deleteps;}private:std::string*ps;inti;}我们的类足够简单在类内就已定义了除赋值运算符之外的所有成员函数。第一个构造函数接受一个(可选的)string参数。这个构造函数动态分配它自己的string副本并将指向string的指针保存在ps中。拷贝构造函数也分配它自己的string副本。析构函数对指针成员ps执行delete释放构造函数中分配的内存。类值拷贝赋值运算符赋值运算符通常组合了析构函数和构造函数的操作。类似析构函数赋值操作会销毁左侧运算对象的资源。类似拷贝构造函数赋值操作会从右侧运算对象拷贝数据。但是非常重要的一点是这些操作是以正确的顺序执行的即使将一个对象赋予它自身也保证正确。而且如果可能我们编写的赋值运算符还应该是异常安全的一一当异常发生时能将左侧运算对象置于一个有意义的状态。在本例中通过先拷贝右侧运算对象我们可以处理自赋值情况并能保证在异常发生时代码也是安全的。在完成拷贝后我们释放左侧运算对象的资源并更新指针指向新分配的string:12345678HasPtrHasPtr::operator(constHasPtrrhs){auto newpnewstring(*rhs.ps);//指贝底层stringdeleteps;//释放旧内存ps newp;//从右侧运算对象指贝数据到本对象i rhs;return*this;//返回本对象}在这个赋值运算符中非常清楚我们首先进行了构造函数的工作:newp的初始化器等价于HasPtr的拷贝构造函数中ps的初始化器。接下来与析构函数一样我们delete当前ps指向的string。然后就只剩下拷贝指向新分配的string的指针以及从rhs拷贝int值到本对象了。关键概念:赋值运算征当你编写赋值运算符时:有两点需要记住:如果将一个对象赋予它自身贼值运算待必须能正确工作。天多数赋值运算符组各了析构函数和拷贝构造函数的工作。当你编写一个赋值运算符时一个好的模式是先将右侧运算对象赋值到一个局部临时对象中。当拷贝完成后销毁左侧运算对象的现有成员就是安全的了。一旦左侧运算对象的资源被销毁就只剩下将数据从临时对象拷贝到左侧运算对象的成员中了。为了说明防范自赋值操作的重要性考虑如果赋值运算符如下编写将会发生什么12345678910//这样编写赋值运算符是错误的!HasPtrHasPtr::operator(constHasPtrzhs){deleteps;//释放对象指向的string//如果rhs和*this是同一个对象,我们就将从已释放的内存中拷贝数据!psnewstring(*(rhs.ps));irhs.i;return*this;}如果zhs和本对象是同一个对象delete ps会释放this和rhs指向的string。接下来当我们在new表达式中试图拷贝(rhs.ps)时就会访问一个指向无效内存的指针其行为和结果是未定义的。这样我们的StrBlobPtr类就仍能使用指向vector的weak_ptr了。你修改后的类将需要一个拷贝构造函数和一个拷贝赋值运算符但不需要析构函数。解释拷贝构造出数和拷贝赋值运算符必须要做什么。解释为什么不需要析构函数。定义行为像指针的类对于行为类似指针的类我们需要为其定义拷贝构造函数和拷贝赋值运算符来拷贝指针成员本身而不是它指向的string。我们的类仍然需要自己的析构函数来释放接受string参数的构造函数分配的内存。但是在本例中析构函数不能单方面地释放关联的string。只有当最后一个指向string的HasPtr销毁时它才可以释放string。令一个类展现类似指针的行为的最好方法是使用shared_ptr来管理类中的资源。拷贝(或赋值)一个shared_ptr会拷贝(赋值)shared_ptr所指向的指针。shared_ptr类自己记录有多少用户共享它所指向的对象。当没有用户使用对象时shared_ptr类负责释放资源。但是有时我们希望直接管理资源。在这种情况下使用引用计数(reference count)就很有用了。为了说明引用计数如何工作我们将重新定义HasPtr令其行为像指针一样但我们不使用shared_ptr而是设计自己的引用计数。引用计数引用计数的工作方式如下除了初始化对象外每个构造函数(拷贝构造函数除外)还要创建一个引用计数用来记录有多少对象与正在创建的对象共享状态。当我们创建一个对象时只有一个对象共享状态因此将计数器初始化为1。拷贝构造函数不分配新的计数器而是拷贝给定对象的数据成员包括计数器。拷贝构造函数递增共享的计数器指出给定对象的状态又被一个新用户所共享。“析构函数递减计数器指出共享状态的用户少了一个。如果计数器变为0则析构函数释放状态。拷贝赋值运算符递增右侧运算对象的计数器递减左侧运算对象的计数器。如果左侧运算对象的计数器变为0意味着它的共享状态没有用户了拷贝赋值运算符就必须销毁状态。