C++中的RAII和拷贝控制


前言

最近在实现一个功能的时候,涉及到一个自定义RAII类的拷贝控制操作,这部分知识有些遗忘了,再加上以前写C++程序的时候对这一块的一些细节并没有太多深入理解,所以最近重新阅读了《C++ Primer》(第5版)中的第13章以及《Effective C++》(第3版)中的第2和第3章,对拷贝控制有了一些新的理解,这里做个简单的总结(现在发现《Effective C++》真的是本经典的书,以前上学的时候虽然看过,但是很多东西并没有消化,根本就不理解里面的含义)。
注:关于RAII和拷贝控制操作的基本概念参考《C++ Primer》(第5版)中的第13章以及《Effective C++》(第3版)中的第2和第3章,这里不再赘述。



如何设计一个类的拷贝控制成员

在设计一个类的拷贝控制成员的时候,最先要考虑的一个问题就是这个类能不能被拷贝(包括拷贝构造和拷贝赋值)。如果这个类不能被拷贝,则直接禁用就可以了,如果这个类必须要实现拷贝,那问题就有点小复杂了。下面的这个图提供了设计一个类的拷贝控制成员的基本思路(其实这张图还是有点复杂了,原因参考本文最后的总结,这里只是提供一些基本思路供大家参考)。
在这里插入图片描述

如果一个类必须要实现拷贝操作,那需要知道这个类的数据是否可以被共享,如果这个类的数据可以被多个对象共享,也就是浅拷贝,那么可以使用引用计数机制来实现,在C++11中可以使用std::shared_ptr来实现引用计数。如果一个类的数据不能被共享,那就要实现深拷贝机制,这种情况下,如果这个类的所有成员变量都支持深拷贝,则可以直接使用默认生成的,实际上只要我们在设计的时候选择合适的类型,我们遇到的大部分类都属于这种类型,根本不需要我们自己定义拷贝控制成员。但是如果一个类中有成员变量不支持深拷贝,那么就需要自定义拷贝控制成员了。

下面针对一些典型情况给出一些具体示例看看如何实现拷贝控制成员。


拷贝控制设计示例

禁止拷贝控制成员

在C++11之前禁止拷贝控制成员是将一个类的拷贝构造和拷贝赋值设置为private并不予实现。

class UnCopy
{
private:
    UnCopy(const UnCopy&);
    UnCopy& operator=(const UnCopy&);
};

C++11出来之后,可以将这两个成员函数设置为delete。

class UnCopy
{
public:
    UnCopy(const UnCopy&) = delete;
    UnCopy& operator=(const UnCopy&) = delete;
};

其实在C++11中还有一个更简单的方式,就是使用std::unique_ptr智能指针管理资源,然后什么也不用做,直接采用编译器默认生成的拷贝控制成员就可以了。

手动实现引用计数机制

在我开源的图像处理库中,Mat结构的设计就是手动实现的引用计数。Mat是一个参考OpenCV中cv::Mat设计的图像类。
具体链接参考:https://github.com/qianqing13579/QQImageProcess/blob/master/Src/Utility/Mat.h

下面看一下跟拷贝控制成员有关的代码:

template <typename T>
class  Mat
{
public:
	// 构造函数
	Mat();
	Mat(const Mat<T> &m);// 拷贝构造
	Mat(Mat<T> &&m) noexcept;// 移动构造
	Mat(int _rows, int _cols, int _numberOfChannels);
	
	// 析构函数
	virtual ~Mat();//调用Release()
	void Release();//引用计数减1
	void Deallocate();//释放数据

	// 自动分配内存
	void Create(int _rows, int _cols, int _numberOfChannels);

	// 赋值操作符(可实现拷贝赋值和移动赋值)
	Mat& operator = (Mat dstMat);

	void swap(Mat &dstMat);

	void InitEmpty();

public:
	int rows;
	int cols;
	int numberOfChannels;// 通道数
	int step;// 步长(每行字节数)
	
	uchar *data;	

	// 引用计数,当Mat指向外部数据的时候,refCount为NULL,不需要释放内存
	int *refCount;

};// Mat


//Mat的实现

template <typename T>
inline Mat<T>::Mat()
{
	InitEmpty();

}

template <typename T>
inline Mat<T>::Mat(const Mat<T> &m)
{
	// 引用计数加1
	refCount = m.refCount;
	if(refCount!=NULL)
	{
		(*refCount)++;
	}

	rows = m.rows;
	cols = m.cols;
	numberOfChannels = m.numberOfChannels;
	step = m.step;
	data = m.data; 

}

// 移动构造
template <typename T>
inline Mat<T>::Mat(Mat<T> &&m) noexcept
{
	// 移动资源
	data = m.data;
	refCount = m.refCount;
	rows = m.rows;
	cols = m.cols;
	numberOfChannels = m.numberOfChannels;
	step = m.step;

	// 使得m运行析构函数是安全的
	m.refCount=NULL;
	
}

template <typename T>
inline void Mat<T>::InitEmpty()
{
	rows = cols = numberOfChannels = 0;
	data = 0;
	refCount = NULL;

}

template <typename T>
inline Mat<T>::Mat(int _rows, int _cols, int _numberOfChannels)
{
	InitEmpty();
	Create(_rows, _cols, _numberOfChannels);
}

template <typename T>
Mat<T>::~Mat()
{
	Release();//释放
}

// 引用计数减1,如果引用计数为0了,调用Deallocate()
template <typename T>
inline void Mat<T>::Release()
{
	//引用计数减1,如果引用计数为0,说明没有引用,释放数据
	if ((refCount!=NULL) && ((*refCount)-- == 1))
	{
		Deallocate();
	}

	InitEmpty();

}
//释放数据
template <typename T>
inline void Mat<T>::Deallocate()
{

	AlignedFree(data);


}

template <typename T>
inline void Mat<T>::Create(int _rows, int _cols, int _numberOfChannels)
{
	if (rows == _rows&&cols == _cols&&numberOfChannels == _numberOfChannels)
	{
		return;
	}
	else
	{
		//如果不一致,引用计数减1,此时引用计数为0,释放数据和引用计数
		Release();

		rows = _rows;
		cols = _cols;
		numberOfChannels = _numberOfChannels;
		step = cols*numberOfChannels*sizeof(T);

		// 内存地址16字节对齐(用于指令集优化)
		data = (uchar *)AlignedMalloc((step*rows + (int)sizeof(int)), 16);

		refCount = (int*)(data + step*rows);
		*refCount = 1;

	}
}

template <typename T>
inline void Mat<T>::swap(Mat &dstMat)
{
	using std::swap;
	swap(this->rows,dstMat.rows);
	swap(this->cols,dstMat.cols);
	swap(this->numberOfChannels,dstMat.numberOfChannels);
	swap(this->step,dstMat.step);
	swap(this->data,dstMat.data);
	swap(this->refCount,dstMat.refCount);

}

template <typename T>
inline void swap(Mat<T> &a,Mat<T> &b)
{
	a.swap(b);
}

template <typename T>
inline Mat<T>& Mat<T>::operator = (Mat<T> dstMat)
{
	// copy-swap
	using std::swap;
	swap(*this,dstMat);
	return *this;
}


#endif

下面重点说一下两个部分:

  1. 定义拷贝构造,移动构造和析构函数
  2. 定义拷贝赋值和移动赋值

定义拷贝构造,移动构造和析构函数

为什么将这三个放在一起说呢,因为这三个都可以归结于对象的构造和销毁。
下面看一下拷贝构造的实现:

template <typename T>
inline Mat<T>::Mat(const Mat<T> &m)
{
	// 引用计数加1
	refCount = m.refCount;
	if(refCount!=NULL)
	{
		(*refCount)++;
	}

	rows = m.rows;
	cols = m.cols;
	numberOfChannels = m.numberOfChannels;
	step = m.step;
	data = m.data; 

}

引用计数机制中,拷贝构造需要对引用计数+1,这里需要注意一点,在+1的时候需要判断refCount是否为空,因为Mat中如果使用的是外部数据,则该Mat是没有引用计数的,所以refCount为空。此时Mat也不会负责对该数据进行释放。

移动构造的实现比较简单:

// 移动构造
template <typename T>
inline Mat<T>::Mat(Mat<T> &&m) noexcept
{
	// 移动资源
	data = m.data;
	refCount = m.refCount;
	rows = m.rows;
	cols = m.cols;
	numberOfChannels = m.numberOfChannels;
	step = m.step;

	// 使得m运行析构函数是安全的
	m.refCount=NULL;
	
}

移动构造就是先移动资源,然后设置源对象状态,一般是清零。

析构函数需要对引用计数减1,如果引用计数为0则需要释放资源:

template <typename T>
Mat<T>::~Mat()
{
	Release();//释放
}

// 引用计数减1,如果引用计数为0了,调用Deallocate()
template <typename T>
inline void Mat<T>::Release()
{
	//引用计数减1,如果引用计数为0,说明没有引用,释放数据
	if ((refCount!=NULL) && ((*refCount)-- == 1))
	{
		Deallocate();
	}

	InitEmpty();

}

定义拷贝赋值和移动赋值

拷贝赋值和移动赋值都属于赋值操作,实际上我们可以将赋值操作看成一次对象创建和对象销毁。
在《Effective C++》(第3版)中的条款11中详细讨论了赋值操作的各种细节以及注意点,这里不再赘述,Mat的设计采用了里面给出的最佳实践copy-swap技术来实现赋值操作。具体实现如下:

template <typename T>
inline void Mat<T>::swap(Mat &dstMat)
{
	using std::swap;
	swap(this->rows,dstMat.rows);
	swap(this->cols,dstMat.cols);
	swap(this->numberOfChannels,dstMat.numberOfChannels);
	swap(this->step,dstMat.step);
	swap(this->data,dstMat.data);
	swap(this->refCount,dstMat.refCount);

}

template <typename T>
inline void swap(Mat<T> &a,Mat<T> &b)
{
	a.swap(b);
}

template <typename T>
inline Mat<T>& Mat<T>::operator = (Mat<T> dstMat)
{
	// copy-swap
	using std::swap;
	swap(*this,dstMat);
	return *this;
}

这个赋值操作可以同时实现拷贝赋值和移动赋值。

Mat<uchar> a(512,512,3);
Mat<uchar> b(512,512,3);
Mat<uchar> c(512,512,3);
a=b;// 拷贝赋值
a=std::move(c);// 移动赋值

当使用a=b的时候,dstMat采用的是拷贝构造,实现了拷贝赋值。当使用a=std::move©的时候,dstMat采用的是移动构造,实现了移动赋值。copy-swap技术其实就是一次构造+一次swap。这里swap函数的实现采用的也是《Effective C++》中的条款25给出的最佳实践:

  1. 首先在类中定义swap成员函数
  2. 然后定义一个非成员函数版本的swap函数
    注意:在使用swap函数的时候要使用using 声明。
    详细的解释参考《Effective C++》。

深拷贝机制

采用编译器默认生成的拷贝控制成员

在平时工作中,只要在设计类的时候选择合适的类型,都可以采用这种模式,比如下面的类:

class A
{
private:
    int a;
    std::string b;
    std::vector<std::string> c;
};

因为成员变量a,b,c都是支持深度拷贝的,所以编译器自动生成的拷贝控制成员就能够正常工作。

手动实现深拷贝

当类中包含有不能实现深拷贝的成员变量(比如指针类型的成员变量)的时候,就需要手动实现深拷贝。比如下面的这个类:

class A
{
public:
    A(const std::string &s = std::string()):a(0),b(new std::string(s)){ }

    // 拷贝构造
    A(const A &s):a(s.a),b(new std::string(*s.b)){ }

    // 拷贝赋值
    A& operator=(const A &dst)
    {
        // 拷贝目标对象中的资源
        auto newp = new string(*dst.b);

        // 删除源对象资源
        delete b;

        // 赋值
        b = newp;
        a = dst.a;
        return *this;
    }
    ~A() { delete b; }
private:
    int a;
    std::string *b;
};

这里仅以拷贝构造和拷贝赋值操作为例说明深度拷贝的概念。这里的赋值操作也可以写成上面Mat类的copy-swap形式,有兴趣的朋友可以自己实现一下。


总结

前面分析了那么多种情况,其实最后你会发现:对于大部分C++类,通过选择合适的数据类型以及智能指针可以直接使用编译器默认生成的拷贝控制成员就可以了,连一个拷贝控制成员都不需要写


2022-1-18 18:21:04

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值