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++对这两个操作符做了严格的行为定义
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[]返回的内存空间可能比期望的要多(需额外保存数组长度等信息)