[笔记]指针与函数

前言:在我的认知里,指针与函数不过就是传值和传址的作用,在深入学习的途中,我发现我的知识面太过于薄弱,下面通过借鉴“那年那聪”大佬下CSDN中发布的博客进行一个整理和归纳,顺便增加自己的理解,以便于学习和对新手的帮助!

指针函数函数指针的表达方法不同,千万不要混淆

最简单的辨别方式就是看函数名前面的指针*号有没有被括号()包含

如果被包含就是函数指针,反之就是指针函数

主要的区别是一个是指针变量,一个是函数,在使用是必须要明白才能正确的使用。

在学习指针与函数之前,我们先来复习一下什么是函数:

  • 概念:函数是一个功能的集合,它可以根据输入完成特写的功能,并将结果输出(当然有的时候函数只是为了实现一些特写功能而不一定要有输入或者输出)
  • 函数分为库函数(library function)他是编译程序提供的,二为用户自定义函数(user-defined function),它是由用户编写的,本章笔记只针对后者
  • 函数的调用可分为传值调用(caa by value)传址调用(call by address)
  • 传值调用表示实际参数传给形式参数的是值(value),而传址调用则是传地址(address)
  • 这个说法很抽象,因为在C语音中并无绝对的传址调用,它调用的无非就是指针的己值罢了
  • 格式:返回类型_函数名_参数表(输出值)

我们来看几个简单的函数声明

img简单的函数声明

声明了函数,我们就可以调用他,下面来看看

img

为什么要前置声明呢?在以后的学习当中,我们编写的函数往往会提供给很多程序来调用,而且在函数内部往往也要调用其他函数

如果不对函数做前置声明,多个相互调用的函数将无法通过编译器编译,也就无法使用

我们在剖析一个简单的代码

img简单的函数代码

上图函数的作用为计算两值的平均数,返回给average,为传值调用

总结来说,函数就是把功能包装起来,以便调用,我们先来解决函数的第一个难题,参数类型为指针,数组,和指向函数的指针

函数还有很多难点,令我最感到有趣的就是递归跟可变参数列表(stdarg宏)这个在以后的复习中会记录下来

指针为参数的函数(传址调用)

我们来看看最著名的例题:两数对调案例,他能很好的解释传值和传址的概念

#include<stdio.h>
#include<conio.h>
void swap_by_address(int*, int*);//前置声明函数,参数为int指针(int*)类型
void swap_by_value(int, int);//前置声明函数,参数为int类型
int main()
{
    int x = 100, y = 200;
    printf("传值调用\n");
    printf("经过value以前\n");
    printf("x=%d,y=%d\n", x, y);
    
    swap_by_value(x, y);
    //调用value函数,将x,y拷贝一份传给形参
    
    printf("经过value函数之后\n");
    printf("x=%d,y=%d\n\n", x, y);

    x = 100; y = 200;
    printf("传址调用\n");
    printf("经过address函数之前\n");
    printf("x=%d,y=%d\n", x, y);
    printf("经过address函数之后\n");
    
    swap_by_address(&x, &y);
    //调用address函数,将x,y的地址拷贝一份传给形参
    
    printf("x=%d,y=%d\n\n", x, y);
    
    getchar();
    return 0;

}

void swap_by_address(int* a, int* b)
{
    int temp;//定义一个局部变量
    temp = *a;//取到x的值赋给temp
    *a = *b;//取到y的值传入x的地址内
    *b = temp;//取到temp的值赋给y的地址内
}//完成两数对调

void swap_by_value(int a, int b)
{
    int temp;
    temp = a;
    a = b;
    b = temp;
}

这段代码中最重要的语句是:

swap_by_address(&x, &y);

它的作用是将取x,y两个变量的地址传入给形参,那么在address函数中操作的便是他的地址

value和address的区别在哪呢?我们来看组分析图

img

从图中我们可以看到,实参x,y只是想数值拷贝了一份给形参a,b

在value函数中形参数值进行转换,结果并不会影响实参的数值

再看address函数的转换图

img画的不好别嫌弃

从图中可以看出,因为把x,y的地址作为实参传入给形参x,y

所以函数中一切转换都是在地址上操作,取某一地址的值传入某一地址

最后的结果存储在地址内,所以在使用address函数之后,x,y地址内存储的数值已经发生了改变

最后看看运行结果:

img

再看一个著名案例:寻找数组中的最大值

#include<stdio.h>
#include<conio.h>
int findMax(int[], int);
//前置声明函数,参数为一个int类型的数组,一个int类型变量
int main()
{
    int i;
    int arr[] = { 20,10,100,40,60,80,90 };
    //定义一个int类型的数组,长度由计算机分配
    int elements = sizeof(arr) / sizeof(arr[0]);
    //计算数组长度,利用总长度除以单个元素长度
    int maxNumber = finMax(arr, elements);
    //将arr的首地址和数组长度作为实参传给形参
    //将finMax返回的值赋给maxNumber变量
    
    for (i = 0; i < elements; i++)
        //定义一个for循环,判定条件为i<数组长度,运算为i++
    {
        printf("arr[%d]=%d\n", i, arr[i]);
        //逐个输出数组值
    }
    printf("\n");
    printf("最大值为:%d", maxNumber);
    getchar();
    return 0;

}

finMax(int x[], int n)//定义函数finMax
{
    int j;
    int max = x[0];//定义一个max变量,将x[0]的值赋给它
    for (j = 1; j < n; j++)
        //定义一个for循环,从1开始,判定条件小于形参n,运算递增
    {
        if (x[j] > max)
            //判定x[j]是否小于x[0]
        {
            max = x[j];
            //小于的话将x[j]赋值给max
        }
    }
    return max;
    //函数返回max
}

这种写法似乎更好理解,arr为一个数组

finMax(arr, elements);

这一语句将arr的首地址,也就是arr[0]的地址拷贝传给了形参x

函数形参中x[]的[]标识符可以意会成*标识符,由于数组分配地址为连续性的,所以函数得以操作整个数组

函数在传参时,类型必须一致,否则将造成程序错误

我们来看看运行结果,详细的都在注释内:

img

为了更好的理解传址调用,我们再来看一个案例:一维数组求和

#include<stdio.h>
#include<stdlib.h>
int sum(int x[], int n);
//前置声明sum函数,参数为一个int型数组,一个int型变量
int main()
{
    int i;
    int arr[] = { 10,20,30,40,50,60,70,80,90,100 };
    //定义一个int型数组,数组长度为计算机自定义
    int elements = sizeof(arr) / sizeof(arr[0]);
    //计算数组下标
    int total=sun(arr,elements);
    //将arr的首地址和数组下标数作为实参传入给sum函数

    for (i = 0; i < elements; i++)
        //定义一个for循环,从0开始,判定为小于下标数,运算递增
    {
        printf("arr[%d]=%d\n", i, arr[i]);
        //逐个输出数组元素
    }
    printf("数组元素之和为:%d\n", total);
    //输出数组之和
    getchar();
    return 0;

}
int sun(int x[], int n)
//定义sum函数
{
    int t = 0;
    for (int j = 0; j < n; j++)
        //定义一个for循环,从0开始,判定条件小于数组下标,运算递增
    {
        t += x[j];//将t+x[j]的值赋值给t
    }
    return t;//返回t
}

在这一代码内,也是将arr的首地址拷贝一份给形参,从而操作整个数组

sun(arr[],elements)可以转换成sum(*arr,elements)

arr[j]也可以转换成*(arr+j)

一样都是取地址内的值在函数体内进行操作,我们来看看运算结果

img

下面重头戏:二维数组求和

这次我们用数组指针来写代码

#include<stdio.h>
#include<stdlib.h>
int sum(int(*p2)[], int n);//前置声明变量sum
//拥有一个数组指针参数,一个int变量参数
int main()
{
    int i,j,row,column,total = 0;
    //定义五个变量全等于0
    int arr[][2] = { 10,20,30,40,50,60,70,80,90,100 };
    //定义一个二维数组,拥有两列,行由计算机自分配
    int elements = sizeof(arr) / sizeof(arr[0][0]);
    //计算数组长度,由数组总长度除以数组单个元素长度
    row = elements / 2;
    //计算行长度,利用数组长度除以列长度

    column = 2;
    total = sum(&arr[0][0], row);
    //将数组首地址与行数作为实参传给sum形参
    printf("数组的行长度为%d\n", row);
    
    for (i = 0; i < row; i++)
        //定义for循环,从0开始,判定条件小于数组行数,运算递增
    {
        for (j = 0; j < column; j++)
            //内嵌for循环,从0开始,判定条件小于数组列数,运算递增
        {
            printf("arr[%d][%d]=%d\n",i,j ,arr[i][j]);
            //逐个输出数组数值
        }
    }

    printf("二维数组之和为:%d", total);
    getchar();
    return 0;
}
int sum(int (*p2)[2], int n)
//定义函数,参数为一个数组指针,指向一个拥有两列的数组
//二参数为一个int型变量
{
    int i, j, t = 0;
    for (i = 0; i < n; i++)
        //定义for循环,从0开始,判定条件小于行数,运算递增
    {
        for (j = 0; j < 2; j++)
            //内嵌for循环,从0开始,判定条件小于列数,运算递增
        {
            t += *(*(p2 + i) + j);
            //t=t+数组值
        }
    }
    return t;
}

在上一轮复习的时候我们知道,数组指针有一个别名,叫做行指针,它用于指向二维数组,更好的操作二维数组中的值

在代码中,sum(arr,row)中的arr为数组的首地址,也就是0行0列的地址,我们可以把它改为&arr,&arr0,&arr[0],*(arr+0)+0,结果都是一样的,不过他们虽然值一样,都是数组的入口地址,但是意义可不一样,最好使用arr或者&arr

在函数定义中的*(*(p2 + i) + j);,其实就等于p2i,我们来剖析下这段语句

p2先于括号里的结合得到p2[i]的地址,再取到p2[i]的地址得到p2[i],就是数组的i行首地址,再加上j,偏移j个int型字节,得到数组i行j列的地址,然取到p2i的值,加上t赋给t,这样就完成了所以数组的值求和了

我们来看看运行结果

img

总结:

上面我们说了函数的两种传参类型,传值和传址的区别,通俗点说,传值就是把某一变量的值作为实参拷贝一份给形参,这并不影响原地址内的值,而传址就是把某一变量的地址,也就是某一指针的值作为实参拷贝一份给了形参,这样函数内的所有运算都基于地址,这会改变原有地址内的值

至于第三种的引用传递是C++的方式,在此处我们不做多详解,后面的Java笔记也会涉及到引用,那是后话

在学习完两种参数的传递方式后,我们来了解一下指针函数和函数指针

一、指针函数

  • 定义:带指针的函数,它的本质是一个函数,它的返回类型是某一类型的指针(一变量地址)
  • 格式:类型标识符* 函数名(参数表)

例如:int* f(x,y)

上例语句:

  • int为类型标识符
  • f为函数名
  • (x,y)为参数

首先它是一个函数,只不过这个函数的返回值是一个地址值,指针函数一定要有函数返回值,而且在主调函数中,函数返回值必须赋给同类型的指针变量

换个说法,就是需要将函数返回值赋给其他变量时,函数返回值的类型必须跟该变量的类型相同

例如:

float *fun();//定义一个浮点型的指针函数,函数名为fun,无参数,返回类型为浮点型
float *p;//定义一个浮点型的指针,指针名为p
p=fun();//将p指向fun的首地址

当一个函数声明其返回值为一个指针时,实际上就是返回一个地址给调用函数,以用于需要指针或地址的表达式中

由于返回的是一个地址,所以类型说明符一般都是int

int *f(int a , int b);

上面函数声明又可以写成如下形式:

int* f(int a , int b);

让指针标识符 * 与 int 紧贴到一起,而与函数名 f 间隔开,这样看起来明了些, f 是函数名,返回值类型是一个 int 类型的指针。

#include<stdio.h>
#include<conio.h>
int* pf(int[], int);
//前置声明指针函数,它本身是一个函数,pf是它的函数名
//它返回的类型为一个指针类型,函数有两个参数
//一个为int数组类型,一个为int型变量
#define MAX 5
//定义一个全局常量,MAX,常量为5
int k[MAX];
//定义一个全局数组型变量,数组长度为5
int main()
{
    int total = 0, k;
    int i[MAX] = { 10,20,30,40,50 };
    //定义一个整型数组变量,数组长度为5
    int* ptr;
    //定义一个一级指针变量
    ptr = pf(i, MAX);
    //将指针指向pf函数的首地址
    //函数传入参数为i的首地址和MAX的值
    printf("ptr=%#x\n\n", ptr);
    //输出指针的地址
    for (k = 0; k < MAX; k++)
        //定义一个for循环,从0开始,判定条件为小于5,运算递增
    {
        printf("k[%d]=%d\n", k, *(ptr + k));
        total += *(ptr + k);
    }
    printf("数组之和=%d\n", total);
    getchar();
    return 0;
}
int* pf(int x[], int n)
{
    int m;
    int j[] = { 100,200,300,400,500 };
    for (m = 0; m < n; m++)
    {
        k[m] = j[m] + x[m];
        printf("k[%d]=%d\n", m, k[m]);
    }
    printf("\npf=%#x\nk=%#x\n", pf, k);
    return k;
}

没加注释是因为语句需要一句一句的来讲解

int* pf(int[], int);

我们看这段话,它声明了pf是一个函数,因为()优先级最大,它先于pf结合,函数内有两个参数,一个为int数组类型, 一个为int变量类型,它返回的是一个int指针型变量!!!也就是说,它返回的是一个地址

再来看看:ptr = pf(i, MAX);

将pf的返回值赋给ptr,等同于,将ptr指向pf的返回值!!

我们要看看函数内的功能是什么,下面是重点:

img

它将i的首地址作为实参传入给pf函数的形参,这时x[]形参的值就是i[]的值

这时它又定义了一个数组,j,具体看图

img

定义了一个for循环,从0开始,判定小于n,这时n形参等于MAX值的拷贝,也就是5,这段语句等同于for(m=0;m<5;m++)

img

m从0开始进入循环,我们把它转换成第一遍循环

k[0]=j[0]+x[0]

  • k[0]是一个左值,我们先不看
  • j[0]是取到j+0的值,就是100,
  • x[0]等同于i[0]就是10
  • 这段语句等同于将100+10赋给k+1的值,就是k[0],这时k[0]等于110

这个循环等于遍历k数组

img

这段代码我一直不明白作用在哪,如果有懂的可以跟我说,原书题目是p,k,但是我找了半小时都不知道p在哪里,k可以理解成输出全局数变量k的地址

img

最后返回k的值,但是k是一个地址,是函数的首地址,在经历过for循环后,k+0的值已经为110,则返回地址内存储的值也是110

函数的功能剖析完了,我们可以看看主函数的功能了

img

主函数的主要语段在这个for循环上,在for循环之前,ptr已经指向了函数的返回值k,等同于,ptr的值就是k的地址

for循环的作用在于遍历k数组,*(ptr+k),我们来模拟它的第一遍循环

ptr先于0相加,得到ptr+0的地址,然后取到地址ptr[0]的值,为110

在将total+110赋给total,得到求和目的

我们来看看结果:

img

注意:全局变量k[]中k是数组名,而主函数的局部变量k的变量名,全局和局部关乎于malloc和free与堆栈,在后续复习中将会记到

总结

指针函数总的来说就是返回的是一个指针类型变量,它可以赋给同一类型的指针,该指针可以通过返回的地址去操作地址内的值

函数指针:

看完指针函数,我们来了解了解函数指针

  • 定义:指向函数的指针,它本身是一个指针,指向具有相同参数类型的函数(指向函数名,函数名为函数的首地址)
  • 格式:类型标识符(*指针名)(参数表)
  • 说明:指针与指向的函数必须:返回值类型一致,参数类型,个数一致,它指向的是函数的地址,也就是函数执行第一段代码的地址

在处理函数指针的时候,有一些特别需要注意的地方,我们来看看下面几段代码:

  • int add(int, int);
  • int (*pf)(int, int);
  • pf=add;

pf为指针名,它指向拥有两个参数,参数类型为int的函数,因为运算符优先级,()比优先级大,所以pf必须用括号括起来,否则就成了指针函数

上面的使用方法是正确的,函数指针的类型,参数都与需要指向的add函数相对应,通过pf可以调用add

我们来看看反面例子

  • double add1(double,double);
  • int add2(int );
  • int (*pf)(int ,int);
  • pf=add1;
  • pf=add2;

上面两段赋值都是错误的,pf为一指针,它指向拥有两个参数类型为int的函数,返回值为int类型

而add1是一个返回类型为double,参数两个为double的函数

而add2为一个返回类型为int,参数为一个int类型的函数

一样,看看一段案例:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<conio.h>
int add(int, int);//前置声明函数,返回为int类型,两个参数为int类型
int sub(int, int);
int mul(int, int);
int div(int, int);
int (*op)(int, int);//定义一个指针,指向拥有两个int类型的函数,返回为int类型
int main()
{
    int x, y, output;
    printf("请输入x与y:");
    scanf("%d,%d", &x, &y);//将输入的值传入x,y的地址

    op = add;
    //将op指向add的首地址
    output = (*op)(x, y);
    //将函数返回的值传给output
    printf("%d+%d=%d\n", x, y, output);

    op = sub;
    output = (*op)(x, y);
    printf("%d-%d=%d\n", x, y, output);

    op = mul;
    output = (*op)(x, y);
    printf("%d*%d=%d\n", x, y, output);

    op = div;
    output = (*op)(x, y);
    printf("%d/%d=%d\n", x, y, output);

    getchar();
    return 0;
}
int add(int a, int b)
{
    return a + b;
    //返回a+b
}

int sub(int a, int b)
{
    return a - b;
}

int mul(int a, int b)
{
    return a * b;
}

int div(int a, int b)
{
    return a + b;
}

为什么注释那么少,因为代码太简单了

op的作用是指向某一函数,它可以指向任何满足要求的函数,op的值为该函数的首地址,也就是函数内第一段代码的地址

程序中有四个函数,分别是加(add),减(sub),乘(mul),除(div),同时也声明了一个函数指针(指向函数的指针),当要处理加法时,则将add指定给op,同理,要处理减法时,将sub指定给op便可

总结:

函数指针的功能相当于数组指针,一个是指向二维数组,好操作数组内的值,一个是指向函数,以便于操作函数,而不用重复调用造成代码混乱以及出错,它两都需要指向的对象满足要求,参数一样,列数一样等等

到这里指针与函数的基本学习就结束的,我们进入问题演练

本文链接:

https://nullcode.fun/107.html
1 + 9 =
快来做第一个评论的人吧~