第69 技巧:自定义内存管理

本文深入探讨C++中mutable关键字的应用及new/delete操作符的重载技术,包括如何自定义内存管理,限定对象生成个数,形成多例模式,并提供动态数组内存管理的实例。

1、遗失的关键字::mutable

    1.1、mutable关键字
        1.1.1、mutable是为了突破const函数的限制而设计的
        1.1.2、mutable成员变量将永远处于可改变的状态
        1.1.3、mutable在实际的项目开发中被严禁滥用
    1.2、深入分析mutable
        1.2.1、mutable成员变量破坏了只读对象的内部状态
        1.2.2、const成员函数保证只读对象的状态不变性
        1.2.3、mutable成员变量的出现无法保证状态(const失效)不变性
                     编程实现:成员变量的访问次数统计(set,get各算一次)
        
/************       方案1:利用mutable关键字来实现     *********/
#include <iostream>
using namespace std;
class Test
{
    int m_value;
    //普通变量也可以实现,那为什么用mutable
    //mutable关键字,表示该变量是可以被改变的,哪怕这个变量在const函数中,也可以被改变。
    mutable int m_count;    //统计value被使用的次数
public:
    Test(int value = 0)
    {
        m_value = value;
        m_count = 0;
    }
            //const成员函数,里面的成员变量不能被修改了。
    //const成员函数目的是让普通对象和const对象都可以访问
    int getValue() const
    {
        ++m_count;//被mutable修饰,该变量哪怕在const函数中也可被改变!
        return m_value;
    }
    
    void setValue(int value) //这里不能加const  因为m_value的值要被改变,而m_value并没有被声明为mutable。
    {
        ++m_count;
        m_value = value;
    }
    
    //const成员函数目的是让普通对象和const对象都可以访问。
    int getCount() const
    {
        return m_count;
    }
};
int main()
{
    Test t;  //普通对象
    t.setValue(100);    //第一次访问t.m_value,
    
    cout << "t.m_value = " << t.getValue() << endl;  // 第二次访问
    cout << "t.m_count = " << t.getCount() << endl;  //打印是2
    const Test ct;
    cout << "ct.m_value = " << ct.getValue() << endl; //第1次访问ct.m_value
    cout << "ct.m_count = " << ct.getCount() << endl; //ct.m_count = 1;
    return 0;
}


                                    方案二:
              /********     利用指针或者指针常量来实现    ********/
#include <iostream>
using namespace std;
class Test
{
    int m_value;
    
    //m_pCount可以是一般的成员变量,而不一定非得是const成员变量。
    int* const m_pCount;    //常量指针,统计m_value被使用的次数
    
public:
    Test(int value = 0):m_pCount(new int(0))//m_pCount是从堆空间申请来的。值为0
    {
        m_value = value;
    }
    
    //const成员函数目的是让普通对象和const对象都可以访问
    int getValue() const //注意,是const成员函数
    {
        *m_pCount =*m_pCount + 1;//通过改变指针指向的那个数。
                   
        return m_value;
    }
    
    void setValue(int value)
    {
        *m_pCount = *m_pCount + 1;
        m_value = value;
    }
    
    //const成员函数目的是让普通对象和const对象都可以访问
    int getCount() const
    {
        return *m_pCount;   //返回地址指向的值
    }
};        
int main()
{
    Test t; //普通对象
    t.setValue(100); //第1次访问t.m_value    
    
    cout << "t.m_value = " << t.getValue() << endl; //第2次访问t.m_value
    cout << "t.m_count = " << t.getCount() << endl; //t.m_count = 2;
    
    const Test ct(200); //const对象
    
    cout << "ct.m_value = " << ct.getValue() << endl; //第1次访问ct.m_value
    cout << "ct.m_count = " << ct.getCount() << endl; //ct.m_count = 1;
    
    return 0;
}


2、被忽略的事实

    2.1、new/delete关键字,其本质是预定义的操作符,因此支持重载
    2.2、C++对这两个操作符做了严格的行为定义                                                           
                                  面试题(new创建出来的对象位于哪里?)    

    2.3、C++中new/delete操作符重载的两种方式
        2.3.1、全局重载:会影响所有的类(不推荐
        2.3.2、局部重载:针对具体类进行重载
    2.4、重载new/delete的意义:在于改变动态对象创建时的内存分配方式(譬如下面我们要演示在静态存储区创建对象,而不是在堆上,这就需要我们自定义内存管理了。)
    2.5、new/delete的重载示例
 
   //new/delete会被默认的定义为静态成员函数,哪怕在定义时没有显式指出。因为在调用new的时候,对象还没创建出来,(静态成员是从属于类的,不是对象)所以只能通过静态成员函数来调用。静态成员函数通过类名直接调用

//静态成员函数(即使没写static,也会被自动声明为static函数,默认的)
void* operator new (unsigned int size)
{
    void* ret = NULL;
    
    /*ret指向一片刚分配好的内存*/
     
    return ret;
}

//静态成员函数
void operator delete(void* p)
{
    /*释放p所指定的内存空间*/
}


    2.6、注意:
        2.6.1、直接使用new关键字与直接调用Test::operator new(...)函数是有区别的。前者会调用Test类的构造函数,而后者函数调用方式则不会
        2.6.2、同理,直接使用delete和直接Test::operator delete(…)函数调用时,前者会调用析构函数,后者不会

/************     静态存储区中创建动态对象     ***********/
#include <iostream>
using namespace std;
class Test
{
//存储在静态区中Test对象的最大数量,固定对象的数量!
static const unsigned int COUNT = 4;
//用于存储Test对象的静态内存区,注意为static变量。
static char c_buffer[];//内存字节数
//用于标识内存空间的使用情况。按这片内存区能存放几个对象来划分。
static char c_map[];//定义为char的时候,在以前的8位机上节省资源?

public:
    Test()
    {
        cout << "Test()" << endl;
    }

    ~Test()
    {
        cout << "~Test()" << endl;
    }

//重载new操作符,将对象分配在静态区(而非堆上!)直接调用函数类型的new不会调用构造函数初始化变量。
void* operator new (unsigned int size)   //默认为静态函数static的
{
    void* ret = NULL;
    //查找静态区中空闲的内存空间
    for(unsigned int i=0; i<COUNT; i++)
    {
        if(!c_map[i])//空闲时
        {
            c_map[i] = 1;   //标志为正在使用。

            //查找c_buffer这块可用空间的首地址,ret是指针
            //这里就是申请到的起始地址,i*sizeof(Test)就是跳过中间的存储空间。
            ret = c_buffer + i * sizeof(Test);
            cout << "succeed to allocate memory: " << ret << endl;

            break;
        }
    }
    return ret;//当返回后,编译器会自动生成调用构造函数的来初始化这片内存空间的代码
}

//重载delete操作符,将释放对应的内存空间(标志为空闲)
void operator delete(void* p)
{       
    if( p!= NULL)
    {   //指针类型之间,整数和指针类型之间。地址的值都是一样的,就是指向的数解析起来不同。
        char* mem = reinterpret_cast<char*>(p);//将p强制类型转换为char*
        //指针的运算,index是要释放的动态对象在c_map数组中的位置position。
        unsigned int index = (mem - c_buffer) / sizeof(Test);//(对象的起始地址 - 数组的起始地址) / 类实际的字节数 = 要释放的对象在c_map数组中的位置。
        //flag表示位置是固定的,flag不为0表示指针不合法。
        unsigned int flag = (mem - c_buffer) % sizeof(Test);
        //取余操作,这里判断中间相隔的地址,一定是类内存的整数倍。

        //flag==0是保证要释放的内存在c_buffer里面。
        if( (flag == 0) && (0 <= index) && (index < COUNT) )
        {
            c_map[index] = 0;   //标志为空闲,//就是一个标记,然后地址抵消的操作。
        }

        cout << "succeed to free memory: " << p << endl;
    }
    //函数返回后,编译器自动生成调用析构函数的代码。

}
};

//类外面对静态变量进行初始化声明
char Test::c_buffer[sizeof(Test) * Test::COUNT] = {0};//最多4个对象的内存大小。内存字节数,静态内存区。
char Test::c_map[Test::COUNT] = {0};//这个数组默认有四个元素.标志位。这个数组的作用就是标记可用,不可用。

int main()
{
cout << "=====   Test  Single  Object   =====" << endl;

Test* pt = new Test(); // new Test()  相当于(Test*)Test::operator new(sizeof(Test)):并不相当于,因为直接函数类型的调用,不会自动生成调用构造函数初始化的代码

delete pt;//G++下面是先调用构造函数,在释放

cout << "=======   Test Object Array   =======" << endl;
//数组初始化为0,NULL.
Test* pa[10] = {0};  //数组里面存放了10个对象指针。因为Test内部做了限制,只能产生4个对象。

//模拟内存管理,因内存区最多只能分配4个Test对象,当申请第5个对象时(虽然会调用构造函数),但仍会返回NULL,重载new 结合二阶构造可以用来生成《多例模式》
for(int i=0; i<10; i++)
{
    pa[i] = new Test();       //数组里面存放的是对象指针。
    cout << "pa[" << i << "] = " << pa[i] << endl;
}

for(int i=0; i<10; i++)
{
    cout << "delete  =" << pa[i] << endl;
    delete pa[i];
}
return 0;
}


【心得体会】:(自定义内存管理(内存分配在静态存储区),限定对象生成的个数,形成多例模式)
    new的两个主要任务:1.分配内存,2.调用构造函数初始化。而上例子中的operator new函数却只见分配内存,不见初始化工作,这看起来有点不可思议。其实重载的operator new函数确实只需完成前一半的功能,那初始化工作在什么时候实现的呢?答案是当调用new Test时,会先调用operator new,之后编译器自动地为我们插入了初始化的代码。这点也可以从Test* pt = new Test和 Test* pt =  Test::operator new(...)表现出来的行为不同看出来。前者会调用构造函数,而后者不会,只是一个普通的函数调用。可见operator new并未完全地重载new的两个功能,否则这两个调用结果应该是完全一样的。 这也是new操作符与一般的操作符重载的不同之处。

3、在指定地址上创建C++对象的解决方案(面试题)

    3.1、在类中重载new/delete操作符
    3.2、new的操作符重载函数中返回指定的地址
    3.3、delete操作符重载中标记对应的地址可用

/************      自定义动态对象的存储空间      *********/
#include <iostream>
#include <cstdlib>
using namespace std;
class Test
{
    //内存中存储Test对象的最大数量
    static unsigned int c_count;
    //用于存储Test对象的内存区
    static char* c_buffer;      //static变量的内存不属于类中。
    //用于标识内存空间的使用情况
    static char* c_map;
    
    int m_value;                //普通变量的内存属于类
    
public:
    //设置将对象存储在指定的内存地址处,数组首元素首地址,数组的大小byte
    static bool SetMemorySource(char* memory, unsigned int size)
    {
        bool ret = false;
        //动态计算
        c_count = size / sizeof(Test);  //计算对象的个数。数组容量 / 类的大小。传进来的空间能创建几个对象?如果一个对象都不能创建返回false。
        
        //calloc从堆空间申请来后会自动初始化为0; c_map不能为NULL,指向这片空间起始位置。
        ret = ( (memory != NULL) && c_count && (c_map = reinterpret_cast<char*>(calloc(c_count, sizeof(char))))); //char不理解?难道不是按照sizeof(Test)的长度分配c_count个连续区域;//c_map[4]使用情况的数组。有几个对象map就有多大。
    
        if(ret)
        {
            c_buffer = memory;//申请内存的起始地址
        }
        else
        {
            free(c_map);
            c_map = NULL;
            c_buffer = NULL;
            c_count = 0;
        }
        
        return ret;
   }
        //重载new操作符
    void* operator new (unsigned int size)
    {
        void* ret = NULL;
        
        //1.先在指定的地址空间分配对象,对象个数大于0
        if(c_count > 0)
        {
            //查询指定内存是否有空闲的
            for(int i=0; i<c_count; i++)
            {
                if(!c_map[i])//空闲状态
                {
                    c_map[i] = 1;//标记为正在使用
        //这里就是申请到的可用的起始地址,i*sizeof(Test)就是跳过中间(因为中间不是空闲状态)的存储空间。
                    ret = c_buffer + i*sizeof(Test);
                    
                    cout << "succeed to allocate memory: " << ret << endl;
                    
                    break;
                }
                //空闲已满,直接返回NULL
            }
        }
        else
        {
            //当没有调用SetMemorySource来要求在指定的地址上分配对象(默认情况),此时直接在堆上创建对象。
            ret = malloc(size);
            cout << "allocate in Heap memory: " << ret << endl;
            
        }
        
        return ret;
    }
    
    //重载delete操作符
    void operator delete (void* p)
    {
        if (p!=NULL)
        {
            //在指定地址分配对象
            if( c_count>0 )
            {
                char* mem = reinterpret_cast<char*>(p);
                //(对象的起始地址 - 数组的起始地址) / 类实际的字节数 = 要释放的对象在c_map数组中的位置。
                int index = (mem - c_buffer) / sizeof(Test);//index表示要释放的对象在c_map数组中的位置
                int flag = (mem - c_buffer) % sizeof(Test);
                //取余操作,这里判断中间相隔的地址,一定是类内存的整数倍。flag不为0表示不合法。
                if( (flag == 0) && (0<=index) && (index < c_count) )
                {
                    c_map[index] = 0;   //标记为空闲
                    
                    cout << "succeed to free memory: " << p << endl;
                }
            }
            else
            {
                //默认是在堆空间上创建对象
                free(p);
                cout << "free Heap memory: " << p << endl;
            }
        }
    }
};
unsigned int Test::c_count = 0;
char* Test::c_buffer = NULL;
char* Test::c_map = NULL;

int main()
{
    cout << "sizeof(Test) = " << sizeof(Test) << endl; //这个类4字节。
    
    //默认行为
    cout << "====  Default  Behavior  ===== " << endl;
    Test* pt = new Test;    //默认是在堆上创建的
    delete pt;
    
    //指定new出来的对象分配在栈上。
    char buffer[12] = {0};            //动态分配在栈上。
    Test::SetMemorySource(buffer, sizeof(buffer)); //指定在栈上分配对象(最多3个对象)
    cout << "====   Test  Single   Object  ======" << endl;
    pt = new Test;  //  分配在栈上
    delete pt;
    
    cout << "==== Test Object Array======" << endl;
    Test* pa[5] = {0};
    
    //分配5个对象,但只有前3个(因为最多只有三个对象)会成功,后面两个返回NULL
    for(int i=0; i<5; i++)
    {
        pa[i] = new Test;//重载new,调用类的成员方法
        cout << "pa[" << i << "] = " << pa[i] << endl;
    }
    
    for(int i=0; i<5; i++)
    {
        cout << "delete " << pa[i] << endl;
        delete pa[i];
    }
    return 0;
}

4、new[]和delete[]操作符

    4.1、new[]/delete[]与new/delete完全不同。可以将new[]和new看作是两个完全不同的的操作符。同理delete[]和delete也是两个完全不同的操作符
        4.1.1、动态对象数组的创建通过new[]完成
        4.1.2、动态对象数组的销毁通过delete[]完成
    4.2、new[]/delete[]能够被重载,进而改变内存管理方式如在堆或静态区中分配内存
    4.3、new[]和delete[]的重载示例

//静态成员函数
void* operator new[] (unsigned int size)
{
    return malloc(size);
}
//静态成员函数
void operator delete[] (void* p)
{
    free(p);
}

    4.4、注意事项
        4.4.1、new[]实际需要返回的内存空间可能比期望的要多(因为需额外保存数组长度等信息)
        4.4.2、对象数组占用的内存中需要保存数组信息
        4.4.3、数组信息用于确定构造函数和析构函数的调用次数
        4.4.4、直接使用new[]关键字与直接使用Test::operator new[](...)函数是有区别的。前者会调用Test类的构造函数,而后者函数调用方式则不会
        4.4.5、同理,直接使用delete[]和直接Test::operator delete[](…)函数调用时,前者会调用析构函数,后者不会

/**************      动态数组的内存管理     ************/
#include <iostream>
#include <cstdlib>  //for  malloc函数
using namespace std;
class Test
{
    int m_value;
public:
    Test()
    {
        m_value = 0;
        cout << "Test() " << endl;
    }
    ~Test()
    {
        cout << "~Test()" << endl;
    }
    
    //重载new操作符
    void* operator new (unsigned int size)
    {
        void* ret = NULL;
        ret = malloc(size);
        cout << "operator new = " << ret << "size = " << size << endl;
        return ret;//编译器自动添加调用构造函数的代码。
    }
    
    void operator delete (void* p)
    {
        cout << "operator delete = " << p << endl;
        free(p);
    }
    
    //重载new[]操作符
    void* operator new[] (unsigned int size)
    {
        void* ret = NULL;
        ret = malloc(size);
        cout <<"operator new[]: " << ret << " size: " <<size << endl;
        return ret;    //编译器自动添加调用构造函数的代码。
    }
    
    //重载delete[]操作符
    void operator delete[] (void* p)
    {
        cout <<"operator delete[]: " << p << endl;
        free(p);
    }    
    
};
int main()
{
    Test* pt = NULL;
    pt = new Test;//注意使用new关键字与直接使用Test::operator new(...)函数是有区别的。//new Test会调用Test的构造函数,而函数调用方式则不会。
    delete pt;
    
    //当用new Test[5]时,只须传入数组元素的个数,编译器会向operator new[](...)函数中
    //传入的参数为5*sizeof(Test) + sizeof(unsigned int),其中的sizeof(unsigned int)为额外
    //空间,用于保存元素的个数。同理new Test[5]与直接使用Test::operator new[](...)是有区别的。
    //前者会调用构造函数,后者则不会。
    pt = new Test[5];//相当于Test::operator new(5*sizeof(Test) + sizeof(unsigned int))
    delete[] pt;
    
    return 0;
}

5、 小结

    5.1、new/delete的本质为操作符
    5.2、可以通过全局函数重载new/delete(不推荐)
    5.3、可以针对具体的类重载new/delete
    5.4、new[]/delete[]与new/delete完全不同
    5.5、new[]/delete[]也是可以被重载的操作符
    5.6、new[]返回的内存空间可能比期望的要多(需额外保存数组长度等信息)







































































































































































































评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值