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