两者的区别
1、随着程序运行,栈区的内存可能会频繁的初始化和释放;
2、堆区的内存除了用户手动申请释放,否则不会存在内存大小的变化
程序栈是支持函数执行的内存区域,通常和堆共享。也就是说,它们共享同一块内存区域。程序栈通常占据这块区域的下部,而堆用的则是上部。
下图就简单说明程序栈堆的模型
调用函数时,函数的栈帧被推到栈上,栈向上“长出”一个栈帧。当函数终止时,其栈帧从程序栈上弹出。可以理解为直到main函数从程序栈里弹出后,程序就结束了。
- 栈帧所使用的内存不会被清理,但最终可能会被推到程序栈上的另一个栈帧覆盖。
有时候我们听到的一个术语:栈溢出
可以理解为程序栈一直压栈,导致程序的的内存需要超过了真实物理内存,然后程序奔溃;我了解的情况就是有几个:
示意图:
栈帧由以下几种元素组成。
返回地址
函数完成后要返回的程序内部地址。
局部数据存储
为局部变量分配的内存。
参数存储
为函数参数分配的内存。
栈指针和基指针
运行时系统用来管理栈的指针。(我们写代码不用管,系统帮我们来完成)栈指针通常指向栈顶部。基指针(帧指针)通常存在并指向栈帧内部的地址,比如返回地址,用来协助访问栈帧内部的元素。这两个指针都不是C指针,它们是运行时系统管理程序栈的地址
注意要点:
float average(int*arr,int size){
int sum = 0;
printf("arr:%p\n",&arr);
printf("size:%p\n",&size);
printf("sum:%p\n",&sum);
for(int i=0;i<size;i++)
{
sum+=arr[i];
}
return(sum*1.0f)/size;
}
这个代码中,在函数栈里分配空间顺序是: size–>arr–>返回计算值地址–>sum.
1、传递:就是指函数参数是指针,传递的内容是地址
2、返回:就是函数返回值是数据,传递的内容是地址或者数据值
指针传递的优点:
总的来说:为了效率、安全、方便
函数传递的是地址,调用的函数作为处理手段,函数直接修改地址对应的数据值(用指针接引 *pi = 值)。颇有单例模式的味道。
//交换两个变量的值;传入指针,函数对指针进行接引;
//从而获取到指针对应的数据,进而操作数据
void swapwithPointers(int*pnum1,int*pnum2)
{
int tmp;
tmp=*pnum1;
*pnuml=*pnum2;
*pnum2=tmp;
}
// *pnum1 就是接引指针,在main中调用时,其接引值是 n1=5
// *pnum2 就是接引指针,在main中调用时,其接引值是 n2=10
int main()
{
int n1 = 5;
int n2=10;
swapwithPointers(&n1,&n2);
return 0;
}
函数用值传递数据传递的值是一个实参的副本。
用值传递数据一般只为获得一个特定返回值或者是函数的控制参数
void swapwithPointers(int pnum1,int pnum2)
{
int tmp;
tmp = pnum1;
pnuml = pnum2;
pnum2 = tmp;
}
//程序并不能交换数据,
int main()
{
int n1 = 5;
int n2=10;
swapwithPointers(n1,n2);
return 0;
}
指向常量的指针,就是为了 保护指针指向的数据不被修改,设置为只读模式
void passingAddressofConstants(const int*numl, int*num2)
{
*num2 = *num1;
}
void passingAddressofConstantsTest(const int*numl, int*num2)
{
*num2 = 100;
//这里编译报错,说num1是const不能修改;
*num1 = 200;
}
int main()
{
const int limit = 100;
int result = 5;
passingAddressofConstants(&limit,&result);
//使用会报错
passingAddressofConstantsTest(&limit,&result);
return 0;
}
返回指针很容易,只要返回的类型是某种数据类型的指针即可。从函数返回对象时
经常用到以下两种技术。
//申请size x int类型的大小,并用value值初始化的堆区数组
int* allocateArray(int size, int value)
{
int*arr=(int*)malloc(size*sizeof(int));
for(int i=0;i<size;i++)
{
arr[i]=value;
}
return arr;
}
//调用部分
int*vector=allocateArray(5,45);
//这个vector其实我们就可以拿着去干其他事儿
for(int i=0; i < 5; i++)
{
printf("%d\n",vector[i]);
}
尽管上例可以正确工作,但从函数返回指针时可能存在几个潜在的问题,包括:
前三种其实是指针地址不合法问题;最后一种就是内存泄漏问题
函数局部变量的特性是:函数执行完就会销毁;
那返回的地址是局部变量的地址,但是地址的值是有问题的,这个值可能已经被系统给清空、或被其他函数栈给覆盖了
//申请size x int类型的大小,并用value值初始化的堆区数组
int* allocateArray(int value)
{
int arr[20];
for(int i=0;i<size;i++)
{
arr[i]=value;
}
return arr;
}
//调用部分
int*vector=allocateArray(45);
//这里就会出问题
for(int i=0; i < 5; i++)
{
printf("%d\n",vector[i]);
}
解决办法:
就是检查指针是否有效,初始化声明指针时一般都要置其值为:NULL。
int*allocateArray(int*arr,int size,int value)
{
if(arr!=NULL){
for(int i=0;i<size;i++)
{
arr[i]=value;
}
return arr;
}
//调用
int*vector=(int*)malloc(5·sizeof(int));
allocateArray(vector,5,45);
其实就是用来保存指针变量地址的地址;我们使用这样的二维指针来操作一维指针内容。
将指针传递给函数时,传递的是值。如果 我们想修改原指针而不是指针的副本,就需要传递指针的指针。
void allocateArray (int **arr, int size, int value)
{
*arr=(int*)malloc(size*sizeof(int));
if(*arr!=NULL)
{
for(int i=0;i<size;i++)
{
*(*arr+i)=value;
}
}
}
//这样调用
int *vector = NULL
allocateArray(&vector,5,45);
第一次看到声明函数指针的语法时你可能会感到迷惑。不过跟C的很多方面一样,一旦熟悉这种表示法,理解起来就顺理成章了。
下面我们声明一个函数指针,该函数接受空参数,返回空值。
void (*foo)(double, int, /*other parameters*/);
// void : 返回类型
// foo : 函数指针变量名
// double, int, /*other parameters*/ : 函数指针参数类型
几个例子:
//声明一个函数指针
int (*fptrl)(int);
//定义一个函数
int square(int num)
{
return num*num;
}
int n=5;
fptrl=square;
// 也可以使用 取地址符
// fptrl=□
printf("%d squared is %d\n",n, fptr1(n));
//这样定义
typedef int (*funcptr)(int);
使用的例子:
//定义平方函数
int square(int num)
{
return num*num;
}
//定义立方函数
int cube(int num)
{
return num*num*num;
}
typedef int (*funcptr)(int);
funcptr fptr2;
fptr2=square;
printf("%d squared is %d\n", n, fptr2(n));
fptr2=cube;
printf("%d cube is %d\n", n, fptr2(n));
传递函数指针很简单,只要把函数指针声明作为函数参数即可。我们会用下面这个
例子中的add、sub和compute函数来说明如何传递函数指针:
//加法
int add(int numl,int num2)
{
return numl+num2;
}
//减法
int subtract(int numl,int num2)
{
return numl-num2;
}
//定义一个函数指针
typedef int (*fptrOperation)(int,int);
//把函数指针当成一个变量传入
int compute(fptroperation operation,int num1,int num2)
{
return operation(numl,num2);
}
//下面的代码片段说明如何使用这些函数:
printf("%d\n",compute(add,5,6));
printf("%d\n",compute(sub,5,6));
输出是11和-1。add和sub函数的地址被传递给compute函数,后者使用这些地
址来调用对应的操作。本例也说明了使用函数指针可以让代码变得更灵活。
我们可以自主的控制函数流。
指针是一个变量,就可以当成函数返回值来使用
我们用下面的select函数基于输入的字符来返回一个指向对应操作的函数指针。
取决于传入的操作码,它要么返回add函数,要么返回sub函数。
//加法
int add(int numl,int num2)
{
return numl+num2;
}
//减法
int subtract(int numl,int num2)
{
return numl-num2;
}
//定义一个函数指针
typedef int (*fptrOperation)(int,int);
//函数指针返回
fptroperation select(char opcode)
{
switch(opcode)
{
case '+': return add;
case '-': return subtract ;
}
}
//综合来根据计算符号计算
int evaluate(char opcode,int numl,int num2)
{
fptroperation operation=select(opcode);
return operation(num1,num2);
}
// evaluate函数及printf语句的用法如下所示:
printf("%d\n",evaluate('+',5,6));
printf("%d\n",evaluate('-',5,6));
输出是11和-1。
函数指针数组可以基于某些条件选择要执行的函数,声明这种数组很简单,只要把
函数指针声明为数组的类型即可,如下所示。这个数组的所有元素都被初始化为
NULL。如果数组的初始化值是一个语句块,系统会将块中的值赋给连续的数组元
素。
其实就两步:
1、使用typedef把函数指针取为 别名
2、这个别名就可以看出一个变量类型,这个变量类型和基本数据类型一样的声明数组
就像int array[10]; 函数指针别名 array[10]
//这种方法更好理解,一眼能看明白
typedef int (*operation)(int, int);
operation operations [128] = {NULL};
//这个就观感不好,不太好看明白;推荐上面那种
//也可以不用typedef来声明这个数组,如下:
int (*operations[128])(int,int)={NULL};
函数指针数组我知道的就是用在 状态机编程 里比较多。我们可以通过一系列算法,把该调用的函数映射为函数指针数组的索引位置。
就如下面的例子(我们利用的是ASCIl字符的性质,该字符是特殊的int类型,就对应数组的索引)
数组初始化为NULL后,我们把add和sub函数赋给加号和减号对应的元素:
//加法
int add(int numl,int num2)
{
return numl+num2;
}
//减法
int subtract(int numl,int num2)
{
return numl-num2;
}
typedef int (*operation)(int, int);
operation operations [128] = {NULL};
//也可以不用typedef来声明这个数组,如下:
int (*operations[128])(int,int)={NULL};
void initializeOperationsArray()
{
operations['+']=add
operations ['-'] = subtract;
}
int evaluateArray ( char opcode , int numl , int num2 )
{
fptroperation operation;
operation=operations[opcode];
return operation(numl,num2);
}
//用下面的代码测试这些函数:
initializeoperationsArray();
printf("%d\n",evaluateArray('+',5,6));
printf("%d\n",evaluateArray('-',5,6));
执行结果是11和-1。更健壮的evaluateArray函数版本需要在执行函数之前检
查空指针。
一般就是配合函数指针数组进行使用,就是看计算的函数索引值会不会越界。
同时,数组索引的功能,函数指针数组一样都是支持的。【支持等于,数组位置大小比较之类的】
fptroperation fptrl=add;
if(fptrl==add)
{
printf("fptrl points to add function\n");
} else
{
printf("fptrl does not point to add function\n");
}
1、我们可以将指向某个函数的指针转换为其他类型的指针,不过要谨慎使用,因为运行时系统不会验证函数指针所用的参数是否正确。
2、也可以把一种函数指针转换为另一种再转换回来,得到的结果和原指针相同,但函数指针的长度不一定相等。
总之需要慎重使用。
解决参数不同的情况时,我们可以统一定义一个结构体作为通信的包
就像如下例子
typedef struct
{
int operationOne;
int operationTwo;
/*
其他结构体,其他数据
*/
} Parameters;
//加法
int add(Parameters numbers)
{
return Parameters.operationOne + Parameters.operationTwo;
}
//减法
int subtract(Parameters numbers)
{
return Parameters.operationOne - Parameters.operationTwo;
}
typedef int (*operation)(Parameters);
operation ptrFun;
Parameters operations = {11, 23};
ptrFun = subtract;
printf("subtract outcome: %d", ptrFun(operations));