最近在力扣刷题时,发现普通的创建数组很容易引起编译器报错。
而使用到动态内存分配malloc()函数和初始化函数memset()可以避免这类问题的发生。本文将详细介绍一下两个函数的
malloc()
malloc函数的原型:
(void *)malloc(size_t size)
malloc 函数接受一个 size_t 类型的参数 size,表示要分配的内存块的大小(以字节为单位)。它返回一个指向分配内存块起始位置的 void* 类型指针,即通用指针。内存分配成功之后,malloc函数返回这块内存的首地址,你需要一个指针来接受这个地址。
int *ptr = (int *) malloc(sizeof(int) * length);
malloc分配了内存,但没有为它指定名字。然而,它却可以返回那块内存第一个字节的地址。
因此,可以把那个地址赋值给一个指针变量,并使用该指针来访问那块内存。
因为char代表一个字节,所以传统上曾将malloc()定义为指向char的指针类型。
然而,ANSIC标准使用了一个新类型:指向void的指针。这一类型被用作“通用指针”。
函数malloc()可用来返回数组指针、结构指针等等,因此一般需要把返回值的类型指派为适当的类型。
在ANSIC中,为了程序清晰应对指针进行类型指派,但将void 指针值赋值给其他类型的指针并不构成类型冲突。
需要注意的是,malloc 函数分配的内存块是未初始化的,其中的内容是不确定的。如果你需要初始化内存块为特定的值,可以使用 calloc 函数来分配并初始化内存。
另外,如果 malloc 函数无法成功分配所需的内存块,它将返回 NULL。因此,在使用分配的内存之前,应该始终检查返回的指针是否为 NULL,以确保内存分配成功。
我们使用malloc()来创建一个 数组。可以在程序运行时使用malloc()请求一个存储块,另外还需要一个指针来存放该块在内存中的位置。
例如,如下代码:
double * ptd;
ptd = (double * ) malloc (30 * sizeof(double));
这段代码请求30个double类型值的空间,并且把ptd指向该空间所在位置。
注意:ptd是作为指向一个double类型值的指针声明的,而不是指向30个double类型值的数据块的指针。
记住:数组的名字是它第一个元素的地址。
因此,如果令ptd指向一个内存块的第一个元素,就可以像使用数组名一样使用它。
也就是说,可以使用表达式ptd[0]来访问内存块的第一个元素,pd[1]来访问第二个元素,依此类推。
正如前面所学,可以在指针符号中使用数组名,也可以在数组符号中使用指针。
现在,创建一个数组有三种方法:
1.声明一个数组,声明时用常量表达式指定数组维数,然后可以用数组名访问数组元素。
2.声明一个变长数组,声明时用变量表达式指定数组维数,然后用数组名来访问数组元素(这是C99的一个特性)。
3.声明一个指针,调用malloc(),然后使用该指针来访问数组元素。
一般地,对应每个malloc()调用,应该调用一次free()。
函数free()的参数是先前malloc()返问的地址,它释放先前分配的内存。
这样,所分配内存的持续时间从调用malloc()分配内存开始,到调用free()释放内存以供再使用为止。free()的参数应是一指针,指向由malloc()分配的内存块,
我们来看一个理想模型,可以认为程序将它的可用内存分成了三个独立的部分:
一个是具有外部链接的、具有内部链接的以及具有空链接的静态变量的:
一个是自动变量的:
(静态变量):
在编译时就已经知道了静态存储时期存储类变量所需的内存数量,存储在这一部分的数据在整个程序运行期间都可用。
这一类型的每个变量在程序开始时就已存在,到程序结束时终止。
(动态变量):
然而,一个自动变量在程序进入包含该变量定义的代码块时产生,在退出这一代码块时终止。
因此,伴随着程序对函数的调用和终止,自动变量使用的内存数量也在增加和减少。
典型地,将这一部分内存处理为一个堆栈。这意味着在内存中,新变量在创建时按顺序加入,在消亡时按相反顺序移除。
(动态内存分配):
动态分配的内存在调用malloc()或相关函数时产生,在调用free()时释放。
由程序员而不是一系列固定的规则控制内存持续时间,因此内存块可在一个函数中创建,而在另一个函数中释放。(malloc 可以跨函数调用)
由于这点,动态内存分配所用的内存部分可能变成碎片状,也就是说,在活动的内存块之间散布着未使用的字节片。不管怎样,使用动态内存往往导致进程比使用堆栈内存慢。
free()
malloc与free函数如影随形,free函数用于释放通过动态分配函数(如malloc、calloc或realloc)分配的内存块。它的函数原型如下:
void free(void* ptr);
free函数接受一个指向动态分配内存块的指针作为参数,将该内存块释放,并返回给内存管理系统以供重用。
使用free函数时需要注意以下几点:
- 传递给free函数的指针必须是通过动态分配函数分配的有效指针。如果传递给free的指针是NULL,则函数不执行任何操作。
- 一旦内存被释放,就不应再访问该内存块,否则会导致未定义的行为。
- 不要试图释放静态分配的内存(如全局变量或自动变量),这将导致未定义的行为。
- 可以多次调用free函数释放同一块内存,但是这样做是不推荐的,因为重复释放同一块内存可能导致问题。
以下是一个示例,展示如何使用malloc和free函数分配和释放内存:
int* ptr = (int*)malloc(sizeof(int)); // 分配内存
if (ptr != NULL) {
*ptr = 10; // 使用内存
printf("Value: %d\n", *ptr);
free(ptr); // 释放内存
}
在调用 free 函数释放内存后,指针本身并不会消失。指针变量仍然存在,但是它不再指向有效的内存块。
当你调用 free 函数释放内存时,它会将该内存块返回给内存管理系统以供重用,但并不会对指针变量本身做任何修改。指针仍然指向原来的地址,但这块地址不再是有效的动态分配内存。在释放内存后,访问已释放的内存块将导致未定义的行为,可能会引发访问非法内存的错误。
为了避免访问已释放的内存块,一种良好的实践是在释放内存后将指针设置为 NULL,示例如下:
int* ptr = (int*)malloc(sizeof(int)); // 分配内存
if (ptr != NULL) {
// 使用内存
*ptr = 10;
// 释放内存
free(ptr);
ptr = NULL; // 将指针设置为 NULL
}
// 现在 ptr 是一个空指针,指向 NULL
可以将 free 理解为解除内存和指针之间的联系。当你调用 free 函数时,它将释放动态分配的内存块,并将该内存块返回给内存管理系统以供重用。这个操作会使指针与之前指向的内存块之间的联系断开。
在调用 free 之后,指针仍然存在,但它不再指向有效的内存块。这意味着你不能再通过该指针来访问已释放的内存块,因为该内存块已经不再属于你的程序。尝试访问已释放的内存块将导致未定义的行为,可能会产生不可预测的结果。
通过释放内存并解除与指针之间的联系,你告诉内存管理系统可以将该内存块重新分配给其他需要它的部分。这有助于有效地管理内存并避免内存泄漏。
记住,在调用 free 后,为了避免使用野指针,最好将指针设置为 NULL,以表示它不再指向有效的内存块。这样,如果你意外地尝试使用指针,程序会引发空指针异常,从而提醒你检查和修复代码中的问题。
memset()
memset是一个初始化函数,作用是将某一块连续的内存中全部初始化为指定的值。
void *memset(void *s, int c, size_t n);
s指向要填充的内存块。
c是要被设置的值。
n是要被设置该值的字符数。
返回类型是一个指向存储区s的指针。
总的作用:将已开辟内存空间 s 的首 n 个字节的值设为值 c。
memset可以方便的清空一个结构类型的变量或数组
如:struct sample_struct
{
char csName[16];
int iSeq;
int iType;
};
对于变量:struct sample_strcut stTest;
一般情况下,清空stTest的方法:
stTest.csName[0]=‘/0’;
stTest.iSeq=0;
stTest.iType=0;
用memset就非常方便:
memset(&stTest,0,sizeof(struct sample_struct));
如果是数组:struct sample_struct TEST[10];
则memset(TEST,0,sizeof(struct sample_struct)*10);
常见错误:
第一:memset函数按字节对内存块进行初始化,所以不能用它将int数组初始化为0和-1之外的其他值(除非该值高字节和低字节相同)。
第二:memset(void *s, int ch,size_t n);中ch实际范围应该在0~~255,因为该函数只能取ch的后八位赋值给你所输入的范围的每个字节,比如int a[5]赋值memset(a,-1,sizeof(int )*5)与memset(a,511,sizeof(int )*5) 所赋值的结果是一样的都为-1;因为-1的二进制码为(11111111 11111111 11111111 11111111)而511的二进制码为(00000000 00000000 00000001 11111111)后八位都为(11111111),所以数组中每个字节,如a[0]含四个字节都被赋值为(11111111),其结果为a[0](11111111 11111111 11111111 11111111),即a[0]=-1,因此无论ch多大只有后八位二进制有效,而后八位二进制的范围在(0~255)中改。而对字符数组操作时则取后八位赋值给字符数组,其八位值作为ASCII码。
第四: 过度使用memset.
char buffer[4];
memset(buffer,0,sizeof(char)*4);
strcpy(buffer,"123");
//"123"中最后隐藏的'\0'占一位,总长4位。
这里的memset是多余的. 因为这块内存马上就被全部覆盖,清零没有意义.
另:以下情况并不多余,因某些编译器分配空间时,内存中默认值并不为0:
char buffer[20];
memset(buffer,0,sizeof(char)*20);
memcpy(buffer,"123",3);
//这一条的memset并不多余,memcpy并没把buffer全部覆盖,如果没有memset,
//用printf打印buffer会有乱码甚至会出现段错误。
//如果此处是strcpy(buffer,"123");便不用memset,
//strcpy虽然不会覆盖buffer但是会拷贝字符串结束符
memset函数在初始化处理时非常方便,但也有其局限性,比如要注意初始化数值,要注意字节数等等。当然,直接选择用for循环或while循环来进行初始化也是可以的,只不过memset更快捷一些。