《effective C++》
会造成内存泄漏的情况:
(检测内存泄漏的函数 _CrtDumpMemoryLeaks(); 需要 #include <crtdbg.h>)
1.“局部销毁” 的情况。(后面说)
2.定义了一个placement new,但是没有对应定义placement delete。
那么如果在placement new 一个类的时候,这个类的构造函数如果抛出了异常,那么编译器会去调用析构,并且调用对应的delete,如果找不到对应的就会出现内存泄露了。
3.逻辑层面:new delete没有成对出现,new[] delete[]没有成对出现
比如在try中new了,在后面各种情况下catch中没有delete
在构造中new了,在析构中没有delete
注意几个点:
1.new 表达式 和 operator new 函数 的区别:
我们不能重新定义new表达式的行为,但是可以重新定义operator new的行为
2.对于new和delete这个主题来说,会遇到的目的有:把内存分配 和 对象构造 分离开来
3.C++中只分配和释放内存的方法
1.allocator类的allocate函数和deallocate函数;
2.operator new 和 operator delete;(跟上面的区别是,他们是在void*指针 而不是类型化的指针上进行操作,具体可以看下面的函数签名,所以使用allocator比使用operator new 和delete 更为类型安全,可以理解位allocator的allocate函数和deallocate函数比较高级)
C++中只在内存中构造和销毁对象的方法
1.allocator类的construct函数和destory函数;
2.狭义的placement new——构造(跟上面allocator类的construct函数的区别是,placement new可以使用任何构造函数,但是construct函数必须使用复制构造函数);直接调用对象的析构函数——销毁(p->~T());
3.算法uninitialized_fill 和 uninitialized_copy 像 fill 和 copy 算法一样执行(不懂);
4.allocator类
stl的vector容器,就是使用allocate函数预先分配内容,然后真正push元素的使用再用construct函数构造元素。
4.一个内存分配基类的实现
template <class T>
class CacheObj{
public:
void* operator new(std::size_t);
void operator delete(void*, std::size_t);
~CacheObj(){};
protected:
T* next;
private:
static void add_to_freelist(T*);
static std::allocator<T> alloc_mem;
static T* freeStore;
static const std::size_t chunk;
};
template <class T>
void* CacheObj<T>::operator new(std::size_t sz)
{
if (sz != sizeof(T))
{
throw std::runtime_error("CacheObj wrong size in operator new");
}
if (!freeStore)
{
T* array = alloc_mem.allocate(chunk);
for(size_t i = 0; i != chunk; ++i)
{
add_to_freelist(&array[i]);
}
}
T* p = freeStore;
freeStore = freeStore->CacheObj<T>::next;
return p;
}
template <class T>
void CacheObj<T>::operator delete(void* p, std::size_t sz)
{
if (p != 0)
{
add_to_freelist(static_cast<T*>(p));
}
}
template <class T>
void CacheObj<T>::add_to_freelist(T* p)
{
p->CacheObj<T>::next = freeStore;
freeStore = p;
}
比如如果想要优化Screen类的内存管理,也就是调用自己内存分配功能,就使用如下定义,也是一种CRTP
class Screen: public CachedObj<Screen>
条款16:使用new和delete的时候要采用相同的形式。
如果你调用new时使用[],你必须在对应调用delete时也使用[]。
如果你调用new时没有使用[],那么也不应该在调用delete时候使用[]。
如下:
std::string* stringPtr1 = new std::string;
std::string* stringPtr2 = new std::string[100];
delete stringPtr1; //删除一个对象
delete[] stringPtr2; //删除一个由对象组成的数组
ps:对于书中的一个例子:(对于typedef 数组的理解和new数组 返回类型的理解,记录一下)
debug下的一些机制
在new的时候会系统有分配链表(CRTdbg.h),_CrtDumMemoryLeaks()函数,类_CrtMemBlockHeader
对应的delete的时候就会做检查,是不是这个地址就经过new分配的
delete 和 delete[] 的区别
总结一下看的一篇文章
为何new出的对象数组必须要用delete[]删除,而普通数组delete和delete[]都一样
文章中验证了 对象数组 用 delete 删除不行(因为对象数组中还有记录一个元素大小的4个字节字段,delete函数内操作的内存中会有4个字节的偏移,导致数据混乱)
但没有回答普通数组为什么可以用delete 删除。
下面是一些总结
1.当 new 一个有析构函数的对象数组的时候 必须要用 delete[]删除
是任何对象数组都必须要用delete[ ]删除么?不是
经过自己的试验,只有 new 有析构函数(不管是程序员显式定义的 还是由编译器创建的nontrival的析构函数《深度探索C++对象模型》,trival的就不算) 的对象的数组才会在内存中占用空间来记录对象个数。
所以,普通数组和没有析构函数的对象数组用delete删除就没事。
那么,为什么需要记录有析构函数的对象数组个数呢,就是为了要依次执行析构函数。几个对象就执行几次析构函数。因为析构函数时nontrival的。
而定义 delete [ ] 的原因,就是为了迎合 c++ 的 class 的析构函数的执行。
2.为什么普通数组释放 delete 和 delete[] 都一样
没有找到delete[] 的具体实现 调试也进不去,猜测下:(以文章中最后的例子为例子)
delete 是默认调用一次析构函数,如果有需要调用的话;
delete[] 是根据 存的数组个数 来决定调用几次析构函数,当然如果没有存对象数组的个数,也就不需要调用析构函数了。
如果是delete p;
不管p是什么 都把指针往前移动多少 8*4个字节,在清空内存之前 会先判断某个值是不是对的。---就像对象数组用了delete 就会出现某个值不对,就会出现断言错误
如果是delete[] p;
判断p是不是有nontrival析构函数的对象数组,
如果是的话就往前移动 9*4个字节,然后将前8*4个字节当作头,将 (数组元素个数+<yout data>)整个数据当成 pUserData,然后再进行析构函数的调用 和 一次性释放内存。
如果不是的话,就调用delete p;----(猜测)这就是普通数组释放内存的情况。’
这些本质上都是在new的时候分配的内存结构有关。
3.如果new[]不需要记录对象大小,而是用pHead中的nDataSize来推算会怎么样
文章中有pHead中的nDataSize成员,记录了元素大小 ,从而可以推出元素个数:
元素对象个数 = pHead->nDataSize / sizeof(T)
但是事实上,new / malloc 在分配内存的时候会 round up 到某个数的倍数(8 或 16 等,跟 malloc 具体实现有关)。也就是说nDataSize并不是等于 sizeof(T)*个数(会大于等于这个数)。根据这样来推算,就有可能多调用析构函数。
条款49:了解new分配不成功时,new-handler的行为
new-handler就是当new不成功的时候,处理函数的行为。
你可以自定义new,也可以不实现就是调用库函数中的实现编译器会把相应的代码编译进去。
set_new_handler :
1)当operator new 无法满足内存分配需求的时候他会抛出异常——bad_alloc异常(异常相关可以看异常那篇文章的最后的C++异常)。
(旧式的编译器的做法是返回null,不抛出异常。)
(也可以调用不抛出异常的new ,比如Widget* pw = new (std::nothrow) Widget。但是这没什么意义,因为调用构造函数的时候如果需要new东西,然后又再调用正常的new(没有std::nothrow),这句话也可能抛出异常)
2)但是在抛出异常以前,如果设定了一个handler,会去调用这个handler,而不是抛出异常了。
怎么设定 handler呢?通过一个 set_new_handler 这个api (声明在<new>中)
函数声明:
条款29
(typedef 一个函数new_handler 的写法)
set_new_handler 的参数 (new_handler p) 就是自己要传入的一个处理函数。而set_new_handler的功能就是把传入的处理函数安装为 new分配不成功的时候的handler,返回的是 安装这个handler之前的那个handler。
在(new_handler p) 自己要传入的一个处理函数,这个函数执行的过程中又进行了new,又分配失败了,而进行new之前又没有进行set_new_handler 的重新设置的话,岂不是进入了无限循环了么?
所以这个处理函数要注意几点:
(可以让程序一开始执行就分配一大块内存,然后当第一次调用new-handler的时候,让以后从这里分配 以避免再new没东西分配了)
1)安装另外一个new-handler 或者 把new-handler置为空(即卸载new-handler)。
2)不返回,直接调用abort 或者 exit 退出。
获取当前的new-handler的方法:
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler );
————先讲new-handling设置为null,然后又立刻恢复原样。对于单线程来说是没问题的,多线程加锁等方法。(涉及到全局变量就要考虑到是单线程还是多线程。)
Widget的专属new-handler的实现
如何让某一个类的new调用自己的 new-handler ,而让其他的 new 还是调用本来的 new-handler 呢
1)这里需要自定类某一个类的 operator new 和 自己的new-handler变量 和 set_new_handler,
Widget::set_new_handler 就是 设置 Widget.currentHandler的值。
2)然后在自定义的 operator new 之前,安装 自己的 new-handler 。完了以后才需要恢复回去 全局的 new-handler。
(把全局的 new-handler 当作一种全局的资源对象进行管理。资源避免复制,接口给别人用的时候别人会用错的。条款14。)
以上就是New-handler的类,构造的时候把原本(全局)的new-handler赋值给成handler,析构的时候自动恢复回去。
什么时候调用 这个类,就是在类的operator new的之前。这样,才退出operator new以后就会调用析构函数,恢复回去了。
类之专属的new-handler的模板的实现
修改上面的NewHandlerHolder:
1)mixin风格的baseclass (mixin风格的代码,容易导致多重继承的问题。条款40。)
(mixin风格:这个base class 用来允许derived classes 继承单一特定能力——这里是继承所需要的set_new_handler 和 operator new 。)
2)然后把这个baseclass 转化为 template。
(这个template 确保每一个derived classes 获得实体互异的class data复件——这里是指static的currentHandler 成员。模版的参数T只是用来区别不同的derived class。Template机制为每一个T(NewHandlerSupport赖以具现化的根据)生成一份currentHandler )
实现一个类中专属的 set_new_handler,这个是用来设置类的当前的currentHandler,返回前一个旧的currentHandler。
而类中专属的new,还是调用std::set_new_handler,因为复原全局的new_handler,而不是类专属的旧的currentHandler。
为Widget添加new-handler实现为:
CRTP:怪异的循环模版模式
条款50:什么时候要重新写operator new 和 operator delete
1.heap坏了——对heap的运用错误进行调试。
发现堆被写坏了。需要检查 后面写坏(overruns)还是前面写坏(underruns)。
在operator new 的时候 在前后加上 签名。然后delete的时候检查一下 签名是不是对着呢。
当然也可以在运行过程中你想检查的时候检查。
2.想自己来分配内存,不想用默认的分配方式——改善效能。
基于对自己的应用程序的动态内存运行形态的了解情况。
定制合适自己的应用程序的内存分配策略。
比如默认的new是多线程安全的,我们程序是单线程的,就可以写个单线程的new就可以了,分配效率会更高。
空间上,也是自己使用了自己的内存分配策略产生的碎片会更小。(集簇行为)
自己分配要注意什么?
1.局部析构的行为
2.继承导致的非本意行为
3.new-handle机制(条款49)
4.placement new/delete 成对出现 条款52
3.想了解应用程序的动态内存的运行形态——收集heap的使用信息。
加一些统计信息,用来分析
1)申请和归还的顺序,先进先出 还是 先进后出。
2)都哪些对象频繁的分配和归还,不同的运行阶段有什么不同,比如一开始 和 开始以后。了解默认的分配规则,好像最近两次的都是同个地方去分配的。
3)最大的new的大小有多大。
如何定制的:
1)买编译器默认的替代品
2)看一些优秀的源码,比如boost库的pool,针对小型对象的分配。
疑问:
考虑到对齐的问题。这里涉及到什么问题呢?
条款51:写自己的operator new 和 operator delete的时候要注意什么
首先分类:
1.member new —— class 专属的 operator new 和 operator delete ——将可能会涉及到基类,派生类有多态的情况。——当编译器看到类类型的new或delete表达式的时候,会查看该类是否有operator new 或者 operator delete成员。
2.no-member new —— 全局的
类的成员new和delete函数
1. 默认就是静态的,要访问也只能访问类的静态数据成员
2.
new是具有返回类型void*,输入参数size_t
delete是具有返回类型void,输入参数可以是只有void*,也可以是void* 和 size_t(如果提供了size_t形参,那么就由编译器用第一个形参所指对象的字节大小自动初始化size_t形参)。
operator new 总体需要注意的:
1.如果分配成功,返回指针。如果分配失败,根据条款49需要调用new-handler,然后再次分配,如果new-handler为0,那么抛出bad_alloc异常。
2.对于0字节内存申请的处理(转化为1字节)。
3.避免覆盖了正常形式的new。
一个例子:
关于基类 operator new 和 operator delete 特别要注意的:
注意 operator new 和 operator delete 也是会继承基类的。
如果只是想让基类,用自己的定制的operator new (delete),可以用大小来判断是不是基类,而让派生类自己生成自己的。
一个例子:
请问,如果想要调用Base::operator delete 是不是应该写成 delete (sizeof(Base)) ptr; 不是,直接写 delete ptr 就会调用到定义得这个delete中。delete 总是这个形式的,他会找合适的函数调用,找不到就报错。
关于 operator delete 要注意的
有一个承诺:
delete 空指针永远安全
——所以你应该在operator delete的时候,(实现的时候 如果对delete进行重写)对传入的指针做判断,当是null的时候,就do nothing, 然后返回即可。
“局部销毁”的问题
Derived* pDerived = new Derived;
Base* pBase = pDerived;
delete pBase;
————如果此时,pBase的析构函数不是虚函数,那么,将会造成局部销毁的问题。也就是并没有完全销毁,不会去调用派生类的析构函数。
所以,如果一个基类是作为多态使用的,作为多态使用的类肯定是有虚函数的,因为虚函数是以客制版来使用的,那么其析构函数一定要定义成虚函数。
(
这里说明一点:
并不是所有的类都是作为基类使用的,标准string和stl容器都不被设计为基类使用,更不用说多态用途了。
并不是所有的基类的设计目的都是为了多态用途的,不需要为了base接口去处理derived对象,因此他们不需要虚函数,更不用说虚析构函数了。)
条款52:placement new 和 placement delete
1.什么叫对应的new 和 delete
就是拥有相同的参数和类型的额外参数的或者不含额外参数的一对 new 和 delete,比如下面的三对:
函数实现只是封装了系统提供的new 和 delete的形式,都是纳入了C++标准的new和delete。都定义在<new>中。
注意一个类里面定义了一个new,就会覆盖其他形式的,如果你想要各种形式都用,那么可以根据就是全写上或者定义一个上述的基类,让自己的类继承自他。然后再在派生类中写自己定义的一种形式的new
2.什么叫placement new
狭义的placement new指的是是包含的额外参数是 void* ptr,也就是上上图的第二种形式。广义的placement new 就是指包含额外的任意的类型的参数,上上
(狭义的placement new,作用是在ptr指的内存中分配内存。)
也可以是其他形式,比如我们可以定义传入一个ostream,用来输出一些分配相关的日志信息的placement new。
void* operator new(std::size_t size, std::ostream& logSteam) throw(std::bad_alloc);
3.为什么定义了placement new,一定要定义对应的placement delete
用上面记日志的那个placement new
假设我们在调用那个placement new ,new 一个类,然后第一步new内存成功了,但是第二布调用这个类的构造函数中抛出异常了,如果没有定义对应的placement delete,系统将无法找到合适的delete,来归还第一步申请的内存。这是系统运行期间自己隐式执行的。
例子: