在 C 语言中,当一个程序被加载到内存中运行,系统会为该程序分配一块独立的内存空间,并且这块内存空间又可以再被细分为很多区域,比如:栈区、堆区、静态区、全局区等
栈区:保存局部变量。存储在栈区的变量,在函数执行结束后,会被系统自动释放。
堆区:由 malloc、calloc、realloc……等函数分配内存。其生命周期由 free 函数控制,在没有被释放之前一直存在,直到程序运行结束。
定义在函数内部的局部变量,都保存在栈区。栈区的特点是:函数执行结束后,由系统 “自动回收”局部变量所对应的内存空间。所谓的“自动回收”其实是操作系统将这块栈内存又分配给其他函数中的局部变量使用。
当定义局部变量时,系统会在栈区为其分配一块内存空间,当函数执行结束后系统负责回收这块内存,又分配给其他局部变量使用。
以下代码执行后,会发现输出的a,b变量内存地址是一样的。可以说明局部变量对应的内存在函数执行结束后,会被系统回收分配给其他函数中的局部变量使用。因此,在C语言中,不能将局部变量的地址作为函数返回值返回,否则可能出现问题
#include<stdio.h> void showA() { int a; printf("&a=%p\n",&a); } void showB() { int b; printf("&b=%p\n",&b); } int main(void) { showA(); showB(); getchar(); return 0; }
栈内存:
(1) 由系统自动分配、释放。如:函数形参、局部变量。
(2) 栈内存比较小,在 VS2012 中,栈内存默认最大为 1M,如果局部变量占用的栈内存过大,会发生栈溢出。
#include<stdio.h> int main(void) { int a[9900000];//会产生StackOverflow getchar(); return 0; }
使用 malloc 系列函数分配的内存都属于堆区,使用完后调用 free 函数进行释放,否则可能会造成内存泄漏(即这块内存无法被再次使用)。
malloc 函数
函数原型: void *malloc(int size);
头文件: #include <stdlib.h>
参数列表: size:分配多少个字节。
功能: 申请指定大小的堆内存。
返回值:如果分配成功则返回指向被分配内存的指针,否则返回空指针 NULL。
void *表示“不确定指向类型”的指针,使用前必须进行强制类型转化,将 void*转化为“确定指向类型”的指针。
free 函数
函数原型: void free(void* ptr);
头文件: #include <stdlib.h>
参数列表: ptr:指向要被释放的堆内存。
功能: 释放 ptr 指向的内存空间。
在C语言中,被 free 之后的堆内存,将会被操作系统回收再分配,不建议继续使用, 否则输出的结果将难以预料。
#include<stdio.h> #include<stdlib.h> int main(void) { int *p = (int *)malloc(sizeof(int));//void *类型的返回值必须进行强制类型转化 *p = 200; printf("%p, %d\n", p, *p);//输出指针p所指向的内存地址,输出此内存地址中的值 free(p); getchar(); return 0; }
堆内存
(1)由程序员自己申请、释放。如果没有释放,可能会发生内存泄露,直到程序结束后由系统释放。
(2)堆内存比较大,可以分配超过 1G 的内存空间。
函数返回数据的两种方式
#include<stdio.h> #include<stdlib.h> int* getMemory() { int* p_int=(int*)malloc(sizeof(int));//被调函数分配使用内存 *p_int=100; return p_int; } int main(void) { int* p=getMemory(); printf("%d\n",*p); free(p); //主调函数释放内存 getchar(); return 0; }
分配内存与释放内存是分开的,容易导致程序员忘记在主调函数中释放内存,从而导致内存泄漏,
2.在主调函数中分配堆内存,在被调函数中使用堆内存,最后又在主调函数中释放堆内存。
#include<stdio.h> #include<stdlib.h> void fun(int *p_int) { *p_int=100; } int main(void) { int* p=(int*)malloc(sizeof(int)); //主调函数分配堆内存 fun(p); printf("%d",*p); free(p); //主调函数释放堆内存 getchar(); return 0; }
此方法较为推荐
使用 malloc 函数分配的堆内存,系统不会初始化内存,分配的内存中还会残留旧数据。因此,引用未初始化的堆内存,输出的数据也将是未知的。
#include<stdio.h> #include<stdlib.h> int main(void) { int *p_int=(int*)malloc(sizeof(int)); printf("%d",*p_int);//输出随机未知数据 getchar(); return 0; }
为了避免引用堆内存中的未知数据,一般使用 malloc 在堆区分配内存后,需要将这块堆内存初始化为0
函数原型:void* memset(void *dest, int c, size_t size);
头文件: #include <string.h>
参数列表: dest:被初始化的目标内存区域。 c:要设置的字符。 size:初始化 size 个字节。
功能: 将 dest 指向的内存空间前 size 个字节初始化为 c。 返回值: dest的内存地址。
#include<stdio.h> #include<string.h> int main(void) { char arr[10] = {0}; //会把arr的前5个字节的元素设置为@ memset(arr, '@',5); getchar(); return 0; }
函数原型:void* memcpy(void* destination, const void* source, size_t num);
memcpy函数会从source指针的位置向后复制num个字节的数据到destination指针指向的内存的位置中
但是source和destination的num个字节的数据在内存中不能有重叠,否则复制的结果是未知的
memcpy可以复制任意类型的数据
#include<stdio.h> #include<string.h> struct S { int age; char name[5]; }; int main(void) { struct S arr1[] = {{18,"abc"}, {22, "456"}}; struct S arr2[3] = {0}; //从内存中可以看到,arr1的数据已经拷贝到arr2中 memcpy(arr2, arr1, sizeof(arr1)); getchar(); return 0; }
简单实现my_memcpy
void* my_memcpy(void* dest, void* source, size_t num) { assert(dest && source); void* res = dest; while(num--){ *(char*)dest = *(char*)source; ++(char*)dest; ++(char*)source; } return res; }
函数原型:void* memmove(void* destination, const void* source, size_t num);
memmove函数会从source指针的位置向后复制num个字节的数据到destination指针指向的内存的位置中
但是source和destination的num个字节的数据在内存中可以有重叠
函数原型:int memcmp(void* ptr1, void* prt2, size_t num);
比较从ptr1指针以及ptr2指针开始的num个字节的数据的大小
#include<stdio.h> #include<string.h> int main(void) { int res = 0; char arr1[] = {1,2,3,4,5}; char arr2[] = {1,2,4,5,5}; //比较 res = memcmp(arr2, arr1,3); printf("%d", res); getchar(); return 0; }