前言:
随着我们深入的学习c语言,之前使用的静态内存分配已经难以满足我们的实际需求。比如前面我们的通讯录功能的实现,如果只是静态内存分配,那么也就意味着程序开始的内存分配大小就是固定的,应该开多大的空间呢?开大了是浪费,开小了又不能满足自己的需求。而动态内存分配可以完美的解决这个问题,真正地做到需要多少空间就开多大的空间(根据需要动态地分配和释放内存空间).
总的来说,动态内存分配比静态内存分配更灵活,效率也更高,避免了空间的浪费。
下面就开始动态内存的学习吧。
1.动态内存函数:
1.malloc
2.free
3.calloc
4.realloc
这些函数都声明在stdlib头文件中。
1.1 malloc和free
malloc:
void* malloc (size_t size);
malloc函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
参数size表示需要开辟的空间字节大小,返回类型为void*,泛型指针,指向被开辟空间首元素地址。具体指向什么类型的数据需要使用者自己决定。
需要注意的是,如果开辟空间失败这个函数会返回NULL,所以为了避免使用空指针,在使用这个函数之后需要判断是否返回了NULL.
free:
void free (void* ptr);
free函数用来释放掉动态开辟的内存。
参数prt表示的是需要释放动态内存空间的指针。
这个函数非常重要,因为如果动态内存分配的空间没有及时释放的话,可能会造成内存泄漏,而长时间运行的程序中存在内存泄漏会逐渐消耗系统的可用内存,最终可能导致程序崩溃或系统变得不稳定。
所以为了避免内存泄漏的情况发生,我们应该养成即使释放动态内存的习惯。
举例:
int main() { //使用malloc动态开辟空间的例子 int num = 0; scanf("%d", &num); int* ptr = NULL; ptr = malloc(num * sizeof(int));//开辟了num个整数类型元素大小的空间,返回这片空间的起始位置给ptr if (ptr == NULL) { //判断是否申请空间成功 perror("malloc"); } //初始化申请空间的元素 for (int i = 0; i < num; i++) { *(ptr + i) = i; } //释放空间,防止空间泄漏,指针置空,防止野指针 free(ptr); ptr = NULL; return 0; }
这里我输入的num=10.
监视:
我们发现ptr指针确实指向了10个整型大小的空间,能正常初始化。
释放空间后:
为什么要释放掉空间后将ptr置空?
这是因为虽然我们将动态开辟的空间释放掉了,但是ptr依旧指向这片空间,但是此时ptr已经没有权限操作这片空间了,所以我们需要置空。
1.2 calloc
void* calloc (size_t num, size_t size);
calloc函数与malloc函数相似,都是向内存申请一块连续可用的空间,并返回指向这块空间的指针,区别是,calloc申请的空间里的每个字节会默认初始化为0。
第一个参数num是元素的个数,size表示的是元素的字节大小,开辟num个大小为size的元素的空间,返回开辟空间的起始地址。
其实malloc和calloc的用法也基本一致,只不过比malloc更加省事,不用担心初始化的问题。
举例:
int main() { int* p = (int*)calloc(10, sizeof(int)); if (p == NULL) { perror("calloc");//打印错误信息 } free(p); p = NULL; return 0; }
监视指针p:
我们可以看到,在申请到空间后,空间里的元素都被初始化为了0.
1.3 realloc
void* realloc (void* ptr, size_t size);
realloc函数可以做到对动态开辟内存大小的调整,如果此时申请的动态空间不够,那么我们可以使用realloc函数来进行“扩容”。
ptr 是要调整的内存地址,size 调整之后新大小,返回值为调整之后的内存起始位置。 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
realloc申请空间会遇到两种情况:
1.原空间后面有足够空间,会直接在原来ptr指向空间后面追加,返回的地址也是ptr指向的地址。
2.原有空间后面空间不够,那就在堆空间另找一片空间,这个时候返回的的地址就会是一个随机值。
举例:
int main() { int* ptr = (int*)malloc(120); int ptr2 = ptr;//记录原来ptr指向的地址 if (ptr == NULL) { perror("malloc"); } //扩展容量 ptr = (int*)realloc(ptr, 1000); if (ptr == NULL)//如果申请失败返回NULL { perror("realloc"); } if (ptr == ptr2) { printf("情况1\n"); } else { printf("情况2\n"); } free(ptr); ptr = NULL; return 0; }
我们看到,realloc申请的空间返回的地址不是原来的ptr指向的地址.
我们再来改一下代码:
int main() { int* ptr = (int*)malloc(120); int ptr2 = ptr; if (ptr == NULL) { perror("malloc"); } //扩展容量 ptr = (int*)realloc(ptr, 140);//扩容的空间很小 if (ptr == NULL)//如果申请失败返回NULL { perror("realloc"); } if (ptr == ptr2) { printf("情况1\n"); } else { printf("情况2\n"); } free(ptr); ptr = NULL; return 0; }
现在我们把realloc申请的空间改小一点,为了能找到符合情况1(realloc在原来ptr指向空间后面追加).
当然这也只是偶然情况,就算是扩容改小一点也不一定就会直接追加空间在ptr指向空间后面。
2.常见的动态内存错误
在介绍了几个动态内存函数的用法之后,我们再来看看在使用动态内存函数时经常犯的错误吧。
2.1 对NULL指针的解引用操作
void test() { int *p = (int *)malloc(INT_MAX/4); *p = 20;//如果p的值是NULL,就会有问题 free(p); }
如果我们申请的空间太大就有可能开辟空间失败,这个时候指针p接收到的就是NULL,而对NULL解引用操作就会导致错误。
2.2 对动态开辟空间的越界访问
void test() { int i = 0; int *p = (int *)malloc(10*sizeof(int)); if(NULL == p) { exit(EXIT_FAILURE); } for(i=0; i<=10; i++) { *(p+i) = i;//当i是10的时候越界访问 } free(p); }
我们这里只是申请了10个整型元素大小的空间,这个时候*(p+10)其实已经越界访问了,此时程序就会出错。
2.3 对非动态开辟内存使用free释放
void test() { int a = 10; int *p = &a; free(p);//ok? }
上面我们已经知道了free是用来释放动态内存的空间的,而如果对非动态开辟内存使用free释放,等待你的就是这个:
2.4 使用free释放一块动态开辟内存的一部分
void test() { int *p = (int *)malloc(100); p++; free(p);//p不再指向动态内存的起始位置 }
在执行malloc函数分配内存后,指针p指向动态内存的起始位置。然而,在将指针p增加1后,它不再指向分配的内存起始位置。因此,当在之后使用free函数来释放内存时,由于free函数要求传入指向malloc分配内存起始位置的指针,传入p将导致未定义的行为。这可能会导致程序崩溃或出现其他问题。
2.5 对同一块动态内存多次释放
void test() { int *p = (int *)malloc(100); free(p); free(p);//重复释放 }
重复释放同一内存块会导致未定义的行为,可能会导致程序崩溃或出现其他问题。
2.6 动态开辟内存忘记释放(内存泄漏)
void test() { int *p = (int *)malloc(100); if(NULL != p) { *p = 20; } } int main() { test(); while(1); }
内存泄漏指的是程序在执行期间申请了一定量的内存空间,但在使用完毕后没有及时释放,导致这部分内存空间永远无法被程序使用,从而浪费了宝贵的系统资源。所以我们一定要及时释放掉申请的空间,这样才能“有借有还再借不难”。
总结
学习动态内存是如何分配的可以让我们以后写出来的程序更加高效灵活,这也是我们作为程序员的基本素养,同时,我们也应该注意正确使用动态内存分配,避免一些常见的错误。