为什么C语言要有malloc

malloc就是memory allocate动态分配内存,malloc的出现时为了弥补静态内存分配的缺点,静态分配内存有如下缺点:

1、比如说,传统的一维数组,如int a[5],使用传统的一维数组需要事先指定数组的长度,而且数组的长度必须是一个常量(宏定义的 常量)

2、传统数组(静态分配),不能手动释放,只能等待系统释放,静态分配的变量在该函数内运行的时候有效,当静态分配的变量所在函数运行完之后,该内存会自动释放。静态分配的内存,是在栈中分配的,其实在C语言中的函数调用也是通过栈来实现的,栈这种数据结构的一个特点就是(先进后出),所以,在调用函数的时候,都是先压入栈中,然后,再从最上面的函数开始执行,最后,执行到main函数结束。动态分配通过malloc分配,是在堆中分配的,堆不是一种数据结构,它是一种排序方式,堆排序。

3、传统数组的长度一旦定义之后,就不能更改,比如说,如果我有一个业务在这之前给分配的大小为100,但是,我现在由于业务数量的增长,原来的大小就无法满足。

4、静态分配不能跨函数调用,就是无法在另一个函数中,来管理一个函数中的内存。静态分配,只在当前函数有效,当,静态分配所在的函数运行完之后,该变量就不能被其他的函数所调用。

malloc是什么

malloc其实就是一个可以动态分配内存的函数,从而可以很好的弥补上面静态分配的缺点。

malloc怎么用

1、使用malloc函数的时候,需要包含一个头文件#include <malloc.h>

2、malloc函数只接受一个形参如,int *p = (int *)malloc(sizeof(int)).先来解释下这句话的含义,int* p代表一个以int类型地址为内容的指针变量,p这个变量占4个字节(某些计算机),这个p变量是静态分配的一个变量。

在某些计算机的前提下,指针变量所占的大小都是一样的,无论是char * 还是long *,因为,这些指针变量里面存放的是一个8位16进制的地址,所以占四个字节,当然这些都是在某些计算机的前提下,并不是所有的都是这样的。

说道地址的话,就和计算机的地址总线有关,如果计算机的地址总线是32根,每根地址总线只有两种状态(1或0),32根地址线的话,如果全为1的话,刚好就是一个8位十六进制,一位十六进制等于四个二进制(2^4=16)。32根地址总线可以 表示2^10*2^10*2^10*2^2种状态,可以表示的最大内存为4G,也就是说32根地址总线(也就是四个字节 的指针变量)最大可以表示4G内存。

malloc函数会返回开辟空间的首地址,加(int *)的目的是让计算机知道,如何去划分这个开辟的空间,因为char、int 、long这些类型的字节大小是不一样的,我们知道了首地址,还要知道是以几个字节为单元。所以,这句话一共开辟了8个字节(某些计算机上),这也是为什么我写sizeof(int),而不是直接写4的原因。

3、malloc开辟空间所返回的首地址是动态分配的。

malloc使用需要注意的地方

1、申请了内存空间后,必须检查是否分配成功。

2、当不需要再使用申请的内存时,记得释放;释放后应该把指向这块内存的指针指向NULL,防止程序后面不小心使用了它。

3、这两个函数应该是配对。如果申请后不释放就是内存泄露;如果无故释放那就是什么也没有做。释放只能一次,如果释放两次及两次以上会出现错误(释放空指针例外,释放空指针其实也等于啥也没做,所以释放空指针释放多少次都没有问题)。

4、虽然malloc()函数的类型是(void *),任何类型的指针都可以转换成(void *),但是最好还是在前面进行强制类型转换,因为这样可以躲过一些编译器的检查。

malloc从哪里得来了内存空间

1、malloc()到底从哪里得到了内存空间?答案是从堆里面获得空间。也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。

2、什么是堆:堆是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程 初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。

什么是栈:栈是线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立。每个函数都有自己的栈,栈被用来在函数之间传递参数。操作系统在切换线程的时候会自动的切换栈,就是切换SS/ESP寄存器。栈空间不需要在高级语言里面显式的分配和释放。

通过上面对概念的描述,可以知道:

栈是由编译器自动分配释放,存放函数的参数值、局部变量的值等。操作方式类似于数据结构中的栈。堆一般由程序员分配释放,若不释放,程序结束时可能由OS回收。注意这里说是可能,并非一定。所以我想再强调一次,记得要释放!
举个例子,如果你在函数上面定义了一个指针变量,然后在这个函数里申请了一块内存让指针指向它。实际上,这个指针的地址是在栈上,但是它所指向的内容却是在堆上面的!千万不要认为函数返回,函数所在的栈被销毁指针也跟着销毁,申请的内存也就一样跟着销毁了!这绝对是错误的!因为申请的内存在堆上,而函数所在的栈被销毁跟堆完全没有啥关系。

free()到底释放了什么

free()释放的是指针指向的内存,不是指针。指针是一个变量,只有程序结束时才被销毁。释放了内存空间后,原来指向这块空间的指针还是存在。
野指针(wild pointer):
野指针指的是还没有初始化的指针。严格地说,编程语言中每个指针在初始化前都是野指针。一般于未初始化时便使用指针就会产生问题。大多数的编译器都能检测到这一问题并警告用户。
悬空指针(dangling pointer):
当所指向的对象被释放或者收回,但是对该指针没有作任何的修改,以至于该指针仍旧指向已经回收的内存地址,此情况下该指针便称悬空指针。若操作系统将这部分已经释放的内存重新分配给另外一个进程,而原来的程序重新引用现在的悬空指针,则将产生无法预料的后果。因为此时悬空指针所指向的内存现在包含的已经完全是不同的数据。通常来说,若原来的程序继续往悬空指针所指向的内存地址写入数据,这些和原来程序不相关的数据将被损坏,进而导致不可预料的程序错误。这种类型的程序错误,不容易找到问题的原因,通常会导致段错误(Linux系统中)和一般保护错误(Windows系统中)。如果操作系统的内存分配器将已经被覆盖的数据区域再分配,就可能会影响系统的稳定性。

无论是野指针还是悬空指针,都是指向无效内存区域(这里的无效指的是”不安全不可控”)的指针。 访问”不安全可控”(invalid)的内存区域将导致”Undefined Behavior”。也就是说:任何可能都会发生。要么编译失败,要么执行得不正确(崩溃(e.g. segmentation fault)或者悄无声息地产生不正确的执行结果),或者偶尔会正确地产生程序员希望运行的结果。

如何避免使用野指针和悬空指针

对于野指针:养成在定义指针后且在使用之前完成初始化的习惯就好。
对于悬空指针:一个避免这个错误的方法是在释放它的引用后将该指针的值重置为NULL。

在子函数中调用malloc申请内存的方法

方法一:函数返回
将malloc得到的内存首地址通过函数的返回值返回到主函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <malloc.h>
#include <string.h>
char* test()
{
char *p;
p = (char*)malloc(10 * sizeof(char));
strcpy(p, "123456789" );
return p;
}
void main()
{
char *str = NULL ;
str = test();
printf("%s\n", str);
free(str);
}

方法二:二级指针
将malloc得到的内存首地址通过二级指针返回到主函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <malloc.h>
#include <string.h>
void test(char **p)
{
*p = (char*)malloc(10 * sizeof(char));
strcpy(*p, "123456789" );
}
void main()
{
char *str = NULL ;
test(&str);
printf("%s\n", str);
free(str);
}

常见误区:
错误一:使用一级指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <malloc.h>
#include <string.h>
void test(char *p)
{
p = (char*)malloc(10 * sizeof(char));
strcpy(*p, "123456789" );
}
void main()
{
char *str = NULL ;
test(str);
printf("%s\n", str);
free(str);
}

看上去合情合理,把malloc得的地址赋给指针p,这样我们传入的str就指向申请的内存了。但事实是,str的值并没有变化。我们可以先看下方的代码。

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
void test(char c)
{
c = 'B';
}
void main()
{
char ch = 'A' ;
test(ch);
printf("%c\n", ch);
}

调用test()后,主函数里面的ch值还是’A’,而不是’B’。这是因为在调用函数的时候,char c 事实上是被复制进函数内部的,函数内的操作不会影响到原值。 指针也是一样的道理。传入一个一级指针,只能修改它指向的数据,而不能修改它指向的地址。所以我们应该传入一个二级指针,这个指针指向一级指针。这样我们就能修改位于二级指针指向的数据,即一级指针指向的地址了。
错误二:二级指针未指向存在的一级指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <malloc.h>
#include <string.h>
void test(char **p)
{
*p = (char*)malloc(10 * sizeof(char));
strcpy(*p, "123456789" );
}
void main()
{
char **str = NULL ; //原代码:char *str = NULL;
test(str); // test(&str);
printf("%s\n", str);
free(str);
}

为什么我使用了二级指针,仍然是错误的呢?对比下正确的代码,就一目了然了。正确代码中,通过对一级指针str进行取址,得到指向str的二级指针,在子函数中就可以操作str的值了。而错误代码中,二级指针的值为NULL,这样的话,子函数中操作的是地址为NULL的内存,这当然是不对的。

以上内容整理于:
C语言指针之二malloc的用法及详解
C语言中 malloc函数用法
C语言在子函数中调用malloc申请内存的方法