65.9K
CodeProject 正在变化。 阅读更多。
Home

使用模板和类级别的 new/delete 重载在 C++ 中代理对象分配

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (12投票s)

2011 年 9 月 2 日

CPOL

5分钟阅读

viewsIcon

23472

一种使用模板和类级别的 new 和 delete 重载来处理对象分配的技术。

引言

当使用 C++ 等允许您访问操作系统内存的语言时,您几乎肯定会遇到标准内存模型不适合您需求的情况。我想在这里描述一种技术,在您需要更精细地控制对象分配时可能会派上用场。

在我的特定情况下,我需要在静态内存区域(即预先创建的数组)中创建我的对象。原因是服务器应用程序设计得很糟糕,它依赖于动态对象创建,这导致了内存碎片问题。我无法对现有代码进行太多更改,因为这会导致大量的重新测试,而我们没有足够的时间或资源,因此我选择了一种侵入性较小的方法。这就是我想在这篇文章中描述的解决方案。

类级别的 newdelete 重载

正如文章标题所述,我使用了模板和类级别的 new/delete 重载来实现此目的。让我们暂时看看 C++ 如何执行对象分配。其核心是,C++ 运行时在调用 newdelete 时,使用 mallocfree 的某种变体向操作系统请求内存。由于我们可以在类级别重载这些运算符,因此我们可以控制特定类的对象的分配过程。了解这一点后,我们就可以实现一个允许我们拦截内存分配并自行管理的解决方案。

静态成员问题

在类级别简单地重载 newdelete 的一个关键方面是,这些方法是隐式静态的,因此,如果我们想通过继承重用我们的内存分配类在另一组不相关的类中,我们可能会调用相同的内存区域,这很可能不是我们想要的。我称之为静态成员问题。

让我们看一个例子。

上方的幻灯片展示了一个可能的场景,其中实现了必要内存管理例程的基类被两个不一定相关的子类继承。问题在于,由于 newdelete 是隐式静态的,它们访问的任何成员也必须是静态的。在我们的例子中,如果内存管理器类使用某个成员作为 内存存储,那么该成员反过来也必须是静态的,这将导致任何继承自我们的内存管理器类的类也将使用相同的成员作为其存储 - 这不一定是我们想要的。

模板和静态成员问题

如上所述,仅仅在类级别重载 newdelete 最多只能提供一个临时的解决方案,因为存在上述的静态成员限制。但是,有一种方法可以创建这个解决方案的通用版本 - 我们可以使用模板来克服这个限制。

C++ 模板是类的规范,是一种原型类。模板规定了类的意图以及类实现的算法,而不强制规定这些算法将操作的具体类型。模板是完全成熟的编译和运行时实体 - 在编译时,编译器可以检测到与不正确的类型操作相关的错误,因为它能够在模板类实例化时解析类型。编译器为每种参数组合生成具体的实例,到链接器处理代码时,它已经处理的是具体的类,就像您在代码中手动定义它们一样。我们还说它们是运行时实体,因为到代码执行时,内存中创建的对象是您使用特定模板参数组合实例化模板后生成的类型的对象。

换句话说,每次您在代码中用一组不同的模板参数实例化模板时,编译器都会根据该实例化生成一个具体的类,因此在运行时,特定模板实例的每个对象实例化都是一个完全不同类型的对象。

将模板与 newdelete 重载结合使用

我们知道静态成员是类级别的实体,这意味着一个静态成员在特定类型的所有对象之间共享。由于我们为每种模板参数组合生成不同的类类型,我们实际上消除了静态成员问题。我们可以有效地使用模板为每个需要使用它的类创建一个内存管理器类的不同实例。让我们看一个可能的实现

请注意,上面的图形中使用了 CAllocationProxy 模板,它用于将调用路由到内存管理器类。

这是 CAllocationProxy 类的代码

#ifndef ALLOCATION_PROXY_H
#define ALLOCATION_PROXY_H

#include <new>

template < typename TPool, typename TClass >
class CAllocationProxy
{
public:
    CAllocationProxy()
    {
    };

    virtual
    ~CAllocationProxy()
    {
    };

    static void
    InitProxyInstance()
    {
        // Perform required initialization here
    };

    static 
    void* operator new ( size_t nSize )
    {
        void* p = NULL;
        // perform required allocation calls here        
        return p;
    };

    static 
    void operator delete ( void *p, size_t nSize ) throw()
    {
        if ( !p )
        {
            return; // do nothing on null pointer
        }

        // perform dealocation here
    };

private:
    static 
    TPool    m_Pool;
};

template< typename TPool, typename TClass >
TPool CAllocationProxy<TPool,TClass>::m_Pool;
#endif // ALLOCATION_PROXY_H

上述类接受两个模板参数,TPoolTClass

  • TPool 是我们的内存分配器 - 我们在前面几节中讨论过的静态成员。传入的类型本身在这里并不重要,但我最终编写了自己的内存管理器,并将其作为参数传递。
  • TClass 是将由内存管理器分配的对象类型。它在上面的代码中未使用,但可以用来将要分配对象的大小传递给内存管理器。

其余部分不言自明。

让我们看看这段代码如何使用

伪代码
// declare a class A to use the allocation proxy class
class A : public  CAllocationProxy<MemoryManager, A> {
  // implement class...
}

// using class A

// initialize proxy instance...
A::InitProxyInstance();

// all allocations are now done by the proxy... 
A myA = new A(); 
 ....
// deletion is performed by the proxy as well...
delete myA;

结论

我希望这项技术能对和我一样情况的任何人有所帮助 - 即其他解决方案带来的更改量不可接受。但是,我也相信这并不是使用这种特定方法的唯一原因。我认为它比其他更传统的方法(例如放置 new 或某种静态创建方法)提供了更多的代码控制,这些方法可能可以达到类似的效果。除此之外,这将允许您在许多不同的对象和内存管理器之间重用代码。

需要注意一点,请小心类级别的 newdelete 宏重定义。许多运行时和框架会用宏重定义 newdelete,以拦截这些调用,从而提供更多控制并增强错误报告/捕获 - 通常这只在调试版本中进行。要解决此问题,请使用类似以下代码的代码

// undef any new macros
#ifdef new
#define __new new
#undef new
#endif

// undef any delete macros
#ifdef delete
#define __delete delete
#undef delete
#endif 
....
// rest of code here
 ...
// restore new macros
#ifdef __new
#define new __new
#undef __new
#endif

// restore delete macros
#ifdef __delete
#define delete __delete
#undef __delete
#endif

感谢阅读!

© . All rights reserved.