按以前我们学的观念,我们可以将内存区域分为栈区,堆区,静态区。(但其实其内存区域分布复杂多了,这个只是简化版,之后会讲其更细致的内存区域划分版本)
函数使用时分配的内存在栈区,局部变量分配内存也在栈区。函数参数就是在函数内部创造的变量,所以其内存就是在函数分配的内存里,也在栈区。静态函数分配也是在栈区(被static修饰,只能在本文件使用)
全局变量在静态区,静态变量也在静态区(被static修饰的变量),分为静态局部变量和静态全局变量,静态局部变量生命周期跟程序一样长,作用域不变还是只能在其括号里使用;而静态全局变量只能在本文件使用。
堆区中存放的是动态内存,由我们自己去控制分配的。而之后要讲的四个函数malloc,free,calloc,realloc都是跟动态内存有关的函数。 (变量都是在栈区或静态区分配的,不是在堆区)
void* malloc (size_t size);
malloc申请的内存在堆区中分配的,为动态内存。
void free (void* ptr);
2.其中的参数ptr指向的必须为动态内存的起始位置,不能指向动态内存的中间位置,否则会报错
3.别对同一块内存多次释放
4.动态内存开辟后一定要释放,否则会发生内存泄露的问题。
5.对于ptr作用于free后,动态内存被释放,但ptr的值依然不会变(变为野指针),所以因为他变为了野指针,我们此时应该及时将其变为空指针(NULL,NULL被使用需要加头文件stdio.h)
还要额外说一些点。
1.对于被释放的内存我们不能再访问了,再访问属于非法访问系统报错。只能访问被申请的内存。(数组越界访问属于这种);
2.当一个内存被释放后,其存的值并不会被改变,只是其不能再访问了。
对于一个动态内存要被释放掉只有两种方式
1.用free函数释放
2.程序结束 动态内存就自动被释放掉
这是因为动态内存的作用域和生命周期都是一整个程序。所以分配后的动态内存其能用在程序的任何一个地方,并且只有当程序结束时才会被释放。
所以这很容易造成内存泄露问题,导致内存积累,程序运行过慢,解决方法就是当我们用完开辟后的动态内存后就及时释放掉以免造成这种问题
内存泄漏是指程序中已动态分配的堆区内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
void* calloc (size_t num, size_t size);
calloc开辟内存失败同样会返回NULL。 (内存不够就会开辟失败)
void* realloc (void* ptr, size_t size);
ptr 为要调整的动态内存地址(所以在实施realloc之前我们还需要开辟一块内存空间)
size 为调整之后内存空间新⼤⼩(可以变大可以变小)
由于上述的将空间变大时的两种不同情况,realloc函数的使⽤就要注意⼀些。
#include
#include
int main()
{
int *ptr = (int*)malloc(100);
if(ptr != NULL)
{
//业务处理
}
else
{
return 1;
}
//扩展容量
//代码1 - 直接将realloc的返回值放到ptr中
ptr = (int*)realloc(ptr, 1000);//这样可以吗?(如果申请失败会如何?)
//失败的话指向原空间的地址也会变NULL,我们就找不到原空间,它会变为一个隐患,所以代码1不行
//代码2 - 先将realloc函数的返回值放在p中,不为NULL,在放ptr中
int*p = NULL;
p = realloc(ptr, 1000);
if(p != NULL)
{
ptr = p;
//业务处理
}
free(ptr);
return 0;
对于realloc还规定,当其为realloc(NULL,数字)时相当于malloc(数字),其也可以开辟内存空间跟malloc一模一样(同样并不会初始化其内存的值)
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
因为malloc开辟失败的话会返回NULL,这时对其解引用会发生系统错误,所以我们需对其进行条件的区分。如下
int main()
{
int* p = (int*)malloc( 1000000000000000000);
if (p != NULL)
*p = 20;//如果p的值是NULL,就会有问题
else
perror("malloc");
free(p);
}
这样才是严谨的,防止系统发生错误。
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
exit(EXIT_FAILURE);
}
for(i=0; i<=10; i++)
{
*(p+i) = i;//当i是10的时候越界访问
}
free(p);
}
当i=10时访问了未开辟的空间,从而非法访问未开辟的内存,造成系统错误。
这个在讲free函数时也讲过 ,其不能释放非动态开辟内存,否则系统发生错误
void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
}
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
之前在讲free时就讲过这个点,free参数中的指针必须为动态内存的起始位置,不能在动态内存其他位置上否则会导致系统发生错误。
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
在讲free时我们就讲过对同一块内存不能多次释放,否则系统会崩溃
解决方法就是把该指针在执行完后变为NULL,这样就算你手误再次执行该操作,因为之前讲过free(NULL)时什么都不会发生,系统也不会崩溃,所以这样就能防止发生错误系统崩溃
之前在讲free时就讲过内存泄露问题,在使用完该动态内存之后一定要释放,否则会造成内存泄露问题。
现在再次说下。
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
这题错哪了,,大家可能觉得printf(str)是错的 ,但其实它是没问题的,这是一种特殊形式。
错的是其他地方。
当printf(“%s”,地址)时其实也可以变为printf(地址),其可以达到一样的效果。注意如果要用printf(地址)形式写则该地址必须是char*或者const char*形式。 (注意我们只要知道这种情况就行,在代码中看到有人这样用就能知道这代码是干什么的,不至于都不清楚,一般我们自己都不会用这种形式的(少炫点技))
1.注意其传递是值传递,所以str在getmemory后依旧是NULL,从而导致strcpy时发生错误,代码错误。
2.此外还有个小问题,malloc开辟的内存没释放从而导致内存泄露
所以我们应该修改为如下代码才能达到想要的效果
char* GetMemory(void)
{
char * p = (char*)malloc(100);
return p;
}
int main()
{
char* str = NULL;
str = GetMemory();
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
为什么出现这种状况,究其原因是str变成了野指针。
在getmemory中创建了数组p,而后使用完该函数后就销毁了该函数开辟的栈帧(空间)。使str接受的地址变为未开辟的空间,该指针变为野指针。从而在后续用printf函数时其开辟的空间肯定会与getmemory之前开辟的空间有重叠,其printf函数在使用时可能就会重置到数组p所在的空间,其中的值就会被改变,从而打印str时出现上述这种情况。
如果出现这种相似的情况但其结果依然是正常打印出想要的结果,那你完全是运气好,其新函数开辟的空间刚好没在这地址上面或者其新函数在这地址上面开辟但是其要重置的部分刚好不在这地址上,从而就没被修改,打印出正常的结果。这种纯看运气,下次修改下代码可能就会导致打印出的结果发生错误,所以切记不要使用野指针(要及时发现野指针)。
如果要修改就是用static修饰数组,使其在静态区开辟空间,其空间生命周期就跟程序一样长,只有程序结束其才被销毁。 此时它就能实现应有的效果。
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
该题跟第二题很像,都是将str变为野指针,只不过该函数最终能打印出正常结果world,然而它打印出来不要以为代码正确,这只是运气好,printf函数开辟空间后重置的地方没影响到打印正常结果。即使打印出正常结果,代码依然是错误的。
也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
struct cat
{
int i;
int arr[0];//结构体中的柔性数组成员
}
有些编译器会报错无法编译所以我们也可以改成
struct cat
{
int i;
int arr[];//结构体中的柔性数组成员
}
所以这就是柔性数组成员的结构体的声明
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
int main()
{
printf("%d\n", sizeof(type_a));//输出的是4
return 0;
}
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
int main()
{
int i = 0;
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
//业务处理
p->i = 100;
for(i=0; i<100; i++)
{
p->a[i] = i;
}
free(p);
return 0;
}
这就是柔性数组的使用,如上图,含柔性数组的结构体的就是在堆区中分配的。(用了malloc函数)
//代码2
#include
#include
typedef struct st_type
{
int i;
int *p_a;
}type_a;
int main()
{
type_a *p = (type_a *)malloc(sizeof(type_a));
p->i = 100;
p->p_a = (int *)malloc(p->i*sizeof(int));
//业务处理
for(i=0; i<100; i++)
{
p->p_a[i] = i;
}
//释放空间
free(p->p_a);
p->p_a = NULL;
free(p);
p = NULL;
return 0;
}
像该代码不用柔性数组同样可以实现相同的效果。
但是用柔性数组有两个好处:
扩展阅读:这个文章很好,推荐看下,让你对其有更清楚的认知:
C语言结构体里的成员数组和指针 | 酷 壳 - CoolShell
之前讲的内存区域划分为了便于我们理解,都是简化后的版本。没简化的内存区域划分比这复杂多了,现在我们讲讲没简化的内存区域划分。(它们属于c/c++中的内存区域划分)
内存分为如上六大空间:
内核空间相当于我们的禁区,用户代码不能读写在其上面,我们写代码时是用不到内核空间的(它是给系统操作系统自己用的,)
栈区我们之前就讲过了,这里就不讲了,它的内存分配是由高地址到低地址分配的。
内存映射段我们现在学的太少了,就先不讲了。
堆区我们之前也讲过了,这里也不讲了,它的内存分配是由低地址到高地址分配的。
数据段就是静态区,我们也讲清楚了。
代码段存放的是可执行代码和只读常量。我们写的代码经过编译和链接最终形成二进制指令,这些二进制指令就是我们可执行代码,它们总要存放吧,于是就存放在代码段。 而只读常量就比如我们的常量字符串(“adsds”)和常量数字(如40),它们也存放在代码段中,这些只读常量只能被读取使用,不能被修改。
所以这就是c/c++的内存区域划分。
所以现在我们就把动态内存管理这一篇章给讲完了,下篇文章我将给大家介绍文件操作这一篇章。
感谢大家的支持❤️(*/ω\*)!!!