目录
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟十个字节的连续空间
上述的开辟空间的方式有两个特点:
1.空间开辟大小是固定的
2.数组在声明的时候,必须指定数组的长度,它所需要的内存在编译的时候分配
但是对于空间的需求,有时候我们需要的空间大小在运行程序的时候才知道。这时动态内存开辟就可以满足我们的需求。
动态内存函数
malloc
void *malloc(size_t size);
所需引用头文件:<stdlib.h>或<malloc.h>
返回值:
返回类型是void*(使用时自己决定开辟空间的类型)
如果开辟成功,返回一个指向开辟空间的指针;
如果开辟不成功,返回一个空指针
对NULL指针的解引用操作(error)
int main()
{
int* p = (int*)malloc(1000);
if (p == NULL)//当malloc申请失败会返回空指针
{
perror("malloc");
return 1;
}
for (int i = 0; i < 250; i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
free
C语言提供了函数free,专门用作动态内存的释放和回收
使用需引用头文件:<stdlib.h>或<malloc.h>
void free(void *ptr);
如果参数ptr指向的空间不是动态开辟的,那么free函数的行为是未定义的
如果参数ptr是空指针,那么函数什么都不做
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* ptr = (int*)malloc(40);
int* p;
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i < 10; i++)
{
*p = i;
p++;
}
free(ptr);
ptr = NULL;//在释放掉地址后尽量让指针置空 防止指针指向其他地方导致程序崩溃
return 0;
}
对非动态内存使用free释放(error)
运行后会报错。malloc开辟的动态内存使用完后,需要我们手动回收。局部变量出了作用域会主动回收,不需要我们手动。
#include<stdlib.h>
int main()
{
int a = 10;
int* p = &a;
free(p);
p = NULL;
return 0;
}
使用free释放一块动态开辟内存的一部分(error)
释放空间必须在动态内存的起始地址
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(100);
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i < 10; i++)
{
*p = i;
p++;
}
free(p);//这里的p已经不是指向动态内存起始地址的指针了
p = NULL;
return 0;
}
对同一块动态内存的多次释放
int main()
{
int* p = malloc(100);
if (p == NULL)
{
return 1;
}
free(p);
// ……
free(p);
p = NULL;
return 0;
}
动态开辟的内存忘记释放
当我们不释放动态申请的内存时。如果程序结束,动态申请的内存由操作系统自动回收;如果程序不结束,动态内存是不会自动回收的,会形成内存泄漏的问题。
void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while (1)
{
;
}
}
calloc
void *calloc(size_t num, size_size);
calloc函数是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0
realloc
使用需引用头文件:<stdlib.h>或<malloc.h>
void *realloc(void *ptr, size_t size);
prt是要调整的内存地址
size是调整之后的新大小
返回值为调整之后的内存起始地址
realloc函数在调整原内存空间大小的基础上,还会将原来的内存中的数据移动到新的空间,并且原本的空间释放掉 realloc调整内存空间:
- 原本空间之后有足够的空间: 直接在原有的内存空间后面追加空间,原来的空间的数据不发生变化
- 原本空间之后没有足够的空间 在堆空间上另找一个大小合适的连续空间来使用。如果找不到足够的空间,会返回NULL。
练习
1
运行后会有什么结果:
#include<stdio.h>
#include<string.h>
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}
GetMemory函数结束后,p被销毁了,str依旧是空指针。strcpy对NULL的解引用操作使程序崩溃
且没有进行动态内存释放,发生了内存泄漏
对于上述代码的修正:
void GetMemory(char **p)
{
*p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
2 返回栈空间地址问题
运行后会有什么结果:
#include<stdio.h>
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
出了GetMemory函数,p[]被销毁,malloc开辟的空间也还给了操作系统(没有访问权限),但是返回了“hello world”的地址。此时,这个str是野指针。
栈的空间地址出了作用域就销毁了,所以不要轻易返回,容易产生野指针的问题
比如以下代码:
#include<stdio.h>
int* test()
{
int a = 10;
return &a;
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}
虽然上述代码确实打印出了10,但是这里的p是野指针。只是恰好这里原本a的空间地址没有被覆盖掉。
再比如上述代码再打印一次:
Test函数返回的时候,栈帧的空间没有被覆盖,所以用*p访问的时候还是原来的数据。
*调用printf时候,栈帧空间可能被覆盖了,再通过**p去访问就不一定是原来的数据了
但是返回栈空间的变量时,不会产生野指针的问题:
会先把变量放到寄存器,然后销毁。
#include<stdio.h>
int* test()
{
int a = 10;
return a;
}
int main()
{
int ret = test();
printf("%d\n", ret);
return 0;
}
3
运行会有什么结果:
#include<stdio.h>
#include<string.h>
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
int main()
{
Test();
return 0;
}
会正常打印hello,但是没有释放malloc开辟的动态内存,也没有判断p是否为空指针
4
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
使用free回收动态内存后,就没有访问权限了。但是str依旧指指向原本开辟的动态内存,且free后面没有把str置为空指针。而后,strcpy复制属于非法访问了
C/C++程序的内存开辟
C/C++程序内存分配的几个区域:
- 栈区:在执行函数时,函数内局部变量的存储单元都可以在栈上创建。函数执行结束时,这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
- 堆区:一般人为分配释放,如果不手动释放,程序结束时,可能由OS回收
- 数据段(静态区):存放全局变量、静态数据。程序结束后,由系统释放
- 代码段:存放函数体(类成员函数和全局函数)的二进制代码
柔性数组
C99中,结构体的最后一个元素允许是大小位置的数组
struct S
{
int num;
double d;
int arr[];//柔性数组成员
};
struct SS
{
int num;
double d;
int arr[0];//柔性数组成员 这里的0并不表示0个元素,而表示arr是柔性数组
};
柔性数组特点
结构中的柔性数组成员前面必须至少有一个成员
sizeof返回这种结构体的大小不包括柔型数组的内存
包含柔性数组成员的结构体用malloc函数进行内存的动态分配,并且应该大于结构体大小,以适应柔性数组的预期大小
typedef struct S
{
int i;
int arr[0];
}S;
int main()
{
printf("%d\n", sizeof(S));
return 0;//这里打印的结果:4
}
柔性数组的使用
#include<stdio.h>
#include<stdlib.h>
struct S
{
int num;
int arr[];
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);//动态分配内存
ps->num = 100;
for (int i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
struct S3 *pr = (struct S3*)realloc(ps, sizeof(struct S) + 80);//扩容
if (pr == NULL)
{
perror("realloc");
return 1;
}
else
{
ps = pr;
}
for (int i = 10; i < 20; i++)
{
ps->arr[i] = i;
}
for (int i = 0; i < 20; i++)
{
printf("%d ", ps->arr[i]);
}
free(ps);
ps = NULL;
return 0;
}
柔性数组的优势
#include<stdlib.h>
struct S
{
int num;
int arr[];
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);
ps->num = 10;
for (int i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", ps->arr[i]);
}
free(ps);
ps = NULL;
return 0;
}
上述代码也可以这样设计:
#include<stdlib.h>
struct S
{
int num;
int *pa;
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S));
ps->num = 40;
ps->pa = (int*)malloc(10 * sizeof(int));
for (int i = 0; i < 10; i++)
{
ps->pa[i] = i;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", ps->pa[i]);
}
free(ps->pa);
ps->pa = NULL;
free(ps);
ps = NULL;
return 0;
}
上述两个代码,前者对于后者而言,有两个好处
- 方便内存释放
- 有利于访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片