Chinaunix首页 | 论坛 | 博客
  • 博客访问: 187610
  • 博文数量: 174
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 1800
  • 用 户 组: 普通用户
  • 注册时间: 2018-04-16 06:56
文章分类

全部博文(174)

文章存档

2020年(7)

2019年(29)

2018年(138)

我的朋友

分类: IT业界

2018-08-06 19:41:29

C++(浅析)智能指针及C#GC(垃圾回收机制)分析[图]
c++中我们常常使用运算符new和delete来分配和释放动态内存,然而动态内存的管理非常容易出错 
使用new 和delete 管理内存存在三个常见问题: 
1.忘记delete(释放) 内存。(或者异常导致程序过早退出,没有执行 delete)忘记释放动态内存会导致人们常说的 内存泄露 问题,你申请了内存而为归还给操作系统长时间这样会导致系统内存越来越小。 
(内存泄露问题往往很难查找到,内存耗尽时,才能检测出这种错误) 
2.使用已经释放掉的对象。比如:我们使用delete释放掉申请的内存空间,但并未干掉指向这片空间的指针,此时指针指向的就是“垃圾”内存。 
3.同一块内存释放两次。当有两个指针指向相同的动态内存分配对象时,其中一个进行了delete操作 对象内存就还给了操作系统 ,如果我们要delete第二个指针,那么内存有可能遭到破坏(浅拷贝问题)
为了更容易同时也更安全的使用动态内存标准库为我们提供了 智能指针 来管理动态对象,智能指针的行为类似常规的指针,但是它并非指针,它是一个存储指向动态分配(堆)对象指针的类。 
这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针(释放管理的堆空间)。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放 
因为智能指针行为类似于指针 所以一般的指针行为智能指针也具备(每个智能指针重载了->和 *)
T& opertaor*(){
return *_ptr;   
} //这里返回了一个对象T 我们需要对T进行改动 所以返回T&
T* operator->(){
return _ptr; 
} //这里相当于是返回了一个指针,然后在使用这个指针指向一个内容
话不多说首先让我们来看第一种智能指针auto_ptr(管理权限转让) 
我们经常会遇到这种问题比如说浅拷贝的时候多个指针管理空间 但是当其中一个指针先结束释放了这块空间 ,那么当其他指针结束时在对这片空间进行释放 程序就会崩溃。 
为了方便解决上述问题我就可以使用智能指针auto_ptr 其主要原理是是在构造对象时赋予其管理空间的所有权,在拷贝或赋值中转移空间的所有权 拷贝和赋值后直接将_ptr赋为空,禁止其再次访问原来的内存空间。 
//auto_ptr的简单实现
template
class Autoptr
{
public:
Autoptr(T* ptr=NULL)
:_ptr(ptr)
{}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
Autoptr(Autoptr& ap){ //拷贝构造
this->_ptr = ap._ptr;
ap._ptr = NULL;
}
Autoptr& operator=(Autoptr& ap){
if (this != &ap){
delete this->_ptr;
this->_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~Autoptr(){
if (_ptr){
cout << "释放空间le" << endl;
delete _ptr;
}
}
private:
T* _ptr;
};
int main(){
//Autoptrap1(new int);
//*ap1 = 10;
//Autoptrap2(ap1);
//Autoptrap3(ap2);
//*ap3 = 20;
//ap2 = ap3;
//cout << *ap2 << endl;
Autoptrap1(new int);
*ap1 = 10;
Autoptrap2(ap1);
cout << *ap1 << endl;//调试到这一步程序崩溃了,罪魁祸首就是   AutoPtrap2(ap1),
//这里原因就是ap2完全的夺取了ap1的管理权。
//导致ap1被置为NULL,访问它的时候程序就会崩溃。
system("pause");
return 0;
}
由于它实现了完全的权限转移,所以导致在拷贝构造和赋值之后只有一个指针可以使用,而其他指针都置为NULL,使用很不方便,而且还很容易对NULL指针进行解引用,导致程序崩溃,其危害也是比较大的。 
为了解决 auto_ptr 带来的问题另一种智能指针横空出世—–scoped_ptr(防拷贝) 它的实现原理是直接将拷贝构造和赋值运算符设置为私有或保护只声明不定义 
防止他人在类外定义,这样一次就只有一个指针对空间进行管理就不会出现上面的问题。
scopd_ptr ( unique_ptr )
//Scoped_ptr 独占资源
template
class Scoped_ptr
{
public:
Scoped_ptr(T* _Ptr=NULL)
:_ptr(ptr)
{}
T& operator*(){
return *_ptr;
}
T* operator->(){
return _ptr;
}
~Scoped_ptr()
{
if (_ptr){
delete _ptr;
}
}
private:
Scoped_ptr(const Scoped_ptr&);
//{}
Scoped_ptr& operator=(Scoped_ptr& ap);
private:
T* _ptr;
};
//int main(){
//Scoped_ptr sp1(new int);
//Scoped_ptr sp2(sp1);  不可以进行拷贝构造 此函数设置为私有 不可进行访问
//system("pause");
//return 0;
//}
第三种智能指针shared_ptr (引用计数版本)允许多个指针指向同一对象 
其原理是 通过引用计数记录对当前操作的指针的个数当进行拷贝构造或者赋值时_pCount++, 析构时当_pCount为0时才进行释放空间。 
//简单实现
template
class Shared_ptr{
public:
Shared_ptr(T* ptr = NUll)
:_ptr(ptr)
, _pCount(new int(1))
{}
/*{
if (_ptr){
_pCount = new int(1);
}
}*/
Shared_ptr(const Shared_ptr& sp)
:_ptr(sp._ptr)
,_pCount(sp._pCount)
{
++GetRef();
}
//当进行拷贝或赋值操作时 每个shared_ptr都会有一个计数器 记录着和它指向相同空间的shared_ptr的个数
Shared_ptr& operator=(const Shared_ptr& sp){
if (this != &sp)
{
Release();
_ptr = sp._ptr;
_pCount = sp._pCount;
++GetRef();
}
return *this;
}
T& operator*(){
return *_ptr;
}
T* operator->(){
return _ptr;
}
~Shared_ptr(){
Release();
}//当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类才会自动销毁此对象
int Usecount(){
return GetRef();
}
private:
void Release(){
if (0 == --*_pCount &&_ptr){
delete _ptr;
delete _pCount;
_ptr = NULL;
_pCount = NULL;
}
}
int& GetRef(){
return *_pCount;
}
private:
T* _ptr;
T* _pCount;
};
/*int main(){
Shared_ptr sp1(new int(10));
Shared_ptr sp2(sp1);
*sp1 = 10;
*sp2 = 20;
cout << sp1.Usecount() << endl;
cout << sp2.Usecount() << endl;
Shared_ptr sp3(new int(30));
Shared_ptr sp4(sp3);
sp4 = sp2;
*sp4 = 40;
cout << sp3.Usecount() << endl;
cout << sp4.Usecount() << endl;
system("pause");
return 0;
}*/
看完shared_ptr基本原理 我们在来分析分析它的缺陷(循环引用问题) 
例:
#include
template
struct ListNode{
//ListNode* _next;
shared_ptr> _next;
//ListNode* _prev;
shared_ptr> _prev;
T _data;
ListNode(const T& data = T())
:_next(NULL)
, _prev(NULL)
, _data(data)
{
cout << "LisNode(const T& data)" << this << endl;
}
~ListNode(){
cout << "~ListNode():" << this << endl;
}
};
void Testshared_ptr(){
shared_ptr> p1(new ListNode(10));
shared_ptr> p2(new ListNode(20));
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
p1->_next = p2;
p2->_prev = p1;
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
}
int main(){
Testshared_ptr();
system("pause");
return 0;
}
这里写图片描述
为了解决shared_ptr中的循环引用问题 我们引入shared_ptr的助手指针 weak_ptr 我们来看看代码怎么改
#include
template
struct ListNode{
//ListNode* _next;
weak_ptr> _next;
//shared_ptr> _next;
//ListNode* _prev;
weak_ptr> _prev;
//shared_ptr> _prev;
T _data;
ListNode(const T& data = T())
/*:_next(NULL)
, _prev(NULL)
, _data(data)*/
:_data(data)
{
cout << "LisNode(const T& data):" << this << endl;
}
~ListNode(){
cout << "~ListNode():" << this << endl;
}
};
这里写图片描述
weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。同时,weak_ptr 必须从一个share_ptr或者另一个weak_ptr转换而来,不能使用new 对象进行构造。由于弱引用不更改引用计数,类似普通指针,只要把循环引用的一方使用弱引用,即可解除循环引用。
C# GC(垃圾回收机制):
1,什么是资源:
所谓的资源就是程序中可利用的数据,譬如:字符串、图片和任何二进制数据,包括任何类型的文件。
2,访问资源的步骤:
1)分配内存:分配一定的内存空间。
2)  初始化内存: 一个类型的实例构造器负责这样的初始化工作。
3)使用资源: 通过访问类型成员来使用资源。根据需要会有反复。
4)销毁资源: 执行清理工作。
什么是托管资源,非托管资源:托管资源是由CLR全权负责的资源,CLR不负责的资源位非托管资源。
对于托管资源通过GC自动清理回收。对于非托管资源,通过代码调用手动进行清除,再由GC回收。
如何正确的释放资源:对于非托管的资源,一般就是,Stream(流),数据库的连接,网络连接等的这些操作系统资源,需要我们手动去释放。
Net提供了三种释放方法:Dispose,Close,析构函数(也就是Finalize方法)
第一种:提供Close方法:
介绍:关闭对象资源,在显示调用时被调用。
Close 表示什么意思,它会不会释放资源,完全由类设计者决定。因为Close方法是不存在的。你不写就没有。那为什么要加一个Close方法呢?为了避免不熟悉C#语法的开发人员更直观的释放资源,因此提供了Close方法。提供一个Close方法仅仅是为了更符合其他语言(如C++)的规范。正常情况Close方法里面会调用Dispose()方法。
第二种:Dispose
继承IDisposable接口,实现Dispose方法;
介绍:调用Dispose方法,销毁对象,需要显示调用或者通过using语句,在显示调用或者离开using程序块时被调用。
Dispose方法用于清理对象封装的非托管资源,而不是释放对象的内存,对象的内存依然由垃圾回收器控制。
Dispose方法调用,不但释放该类的非托管资源,还释放了引用的类的非托管资源。
Dispose模式就是一种强制资源清理所要遵守的约定;Dispose模式实现IDisposable接口,从而使得该类型提供一个公有的Dispose方法。
疑问1:Dispose内部到底如何去清理资源的?
第三种:析构函数(也就是Finalize方法)
一个正常情况的类是不会写析构函数的,而一旦一个类写了析构函数,就意味着GC会在不确定的时间调用该类的析构函数,判断该类的资源是否需要释放,然后调用finalize方法,如果重写了finalize方法则调用重写的finalize方法。
Finalize方法的作用是保证.NET对象能在垃圾回收时清除非托管资源。
在.NET中,Object.Finalize()方法是无法重载的,编译器是根据类的析构函数来自动生成Object.Finalize()方法的
finalize由垃圾回收器调用;dispose由对象调用。
finalize无需担心因为没有调用finalize而使非托管资源得不到释放,因为GC会在不确定时间调用,当然,你也可以手动调用finalize方法,而dispose必须手动调用。
finalize虽然无需担心因为没有调用finalize而使非托管资源得不到释放,但因为由垃圾回收器管理,不能保证立即释放非托管资源;而dispose一调用便释放非托管资源。
只有类类型才能重写finalize,而结构不能;类和结构都能实现IDispose.原因请看Finalize()特性。
虽然可以手动释放非托管资源,我们仍然要在析构函数中释放非托管资源,这样才是安全的应用程序。否则如果因为程序员的疏忽忘记了手动释放非托管资源, 那么就会带来灾难性的后果。心得体会,所以说在析构函数中释放非托管资源,是一种补救的措施,至少对于大多数类来说是如此。 
由于析构函数的调用将导致GC对对象回收的效率降低,所以如果已经完成了析构函数该干的事情(例如释放非托管资源),就应当使用SuppressFinalize方法告诉GC不需要再执行某个对象的析构函数。 
析构函数中只能释放非托管资源而不能对任何托管的对象/资源进行操作。因为你无法预测析构函数的运行时机,所以,当析构函数被执行的时候,也许你进行操作的托管资源已经被释放了。这样将导致严重的后果。 
在结构上重写Finalize是不合法的,因为结构是值类型,不在堆上,Finalize是垃圾回收器调用来清理托管堆的,而结构不在堆上。
带有析构函数的类,生命周期会变长。内存空间需要两次垃圾回收才会被释放,导致性能下降。
一个带有析构的类,它引用了很多其他的类,这将导致这些类都升到第1代。(gc有0,1,2三代回收机制)。
5)释放内存:托管堆上的内存由GC全权负责, 值引用的在栈上的内存会随着栈空间的消亡而自动消失。
GC.Collect(); //强制对所有代进行即时垃圾回收
当应用程序代码中某个确定的点上使用的内存量大量减少时,在这种情况下使用 GC.Collect 方法可能比较合适。
疑问2:垃圾回收机制的原理?强制垃圾回收是怎么一回事?
3,释放模式:是一种微软建议的写法,先手动显示去释放资源,如果忘记了,再让finalize释放资源。如果dispose调用了,则析构不会再调用(使用SuppressFinalize方法取消析构函数的调用)。
其他:
如果引用类型对象不再需要,是否需要显式=null;答案是:即使不这样做,GC也会进行垃圾回收。
阅读(1150) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~