智能指针/内存泄露/类型转换
1.智能指针的使用及原理
RAII:RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
//RaII它是一种通对象生命周期控制程序资源,构造的时候进行资源分配,析构的时候进行一个资源的释放!
template<class T>
class Smart_ptr
{
public:
Smart_ptr(T*ptr ):_ptr(ptr){}
~Smart_ptr()
{
delete _ptr;
}
//下面就是像指针一样正常使用!
T& operator*()//*被用了以后实际上是调用的重载函数,返回对象所关联的目标值
{
return *_ptr;
}
Smart_ptr(Smart_ptr<T>& pp):_ptr(pp._ptr)//管理权转移!
{
pp._ptr = nullptr;
}
T* operator->()//->被用了它会调用相应的函数,就相当于一个普通指针!
{
return _ptr;//->用*搭配是因为它本来就是用来放回本身的指针
//在c++中当你指向一个自定义类型多数据的时候就要用->,->相当于你对这个地址刚解引用然后再去用.就要可以找到对应的数据了
}
private:
T* _ptr;
};
工作原理
不同类型的智能指针工作原理有所不同:
std::unique_ptr
:通过禁止拷贝构造函数和赋值运算符重载,确保同一时间只有一个std::unique_ptr
指向所管理的内存。当std::unique_ptr
被销毁时,其析构函数会调用delete
释放内存。
//独占资源指针,它是禁止拷贝构造的!
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr) :_ptr(ptr) {}
~unique_ptr()
{
delete _ptr;
}
//下面就是像指针一样正常使用!
T& operator*()//*被用了以后实际上是调用的重载函数,返回对象所关联的目标值
{
return *_ptr;
}
unique_ptr(unique_ptr<T>& pp); //管理权转移!
T* operator->()//->被用了它会调用相应的函数,就相当于一个普通指针!
{
return _ptr;//->用*搭配是因为它本来就是用来放回本身的指针
//在c++中当你指向一个自定义类型多数据的时候就要用->,->相当于你对这个地址刚解引用然后再去用.就要可以找到对应的数据了
}
unique_ptr(const unique_ptr<T>& ptr) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& ptr) = delete;
private:
T* _ptr;
};
std::shared_ptr
:使用引用计数来管理内存的生命周期。在创建std::shared_ptr
时,会分配一个额外的引用计数对象,每当有新的std::shared_ptr
指向该内存时,引用计数加 1;当std::shared_ptr
被销毁时,引用计数减 1。当引用计数为 0 时,释放所管理的内存和引用计数对象。
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr=nullptr) :_ptr(ptr),_pcount(new int(1)) {}
template<class D>
shared_ptr(T* ptr,D del/*=DelArry<T>()*/):_ptr(ptr),_pcount(new int(1)),_del(del){}
shared_ptr(const shared_ptr<T>& sp) :_ptr(sp._ptr), _pcount(sp._pcount)
{
++(*_pcount);
}
T& operator*()//*被用了以后实际上是调用的重载函数,返回对象所关联的目标值
{
return *_ptr;
}
T* operator->()//->被用了它会调用相应的函数,就相当于一个普通指针!
{
return _ptr;//->用*搭配是因为它本来就是用来放回本身的指针
//在c++中当你指向一个自定义类型多数据的时候就要用->,->相当于你对这个地址刚解引用然后再去用.就要可以找到对应的数据了
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)//不允许自己给自己赋值!
{
//这里刚进来需要看是否要进行空间资源的释放,
//这是因为期初sp1指向a,现在sp1=sp2;所以sp1就要断开指向a的线,需要重新连接b
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
int use_count() const
{
return *_pcount;
}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
delete _ptr;//这里执行析构!
}
}
T* get() const
{
return _ptr;
}
template<class D>
struct DelArry//删除器
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
private:
T* _ptr;
int* _pcount;
/*DelArry<T> _deleter;*/
function<void(T*)> _del = [](T* ptr)->void {delete ptr; };//包装器
};
std::weak_ptr
:它是对std::shared_ptr
的弱引用,不增加引用计数。可以通过lock()
方法获取一个std::shared_ptr
,如果所指向的内存已经被释放,lock()
会返回一个空的std::shared_ptr
。
template<class T>
class weak_ptr
{
public:
weak_ptr():_ptr(nullptr){}
weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
T& operator*()//*被用了以后实际上是调用的重载函数,返回对象所关联的目标值
{
return *_ptr;
}
T* operator->()//->被用了它会调用相应的函数,就相当于一个普通指针!
{
return _ptr;//->用*搭配是因为它本来就是用来放回本身的指针
//在c++中当你指向一个自定义类型多数据的时候就要用->,->相当于你对这个地址刚解引用然后再去用.就要可以找到对应的数据了
}
private:
T* _ptr;
};
使用注意事项
- 避免循环引用:在使用
std::shared_ptr
时,要注意避免循环引用问题,否则会导致内存泄漏。可以使用std::weak_ptr
来打破循环引用。
- 不要混合使用普通指针和智能指针:尽量避免将普通指针和智能指针混用,以免造成内存管理混乱。如果需要将普通指针传递给智能指针,可以使用
std::unique_ptr
的构造函数或std::make_shared
等函数。 - 自定义删除器:在某些情况下,可能需要自定义删除器来释放资源,例如使用
new[]
分配的数组需要使用delete[]
释放。可以通过模板参数或构造函数参数来指定自定义删除器。
2. 内存泄漏
什么是内存泄漏,内存泄漏的危害
- 什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
- 内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
void MemoryLeaks()
{
// 1.内存申请了忘记释放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 2.异常安全问题
int* p3 = new int[10];
Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放。
delete[] p3;
}
C/C++ 程序中内存泄漏分类
- 堆内存泄漏(Heap leak):程序执行时通过malloc、calloc、realloc、new等从堆中分配内存,使用后需用free或delete释放。若因设计错误未释放,该内存空间无法再用,产生堆内存泄漏。
- 系统资源泄漏:程序使用系统分配的套接字、文件描述符、管道等资源,未用对应函数释放,造成系统资源浪费,严重时会使系统效能降低、执行不稳定。
如何避免内存泄漏
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记得匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
- 采用 RAII 思想或者智能指针来管理资源。
- 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
- 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总结一下:
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具:valgrind,vld。
3.C 语言中的类型转换
在 C 语言中,如果赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与接收返回值类型不一致时,就需要发生类型转化,C 语言中总共有两种形式的类型转换:隐式类型转换和显式类型转换。
- 隐式类型转化:编译器在编译阶段自动进行,能转就转,不能转就编译失败。
- 显式类型转化:需要用户自己处理。
void Test ()
{
int i = 1;
// 隐式类型转换
double d = i;
printf("%d, %.2f\n", i, d);
int* p = &i;
// 显示的强制类型转换
int address = (int) p;
printf("%x, %d\n", p, address);
}
缺陷:转换的可视性比较差,所有的转换形式都是以一种相同形式书写,难以跟踪错误的转换。
4. C++ 强制类型转换
标准 C++ 为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:static_cast
、reinterpret_cast
、const_cast
、dynamic_cast
void test1()
{
//static_cast<>()//这种适合相近类型的转换,对标c语言的隐式类型转换!
double d = 9.9;
int i=static_cast<int>(d);
//有一定关联但是意义并不相近,用reinterpret_cast<>()
// 例如下面这个有一定关联是因为都是int但是意义不相近一个是指针
int* a = reinterpret_cast<int*>(i);
//const_case<>()//这是可以把const类型转换为不const
//const int c = 9;//一般情况下面编译器会自动优化const,
///编译器会认为const就是不能被修改的变量,所以编译器编译的时候会自动把c规定不可变,
/// 编译器直接把c为9嵌入到代码里面
///所以等你取c的时候其实编译器并没有次次都去从内存中去取。所以才有下面那种情况!
/// //解决上面这种情况就是用关键字volatile,加了这个关键字就是告诉编译器这个代码你不要优化,任何时候你都去内存中去取!
volatile const int c = 9;//
int* f = const_cast<int*>(&c);
*f=2;
cout << c << endl;
cout << *f << endl;
//这种变成1就是这个cout输出流的一个缺陷,它会类型匹配失败,要想正确匹配出来还得手动转换!
cout <<(void*) & c << endl;
cout << f << endl;
char ch = 'c';
cout << (void*) & ch << endl;//这里也有,也得进行转换一下
}
class A
{
public:
A();
virtual ~A();
int _a;
};
class T :public A
{
public:
int _b;
};
void func(A* pa)
{
//T* ptr = (T*)pa;//这样强制转换是不安全的,原因就是:你一旦进行了强制转换就会导致越界,
///因为本来是父类的指针是看不到子类的,你强制转换完了以后就会导致,转换后的指针可以访问到子类的数据!
T* ptr = dynamic_cast<T*>(pa); //是如果你本身是子类可以转换成功,如果你本身是父类它会返回空!
ptr->_a;
ptr->_b;//首先父类转成子类目的就是,想这个指针可以看到子类和父类的成员,
//:父类对象不能直接转换成子类对象,因为父类对象可能不具备子类特有的成员和行为。像代码中func(&a); ,
// 这里传递父类对象指针给func 函数,在函数内部若想通过dynamic_cast转换为子类指针,
}
void test2()
{
A obj1;
T obj2;
obj1 = obj2;//这里不会报错,因为这属于向上转换!编译器是允许向上转换的,其实底层实际就是一个切割赋值
//向下转换的规则:首先就是父类对象不能转换成子类对象!但是父类指针和引用可以转换为子类的指针和引用!
A a;
T t;
func(&a);
func(&t);
}
注意这里:上面代码注释中还提到了cout输出流的缺陷应该也去注意一下!
还要注意这种才是向下转型:
Base* basePtr = new Derived();
// 使用 dynamic_cast 进行向下转型
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
而这种是错误的:
// 创建一个 Derived2 对象,并使用基类指针指向它
Base* basePtr = new Derived2();
// 错误转型:尝试将指向 Derived2 对象的基类指针转换为 Derived1 指针
Derived1* derived1Ptr = dynamic_cast<Derived1*>(basePtr);
5.RTTI(了解)
RTTI:Run - time Type identification 的简称,即:运行时类型识别。
C++ 通过以下方式来支持 RTTI:
typeid
运算符:查看类型dynamic_cast
运算符:向下安全转型,如果不安全会返回空decltype:查看类型,但是与typeid不同的是,它可以带入参数