学习进步、培养兴趣、发掘潜力。
十、指针和动态内存
1、堆与栈
在一个典型的架构中,分配给应用程序的内存可以被分为4个区段,如下图所示:
来看下面这段代码:
#include<stdio.h>
int total;
int Square(int x)
{
return x*x;
}
int SquareOfSum(int x,int y)
{
int z=Square(x+y);
return z;
}
int main()
{
int a=4,b=8;
total=SquareOfSum(a,b);
printf("output = %d",total);
}
当程序执行时,系统的栈上的内存分配如下图:
在任何时候,都是栈顶的函数在执行,而其他的函数会暂停,等待上面的函数返回一些值然后再恢复执行。而一旦函数结束执行,其之前占用的栈上内存也将被清除。
注意:如果栈的增长超出了预留的内存空间,导致最后耗尽了栈空间(也被称为栈溢出),那么程序将会崩溃。(例如无穷递归)
与栈不同,应用程序的堆并不是固定的,大小可变、也没有特定规则来分配和销毁相应的内存。
为了在C中使用动态内存,我们需要知道4个函数:malloc、calloc、realloc、free。
为了在C++中使用动态内存,我们需要知道两个操作符:new、delete
malloc返回的是void型指针,实际上,malloc函数所做的工作就是:从堆上找到空闲的内存、预留内存并通过指针返回。需要注意的是,当我们调用完malloc函数后,其仍在堆上占有内存,所以我们需要使用free来释放内存。(与栈不同,栈上的内存在程序执行完后会自动释放)
如果malloc找不到空闲的内存块、将返回NULL。
2、malloc calloc realloc free
malloc:malloc是在进行动态内存分配时最常使用的库函数之一,malloc的函数定义为:void* malloc(size_t size);参数是内存块的字节数大小,size_t指的是正整数,类似于unsigned int。因为malloc返回的是一个void指针,所以我们在使用内存前,首先需要进行指针类型转换。
calloc:calloc的函数定义为:void* calloc(size_t num,size_t size);calloc同样返回一个void指针,但是calloc接收的是两个参数,第一个参数是特定类型元素的数量、第二个参数是类型的大小。malloc与calloc有一些不同之处,当malloc分配完内存后,并不会对其进行初始化,但是如果使用的是calloc,会对其进行初始化为0。
下面这个例程很好的解释了calloc的用法:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int n;
printf("Enter size of array\n");
scanf("%d",&n);
int *A =(int*)calloc(n,sizeof(int));
for(int i=0;i<n;i++)
{
A[i] = i+1;
}
}
realloc:如果拥有一块动态分配的内存,当想修改内存块的大小时,可以使用realloc。realloc函数定义为:void* realloc(void* ptr,size_t size);当传入恰当的参数时,realloc可以作为free或者malloc的替代品。
free:任何分配了的动态内存在程序结束之前都会一直存在。为了释放内存,可以使用函数free。
3、内存泄露
概念:内存泄露指的是动态申请了内存后,但是使用完之后却没有释放内存。
十一、函数返回指针
首先来看下面这段代码:
#include<stdio.h>
#include<stdlib.h>
int Add(int* a,int* b)//a和b是整型指针
{
printf("Add函数中a的地址 = %d\n",&a);
printf("Add函数中a的值(main函数中a的地址) = %d\n",a);
printf("存储在Add函数中a地址内的值 = %d\n",*a);
int c= (*a)+(*b);
return c;
}
int main()
{
int a=2,b=4;
printf("main函数中a的地址= %d\n",&a);
int c=Add(&a,&b);
printf("Sum = %d\n",c);
}
可以看到,当程序运行后,Add函数中a的值(main函数中a的地址)与main函数中a的地址的结果是一模一样的。
接下来看下面这段程序:
#include<stdio.h>
#include<stdlib.h>
void PrintHelloWorld()
{
printf("Hello World\n");
}
int *Add(int* a,int* b)
{
int c = (*a)+(*b);
return &c;
}
int main()
{
int a=2,b=4;
int* ptr = Add(&a,&b);
PrintHelloWorld();
printf("Sum = %d\n",*ptr);
}
仅仅是加上了一个打印Hello World的函数,为什么就无法得到想要的结果呢?
***让我们来分析一下:当发生函数调用时,栈上将会分配一些内存,这些内存就被称为那个函数的“栈帧”,当程序执行时,首先main函数被调用,栈上分配一个供main函数运行的栈帧。main函数的局部变量放在这个栈帧中。当main函数执行到调用Add函数时,main函数将会暂停,而在栈上会分配Add函数执行所需要的内存,而之前说过,在任何时间,在执行的函数都会是栈顶的那个函数,所以main函数将等待Add函数完成返回。C计算*a+*b的值,最后,将C的地址写入ptr。但是,当Add函数调用完成后,系统将擦除Add在栈上所占的内存空间,而这时再调用PHW函数时,之前的ptr内地址可能已经分配给了PHW函数使用,这就导致了数据丢失。
从栈底向上传输参数是可行的,但是从栈顶向下传输参数时,数据可能会丢失。
那么,在什么情况下,我们我们想要从函数返回一个指针呢?
当我们在堆上有一个内存地址,或者在全局区有一个变量时,我们就可以安全返回它们的地址。因为堆上分配的内存需要显式释放(这点与栈不同,栈上内存是自动释放的)。而全局区的任何东西的生命周期是整个程序的执行期间。
十二、函数指针
函数指针,顾名思义就是用来存储函数的地址的指针。通过应用函数指针,可以解引用和执行函数。
首先,我们需要理解程序的定义:程序可以认为是一组顺序的计算机指令集合,我们可以使用C语言进行编程,但是在计算机的底层,他们最终都将以二进制的格式执行,任何需要被执行的程序都将被编码为2进制格式。所以实际上,当我们编写程序时,我们使用C或C++作为高层语言,再讲这些源代码作为编译器的输入,编译器进而编译出机器代码。
.c文件->.exe文件(可执行文件)
#include<stdio.h>
int Add(int a,int b)
{
return a+b;
}
int main()
{
int c;
int(*p)(int,int);//函数指针中的参数要和所指向的这个函数的参数类型一致。
p=&Add;
c=(*p)(2,3);
printf("%d",c);
}
十三、函数指针的使用案例
回调函数:一个函数的引用传给另外一个函数时,那个函数就被称为回调函数。下面的A就是一个回调函数。
#include<stdio.h>
void A()
{
printf("Hello");
}
void B(void (*ptr)())
{
ptr();
}
int main()
{
B(A);//A是一个回调函数
}