20. 内存管理
一、内存的组织方式
程序员编写完程序之后,程序要先加载在计算机的内存中,再运行程序。在 C 语言中,不同数据在内存中所存储的位置也不一样。全局变量存储在内存中的静态存储区,非静态的局部变量存储在内存中的动态存储区(栈)。临时使用的数据建立动态内存分配区域,需要的时候开辟,不需要时及时释放(堆)。
通过内存注释方式可以看出,堆用来存放动态分配内存空间,而栈用来存放局部数据变量、函数的参数以及调用函数与被调函数的联系。
在内存的全局存储空间中,用于程序动态分配和释放的内存块称为自由存储空间,通常也称之为堆。在 C 程序中,使用 malloc() 函数和 free() 函数来从堆中动态的分配内存和释放内存。
程序不会像处理堆那样在栈中显示地分配内存。当程序调用函数或声明局部变量时,系统将自动分配内存。
栈是一个后进先出的压入弹出式的数据结构。在程序运行时,需要每次向栈中压入一个对象,然后栈指针向下移动一个位置。当系统从栈中弹出一个对象时,最晚进栈的对象将被弹出,然后栈指针向上移动一个位置。如果栈指针位于栈顶,则表示栈是空的;如果栈指针指向最下面的数据项的后一个位置,则表示栈为满的。
二、动态内存管理
在头文件 stdlib.h 中声明了四个有关内存动态分配的函数。
2.1、malloc()函数
malloc() 函数的原型如下:
void* malloc( size_t size );
该函数的作用是在内存中动态分配一个 size 大小的内存空间。malloc() 函数会返回一个指针,该指针指向分配的内存空间的第一个地址,如果出现错误,则返回 NULL。
使用 malloc() 函数分配的内存空间是在堆中,而不是在栈中。因此在使用完这块内存中间之后一定要将其释放掉,释放内存空间使用的是 free() 函数。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p;
int i = 0;
p = (int*)malloc(5*sizeof(int));
printf("请输入5个整数:\n");
for(i = 0;i < 5; i++)
{
scanf("%d",p+i);
}
printf("你输入的数据是:\n");
for(i = 0; i < 5; i++)
{
printf("%d ",*(p+i));
}
free(p);
return 0;
}
2.2、calloc()函数
calloc() 函数的原型如下:
void* calloc( size_t num, size_t size );
该函数的作用是在内存中动态分配 num 个长度为 size 的连续内存空间数组,并将该内存中间的字节初始化为 0。calloc() 函数会返回一个指针,该指针指向动态分配的连续内存空间的起始地址。当分配内存空间错误是返回 NULL。
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *str;
str = (char*)calloc(30,sizeof(char));
printf("请输入一个字符串:\n");
gets(str);
printf("你输入的字符串为:\n");
printf("%s\n",str);
free(str);
return 0;
}
2.3、realloc()函数
realloc() 函数的原型如下:
void *realloc( void *ptr, size_t new_size );
该函数的作用是重新分配给定的内存区域。它重新分配 malloc() 函数 或 calloc() 函数 获得的动态空间大小。如果 ptr 的值为为空指针,则会分配一个新的内存块,且函数返回一个指向它的指针。如果 ptr 不为空,它先判断当前的指针是否有足够的连续空间,如果有,扩大 ptr 指向的地址,并且将 ptr 的值返回,如果空间不够,先按照 newsize 指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来 ptr 所指内存区域(注意:原来指针是自动释放,不需要使用 free() 函数),同时返回新分配的内存区域的首地址。即重新分配存储器块的地址。设定上 new_size 大小是任意的,也就是说既可以比原来的数值大,也可以比原来的数值小。该函数的返回值是一个指向新地址的指针,如果出现错误,则返回 NULL。
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *str;
str = (char*)malloc(30*sizeof(char));
str = realloc(str,15*sizeof(char));
free(str);
return 0;
}
2.4、free()函数
free() 函数的原型如下:
void free( void* ptr );
该函数的作用是释放指针 ptr 指向的内存区,使部分内存区能被其它变量使用。ptr 是最近一次调用 calloc() 函数 或 malloc() 函数时的返回值。free() 函数所用的指针变量可以与 malloc() 函数 或 calloc() 函数不同,但是两个指针必须储存相同的地址。并且,不能释放同一个内存两次。内存块可以在一个函数中创建,在另一个函数中销毁。
三、动态分配内存的基本原则
- 避免分配大量的小内存块。分配堆上的内存有一些系统开销,所以分配许多小的内存块比分配几个大的内存块的系统开销大;
- 仅在需要时分配内存。只要使用完堆上的内存块,就需要及时释放它(如果使用动态分配内存,需要遵循谁分配,谁释放原则),否则可能出现内存泄露;
- 总是确保释放以分配过的内存。在编写分配内存的代码时,就需要确定在代码的什么地方释放内存;
- 在释放内存之前,确保不会无意中覆盖堆上已分配的内存地址,否则程序就会出现内存泄漏。在循环分配内存时,要特别小心。
四、有关内存的函数
在头文件中 string.h 中,提供了几个有关内存操作的函数。
4.1、内存拷贝函数
memcpy() 函数 和 memmove() 函数的原型如下:
void * memcpy( void * restrict dest, const void * restrict src, size_t count );
void * memmove( void * dest, const void * src, size_t count );
memcpy() 函数从 src 的位置开始拷贝 count 字节的数据到目标的内存位置 dest。这个函数在遇到 '\0' 的时候并不会停止。如果 src 和 dest 有任何的重叠,复制的结果都是未定义的;
#include <stdio.h>
#include <string.h>
int main(void)
{
int src[] = {1,2,3,4,5,6,7,8,9,0};
int dest[20] = {0};
int i = 0;
memcpy(dest, src, 40);
for(i = 0; i < sizeof(src)/sizeof(src[0]); i++)
{
printf("%-3d", dest[i]);
}
printf("\n");
return 0;
}
memmove() 函数从 src 的位置开始拷贝 count 字节的数据到目标的内存位置 dest。这个函数在遇到 '\0' 的时候并不会停止。src 和 dest 内存空间可以重叠。
#include <stdio.h>
#include <string.h>
int main(void)
{
int src[] = {1,2,3,4,5,6,7,8,9,0};
int i = 0;
memmove(src + 3, src, 12);
for(i = 0; i < sizeof(src)/sizeof(src[0]); i++)
{
printf("%-3d", src[i]);
}
printf("\n");
return 0;
}
模拟实现 memcpy() 函数:
#include <stdio.h>
#include <assert.h>
void * my_memcpy(void * dest, void * src, size_t num);
int main(void)
{
int src[] = {1,2,3,4,5,6,7,8,9,0};
int dest[20] = {0};
int i = 0;
my_memcpy(dest, src, 40);
for(i = 0; i < sizeof(src)/sizeof(src[0]); i++)
{
printf("%-3d", dest[i]);
}
printf("\n");
return 0;
}
void * my_memcpy(void * dest, void * src, size_t num)
{
void * temp = dest;
assert(src); // 断言,判断指针不能为空
assert(dest); // 断言,判断指针不能为空
while(num--)
{
*(char *)dest = *(char *)src;
dest = (char *)dest + 1;
src = (char *)src + 1;
}
return temp;
}
模拟实现 memmove() 函数:
#include <stdio.h>
#include <assert.h>
void * my_memmove(void * dest, const void * src, size_t num);
int main(void)
{
int src[] = {1,2,3,4,5,6,7,8,9,0};
int i = 0;
my_memmove(src + 3, src, 12);
for(i = 0; i < sizeof(src)/sizeof(src[0]); i++)
{
printf("%-3d", src[i]);
}
printf("\n");
return 0;
}
void * my_memmove(void * dest, const void * src, size_t num)
{
void * temp = dest;
assert(src); // 断言,判断指针不能为空
assert(dest); // 断言,判断指针不能为空
if(dest < src) // 从后向前拷贝
{
while(num--)
{
*(char *)dest = *(char *)src;
dest = (char *)dest + 1;
src = (char *)src + 1;
}
}
else // 从前向后拷贝
{
while(num--)
{
*((char *)dest + num) = *((char *) src + num);
}
}
return temp;
}
4.2、内存比较函数
memcmp() 函数的原型如下:
int memcmp( const void * lhs, const void * rhs, size_t count );
memcmp() 函数比较 lhs 指向的内存空间的数据与 rhs 指向的内存空间的数据前 count 字节数据是否相等。如果 lhs 指向的内存空间的前 count 字节的数据小,则返回负数;如果相等,则返回 0;如果大于,则返回整数;
#include <stdio.h>
#include <string.h>
int main(void)
{
int array1[] = {1,2,3,4,5};
int array2[] = {1,2,3};
int result = memcmp(array1,array2,12);
printf("%d\n", result);
return 0;
}
4.3、内存设置函数
memset() 函数的原型如下:
void * memset( void * dest, int ch, size_t count );
memset() 函数将 dest 指向的内存空间的 count 字节的数据设置为 ch;
#include <stdio.h>
#include <string.h>
int main(void)
{
char array[] = "hello world!";
memset(array+6, 'x', 5);
printf("%s\n", array);
return 0;
}
五、创建的动态内存错误
5.1、对NULL指针的解引用操作
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *p = (int *)malloc(INT_MAX / 4);
if(p == NULL)
{
printf("开辟内存失败!\n");
return 1;
}
// 如果p的值是NULL,就会有问题
*p = 30;
free(p);
p = NULL;
return 0;
}
5.2、对动态开辟空间的越界访问
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int i = 0;
int *p = (int *)malloc(sizeof(int) * 10);
if(p == NULL)
{
printf("开辟内存失败!\n");
return 1;
}
// 对动态开辟空间的越界访问
for(i = 0; i <= 10; i++)
{
p[i] = i;
}
free(p);
p = NULL;
return 0;
}
5.3、对非动态开辟内存使用free()函数
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int i = 0;
int *p = &i;
// 对非动态开辟内存使用free()函数
free(p);
p = NULL;
return 0;
}
5.4、使用free()函数释放一块动态开辟内存的一部分
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int i = 0;
int *p = (int *)malloc(sizeof(int) * 10);
if(p == NULL)
{
printf("开辟内存失败!\n");
return 1;
}
for(i = 0; i < 5; i++)
{
p[i] = i;
p++;
}
// 使用free()函数释放一块动态开辟内存的一部分
free(p);
p = NULL;
return 0;
}
5.5、对同一块动态内存多次释放
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int i = 0;
int *p = (int *)malloc(sizeof(int) * 10);
if(p == NULL)
{
printf("开辟内存失败!\n");
return 1;
}
free(p);
// 对同一块动态内存多次释放
free(p);
p = NULL;
return 0;
}
5.6、动态开辟内存忘记释放
#include <stdio.h>
#include <stdlib.h>
void test(void);
int main(void)
{
test();
return 0;
}
void test(void)
{
int i;
int *p = (int *)malloc(sizeof(int) * 10);
if(p == NULL)
{
printf("开辟内存失败!\n");
return;
}
scanf("%d",&i);
if(i == 5)
{
return ;
}
free(p);
p = NULL;
}