动态内存分配
内存分配失败问题
在动态内存分配中,内存分配失败是有可能的。而通常的解决办法是if(pointer != null)
,避免其他函数访问空指针。不过,若是内存分配失败,如何处理。而若在设计程序时,分配的这块内存又是整个应用程序必须的,那么我们就不得不考虑内存分配失败后,程序如何处理。请看下面代码:
int main()
{
int* array = (int*)malloc(1024*sizeof(int));
if(array != nullptr)
//code...
free(array);
return 0;
}
在分配int数组时,有可能失败。而在失败后,对于变量array的操作都将会无效。也就是说,对于上面代码来说,一旦内存分配失败,那么整个进程直接结束。为了避免类似状况发生,就需要考虑内存分配失败后的处理。首先,能够想到的方法是再次分配,那么以上述代码,下述代码为:
int* create(size)
{
int* array = (int*)malloc(size*sizeof(int));
if(array == nullptr);
create(size);
return array;
}
这段代码是可以解决问题,不过递归函数一旦陷入死循环会消耗大量内存,因此修改代码:
int* create(int size)
{
int* array = (int*)malloc(size*sizeof(int));
return array;
}
int* create_(int size)
{
int* array = nullptr;
while(array == nullptr)
{
array = create(size);
}
return array;
}
这段代码执行时间不确定,需要修改代码:
int count = 0;
int all = 0;
int* create_(int size,int running)
{
int* array = nullptr;
int c = 0;
while((array == nullptr) && (running>0))
{
array = create(size);
++c;
--running;
}
count = c;
all += c;
return array;
}
再次分配方法就笔者目前的知识水平想到的也只有这里了。另外,笔者给出另外一条思路,那就是判断程序所申请的连续内存空间有没有,没有就发送请求,然后线程阻塞等内存空间准备好。这个思路涉及到线程编程,就不给出代码了。
使用动态分配的内存问题
对于使用动态内存分配的函数,为了避免访问到空指针,同样有if(pointer != nullptr)
。只是这样的方法代码量显然增加,即每传入的指针都要保证不能是空指针访问。那么,能不能只做一次判断是不是指向空指针,而多次使用呢。
我们首先所能想到的是,指向指针的指针,即二级指针:typename**
。看下面代码:
void fun(char* data)
{
std::cout<<data<<'\0';
}
int main()
{
char a = 'a';
fun(&a);
return 0;
}
由地址运算符&可以保证传入指针不是空指针,因此我们只需要保证指针所指向的内容不为空。接上一段例子,动态分配初始化函数实现:
void init(int** array,int lenght);
{
for(int index = 0;index < lenght;++index)
*array[index] = 0;
}
这个函数将访问检查交给了调用函数,即是说,在调用函数中:
void fun(int* s)
{
if(s != nullptr)
int* p = s;
init(&p);
}
类似的,查询,添加,删除,排序,复制等等数据操作函数都可以这样实现。
生命周期问题
动态分配内存由于是用户自己创建,也由用户自己释放,所以不可避免需要管理每一块动态分配的内存。而在程序中,很容易出现重复释放,尝试访问已释放内存空间(主要是全局指针)。因此,需要将申请的每一块内存都集中管理。
要实现管理,需要将申请的每块内存的指针保存下来:
typename* name[];
这是采用指针数组保存,我们需要两个指针数组用以保存还在使用,以及已释放内存的指针:
typename* usingPointer[];
typename* usedPointer[];
其次,已经使用过的内存块不着急释放,这是为了程序再次申请时,恰巧内存块在已使用内存块大小之内,就可以从used 转入using,再使用指针。不过,针对这个功能,采用数组的方式不合适了,需要链表:
struct list
{
int flog;
struct list* next;
typename* memory;
}*usingM,usedM;
这里有个问题,那就是这种方法极易产生内存碎片,且需考虑内存不足问题。因此,需要对内存进行释放。有3个步骤,其一,最大内存释放,即定义一个程序总内存申请最大数,using+used;其二,引用计数,设置各块内存引用计数,分配内存;其三,引入定时触发器,每到一定时间进行内存清除。综上所述,其结构为:
unsigned long long time;//计时
int max;
struct Memory
{
struct list* next;
int sign;//是否正在使用
enum type;//类型
int count;//引用计数,用于函数主动释放
void* memory;
};
这里,有个问题,程序没有主动释放,就会造成内存难以申请,所以需要添加代码:
class M
{
public:
struct list* l;
M();
~M();
}
通过类的构造与构析函数自动修改引用计数与标志,其中,构造函数修改标识与计数,构析函数修改标志为used,而当每次进行内存清理时,将used标志的引用计数减一。这样,内存块的生命周期就出来了,同时,因为局部作用域的特性,可以使内存块自动转入used,接着再根据引用计数,与基础生命周期,可以灵活设置内存块的生命周期:
class M
{
public:
static int baseCount;//通过基础生命周期,可以延长内存块的使用时间
struct list* l;
M();
~M();
}
此外,若要实现类似java的自动回收机制,可以小小改变一下,M类构造、构析函数只改变标志位,而每次清理内存时将所有内存块的计数加一,接着根据划分的生命周期对used进行清理。根据静态变量的特性,M类在整个程序结束之前是不会调用构析函数,因此M类可以兼顾全局使用的需求。