C语言函数参数传递

文章目录

  • 问题起源
  • C语言函数参数
  • 修改“实参”的方法
    • 方法1
    • 方法2
    • 两个方法类比理解
  • 函数参数为多层指针
  • 结论

问题起源

在学习单链表的过程中,要用C语言一个函数实现头插入一个元素。在示例中发现这个函数的参数是个两层指针,非常疑惑,因此重新复习了一下相关内容。
先上一段简单的代码。以下代码的目的是要通过函数对变量aa进行加1的操作。

#include 

int increment_by_val(int);
void increment_by_ptr(int *);

int increment_by_val(int a){
    a = a + 1;
    return a;
}

void increment_by_ptr(int *a){
    *a = *a + 1;
}

int main(void){

    int aa = 11;
    printf("before change aa = %d\n", aa);
    aa = increment_by_val(aa);
    printf("after change aa by value = %d\n", aa);
    increment_by_ptr(&aa);
    printf("after change aa by ptr = %d\n", aa);
}

C语言函数参数

  • 函数的形参和实参的概念:在C语言圣经[1]的1.7 Functions中提到,在函数定义的括号中列出的一般叫parameter,而在调用这个函数时写在括号里的叫argument;或者这两者分别被称为formal argument和actual argument。这其实就是我们通常说的“形参”和“实参”。可以对这两种理解为:
    -形参:函数定义时在括号中列出的、并在函数体中使用的参数;
    -实参:调用函数时传入这个函数的变量。
    如上述代码中,在increment_by_val的定义中,参数列表中的a,即会被函数体使用的a,就是形参。当调用increment_by_val是传入的参数aa就是实参。

  • 函数参数传递机制:C中的函数是传值call by value,而不是传地址call by reference。函数的调用者调用这个函数,并且在参数列表中写上变量名,被调用函数使用和操作的是调用者变量的一个临时拷贝,而不是这些变量本身。如在main中调用了increment_by_val(aa),increment_by_val真正使用的不是aa,而是aa的一个临时拷贝。也正因如此,在一个function中对形参的任何修改都不会对实参产生影响;即被调用函数不能直接修改调用者中的变量。

如何做到一个函数对输入的变量进行“真正的”修改(被调用者看到、实参被修改)?

修改“实参”的方法

方法1

调用函数时,函数参数传入值,并且利用函数的返回值
使用increment_by_val,在这个函数体中,“+1”的操作是作用在aa的一个拷贝上,而不是aa本身。要想在函数参数不为指针的情况下、对aa的值进行实际的改变,只能通过让increment_by_val有个返回值,这个在main中将这个函数的返回值赋值给aa。

方法2

调用函数时,函数参数传入要被修改的变量的地址
使用Increment_by_ptr,不需要这个函数有返回值,就可以对aa进行实质的修改,那就是让函数接受aa的指针,在函数中对形参通过解引用修改,如increment_by_ptr(int *a)所示;通过increment_by_ptr(int *a),调用者main来说,它可以“看到”aa确实是被修改了,而不需要让aa接受一个函数的返回值。
如何通过函数改变一个调用者变量的问题其实在[1]中也被提出,如1.8 Arguments – Call by Value一章,解决方案就是方法2:被调用者的形参必须是个指针,调用者提供相关变量的地址。

两个方法类比理解

一个类比:你需要按照某个机构的标准更新某个重要文件,但是你不知道怎么修改。
你就是main函数,aa就是这个文件,机构就是函数(不是100%恰当,并且假设没有互联网、手机的时代,并且文件很重要,原件不能丢)。

  • 按照方法一,你复印了一份这个文件,(这个复印件就是a)然后寄到这个机构,这个机构的人在这个复印件上帮你做好了修改示例给你寄回来,你按照这个示例,在你的原件上进行同样的更新。
  • 按照方法二,机构派人到你家,直接在你的文件上帮你修改

函数参数为多层指针

其实如果理解了函数传参的机制以及两个方法的不同,也就可以理解什么时候需要将多层指针作为函数参数;或者当看到某个函数的参数是多层指针时作用是什么。
示例:实现为单链表头插入元素的函数addElement,要求函数返回值为void。
单链表元素的头插入元素需要:

  • 将单链表的头指针header指向插入的新元素
  • 新元素指向原本的第一个元素,从而成为这个链表的第一个元素

header本身就是一个指针变量。可以想见,addElement要实现让一个指针header改变指向的效果。以下分别写了addElement1和addElement2两种方式。

struct LinkedList{
      int a;
      int b;
      struct LinkedList* next;
};
void addElement1(struct LinkedList* header){
struct LinkedList* newEle;
newEle = (struct LinkedList*)malloc(sizeof(struct LinkedList));
header = newEle;

}

void addElement2(struct LinkedList** header){
struct LinkedList* newEle;
newEle = (struct LinkedList*)malloc(sizeof(struct LinkedList));
*header = newEle;

}

int main(){
struct LinkedList* header_main;
addElement1(header_main);
addElement2(&header_main);

}

在addElement1中,形参为一个LinkedList结构体的指针,实参是header_main,那么,在函数中让header等于一个新元素的地址并不能让实际的header_main指向新的元素。假设header_main的值为0xAAAABBBB,在addElement1中改变的是header_main的一个备份,在main中,进行了addElment(header_main) 并不能改变header_main的值。addElement1的实现并不能达到我们期望的目的,因此是错误的。
因此,应该向addElement输入一个指向指针header的指针。在函数中,对这个二级指针解一层引用,然后修改这个一层解引用的值。在本例中,addElement2中对参数header进行一层解引用,得到的就是内存中的header_main,实参),main中调用addElement2时,参数应当时header_main的地址,通过addElment2的修改才是对“真正”header_main的修改。

结论

如果要求一个函数不能有返回值,而对输入的实参进行修改,则需要将这个实参的地址输入,在函数中对这个实参的解引用进行操作。
如果看到某些函数的参数是个多层指针,并且在函数体中有对这个变量的一级解引用操作,则可以猜想,这个函数是想对一个实参进行修改和作用。(当然这么说可能并不全面,函数体中也可能对这个参数更多级解引用,应当根据实际情况分析函数的作用)

参考:
[1] C程序设计语言(英文版 第2版)机械工业出版社

你可能感兴趣的:(c语言,算法,数据结构)