《深入理解C指针》精华摘录与解读(二)指针&函数
解读《深入理解C指针》,以及附上一些书中未提到的知识点,结合来理解。
关于指针与数组、字符串、结构体的共同应用会在解读(三)&(四)指针&数组、结构体、字符串中。
很多内容可以参考同专栏的其他文章,像指针&数组、结构体、字符串,指针基础概念与内存分配,什么时候要用指针等。
本篇的重点在指针与函数的共同应用。以及内存的详解
.
.
三、指针&函数
3.1 程序的栈和堆
①、程序栈存放栈帧,栈帧存放函数参数和局部变量,而堆管理动态内存。程序栈和堆共享同一内存区域,通常栈占据该内存下部,堆则是上部。
②、栈是先进后出原则(FILO),因此数据只能从栈顶插入和删除。而堆里的内存可以随意存入和移除。
注:堆地址向"上"生长,即向着内存地址增加的方向。而栈地址向下生长,即向着内存地址减小的方向。(说栈“向上”生长,一般指先入栈在底部的形式)在下面第④点会验证。
③、栈帧的组成:
(1)、返回地址:函数完成后要返回的程序内部地址。(例如,调用一个函数时要将当前函数的当前地址入栈,然后结束调用时再出栈返回原先函数调用的位置)
(2)、为局部变量分配的内存。
(3)、为函数参数分配的内存。
(4)、栈指针和基指针(帧指针)
注:栈指针通常指向栈顶部。帧指针通常存在并指向栈帧内部地址。
④、栈帧内存示例:
#include <stdlib.h>
#include <stdio.h>
int arr1[] = {1,3,5,7};
float func(int *arr,int size){
int sum = 0;
printf("arr:%p\n",&arr);
printf("size:%p\n",&size);
printf("sum:%p\n",&sum);
/*
for语句中用到的变量i没有包含在栈帧中。C把块语句当做“微型”函数,会在合适的时机将其推入栈和从栈中弹出。
*/
for(int i=0;i<size;i++){
sum += arr[i];
printf("sum=%d\n",sum);
}
return (sum*1.0f)/size;
}
int main()
{
func(arr1,sizeof(arr1)/sizeof(int));
return 0;
}
编译结果:
调用func函数的入栈示意图如下:
由上2图可知创建栈帧时,先将参数以声明的反顺序入栈,然后才是局部变量。
⑤、每个线程通常都会有自己的程序栈。一个或多个线程访问内存中的同一个对象可能会导致冲突。
3.2 通过指针传递和返回数据
①、传递指针可以让多个函数访问指针所引用的对象,而不用把对象声明为全局可访问。
②、传参时,活用指针可以大大提高效率,例如传递一个大型结构体时,如果传递整个结构体,运行就会很慢,而且栈帧也会占用过多内存,此时传递一个结构体指针便大大提高效率。
3.2.1 指针传递数据和值传递数据
③、指针传递数据和值传递数据:看下面一个例子,可以看出形参传值和传指针的区别。
void swapValue(int num1,int num2)
{
int temp;
temp = num1;
num1 = num2;
num2 = temp;
printf("-------swapValue-------\n\r");
printf("num1=%d\n\r",num1);
printf("num2=%d\n\r",num2);
}
void swapPoint(int *p1,int *p2)
{
int temp;
temp = *p1;
*p1 = *p2;
*p2 = temp;
printf("-------swapPoint-------\n\r");
printf("*p1=%d\n\r",*p1);
printf("*p2=%d\n\r",*p2);
}
int main()
{
int a = 5;
int b = 10;
swapValue(a,b);
printf("a=%d\n",a);
printf("b=%d\n",b);
swapPoint(&a,&b);
printf("a=%d\n",a);
printf("b=%d\n",b);
return 0;
}
编译结果:
注:当形参和实参不是指针类型时,函数运行时,形参和实参是不同的变量。他们在内存中位于不同的位置,形参会将实参的内容复制一份,在函数结束时释放形参,而实参的内容不会改变。
但是函数的参数是指针变量时,调用该函数过程中,传给函数的是实参的地址。即函数操作的就是实参本身,因此可以改变实参的值。
3.2.2 传递指向常量的指针
④、传递指向常量的指针:可以高效率的传递数据,而且数据不会被修改。(传参时注意数据类型)
void passingPci(const int* srcValue,int* dstValue){
*dstValue = *srcValue;
}
int main(){
const int cValue = 10;
int value = 5;
passingPci(&cValue,&value);
printf("after passingPci---\n\r");
printf("value=%d\n\r",value);
return 0;
}
编译结果:
3.2.3 返回指针
⑤、返回指针
使用malloc在函数内部分配内存并返回其地址。调用者负责释放返回的内存。
下例尽管可以正确工作,但任需注意以下问题:
- 返回未初始化的指针;
- 返回指向无效地址的指针和局部变量的指针;(其实已经在函数返回时销毁了)
- 返回指针但是没有释放内存。
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 main(){
int *arrx= allocateArray(5,2); //调用时将地址返回后,arr变量消失,但是所引用的内存还在。因此后面需要释放
for(int i=0;i<5;i++){
printf("arrx[%d]:%d\n\r",i,arrx[i]);
}
free(arrx); //调用者有责释放内存
return 0;
}
编译结果:
这里注意,如果main
函数中for(int i=0;i<5;i++)
改成
for(int i=0;i<sizeof(arrx)/sizeof(arrx[0]);i++)
那编译结果就会是:
因为sizeof(数组名),返回的是数组所有元素占有的内存空间字节数。
sizeof(指针名),返回的是计算机地址字节数。
而*指针名
是数组第一个元素的解引,所以上面sizeof(arrx)/sizeof(arrx[0])
的值其实就等于:4/4=1
3.2.4 返回局部数据指针
⑥、返回局部数据指针
如果上述函数改成下例(声明局部数组赋值),那么函数返回时,返回的数组地址就无效了。
注: 数组对应着一块内存区域,而指针是指向一块内存区域。
int* allocateArray(int size,int value){
int arr[size];
for(int i=0;i<size;i++){
arr[i]=value;
}
return arr;
}
//函数返回到main时,返回的数组地址就无效了,如果接着调用了其他函数,就可能直接覆写原本地址的值。
可以将数组声明为static int arr[size];
这样就将该变量分配在全局数据区(静态存储区)。
⑦、避免空指针错误
当指针做参数传递给函数时,先判断是否为空是个好习惯。
int* allocateArray(int *arr,int size,int value){
if(arr != NULL){
for(int i=0;i<size;i++){
arr[i]=value;
}
}
return arr;
}
3.2.5 传递指针的指针(多级指针)
⑧、传递指针的指针
详细参考同专栏的另一篇为什么要用指针,什么时候该用指针,什么时候该用指针的指针。
像如下代码,因为实参传入形参时,形参p无法改变实参num(因为形参只是拷贝了一份实参,返回时会销毁拷贝的内容)。
void pMalloc(int *p,int size)
{
p = (int *)malloc(size*sizeof(int));
}
int main()
{
int *num = NULL; //如果是全局和static会自动初始化为NULL
pMalloc(num,5);
if(num!=NULL)
{
*num = 200;
printf("num=%d",*num);
}
else
{
printf("Assignment Fail");
}
return 0;
}
编译结果:
而如果将传入的参数变成实参变量的地址的话,即传入的是一个二级指针,那么*p
和**p
都可以改变,不能改变的是p:
(多级指针如果很乱的话,就像下面程序中p=#*p=num;**p=*num
这样抽丝剥茧即可。)
void pMalloc(int **p,int size)
{
*p = (int *)malloc(size*sizeof(int));
}
int main()
{
int *num = NULL; //如果是全局和static会自动初始化为NULL
pMalloc(&num,5); //p=#*p=num;**p=*num
if(num!=NULL)
{
*num = 200;
printf("num=%d",*num);
}
else
{
printf("Assignment Fail");
}
return 0;
}
编译结果:(成功)
3.2.6 重写更安全的free函数
⑨、重写更安全的free函数
free函数
有一些缺陷,例如不会检查传入的指针是否为NULL,也不会将释放的指针置为NULL。因此像如下这样重写一个safeFree
,并用宏精简化:
/* 传入指针的指针以允许修改传入的指针,void*类型可以传入所有类型指针,但是调用时要注意将指针类型显式转换成void* */
/* 假设要释放的是int *pi,那么传入&pi,则p=&pi,*p=pi,**p=*pi
即*p和**p都是可以修改的,只有传入的形参p无法更改 */
void safeFree(void **p)
{
if((p!=NULL)&&(*p!=NULL))
{
free(*p);
*p=NULL;
}
}
/* 用带参宏简化,注意要将&p显式转换成void** */
#define SAFE_FREE(p) safeFree((void**)&(p))
3.3 函数指针
指针也可以指向函数。
3.3.1 声明函数指针
①、声明函数指针:void (*foo)(参数类型)
,即返回值为void,函数指针变量名为foo的函数。如果去掉第一对括号(void* foo()
)就成了返回值为void*
类型的普通函数。
注:使用函数指针时一定要小心,因为C不会检查参数传递是否正确。
②、函数指针在命名约定上的建议是用fptr
做前缀。
3.3.2 使用函数指针
③、传递函数给函数指针就像数组一样,函数名就相当于函数地址,如下例:
int (*fptr1)(int); //声明一个函数指针
int square(int num)
{
return num*num;
}
int main()
{
int n = 5;
fptr1 = square;
printf("%d squared is %d\n", n, fptr1(n));
return 0;
}
/*
编译结果:5 squared is 25
*/
*④、使用typedef
为复杂的声明定义一个新的简单的别名。在声明时就可以用变量名来进行声明,例如下方:
typedef int (*funcptr)(int); //这样就可以用funcptr声明一个返回int,参数int的函数指针。
funcptr fptr2; //等于 int (*fptr2)(int);
fptr2 = square;
printf("%d squared is %d\n", n, fptr2(n));
/*
编译结果:5 squared is 25
*/
3.3.3 传递函数指针
⑤、传递函数指针就是把函数指针声明作为函数参数即可。注意传参时指向的函数的参数是否正确即可。如下例:
typedef int (*funcptr)(int,int);
int add(int num1,int num2)
{
return num1+num2;
}
int sub(int num1,int num2)
{
int value;
if(num1>num2)
value=num1-num2;
else
value=num2-num1;
return value;
}
int calc(funcptr ftpr,int num1,int num2)
{
return ftpr(num1,num2);
}
int main()
{
printf("2+5=%d\n\r",calc(add,2,5));
printf("|2-5|=%d\n\r",calc(sub,2,5));
return 0;
}
/*编译结果:
2+5=7
|2-5|=3
*/
3.3.4 返回函数指针
⑥、在上例增加一个函数select
,编译结果依旧。
funcptr select(char calcSymbol)
{
switch (calcSymbol)
{
case '+':
return add;
case '-':
return sub;
default:
break;
}
}
int main()
{
printf("2+5=%d\n\r",calc(select('+'),2,5));
printf("|2-5|=%d\n\r",calc(select('-'),2,5));
return 0;
}
3.3.5 函数指针数组
⑦、函数指针数组可以基于某些条件选择要执行的函数,下例在⑤的基础上继续修改,这个数组的目的是可以用一个字符索引选择对应的函数来执行。
typedef int (*funcptr)(int,int);
funcptr funop[128]={NULL};
/*也可以直接写:
int (*funop[128])(int,int)={NULL};
*/
void alloArray()
{
funop['+'] = add;
funop['-'] = sub;
}
int calc(funcptr ftpr,int num1,int num2)
{
if(ftpr != NULL) //建议养成好习惯
{
return ftpr(num1,num2);
}
return 0;
}
int main()
{
alloArray();
printf("2+5=%d\n\r",calc(funop['+'],2,5));
printf("|2-5|=%d\n\r",calc(funop['-'],2,5));
return 0;
}
/*编译结果依旧*/
3.3.6 比较函数指针
⑧、可以用相等和不等操作符来比较函数指针。
例如if(ftpr == add)
。
3.3.7 转换函数指针
⑨、转换函数指针时,建议使用既不返回结果也不接受参数的函数指针做过渡函数指针,如typedef void (*fptrBase)()
。
typedef int (*fptr_1)(int);
typedef int (*fptr_2)(int,int);
typedef void (*fptrBase)();
int add(int num1,int num2)
{
return num1+num2;
}
int main()
{
fptr_2 foo2 = add; //双int参数函数指针
fptrBase fooBase; //无参数无返回值
fooBase = (fptr_1)foo2; //转换单int参数函数指针
foo2 = (fptr_2)fooBase; //转换回双int参数函数指针
printf("2+5=%d",foo2(2,5));
return 0;
}
/*编译结果:
2+5=7
*/
注:一定要确保给函数指针传递正确的参数,否则会造成不确定的行为。
*⑩、在嵌入式系统中经常有些不在代码段中的系统函数,在ROM中,只能用函数指针的方式来调用。
结
理解程序栈和堆有助于理解程序的工作方式和指针的行为。
比如,返回指向局部变量的指针是错误的,原因是为局部变量分配的内存会在后续的函数调用中被覆盖。传递指向常量数据的指针很高效,还可以防止函数修改传入的数据。传递指针的指针可以让参数指针指向不同的内存地址。尤其注意形参和实参的作用原理,形参只是拷贝一份实参,在返回时会被释放,因此要想改变实参,只能靠传递指针的形式,又或者指针的指针。
还有函数指针的使用对控制应用程序的执行序列很有用。