目录
一.malloc函数的介绍
malloc的用法
举个例子
注意点
浅谈数据结构里的动态分配空间
二.calloc函数的介绍
三.realloc函数的介绍
四.柔性数组的介绍
为什么有些时候动态内存函数头文件是malloc.h,有些时候却是stdlib.h
一.malloc函数的介绍
malloc其实就是动态开辟空间,其实就是让程序员自己去决定开辟多大空间
int a=5,在栈空间上开辟4个字节去存变量a,char b=1在栈空间上开辟1个字节去存变量b,包括数组char arr[10]以上空间都是固定的,这种开辟空间是编译器自动完成分配的,在程序运行之前就已经分配好了,编译器负责将源代码翻译成机器语言,并确定变量在内存中的存储方式和位置。操作系统负责管理内存并为程序分配空间,但编译器决定了变量在内存中的布局和大小。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间⼤⼩在程序运⾏的时候才能知
道,那数组的编译时开辟空间的⽅式就不能满⾜了。
C语⾔引⼊了动态内存开辟,让程序员⾃⼰可以申请和释放空间,就⽐较灵活了。
malloc的用法
void * malloc (size_t size)
这个函数向内存申请⼀块连续可⽤的空间,并返回指向这块空间的指针。
• 如果开辟成功,则返回⼀个指向开辟好空间的指针。
• 如果开辟失败,则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查。
• 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者⾃
⼰来决定。
• 如果参数 size 为0,malloc的⾏为是标准是未定义的,取决于编译器。
举个例子
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int arr[10] = { 5,4,6,3,4,5,7,8,9,10 };
int* ptr = (int*)malloc(20 * sizeof(int));
if (ptr != NULL) {
memcpy(ptr, arr, 10 * sizeof(int)); // 将原数组的内容拷贝到新分配的内存中
for (int i = 0; i < 10; i++)
{
printf("%d ", *(ptr + i));
}
}
free(ptr);//结束要把指针指向的空间释放,将指针置为空
ptr = NULL;
}
(int*)malloc(20 * sizeof(int)),动态分配20个int类型的空间,最后返回值是指向这片空间的指针,我这片空间是准备用来存整型的(int),所以要强制转换成(int *),用整型指针int *ptr来接收
同样也可以写成这样(int *)malloc(80),给一个具体的要开辟空间的大小数字
动态内存题目
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void Get(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
Get(str);
strcpy(str, "hello world");
printf(str);
}
本意是在函数Get中给str分配空间,然后把字符串“hello world”复制给它
结果是直接报错,运行不起来,为什么呢。
str是个指针,本来应该放置的是它指向空间的地址,但是此时置为空指针,所以传过去用指针形式的p去接收。但是接收到的是str里面的值,而不是str指向地址,此时p里面的值也为NULL。
再然后开辟了一个一百字节大小的空间,并把这个地址赋值给了p
再然后问题就来了,此时str其实是传值传参,形参是实参的一份临时拷贝,形参的改变不会影响实参,str不会去指向开辟的那个空间的地址,str里面依旧为NULL,p出了函数这个作用域就会销毁了,它只能在作用域内使用
所以第一个改进方案来了,形参不是只能在它这个作用域里有效吗,那么我直接在作用域里完成strcpy操作不就行了,p是实打实保存的是指向我们开辟空间的地址,所以在p相关的函数里完成剩下的操作就行
第二种解决方案,str最后因为函数作用域以及传值传参的影响,p的值改变不会影响实参str,但是return过去的值我可以用str接收到啊,直接把p此时的值(此时p内部是保存开辟的100空间的地址)return回去,让str接收依旧能完成接下来的操作,相当于现在str内部保存的不是NULL而是开辟100字节的空间的地址了
第三种解决方案,既然传值传参是临时拷贝,形参的改变不会影响实参,那么 有没有什么办法改变实参呢。传地址传参过去,直接解引用操作不就是在原来的地方操作吗,但是需要注意的是此时str是个指针变量,它取地址传过去要用二级指针接收
别忘了要手动用free释放开辟的空间,同时把指针置为空
注意点
1.使用malloc分配空间一定要先判断接收的指针是否为空,然后再去使用。这是因为malloc函数分配空间不是百分百能成功的,如果分配空间太大,而系统原有的空间不足,这时malloc函数将返回NULL。所以为了确保程序的健壮性,我们通常会在使用malloc函数分配内存空间后,先检查返回的指针是否为NULL
2.在使用完了malloc函数末尾一定要将这片空间释放掉,malloc函数分配的空间不会自动释放,它将一直占用系统资源,直到程序结束才会被系统回收。忘记释放不再使用的动态开辟的空间会造成内存泄漏
3.free释放完空间后,一定要将接收的指针置为空,否则的话该指针会变成野指针。如果我们在使用已经释放的内存空间的指针,可能会导致程序崩溃或者产生不可预期的结果。
浅谈数据结构里的动态分配空间
#include<stdio.h>
typedef char ElemType;
#define MaxSize 50
#include<stdlib.h>
typedef struct
{
ElemType data[MaxSize];//存放线性表中的元素
int length;//存放线性表的长度
}SqList;//顺序表的类型
void CreateList(SqList*& L, ElemType a[], int n)
{
int i = 0, k = 0;
L = (SqList*)malloc(sizeof(SqList));
while (i < n)
{
L->data[k] = a[i];
k++;
i++;
}
L->length = k;
}
以上常见的顺序表的声明和创建顺序表的函数
typedef struct
{
ElemType data[MaxSize];//存放线性表中的元素
int length;//存放线性表的长度
}SqList;//顺序表的类型
正常来讲,结构体末尾应该是结构体变量,但是用了typedef将结构体起个小名为了SqList,所以SqList是个类型名,意思是这个顺序表类型名叫SqList
typedef char ElemType;也是这个意思,把char类型取个小名ElemType,但是注意小名能用,大名同样也能用,都是指同一个东西
void CreateList(SqList*& L, ElemType a[], int n) 这个SqList *L其实是结构体指针,为什么会写得这么奇怪呢SqList*& L,这个&不是取地址的意思,而是引用的意思。c语言里传值传参形参的改变不会影响实参,通过引用可以在函数内部修改指针L所指向的结构体,而不是仅仅修改指针本身的值。但是需要注意的是c语言没有引用,引用是c++里面的,所以有些纯c语言编译器可能会报错。
同样纯c语言编译器中,可以通过用二级指针SqList **L接收L这个指针变量的地址,这时也可以在函数内部修改指针L所指向的结构体
(SqList*)malloc(sizeof(SqList));此时是结构体类型,所以要强制转换成结构体指针类型,然后才能用结构体指针L去接收。L=(SqList *)malloc(sizeof(SqList))意思就是分配一个空间去存顺序表,空间大小为结构体大小
二.calloc函数的介绍
这个函数其实和malloc函数差不多,都是动态分配空间,区别在于malloc开辟了空间,这个空间里的值它不会给你初始化,里面的值一开始都是随机值。而calloc它给你开辟空间的同时,还会帮你初始化为0
可以很明显的看出,malloc函数开辟的空间除了我放的五个值,其他的都是随机值
calloc函数你不指定值,它默认初始化为0
malloc函数参数只有一个 ,纯数字也可以,例如(强制转换的类型)malloc(100)
而calloc函数参数有两个,(强制转换的类型)calloc(要开辟多少个这样尺寸的元素,要转换类型的尺寸)。 例如(int *)calloc(10,sizeof(int))意思就是开辟10个int类型的空间
三.realloc函数的介绍
有时会我们发现过去申请的空间太⼩了,有时候我们⼜会觉得申请的空间过⼤了,那为了合理的时
候内存,我们⼀定会对内存的⼤⼩做灵活的调整。那 realloc 函数就可以做到对动态开辟内存⼤
⼩的调整
说白了,realloc就是对用malloc函数和calloc函数开辟空间大小的修改
void* realloc (void* ptr, size_t size)
ptr 是要调整的内存地址
• size 调整之后新⼤⼩
• 返回值为调整之后的内存起始地址,用指针接收
• 这个函数调整原内存空间⼤⼩的基础上,还会将原来内存中的数据移动到新的空间。
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc( 10*sizeof(int));//开辟一个可以存10个int类型的空间
if (p != NULL)
{
for (int i = 0; i < 5; i++)//只放五个值来观察一下剩下的初始值
{
*(p + i) = i;
}
for (int i = 0; i < 10; i++)//现在看看所有的值的情况
{
printf("%d ", *(p + i));
}
}
int* pre = (int*)realloc(p, 20 * sizeof(int));
if (pre != NULL)
{
p = pre;
}
else
{
printf("出错了");
}
}
以上代码就是malloc函数已经分配了10个int类型空间基础上再用realloc函数扩容至20个整型的情况
realloc扩容有两种情况,在malloc函数扩容空间的后面再加上需要扩容的空间,如果后面空间足够没有被别的数据占据的话,会直接在后面增加需要扩容的空间,同时返回的是之前malloc函数开辟空间的地址。
第二种情况,如果后面扩容的空间不够,已经被别的数据用了,那么realloc函数会在堆区另外找一个合适大小的空间去使用。会把原先malloc开辟空间的数据直接拷贝过来,然后释放删除原先的那块空间,返回的是在堆区新找空间的地址
需要注意的是虽然是扩容,但是realloc函数也可能扩容失败返回NULL ,如果用记住了malloc地址的p指针直接去接收realloc扩容后空间的地址,此时扩容失败返回NULL,会直接失去原先malloc函数中已经保存的数据。所以要先用pre判断是否为空,再然后把记住新空间地址的指针变量pre赋值给p
四.柔性数组的介绍
柔性数组是指结构中最后一个元素允许是未知大小的数组,柔性数组前面必须至少一个其它成员。具体表现形式如下
struct stu
{
int i;
int arr[];//也可以int arr[0];
};
一开始sizeof这个结构体是不会把这个数组加进去计算内存对齐的,结构体的大小只是柔性数组前面成员的大小
只有int类型成员的大小
在使用这个数组之前要采用malloc函数给这个数组开辟至少大于结构体大小的空间
开辟方法可以选择malloc函数开辟结构体字节大小的基础上在加上需要给柔性数组开辟多大的空间的值,这样就保证了分配给柔性数组的大小大于结构体的大小,适应柔性数组预期大小
#include<stdio.h>
#include<stdlib.h>
struct stu
{
int i;
int arr[];//也可以int arr[0];
};
int main()
{
struct stu* pre = (struct stu*)malloc(sizeof(stu) + 100 * sizeof(int));
pre->i = 200;//给int i赋初始值200
if (pre != NULL)
{
for (int j = 0; j < 100; j++)
{
pre->arr[j] = j;//给柔性数组赋值,检验一下,是否开辟空间成功
printf("%d ", pre->arr[j]);
}
free(pre);
pre = NULL;
}
}
此时再来看一下这个结构体的大小
为什么依旧是4呢,这是因为malloc函数是在程序运行时动态分配的 ,而sizeof计算大小是在编译时计算的,所以无法包括柔性数组的大小。要计算柔性数组的大小,需要单独进行计算或者使用其他方法来获取其大小。意思计算sizeof先计算了结构体的大小,然后柔性数组才被分配了空间大小
第二种开辟方法,先单独给结构体开辟空间,然后再给柔性数组开辟空间
第⼀个方法好处是⽅便内存释放
如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯做了⼆次内存分配,并把整个结构体返回给用户。用户调⽤free可以释放结构体,但是⽤⼾并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返回给用户⼀个结构体指针,⽤⼾做⼀次free就可以把所有的内存也给释放掉。
第⼆个方法的好处是:这样有利于访问速度连续的内存有益于提⾼访问速度,也有益于减少内存碎片
为什么有些时候动态内存函数头文件是malloc.h,有些时候却是stdlib.h
据C语言标准,malloc.h不是标准C头文件,而是在早期的一些C编译器中使用的头文件。而stdlib.h是标准C头文件,包含了malloc函数的声明以及其他一些常用函数的声明。
在现代的C编译器中,通常推荐使用stdlib.h而不是malloc.h,因为stdlib.h是标准C头文件,可以在所有符合C标准的编译器中使用,而malloc.h可能不是所有编译器都支持。
因此,尽管在某些旧的代码中可能会看到使用malloc.h的情况,但在新的开发中,应该优先选择使用stdlib.h来包含malloc函数的声明。
虽然malloc.h名字没有包含realloc和calloc,但是其实这两个函数也能用这个头文件。
数据结构课本上有些时候还是使用malloc.h头文件来使用动态内存函数