• C语言深度解析:函数的形参和实参与传值和传址(多角度分析,让你有一种相见恨晚的感觉!!!!)


    目录

    一、前言

    二、函数的形参和实参

    🍑实参

    🍎形参 

    🍇形参与实参之间的区别 

    🍐形参与实参之间的联系(重点!!!!) 

     三、函数的传值和传址(超重点!!!)

    🍒例题分析

    🍍错误分析 

    🥝 错误修改

    🍋 深入理解 值传递与址传递

     💦概念理解

    四、进阶练习(公司面试题)

    🍈题目 

    🍓 题目分析与改正

    五、总结

    六、共勉


    一、前言

    在C语言中指针与函数的结合是一件很麻烦的事情,用起来总是丈二和尚,摸不着头脑,特别是在参数传递,申请空间等,把我折磨的是一头雾水。也许大家对于这些知识都是一知半解(除过一些大佬O),没有真正的去搞透彻,一问我都会(这就是我),但是真正引用的时候就会出现很多问题,而且百度的时候又看不懂,所以为了帮助大家解决这个问题(实际上是自己不会),在这里进行一个全面的介绍(形参和实参与传值和传址)。

     

    二、函数的形参和实参

    🍑实参

    知识点1:实参(实际参数)

    🔑在调用有参函数时,函数名后面括号中的参数被称为“实参”,通俗的来讲,就是我们真实传递给函数的参数。

    🔑实参可以表示为:常量、变量、表达式、函数等。

    🔑无论实参是何种类型的量,在进行函数调用时,他们必须有确定的值,以便于把这些值传递给形参。

    💦代码举例解析

    1. #include
    2. #include
    3. #include
    4. #include
    5. int add(int x, int y) //加法函数
    6. {
    7. int sum = 0;
    8. sum = x + y;
    9. return sum;
    10. }
    11. int main()
    12. {
    13. int a = 0, b = 0;
    14. printf("请输入a,b的值:\n");
    15. scanf("%d %d", &a, &b);
    16. // 真实传递给函数的参数就是实参
    17. int sum1 = add(1, 2); // 实参可以是常量
    18. printf("输出1 + 2 的和:");
    19. printf("%d\n", sum1);
    20. int sum2 = add(a, b); // 实参可以是变量
    21. printf("输出a + b 的和:");
    22. printf("%d\n", sum2);
    23. int sum3 = add(a + b, a - b); //实际参数可以是表达式
    24. printf("输出(a+b)与(a-b)的和:");
    25. printf("%d\n", sum3);
    26. int sum4 = add(add(a, b), 6); //实际参数可以是函数
    27. printf("输出函数(2+3)+5的和:");
    28. printf("%d\n", sum4);
    29. return 0;
    30. }

    💦结果输出

    🍎形参 

    知识点2:形参(形式参数)

    🔑形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的和过程中才实例化(分配内存单元),所以叫形式参数。

    🔑形参出现在函数定义中,在整个函数体内都可以使用,离开函数则不能使用

    🔑形参没有确定的值

    通俗的来讲:形参定义时,编译系统并不为其分配存储空间,也没有初始值,只是在函数调用时,临时分配存储空间,接受来自实参的值。函数调用结束,内存空间释放,值消失。

    💦代码举例解析:还是用上面的代码测试

    1. #include
    2. #include
    3. #include
    4. #include
    5. int subtraction(int x, int y) // 注意:此时在subtraction函数里的 x,y就是形式参数
    6. {
    7. return x - y;
    8. }
    9. int main()
    10. {
    11. int a, b;
    12. printf("请输出a,b的值:\n");
    13. scanf("%d %d", &a, &b);
    14. printf("请输出a-b的值:");
    15. int sum = 0;
    16. sum = subtraction(a, b);
    17. printf("%d\n", sum);
    18. return 0;
    19. }

    🔑注意:

    形参只有在函数中有效

        

    形参在函数调用之前还未创建

    函数调用过程中,形参才被实例化

    函数调用结束,形参生命周期结束,形参会被立刻销毁

     💦结果输出

      

    🍇形参与实参之间的区别 

    知识点3:形参与实参之间的区别

    🍐形参与实参之间的联系(重点!!!!) 

    知识点4:形参与实参之间的联系

    💦在讲到形参与实参的联系,这里就牵扯出了函数调用的两大类(不同的类别,形参与实参之间的联系就不同)

    🔑 调用

    官方理解: 函数的形参和实参分别占有不同的内存块,对形参的修改不会影响实参

    通俗的理解:形参是实参的拷贝,改变形参的值并不会影响外部实参的值。从被调用函数的角度来说,值传递是单向的(实参->形参)

    🔑 调用

    官方理解

    ▶ 传值调用是把函数外部创建的内存地址传递给函数参数的一种调用函数的方式

    ▶ 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量

    通俗的理解:形参为指向实参地址的指针,当对形参进行指向操作时,就相当于实参本身进行的操作。

    💦讲到这里大家肯定会有疑问,那么什么时候用传值调用,什么时候用传址调用,怎么用呢?

    接下来我将回答大家依次解答。

     三、函数的传值和传址(超重点!!!)

    🍒例题分析

    在理解传值调用传址调用的之前我们先来看一道题目,从题目中区分他们之间的联系

    例题:写一个函数可以交换两个整型变量的内容

    例如:

    交换前:20 30
    交换后:30 20

    代码展示:用函数的方式书写:

    1. #include
    2. #include
    3. #include
    4. #include
    5. //交换函数
    6. void Swap1(int x, int y) // 其中 x ,y 是形参
    7. {
    8. int z = 0;
    9. z = x;
    10. x = y;
    11. y = z;
    12. }
    13. int main()
    14. {
    15. int a = 0;
    16. int b = 0;
    17. scanf("%d%d", &a, &b);
    18. //交换两个变量
    19. printf("交换前: a=%d b=%d\n", a, b);
    20. Swap1(a, b); // 其中 a , b 是实参
    21. printf("交换后: a=%d b=%d\n", a, b);
    22. return 0;
    23. }

    💦看看运行结果:

    1. 20 30 // 输入 a b 的值
    2. 交换前:a=20 b=30
    3. 交换后:a=20 b=30

     啊,看到这个结果,大家应该会大吃一惊吧,我们不是已经通过函数进行数值交换了吗?

    怎么会出现的这样的结果呢?

    不急,等我给大家娓娓道来哦。

    🍍错误分析 

    分析: 

    主函数里面的Swap1(a, b)中的ab叫做实参(实际参数)

    而当我们进到Swap1函数内部的时候,Swap1(int x, int y)中的xy叫做形参(形式参数)

    所以我们在调用Swap1函数的时候,是不是把实际参数传给了形式参数

    🔑这个时候结论是这样子的:

    ▶  当函数调用的时候,实参传给形参,形参其实是实参的一份临时拷贝

    🔑此时大家又要问了,那什么是临时拷贝呢?

    ▶  形参有自己的独立空间,我们的操作只是把实参的内容拷贝一份,放到形参里面去了

    🔑 所以我们想要的答案出现了

     ▶ 我们将实参的值拷贝给了形参

     ▶ 但是在 swap1() 函数中 形参的值发生了改变(进行了数值互换),可是实参的值却没有发生改变。

     ▶ 在swap1() 函数结束时,也无法将转换的值返回给实参 因为void():无返回值函数

     ▶ 根据形参的特性:形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。

     ▶ 所以最终导致,实参并没有发生变化。

    💦于此同时我将上面的代码进行改写,这个大家就能更好的理解,为什么会出错。

    1. #include
    2. #include
    3. #include
    4. #include
    5. void Swap1(int x, int y)
    6. {
    7. int z = 0;
    8. z = x;
    9. x = y;
    10. y = z;
    11. printf("inside:形参为x,y\nx=%d,地址为%d\ny=%d,地址为%d\n\n", x, &x, y, &y);
    12. }
    13. int main()
    14. {
    15. int a = 0;
    16. int b = 0;
    17. scanf("%d%d", &a, &b);
    18. //交换两个变量
    19. printf("before:实参为a,b\na=%d,地址为%d\nb=%d,地址为%d\n\n", a, &a, b, &b);
    20. Swap1(a, b);
    21. printf("after:实参为a,b\na=%d,地址为%d\nb=%d,地址为%d\n\n", a, &a, b, &b);
    22. return 0;
    23. }

    💦查看输出结果

    1. 20 30
    2. before:实参为a,b
    3. a=20,地址为-401606620
    4. b=30,地址为-401606588
    5. inside:形参为x,y
    6. x=30,地址为-401606672
    7. y=20,地址为-401606664
    8. after:实参为a,b
    9. a=20,地址为-401606620
    10. b=30,地址为-401606588

    🔑 通过结果中的展示,发现从形参x和y的地址来看,传递的是实参a和b的拷贝。对拷贝的修改不会改变实参的值

    最终的结论:

    🔑当函数调用的时候,实参传给形参,形参其实是实参的一份临时拷贝

    注意:此时大家也会发现上面的代码实参a ,b传输的是 数值这种传递就叫做 值传递

    🥝 错误修改

    修改:

    很明显,刚刚那个代码是错误的,那么如何修改呢?

    在回想一下我们写过的函数:

    ▶ 写了一个Swap1函数,把a、b的值传给了x、y以后,x、y的空间和a、b的空间是独立的空间。

    ▶  所以对 a、b 的修改是不会影响 x、y 的,它们之间是 没有建立连接的
    那么我们怎么让它们之间建立联系呢?

    🔑 答案是 :指针

    💦利用指针进行代码改写

     
    1. #include
    2. #include
    3. #include
    4. #include
    5. void Swap2(int *pa, int *pb) // 其中 *pa *pb 是形参
    6. {
    7. int z = 0;
    8. z = *pa;
    9. *pa = *pb;
    10. *pb = z;
    11. }
    12. int main()
    13. {
    14. int a = 0;
    15. int b = 0;
    16. scanf("%d%d", &a, &b);
    17. //交换两个变量
    18. printf("交换前: a=%d b=%d\n", a, b);
    19. Swap2(&a, &b); // 此时传出过去的实参是 a,b数值的地址
    20. printf("交换后: a=%d b=%d\n", a, b);
    21. return 0;
    22. }

    💦察看运行结果

    1. 20 30
    2. 交换前:a=20 b=30
    3. 交换后:a=30 b=20

    💦代码分析

         我们先创建了 a , b变量,a , b变量里面放入 20、30。

         我们将 a 的地址取出来(实参),放到 pa指针变量(形参)里面去。

         同样我们将 b 的地址取出来(实参),放到 pb指针变量(形参)里面去。

         此时地址 pa 就能找到 a, 地址 pb 就能找到 b。

    ▶  

        那么 *pa = a  , *pb = b  ,在定义一个z变量,用于交换 *pa 和*pb。

    💦代码图解

     

     ✨得出结论:形参为指向实参地址的指针,当对形参进行指向操作时,就相当于实参本身进行的操作。

    注意:此时大家也会发现上面的代码实参a ,b传输的是 地址这种传递就叫做 址传递。

    🍋 深入理解 值传递与址传递

    我们结合连两个代码一起来看看。

    1. #include
    2. #include
    3. void Swap1(int x, int y)
    4. {
    5. int z = 0;
    6. z = x;
    7. x = y;
    8. y = z;
    9. }
    10. void Swap2(int *pa, int *pb)
    11. {
    12. int z = 0;
    13. z = *pa;
    14. *pa = *pb;
    15. *pb = z;
    16. }
    17. int main()
    18. {
    19. int a = 0;
    20. int b = 0;
    21. scanf("%d%d", &a, &b);
    22. //交换两个变量
    23. printf("交换前: a=%d b=%d\n", a, b);
    24. Swap1(a, b);
    25. Swap2(&a, &b);
    26. printf("交换后: a=%d b=%d\n", a, b);
    27. return 0;
    28. }

    🔑 当我去调用Swap1函数的时候,我把a、b本身的变量传到Swap1函数,这种方式叫:传值调用

    🔑当我去调用Swap2函数的时候,我把a、b地址传到Swap2函数,这种方式叫:传址调用

     💦概念理解

    想必大家看了我上面的讲解,对传值与传址调用有了一定的了解,下面我将给大家更加详细的总结。

    如何在函数中区分 什么是传值调用,什么是传址调用呢?(估计细心的大家已经发现了)

    🔑  当在主函数中,调用其他函数,实参为 数值 的时候 此时的函数调用被称为:传值调用

    例如:

    1. int main()
    2. {
    3. int a , b;
    4. scanf("%d %d",&a,&b);
    5. add(a,b); // 传值调用
    6. int *x = &a;
    7. int *y = &b;
    8. add(x,y); // 传值调用
    9. }

    注意:在代码中add(x,y),使用实参 x,y 是值传递,传递的是指针的值,而不是地址。

    🔑当在主函数中,调用其他函数,实参为 地址 的时候 此时的函数调用被称为:传值调用

    如何判断是地址呢?

    在实参里面有取地址符(&)存在的时候,就证明是地址

    例如:

    1. int main()
    2. {
    3. int a , b;
    4. scanf("%d %d",&a,&b);
    5. add(&a,&b); // 传址调用
    6. int *x = &a;
    7. int *y = &b;
    8. add(&x,&y); // 传址调用
    9. }

    在我们自己写代码的时候,何时使用传值调用,何时使用传址调用呢

    🔑 当要使用的函数,是对 实参本身 进行改变时--------使用传址调用

          注意:传址调用,大多数 无返回值 用 void();

    1. // .写一个函数,每调用一次这个函数,就会将 num 的值增加1。
    2. #include
    3. #include
    4. void Add(int* p) {
    5. (*p)++;
    6. }
    7. int main() {
    8. int num = 0;
    9. Add(&num);//1
    10. printf("%d\n", num);
    11. Add(&num);//2
    12. printf("%d\n", num);
    13. Add(&num);//3
    14. printf("%d\n", num);
    15. Add(&num);//4
    16. printf("%d\n", num);
    17. return 0;
    18. }

    🔑 当要使用的函数,只是对实参的值做 加减乘除之类 的操作时---------使用传值调用

          注意:传值调用,大多数需要加上返回值 return ;

    1. #include
    2. int add(int n, int m)
    3. {
    4. return n + m;
    5. }
    6. int main()
    7. {
    8. int a = 10;
    9. int b = 20;
    10. int sum = add(a, b);
    11. printf("sum=%d\n", sum); // sum = 30
    12. return 0;
    13. }

     

    四、进阶练习(公司面试题

    为了使大家更好的理解形参、实参、传值调用、传址调用,我们再次练习一道例题。

    注意:很多公司在面试的时候,经常会在这里设置陷阱

    🍈题目 

    题目:下面这个代码有很多错误,将它的错误找出来,并且改正

    1. #include
    2. void test(char *string)
    3. {
    4. string = "hello world";
    5. }
    6. int main()
    7. {
    8. char *str = NULL;
    9. test(str);
    10. printf("str=%s\n",str);
    11. return 0;
    12. }

    如果大家之前对形参、实参、传值调用、传址调用不清楚的话,要是要到题,估计很多人都会觉得代码是正确的 ,应该输出:str = hello world

    🍓 题目分析与改正

    💦首先将这段代码放在编译器中运行(面试的时候是不可以的哦,所以基础要打好O

     此时我们发现了 问题一 ,这个问题直接导致代码无法运行了

    问题:无法从 const char [12]  转换为 char *  

    🔑针对这个问题我们需要知道:

    指针指向的常量存储区是不允许修改的,这个实参和形参的地址都是指向字符串的地址,是const类型的,所以不能转换。

    💦进行代码修改

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. void test(char* string)
    7. {
    8. //string = "hello world";
    9. char arr[] = "hello world"; // 重新创建一个数组,将字符串赋值给它
    10. string = arr; // 将 hello world 的地址传递给 string
    11. }
    12. int main()
    13. {
    14. char* str = NULL;
    15. test(str); // 传值调用
    16. printf("str=%s\n", str); // str=(null)
    17. return 0;
    18. }

    💦查看输出结果

    1. 输出:
    2. str = (null)

    此时我们发现程序不报错了,但是输出的结果与我们希望的结果副相符合,出现问题二

    此时的问题终于回到我们这篇文章所讲解的知识体系上了。

    很明显,此时函数调用是--------------------------传值调用

    错误问题:当函数调用的时候,实参传给形参,形参其实是实参的一份临时拷贝

     形参是实参的一份拷贝,形参的改变,与实参毫无关系,所以导致输出null

    💦进行代码修改 ---(使用---传值调用的方发修改--------加入返回值即可)

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. char* test(char* string)
    7. {
    8. //string = "hello world";
    9. char arr[] = "hello world";
    10. string = arr; // 将 hello world 的地址传递给 string
    11. return string;
    12. }
    13. int main()
    14. {
    15. char* str = NULL;
    16. str = test(str); // 接收返回的地址
    17. printf("str=%s\n", str); // str=烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫1
    18. return 0;
    19. }

    💦查看输出结果

    1. 输出:
    2. str=烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫1

    此时我们发现返回值有了,但是返回的结果与我们的结果不相符合,出现问题三

    错误分析:此时我们再次发现,我在为了解决 问题一  创建了 arr 数组,当arr 数组出了test函数作用域就会自动销毁,所以打印出来的是随机值。

    此时会有同学问,为什么会自动销毁呢?

    因为arr数组是在 栈帧中创建的 一但出了作用域就会自动销毁。

    此时我们就要想到用 malloc() 函数 在堆上创建数组,这样才不会被销毁

    如果有不懂 malloc() 的老铁,可以去看我之前写的 动态内存分配 这一章节O

    https://blog.csdn.net/weixin_45031801/article/details/127701311

    💦进行代码修改

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. char* test(char* string)
    7. {
    8. string = (char*)malloc(100); // 在堆上创建数组
    9. strcpy(string, "hello world"); // 将数组的地址拷贝给string
    10. return string; // 返回 string 的地址
    11. }
    12. int main()
    13. {
    14. char* str = NULL;
    15. str = test(str);
    16. printf("str=%s\n", str);
    17. getchar();
    18. // 用完之后记得free,就可以解决内存泄露问题
    19. free(str);
    20. str = NULL; // 还要将str置为空指针
    21. return 0;
    22. }

    💦查看输出结果

    1. 输出:
    2. str=hello world

    这是可能还会有同学说,传值调用可以使代码正确,那传址调用可以吗?

    当然可以啦!!

    💦进行代码修改

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. void test(char** string)
    7. {
    8. *string = (char*)malloc(100);
    9. strcpy(*string, "hello world");
    10. }
    11. int main()
    12. {
    13. char* str = NULL;
    14. test(&str); // 传址调用
    15. printf("str=%s\n", str);
    16. //puts(str);
    17. getchar();
    18. // 用完之后记得free,就可以解决内存泄露问题
    19. free(str);
    20. str = NULL; // 还要将str置为空指针
    21. return 0;
    22. }

    💦查看输出结果

    1. 输出:
    2. str=hello world

    到此,这道题就被我们完整的解出来啦!!

    五、总结

    看到这里,估计大家已经对形参、实参、传值调用、传址调用完全掌握了把!

    这里我将给大家列出通过这篇文章大家需要掌握的知识点。

    知识点1:什么是实参?

    知识点2:什么是形参?

    知识点3:实参与形参的区别是什么?

    知识点4:实参与形参的联系是什么?

    知识点5:什么是传值调用?

    知识点6:什么是传址调用?

    知识点7:如何在函数中区分 什么是传值调用,什么是传址调用呢?

    知识点8:在我们自己写代码的时候,何时使用传值调用,何时使用传址调用呢?

    这些知识点,都可以这篇文章中找到,希望对大家有用(主要是怕自己忘记,狗头护体!)

      

    六、共勉

    以下就是我对函数的形参和实参与传值和传址的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新对函数栈帧的创建和销毁的理解,请持续关注我哦!!!!! 

     

  • 相关阅读:
    旅游管理系统,旅游网站的网页设计,旅游网站毕业设计毕设作品
    python3-网站路径扫描
    Nginx (1):nginx虚拟主机配置
    MySQL、高级SQL操作
    变年轻特效怎么制作?这三个方法你值得收藏
    制造不可靠的系统
    基于SpringBoot的校园志愿者管理系统
    Windows OpenGL 波浪特效
    三:MySQL数据库管理事务、存储引擎(2)
    2022 年最新 Java 后端薪资统计出炉,看看你有没有拖后腿
  • 原文地址:https://blog.csdn.net/weixin_45031801/article/details/128069009