C语言在内存中一共可分为如下几个区域:
1、栈区(stack): 存放函数的参数值,局部变量的值等,由编译器自动分配释放;
2、堆区(heap) : 通过new和malloc由低到高分配,由delete或free手动释放,若程序员不释放,程序结束时可能由系统回收;
3、全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,程序结束后有系统释放;(C++中全局变量和静态变量无论有没有被初始化都是放在一起的,共同占用同一块内存区);
4、常量区:字符串常量和常变量存储区域,程序结束后由系统释放;
5、程序代码区:存放函数体的二进制代码;
C++11中有三种智能指针,分别是unique_ptr、shared_ptr、weak_ptr。
当超出作用域时unique_ptr会自动释放资源,在一定程度上解决了裸指针使用完没有及时释放导致的内存泄漏问题,同时也解决了指针的浅拷贝问题,但多个unique_ptr不能指向同一资源。
假设有入下代码(错误代码):
#include
#include
int main() {
std::auto_ptr<int> p1(new int(10));
std::auto_ptr<int> p2(p1); // 拷贝构造,p1释放
std::cout << *p1 << std::endl; // 拷贝构造之后还在使用原来的资源,程序崩溃
}
在上面的代码中,p2是通过p1的拷贝构造而来的,但在拷贝过程中对象p1先调用了release函数,而release函数是先将p1的管理对象暂时保存起来,然后把它管理的对象指向nullptr,再用前面保存的临时对象去初始化p2的对象,此时若继续使用p1则程序会崩溃。为了解决这个问题,unique_ptr应运而生。unique_ptr不允许拷贝,若想将unique_ptr的资源交给另一个unique_ptr管理则需使用move()。
#include
#include
int main() {
std::unique_ptr<int> p1(new int(10));
//std::unique_ptr p2(p1);// unique_ptr不允许拷贝构造,编译器报错
std::unique_ptr<int> p2(std::move(p1));//std:move(p1)之后p1的资源就不可用了
std::cout << *p2 << std::endl; //
}
多个unique_ptr不能指向同一资源,但当想多个指针共享一个资源时可以使用shared_ptr。shared_ptr的核心就是引入了一个引用计数,表示现在有多少个对象(shared_ptr)在共享着一份资源(l里面的裸指针),只要还有一个对象在用着这个指针,那就坚决不把这个资源给释放掉。
例如:
#include
#include
template<typename T>
class CSmartPtr;
template<typename T>
class RefCnt
{
private:
T* m_ptr;
int m_count;
public:
friend CSmartPtr<T>;
RefCnt(T* ptr = nullptr) : m_ptr(ptr), m_count(0)
{
if (m_ptr !=nullptr)
{
ref_add();
}
}
// 增加引用计数
void ref_add() {m_count++;}
// 减少引用计数
int ref_reduce() {return --m_count;}
};
template<typename T>
class CSmartPtr
{
private:
T* m_ptr;
RefCnt<T>* m_pRefCnt;
public:
CSmartPtr(T* ptr = nullptr) : m_ptr(ptr)
{
m_pRefCnt = new RefCnt<T>(m_ptr);
}
CSmartPtr(const CSmartPtr<T>& src) : m_ptr(src.m_ptr),m_pRefCnt(src.m_pRefCnt)
{
m_pRefCnt->ref_add();
}
~CSmartPtr()
{
if (m_pRefCnt->ref_reduce() == 0)
{
delete m_pRefCnt;
delete m_ptr;
}
}
CSmartPtr& operator=(const CSmartPtr& rhs)
{
if (this == &rhs)
{
return *this;
}
if (m_pRefCnt->ref_reduce() == 0)
{
delete m_ptr;
}
m_ptr = rhs.m_ptr;
m_pRefCnt = rhs.m_pRefCnt;
m_pRefCnt->ref_add();
return *this;
}
T& operator*() {return *m_ptr;}
T* operator->() {return m_ptr;}
// 返回引用计数
int use_count() {return m_pRefCnt->m_count;}
};
int main()
{
CSmartPtr<int> p1(new int(100));
std::cout << *p1 << std::endl;
std::cout << p1.use_count() << std::endl;
// 测试拷贝构造
{
std::cout << " ========== copy constructor ====== " << std::endl;
CSmartPtr<int> p2(p1);
std::cout << *p2 << std::endl;
std::cout << p1.use_count() << std::endl;
std::cout << p2.use_count() << std::endl;
}
// p2出了{}作用域,p2被释放,引用计数减1
std::cout << " ========== out of scope ============ " << std::endl;
std::cout << p1.use_count() << std::endl;
return 0;
}
注意:此代码没有考虑线程的安全性,其实在标准库中的shared_ptr,里面的计数量count是原子性的,也就是线程安全的。
由上例可知,只有当shared_ptr的引用计数为0了,才进行资源的释放。但是这又导致了交叉引用的问题。比如有如下代码:
#include
#include
class B;
class A {
public:
A() { std::cout << "A()" << std::endl; }
~A() { std::cout << "~A()" << std::endl; }
std::shared_ptr<B> m_sptrB;
};
class B {
public:
B() { std::cout << "B()" << std::endl; }
~B() { std::cout << "~B()" << std::endl; }
std::shared_ptr<A> m_sptrA;
};
int main() {
std::shared_ptr<A> pA(new A);
std::shared_ptr<B> pB(new B);
std::cout << pA.use_count() << std::endl;
std::cout << pB.use_count() << std::endl;
return 0;
}
在上面的代码中,智能指针A的里面有个对象是智能指针B类型,而智能指针B的里面有个对象是智能指针A类型。此时没有交叉引用的运行结果,一切正常。但当存在交叉引用时会造成内存泄漏。比如以下代码:
#include
#include
class B;
class A {
public:
A() { std::cout << "A()" << std::endl; }
~A() { std::cout << "~A()" << std::endl; }
std::shared_ptr<B> m_sptrB;
};
class B {
public:
B() { std::cout << "B()" << std::endl; }
~B() { std::cout << "~B()" << std::endl; }
std::shared_ptr<A> m_sptrA;
};
int main() {
std::shared_ptr<A> pA(new A);
std::shared_ptr<B> pB(new B);
/*交叉引用*/
pA->m_sptrB = pB;
pB->m_sptrA = pA;
std::cout << pA.use_count() << std::endl;
std::cout << pB.use_count() << std::endl;
return 0;
}
上例中,pA中的m_sptrB指向了pB,pB中的m_sptrA指向了pA,这就导致了引用计数额外各增加了1。要析构pA和pB需要先释放m_sptrA和m_sptrB,将引用计数减为0,但是要释放成员变量m_sptrA和m_sptrB就必须先析构pA和pB,这就进入鸡生蛋蛋生鸡的问题了,所以谁也不能先析构。总之,两个堆空间没有释放是因为指向它们的智能指针成员变量没有析构导致引用计数不为0,这个智能指针成员变量没有析构又是因为它们所属的堆对象没有析构,而这两个堆对象没有析构是因为它们被智能指针保管,该智能指针又被指向的堆对象的智能指针成员变量增加了引用计数。
为解决shared_ptr交叉引用造成的内存泄露问题,weak_ptr应运而生。只需把上面的例子中类A和B里面的对象类型从shared_ptr改成weak_ptr即可。但是,新的问题出现了:weak_ptr只是对象的观察者,不拥有对象本身。从实现上看,weak_ptr没有重载 “operator *” 和 “operator ->”,自然无法使用资源了。
#include
#include
class B;
class A {
public:
A() { std::cout << "A()" << std::endl; }
~A() { std::cout << "~A()" << std::endl; }
std::weak_ptr<B> _sptrB;
};
class B {
public:
B() { std::cout << "B()" << std::endl; }
~B() { std::cout << "~B()" << std::endl; }
std::weak_ptr<A> _sptrA;
void testB() { std::cout << "testB" << std::endl; };
};
int main() {
std::shared_ptr<A> pA(new A);
std::shared_ptr<B> pB(new B);
pA->_sptrB = pB;
pB->_sptrA = pA;
pB->testB();
//pA->_sptrB->testB();//weak_ptr无法使用资源,编译器报错
/*对weak_ptr调用lock函数,把资源“锁住,抓在手里”,再来使用资源*/
std::shared_ptr<B> pBTemp = pA->_sptrB.lock();
if (pBTemp != nullptr)
{
pBTemp->testB();
}
pBTemp.reset();// 使用完记得reset释放
std::cout << pA.use_count() << std::endl;
std::cout << pB.use_count() << std::endl;
return 0;
}
这种抓住资源的动作,其实在多线程的环境中非常有用,在使用共享资源的时候,其实共享资源可能已经被释放了,但本线程并不知道。而有了lock函数之后,就可以尝试“抓住资源”,如果返回不是nullptr,那就可以正常的使用资源了。