都说 C++ 没有 GC,RAII: 那么我算个啥?(赠书福利)
Posted ENG八戒
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了都说 C++ 没有 GC,RAII: 那么我算个啥?(赠书福利)相关的知识,希望对你有一定的参考价值。
*以下内容为本人的学习笔记,如需要转载,请声明原文链接 微信公众号「ENG八戒」https://mp.weixin.qq.com/s/7A9-tGZxf4w_7eZl3OUQ4A
学过 Java、C# 或者其他托管语言(managed languages)的同学,回过头来看 C++ 的时候,第一反应就是 C++ 没有自动垃圾回收器(GC),而不能充分利用的资源被称为垃圾。
那么 C++ 真的不能自动回收垃圾吗?带着这个疑问我们来看看一般 C++ 程序都是怎样回收资源的。
内存在计算机系统中是有限的资源,通常申请内存和释放内存是这样子的,假设有个被调用的函数 function():
void function()
int *p = new int; // 申请内存
// 资源申请下来了,不玩有个 p 用?
// do something
delete p; // 释放内存
这段示例代码在 function() 函数开始的时候申请了一块内存,大小对应于 int 类型,然后在函数结束的时候释放它。通常来说,这看起来很OK,没毛病,但是,如果遇到了下面几种情况呢?
- 程序如果中途有逻辑让它提前退出 function() 函数
- 发生了异常而没有被捕获到
那么在函数尾部执行释放内存的动作有几率不会被执行,意味着发生也会内存泄漏。像上面这段代码,如果调用的次数不多也不碍事,不过,如果循环调用 function(),这时泄露的内存资源会不断累积,而且一直被浪费掉,期间系统无法再次使用这些被浪费的内存,直到进程被终止,严重的话,会导致系统资源被耗尽,跑着跑着系统都崩溃了。这种 bug 在 C 范式的编程语言中真的很常见。
RAII 是什么
众所周知 C++ 具有面向对象的特性,在初始化类对象的时候,系统会调用类构造函数。如果类对象是存放在栈空间的话,比如声明为局部变量,那么当类对象超出生命周期时,比如退出局部变量的作用域,系统会调用这个对象的类析构函数;如果类对象是存放在堆空间的话,比如通过 new 操作符创建的类对象,那么当类对象被销毁时,比如对对象执行 delete 操作,系统同样会调用类析构函数。
C++ 的这个特性可以用来解决上面提到的资源泄露问题,怎么利用呢?
modern C++ 实践建议优先把资源存放在栈上。如果只是个变量类型,完全可以用局部变量的形式定义声明,这样代码块在退出后系统自动回收栈上的资源。
对上面的函数 function() 修改
void function()
// 声明定义为局部变量,资源存储在栈区
int data = 0;
// do something with data
// 函数退出时,自动释放 data 占用的空间
当资源比较占空间时,需要在堆上分配资源,可以通过指针引用它,资源的申请放在类的构造函数里,然后在析构函数里释放。下面举个例子
class Helper
private:
int* data;
public:
Helper()
data = new int; // 在堆上申请内存
~Helper()
delete data; // 释放堆上申请的内存
void do_something_with_data()
;
void function()
// 声明定义为局部变量,对象存储在栈区
// 调用 Helper 类构造函数在堆上申请资源
Helper help;
// 通过对象 help 调用成员 data
// 如果 data 是 Helper 私有成员
// 在类外面必须通过类成员方法调用 data
help.do_something_with_data();
// 函数退出时,自动释放 help 对象占用的栈空间
// 就算发生了异常或者中途退出都会执行这一步
// help 对象被销毁时,调用 Helper 类析构函数
// Helper 类析构函数释放已申请的堆上资源
利用这种特性的行为被 C++ 发明人称呼为 RAII,英文全称是「resource acquisition is initialization」,中文翻译过来是「资源获取即是初始化」。而我喜欢把它叫做上下文管理,实现资源申请释放的类叫做上下文管理器(context manager)。
经典实践--智能指针
上面的示例代码写起来略显啰嗦,为了推广这种设计核心思路和简化代码编写,在 C++ 11 之后标准库里添加了 unique_ptr。
unique_ptr 属于 Smart Points 中的一种,Smart Points 在国内通常翻译为「智能指针」。智能指针负责管理和释放资源。上面的 function() 函数可以改成这样子
#include <memory>
void function()
// 实例化智能指针对象,输入需要被管理的内存首地址
// 对象为局部变量,存储在栈区
std::unique_ptr<int> data(new int);
// 智能指针对象就像普通指针一样调用
printf("data=%d\\n", *data);
// 函数退出时,自动释放 data 对象占用的栈空间
// 就算发生了异常或者中途退出都会执行这一步
// data 对象被销毁时,同步释放被管理的内存资源
可见,用了智能指针后,不需要像之前那样定义类 Helper (上下文管理器)了,代码清爽很多。
不过,上面的示例代码中有个地方需要注意,在实例化智能指针对象时必须传入内存地址,有没有其它更好的方式设置被管理的内存地址?
有的,C++ 14 之后标准库添加了 make_unique,演示一下怎么用
std::unique_ptr<int> data = std::make_unique<int>();
荐书活动
编程的设计思想是一门很有意思的事情,其中有一门前人总结得很到位的学问叫「设计模式」,想深入了解吗?
最近在联合机械工业出版社搞荐书活动,这次参与活动的图书是《深入理解设计模式》,作者是林祥纤。
其中有几本样书,八戒 想送给读者朋友,需要免费领取图书的朋友可以点击文章抬头的原文链接!
图书简介:
本书以作者与虚拟女友(小璐)在生活中遇到的各种问题作为主线,引出设计模式的各种功能、用途,以及解决方法,系统介绍了23种设计模式,根据具体的实例形象化、具体化地进行了代码的编写和详细讲解,让那些本来对设计模式不太了解、一知半解、只有概念的读者,彻底了解和掌握常用的设计模式使用场景及使用方式,并掌握每个设计模式的UML结构和描绘方式。
本书共23章,包括认识设计模式、单例模式、工厂模式、建造者模式、原型模式、适配器模式、装饰器模式、外观模式、桥接模式、组合模式、享元模式、代理模式、策略模式、命令模式、状态模式、模板方法模式、备忘录模式、中介者模式、观察者模式、迭代器模式、责任链模式、访问者模式、解释器模式。
通过以上的知识,让你从模式小白直接升级为模式大神!本书所需源代码,均可通过本书配套下载链接获得。 本书适合编程初学者或希望在面向对象编程上有所提高的开发人员阅读。
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
下面重点说一下两个部分:
- 定义拷贝构造,移动构造和析构函数
- 定义拷贝赋值和移动赋值
定义拷贝构造,移动构造和析构函数
为什么将这三个放在一起说呢,因为这三个都可以归结于对象的构造和销毁。
下面看一下拷贝构造的实现:
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给出的最佳实践:
- 首先在类中定义swap成员函数
- 然后定义一个非成员函数版本的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
以上是关于都说 C++ 没有 GC,RAII: 那么我算个啥?(赠书福利)的主要内容,如果未能解决你的问题,请参考以下文章