《深入理解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=&num;*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=&num;*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中,只能用函数指针的方式来调用。

理解程序栈和堆有助于理解程序的工作方式和指针的行为。
比如,返回指向局部变量的指针是错误的,原因是为局部变量分配的内存会在后续的函数调用中被覆盖。传递指向常量数据的指针很高效,还可以防止函数修改传入的数据。传递指针的指针可以让参数指针指向不同的内存地址。尤其注意形参和实参的作用原理,形参只是拷贝一份实参,在返回时会被释放,因此要想改变实参,只能靠传递指针的形式,又或者指针的指针。
还有函数指针的使用对控制应用程序的执行序列很有用。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Diode丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值