目录
1. 为什么要有动态内存分配
在此之前,我们会通过创建局部变量,在栈区开辟空间。这种开辟空间的方式有两个缺陷:(1)开辟的空间大小是固定的;(2)数组长度指定后,不可调整。
但往往对于空间的需求,不仅仅是上述情况,有时候我们需要的空间在程序运行时才能知道,那数组在编译时开辟的固定空间就不能满足需求了。为此,C语言引入动态内存开辟,程序员可根据自身需求灵活开辟/扩大/缩小空间。
int main()
{
int a = 1; //在栈区开辟4个字节的空间
int arr1[10] = { 0 }; //在栈区开辟40个字节的连续空间
int arr2[] = {1,2,3}; //在栈区开辟14个字节的连续空间
char ch = "abcdef"; //在栈区开辟7个字节的连续空间
return 0;
}
2. malloc 和 free
2.1 malloc
(1)定义:向内存申请一块连续可用的空间,并返回指向该空间的指针
void* malloc (size_t size);
(2)参数:字节大小,即需要申请的空间的大小
(3)返回:指针,指向所申请的连续空间的起始位置
(4)要点:
-
如果空间开辟成功,则返回指向开辟好的空间的指
-
如果空间开辟失败,则返回NULL指针,因此该函数的返回值需要异常检查
-
返回指针为void*,该函数并不知道开辟的空间具体是什么类型,需使用者明确
-
如果参数为0,标准未定义,取决于编译器
(5)实现:
int main()
{
//开辟空间
int* p = (int*)malloc(10 * sizeof(int));
//判断返回是否为NULL指针
if(p == NULL)
{
perror("malloc");
return 1;
}
//赋值
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
//使用
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
2.2 free
C语言提供了另一个函数free,用于动态空间的释放和回收。
(1)定义:释放/回收指针所指向的动态空间。
void free (void* ptr);
(2)参数:指针,即指向动态空间的起始位置的指针
(3)返回:释放/回收动态空间
(4)要点:
- free函数释放/回收malloc/calloc/realloc开辟的动态空间
- 如果参数ptr指向的空间不是动态开辟的,free函数的行为是未定义的
- 如果参数ptr为NULL指针,则free函数什么也不做。
- free函数仅释放指针所指向的动态空间,并不会改变指针。需要注意的是:指针未改变,指针所指向的空间被销毁,指针会变成野指针,因此指针需置为NULL。
(5)实现:
int main()
{
//开辟空间
int* p = (int*)malloc(10 * sizeof(int));
//判断返回是否为NULL指针
if(p == NULL)
{
perror("malloc");
return 1;
}
//赋值
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
//使用
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
//释放空间
free(p);
//p指针置为NULL
p = NULL;
return 0;
}
3. calloc 和 realloc
3.1 calloc
(1)定义:向内存申请一块连续可用的空间,初始化为0,并返回指向该空间的指针。
void* calloc (size_t num, size_t size);
(2)参数:num为元素个数,size为元素大小
(3)返回:指针,指向所申请的连续空间的起始位置
(4)要点:
- 函数功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0
- calloc函数与malloc函数的区别在于:在malloc函数功能基础上,对所开辟的空间进行初始化,初始化值为0
(5)实现:
int main()
{
//开辟空间
int* p = (int*)calloc(4, sizeof(int));
//判断返回是否为NULL指针
if (p == NULL)
{
perror("calloc");
return 1;
}
//使用
for (int i = 0; i < 4; i++)
{
printf("%d ", *(p + i));
}
//释放空间
free(p);
//指针置为NULL
p = NULL;
return 0;
}
3.2 realloc
realloc函数相较于上面的函数更加灵活,除了开辟动态空间外,还能扩大/缩小已开辟的空间。
(1)定义:
void* realloc (void* ptr, size_t size);
(2)参数:ptr为需要调整的内存地址,size为调整后的动态空间大小
(3)返回:调整后动态空间的起始位置
(4)要点:
- 当ptr为NULL指针时,realloc函数和malloc函数功能一样,可用于开辟新的动态空间。
- 当realloc函数调整内存空间时会存在两种情况:1)原有空间之后有足够大的空间:则扩展内存直接在原有空间上追加,原有空间数据不发生变化,返回旧地址;2)原有空间之后没有足够大的空间:在堆区找一块空间大小合适的连续空间,拷贝旧空间数据至新空间,释放旧空间,返回指向新空间的地址。
- 如果调整内存空间失败,realloc函数返回NULL指针。因此对于realloc函数调整的内存空间,不应该用旧地址接收,避免内存空间调整失败,导致已有数据被销毁。
(5)实现:
int main()
{
//开辟动态空间
int* p = (int*)malloc(10 * sizeof(int));
//判断返回是否异常
if (p == NULL)
{
perror("malloc");
return 1;
}
//赋值
for (int i = 0; i < 10; i++)
{
*(p + i) = i;
}
//打印
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
printf("\n");
//空间不够,扩展空间
int * ptr = (int*)realloc(p, 15 * sizeof(int));
if (ptr == NULL)
{
perror("realloc");
return 1;
}
p = ptr; //新地址不为NULL指针的时候,赋值给旧指针变量
ptr=NULL;
//为新增的动态空间赋值
for (int i = 0; i < 5; i++)
{
*(p + 10 + i) = i;
}
//打印
for (int i = 0; i < 15; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}
4. 常见动态内存错误
4.1 对NULL指针解引用
malloc/calloc/realloc返回值可能为NULL指针,直接解引用会有问题,因此需要对动态空间开辟函数的返回值进行检查,非空才可解引用。
int main()
{
int * p = (int*)malloc(10 * sizeof(int));
*p = 20; //返回值可能为NULL指针
return 0;
}
4.2 对动态开辟空间的越界访问
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (NULL == p)
{
perror("malloc");
return 1;
}
for (int i = 0; i <= 12; i++) //开辟40个空间,使用48个空间???
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
4.3 对非动态开辟内存使用free释放
int main()
{
int a = 0;
int* p = &a;
free(p);
return 0;
}
4.4 使用free释放一块动态开辟内存的一部分
int main()
{
int *p = (int*)malloc(10 * sizeof(4));
if (NULL == p)
{
perror("malloc");
return 1;
}
p++; //p不再指向动态空间起始位置
free(p);
return 0;
}
4.5 对同一块动态内存多次释放
int main()
{
int* p = (int*)malloc(10 * sizeof(4));
if (NULL == p)
{
perror("malloc");
return 1;
}
free(p);
p=NULL;
free(p); //多次释放无意义
p=NULL;
return 0;
}
4.6 动态开辟内存忘记释放(内存泄漏)
动态空间释放的方式:1)free函数释放;2)程序结束释放。需要注意的是:以往局部变量开辟的空间,出了作用域,内存空间会被销毁;动态开辟的空间即使出了作用域也不会被释放,直到整个系统程序结束才被销毁。动态开辟的空间一定要释放,并且正确释放,忘记释放不再使用的动态开辟空间会造成内存泄漏。
int main()
{
int* p = (int*)malloc(10 * sizeof(4));
if (NULL == p)
{
perror("malloc");
return 1;
}
return 0;
}
5. 动态内存经典试题解析
5.1 题目1
- str为指针变量,赋值为NULL
- GetMemory函数调用为传值,形参p为实参str的临时拷贝,形参的变化不会影响实参。那么GetMemory(str)并不影响str为NULL指针。这时候,strcpy()相当于向空指针写入内容,程序会出现问题。
- GetMemory函数开辟100个字节动态空间,返回起始位置,并赋值给指针变量p。根据以往的知识点:1)动态空间即使出了作用域也不会被销毁,直到系统程序结束,那么在调用GetMemory函数时,100个字节动态空间仍然存在;2)局部变量出作用域,内存空间会回收,则p变量不存在。
void GetMemory(char* p)
{
p = (char*)malloc(100); //开辟100个字节动态空间,并返回起始位置
}
int main ()
{
char* str = NULL; //str指针变量为NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
return 0;
}
5.2 题目2
- str为指针变量,赋值为NULL
- GetMemory()函数中,返回数组p的起始地址。局部变量出作用域,其内存空间会被销毁,但是指向该空间的指针仍然存在,此时指针为野指针。
- 调用函数并使用,相当于对野指针解引用,程序会出问题。
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
int main ()
{
char* str = NULL;
str = GetMemory();
printf(str);
return 0;
}
5.3 题目3
- 对malloc函数的返回值未作异常检验,可能存在问题
- 对malloc动态开辟的空间,未主动释放,会存在内存泄漏的问题
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
int main ()
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
return 0;
}
5.4 题目4
- 尽管这段代码在VS中有运行结果,但是存在问题的。
- malloc函数动态开辟100个字节,返回起始位置,并赋值给str指针变量
- 拷贝"hello"至开辟的动态空间,能成功打印"hello"
- 释放开辟的100个字节的动态空间
- 根据上面的知识点:free函数仅释放指针所指向的动态空间,并不会改变指针,即指针未改变,指针所指向的空间被销毁,指针会变成野指针。这里对野指针解引用,是有问题的。
int main()
{
char* str = (char*)malloc(100); //malloc函数动态开辟100个字节,返回起始位置,并赋值给str指针变量
strcpy(str, "hello"); //拷贝"hello"至开辟的动态空间,能成功打印"hello"
printf(str);
free(str); //释放开辟的100个字节的动态空间
printf("\nhehe\n");
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
return 0;
}