动态内存分配允许程序根据实际需要来分配内存。这意味着程序可以根据不同的输入和条件来处理不同大小的数据结构,如数组.
下面列举一般的开辟空间的方式:
int a=10;
int arr[10]={0};
int a =10;在栈空间连续开辟了四个字节大小的空间;
int arr[10]={0};在栈空间连续开辟了四十个字节大小的空间;
上述开辟空间的特点:
接下来我们则引入对c语言中动态内存分配的讲解
malloc 是在 C 语言中用于动态内存分配的函数。它的作用是在堆上分配指定字节数的未初始化内存,并返回指向这块内存的指针。如果分配成功,将返回一个指针,该指针可以被转换为适当类型的指针以访问该内存区域。如果分配失败,将返回一个 NULL 指针。
malloc 函数的原型在 stdlib.h 头文件中定义,其函数原型如下:
void* malloc(size_t size);
size 参数是你想要分配的字节数;
返回指的类型是void*,所以malloc函数并不知道开辟空间的类型,具体使用的时候再进行决定;
例如,如果建立一个有十个整形元素的数组,可以这样定义:
int *p = NULL;
int n = 10; // 假设我们要创建大小为10的整型数组
// 动态分配内存
p = (int*)malloc(n * sizeof(int));
由于malloc函数开辟可能会失败,因此malloc的返回值需要做检查
if(p=NULL)
{
perror("malloc");
return 1; // 分配失败,结束程序
}
完整代码:
int main()
{ int *p = NULL;
int n = 10;
p = (int*)malloc(n * sizeof(int));
if(p=NULL)
{
perror("malloc");
return 1;
}
// 使用分配的内存
for (int i = 0; i < n; i++) {
*(p+i) = i; // 初始化内存
}
p仍指向起始地址
// 打印分配的内存
for (int i = 0; i < n; i++) {
printf("%d ", p[i]);
}
}
这里的代码并没有完善,接下来我们再介绍一个必不可少的函数
free 是 C 语言中的一个标准库函数,用于 释放 之前通过 malloc、calloc 或 realloc 等函数动态分配的内存。一旦使用 free 释放了内存,该内存区域就不再属于你的程序,你的程序应该停止访问它。如果尝试访问已释放的内存,会导致未定义的行为,通常称为悬挂指针。
void free(void* ptr);
ptr 参数是一个指向之前通过 malloc、calloc 或 realloc 分配的内存的指针。
如果 ptr 是一个空指针(即 NULL),free 函数不会执行任何操作。
下面是 free 函数的一个使用示例,结合之前的 malloc 例子:
#include
#include
int main()
{ int *p = NULL;
int n = 10;
p = (int*)malloc(n * sizeof(int));
if(p=NULL)
{
perror("malloc");
return 1;
}
// 使用分配的内存
for (int i = 0; i < n; i++) {
*(p+i) = i; // 初始化内存
}
p仍指向起始地址
// 打印分配的内存
for (int i = 0; i < n; i++) {
printf("%d ", p[i]);
}
free(p);
// 为避免悬挂指针,将 p 设置为 NULL
p = NULL;
}
在上面代码中,p指针所指向的内存地址已经“悬空”,也就是说指针并没有被清除或者重置,但它指向的内存已经不再属于你的程序,因此如果你尝试通过悬挂指针访问或者修改数据,会导致未定义行为,如程序崩溃、数据损坏或安全漏洞。
在释放指针指向的内存后立即将指针置为 NULL;
calloc函数用来动态地分配内存,并初始化所有字节为零。这与 malloc 函数不同,malloc分配的内存含有未定义的值。calloc 函数特别适用于分配固定数量和类型的对象时,因为它将所有位初始化为零,这通常代表了数字 0 和空指针等类型的空值。
void* calloc(size_t num, size_t size);
举例如下:
#include
#include
int main()
{
int *p = (int*)calloc(10, sizeof(int));
if(NULL != p)
{
int i = 0;
for(i=0; i<10; i++)
{
printf("%d ", *(p+i));
}
}
free(p);
p = NULL;
return 0;
}
输出结果如下:
0 0 0 0 0 0 0 0 0 0
realloc函数用于调整之前分配的内存块的大小。这个函数特别有用,当你不确定最初需要多少内存或者后来发现需要更多(或更少)内存时,realloc 可以帮助你增加或减少已分配内存的大小,而不需要你手动分配一个新的内存块和复制数据。
void* realloc(void* ptr, size_t size);
继续用之前的例子:在原来基础上扩大二十个整形
#include
#include
int main()
{ int *p = NULL;
int n = 10;
//p = (int*)malloc(n * sizeof(int));
p = (int*)calloc(n ,sizeof(int));
if(p=NULL)
{
perror("malloc");
return 1;
}
// 使用分配的内存
for (int i = 0; i < n; i++) {
*(p+i) = i; // 初始化内存
}
realloc(p,20*sizeof(int));
for (int i = 0; i < n; i++) {
printf("%d ", p[i]);
}
free(p);
p = NULL;
}
这串代码并不完整,我们没有接收realloc的返回值,接下来我们讨论realloc调用时会产生的结果
调用 realloc 时,会发生以下几种情况:
我们想要在已经开辟好的40个空间后面扩展40个空间,发现后面没有足够的空间
在这种情况下,realloc函数会在内存的堆区重新找一个空间(满足新的空间的大小需求的),同时会把旧的数据拷贝到新的新空间,然后释放旧的空间,同时返回新的空间的起始地址
在已经开辟好的空间后边,有足够的空间,直接进行扩大,扩大后,返回旧的空间的起始地址;
所以,对于刚刚的代码
int*ptr=(int *) realloc(p,20*sizeof(int));
if(ptr!=NULL)
{
p=ptr;
}
realloc函数返回值放在一个临时指针ptr中,判断其不为空指针再交给p;
1:对NULL指针的解引用操作
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;
free(p);
}
2.对动态开辟空间的越界访问
int n = 5; // 分配内存用于存储5个整数
int *arr = (int*)malloc(n * sizeof(int));
if (arr == NULL) {
return 1;
}
// 正常使用内存
for (int i = 0; i < n; i++) {
arr[i] = i; // 初始化数组
}
// 越界访问
arr[n] = 10; // 这里访问的是数组的第6个元素,越过了边界
free(arr);
这里越界会导致未定义行为
3.对非动态开辟内存使用free释放
void test()
{
int a = 10;
int *p = &a;
free(p);
}
free 函数只能用于释放动态分配的内存(即通过 malloc、calloc 或 realloc 分配的内存)。尝试释放栈上的内存或者全局/静态变量的内存会导致未定义的行为,通常会导致程序崩溃或其他严重错误。
a 是一个局部变量,它存储在栈上,而不是在堆上,我们在这里补充一个知识:
通过 malloc、calloc、realloc 和 free 等函数手动管理的内存分配在堆区。这部分内存的生命周期由程序员控制,它不会自动被回收。
4.使用free释放一块动态开辟内存的一部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
当 p 被增加之后,它不再指向原先由 malloc 分配的内存的起始地址,而是指向该块内存中的下一个 int 位置。当你尝试用 free§ 释放这个不正确的地址时,你将会传递一个非法指针给 free 函数,因为 free 只能接受之前由 malloc(及其他分配函数如 calloc 或 realloc)返回的指针。
5. 对同⼀块动态内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
在第一次调用 free 后,p 指向的内存已经被释放,操作系统可能已经将其重新分配给其他用途。第二次调用 free 将试图操作一个不再有效的内存地址。
为了避免此类错误,通常的做法是在释放内存后将指针设为 NULL,这样就能防止后续对同一个已释放内存的误用:
void test() {
int *p = (int *)malloc(100); // 分配 100 字节的内存
if (p == NULL) {
// 分配内存失败,应处理错误
return;
}
free(p); // 正确地释放内存
p = NULL; // 将 p 设为 NULL,避免悬垂指针
// ... 其他代码 ...
// 再次检查 p 是否为空,可以避免重复释放
if (p != NULL) {
free(p);
}
}
void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
}
切记:动态开辟的空间⼀定要释放,并且正确释放!
(一)
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
p = (char *)malloc(100); 试图分配 100 个字节的内存给指针 p。
但这里的 p 是一个局部变量,它是主调函数 Test 中 str 的一个拷贝。因此,当 GetMemory 返回时,分配的内存地址并没有传递回 str。str 仍然是 NULL。
对空指针再进行解引用,则程序崩溃。
我们可以进行一个类比:
void zhen(int x)
{
x=10;
}
void test(void)
{
int n=0;
zhen(n);
}
进行这个类比就好理解多了,这里x与p都是临时拷贝,并没有对实参进行任何改变,在之前的学习中,我们知道,想要对实参进行改变,可以使用指针,而对于指针p,则需要使用二级指针:
void GetMemory(char **p) {
*p = (char *)malloc(100);
}
void Test(void) {
char *str = NULL;
GetMemory(&str); // 注意,这里传递的是 str 的地址
if(str != NULL) {
strcpy(str, "hello world");
printf("%s\n", str);
free(str); // 记得释放内存
}
}
(二)
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
这里有两种解决办法:
1.动态分配内存:在堆上分配内存并返回指针
char *GetMemory(void) {
char *p = malloc(12); // "hello world" 加上 null 终止符
if (p != NULL) {
strcpy(p, "hello world");
}
return p;
}
void Test(void) {
char *str = GetMemory();
if (str != NULL) {
printf("%s\n", str);
free(str); // 释放内存
}
}
2.使用静态分配:将局部数组改为静态数组
char *GetMemory(void) {
static char p[] = "hello world";
return p;
}
3.传递缓冲区:让调用者提供内存。
void GetMemory(char *p) {
strcpy(p, "hello world");
}
void Test(void) {
char str[12];
GetMemory(str);
printf("%s\n", str);
}
所以,在处理指针和内存分配时,特别注意内存的有效性和生命周期是非常重要的!
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
这里的问题是,free(str) 调用后,str 依然保持着之前分配的内存的地址,但是这块内存已经被释放,不再属于程序。因此,它现在是一个悬挂指针。接着,代码检查 str != NULL,但这个检查没有任何实际意义,因为 free 函数并不会设置指针为 NULL,它只是释放指针指向的内存。此时,str 仍然是一个非 NULL 的悬垂指针。
尝试访问或操作悬垂指针指向的内存将导致未定义行为,这可能包括数据损坏、程序崩溃、或者安全漏洞。(非法访问)
进行如下修改:
void Test(void) {
// 为 str 分配 100 个字节的内存
char *str = (char *) malloc(100);
if (str != NULL) {
// 将字符串 "hello" 复制到 str 指向的内存
strcpy(str, "hello");
// 释放 str 指向的内存
free(str);
// 将 str 设置为 NULL,防止悬垂指针
str = NULL;
}
// 由于我们将 str 设置为 NULL,这个条件永远不会为真,
// 因此下面的代码不会被执行,从而避免了未定义行为
if (str != NULL) {
strcpy(str, "world");
printf(str);
}
}
柔性数组是 C 语言中的一个特性,允许在结构体的最后声明一个没有指定大小的数组。用于创建含有可变大小数组的结构体。柔性数组通常用于处理动态大小的数据。
声明方式:在结构体中,柔性数组是通过在最后一个成员声明一个数组而不指定其大小来定义的。
struct my_struct {
int length;
char data[]; // 柔性数组成员
};
内存分配:为使用柔性数组的结构体分配内存时,需要根据实际需要的数组大小动态计算所需内存。
int size = 10; // 假设我们需要10个字符的空间
struct my_struct *p = malloc(sizeof(struct my_struct) + size * sizeof(char));
p->length = size;
使用:柔性数组成员像普通数组一样使用,但是你需要确保不要越界访问
strcpy(p->data, "Hello");
#include
#include
#include
struct my_struct {
int length;
char data[]; // 柔性数组
};
int main() {
int size = 20;
struct my_struct *p = malloc(sizeof(struct my_struct) + size * sizeof(char));
if(p=NULL)
{
return 1;
}
p->length = size;
strcpy(p->data, "Hello, World!");
printf("%s\n", p->data);
free(p);
return 0;
}
后面的size*sizeof(char)的大小是给数组的.
如果不使用柔性数组而是使用指向可变数据的指针,需要分别为结构体和数据动态分配内存。下面是不使用柔性数组,而改用指针实现可变大小数组的方法:
定义结构体
#include
#include
#include
struct my_struct {
int length;
char *data; // 指针指向动态分配的数据
};
分配和初始化结构体
int main() {
int size = 20;
// 首先为结构体本身分配内存
struct my_struct * p= malloc(sizeof(struct my_struct));
// 检查分配是否成功
if (p == NULL) {
// 处理内存分配失败的情况
exit(EXIT_FAILURE);
}
// 然后分配结构体中指针所指向的数据空间
p->data = malloc(size * sizeof(char));
// 检查分配是否成功
if (p->data == NULL) {
// 如果数据分配失败,清理已分配的结构体内存
free(p);
// 处理内存分配失败情况
exit(EXIT_FAILURE);
}
p->length = size;
// 使用分配的内存
strcpy(p->data, "Hello, World!");
printf("%s\n", p->data);
// 释放分配的内存
free(p->data); // 首先释放数据指针
free(p); // 然后释放结构体本身
return 0;
}
在这个例子中,我们通过 data 指针来间接引用一块动态分配的内存,用于存储字符串。与柔性数组相比,这种方法需要额外的一个 malloc 调用来分配指向的数据,并且在释放时,需要分别释放数据和结构体本身。
需要注意的是,使用指针时,可以再次为 p->data 分配不同大小的内存或者使用 realloc 来更改内存大小。这提供了更大的灵活性,但同时也需要更多的内存管理工作。
柔性数组相比于这种方法有一些好处:
关于c语言动态内存管理内容就到此结束,希望对大家有收获!感谢观看!