🌶️拷贝控制和资源管理
2022-6-1
| 2023-8-2
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password
Property
 
通常,如果类有自己管理的资源(动态分配的内存、网络、文件句柄等)肯定需要定义拷贝控制成员。这些类需要定义析构函数来释放资源。一旦其需要析构函数,那就意味着肯定需要拷贝构造函数和拷贝赋值操作符。
 
拷贝类对象有两个设计决定:以值的方式拷贝,以指针的方式拷贝。
  • 行为像值的类: 每个类的对象都有自己的实现
  • 行为像指针的类: 所有类的对象共享类的资源(类似于shared_ptr智能指针,每有一个对象持有该资源则引用计数+1,每有一个对象释放该资源则引用计数-1,引用计数为0时释放内存)
行为与值一样的类有其自己的状态,当拷贝这种对象时,拷贝后的对象与原始对象是互相独立的,对任何一方作出改变都不会影响到另外一方;行为与指针类似的类则共享状态,当拷贝这种对象时,原始对象和拷贝后的对象具有相同的底层数据,对任何一样的改变都会影响到另外一方。
通常,类直接拷贝内置类型成员,这些成员就是值,其行为与值是完全一样的。拷贝指针的不同方式将影响对象是值还是指针。
 

行为像值的类

对于类管理的资源,每个对象都应该有一份自己的拷贝。如下面的string类型的指针 ,使用拷贝构造函数或者赋值运算符时,每个对象拷贝的都是指针成员ps指向的string而非ps本身 。换言之,每个对象都有一个ps而不是给ps加引用计数。
 
 
为什么不能像下面这样实现赋值运算符呢?
这是因为如果a*this 是 同一个对象delete ps 会释放 *this 和 a 指向的 string。接下来,当在new表达式中试图拷贝*(a.ps)时,就会访问一个指向无效内存的指针(即空悬指针),其行为和结果是未定义的。
因此,第一种实现方法可以确保销毁 *this 的现有成员操作是绝对安全的,不会产生空悬指针
 
 

行为像指针的类

对于行为类似指针的类,使用拷贝构造函数或者赋值运算符时,每个对象拷贝的都是ps本身而非指针成员ps指向的string。换言之,每有一个对象都是给指向stringps加引用计数。
因此,析构函数不能粗暴地释放 ps 指向的 string ,只有当最后一个指向 string 的 A类对象 销毁时,才可以释放 string 。这个特性很符合shared_ptr的功能,因此可以使用shared_ptr来管理像指针的类中的资源
但是,有时需要程序员直接管理资源,因此就要用到引用计数(reference count) 了。引用计数工作方式:
  • 每个构造函数(拷贝构造函数除外)都要创建一个引用计数,用来记录有多少对象与正在创建的对象共享状态。创建一个对象时,只有一个对象共享状态,因此将计数器初始化为1。
  • 拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员,包括计数器。拷贝构造函数递增共享的计数器,指出给定对象的状态又被一个新用户所共享。
  • 析构函数递减计数器,指出共享状态的用户少了一个。如果计数器变为0,则析构函数释放状态。
  • 拷贝赋值运算符递增右侧运算对象的计数器,递减左侧运算对象的计数器。如果左侧运算对象的计数器变为0,意味着它的共享状态没有用户了,拷贝赋值运算符就必须销毁状态。
 
唯一的难题是确定在哪里存放引用计数。计数器不能直接作为A对象的成员
如果计数器保存在每个对象中,创建a2时可以递增a1的计数器并拷贝到a2中。可创建a3时,诚然可以更新a1的计数器,但怎么找到a2并将它的计数器更新呢?
 
那么怎么处理计数器呢?动态内存实现计数器
偶尔希望直接管理资源时需要自己定义引用计数(reference count),倾向于将引用计数器放在动态内存中,每个对象保留一个指向这个计数器的指针。
其拷贝赋值操作符同时做了拷贝构造函数和析构函数的工作。赋值操作符将增加右操作数的引用计数(拷贝构造函数的工作)并减少左操作数的引用计数,当引用计数变为 0 的时候删除掉其内存(析构函数的工作)。
  • C++
  • 拷贝、赋值与析构类的交换操作
    目录