20251028 Parameter | 参数传递:赋值传递与地址传递

发布于 10 天前  169 次阅读


形式参数与实际参数

函数是执行固定功能的工具,参数是它的重要组成部分。
一个函数可以是无参数的,亦可以是带有参数的。也就是说,函数的定义中,参数是可选的。
我们来考虑这样一个实际问题,定义一个函数,它的具有两个整型(int)参数。它的功能是计算这两个参数的和,并输出。
例如:

void CalculateSumOfTwoNumber(int a, int b) {
    int sum = a + b;
    printf("The sum of given numbers is: %d", sum);2025
}

这样,便定义了一个名为CalculateSumOfTwoNumber的函数,它具有两个整型参数ab。具体来说,函数体内生命了一个整型变量sum用以存储表达式a + b的结果,接着用printf("...", ...);语句输出sum的值。

考虑利用这个函数,计算3 + 518 + 210 + 01 + 2几个表达式的值:

#include <stdio.h>

void CalculateSumOfTwoNumber(int a, int b) {
    int sum = a + b;
    printf("The sum of given numbers is: %d\n", sum);
}

int main() {
    CalculateSumOfTwoNumber(3, 5);
    CalculateSumOfTwoNumber(18, 21);
    int x = 0, y = 0;
    CalculateSumOfTwoNumber(x, y);
    int a = 1, b = 2;
    CalculateSumOfTwoNumber(a, b);
    return 0;
}

利用这个例子,我们便能清晰地说明什么是“形式”的参数,什么是“实际”的参数。我们提到,函数是执行固定功能的工具,在这个例子里,这个“固定功能”就是计算两个数的和并输出,而具体是哪两个数呢?这取决于调用函数时传入的数据。例如,在执行CalculateSumOfTwoNumber(3, 5);时,a的值就变成3b的值就变成5;在执行CalculateSumOfTwoNumber(18, 21);时,a的值就变成18b的值就变成21;...
也就是说,对于在函数定义中声明的参数ab,只是决定了在函数被调用时,传入数据的执行逻辑(先求和int sum = a + b;、再输出printf(...);),而本身并不代表任何实际的数据或意义。因此,我们称写在函数定义里的参数是形式(Formal)的,而对于那些在调用时实际传入的内容,例如例子中的3 518 21x ya b,他们是具有实际意义的值。再函数调用时,他们本身的值会交给函数的形参ab,按照形参规定好的逻辑去执行具体的功能。写在函数调用里的参数是实际(Actual)的。

Remark

细心的你会怀疑上面例子中这一段的合法性:

void CalculateSumOfTwoNumber(int a, int b) {
    int sum = a + b;
    printf("The sum of given numbers is: %d\n", sum);
}

int main() {
    ...
    int a = 1, b = 2;
    CalculateSumOfTwoNumber(a, b);
    ...
}

你疑惑:明明在定义函数的时候,变量ab已经作为形式参数被使用(Occupied)了,为什么在主函数里还能再声明相同名字的变量a = 1b = 2呢?这不会出问题么?
很好的问题,答案是不会的。原因在于在定义函数CalculateSumOfTwoNumber(int a, int b)时中声明的变量absum等都只能在CalculateSumOfTwoNumber(int a, int b)的定义中使用,准确的说是它们的作用域(Scope)被限制在两个大括号之内。形象地说,这里的ab只属于函数CalculateSumOfTwoNumber(int a, int b)。同样,主函数中声明的ab也只能在主函数中使用。
因此,两个位置的变量ab虽然有着相同的名字,但也互不干扰,这也一定程度上体现了在调用函数时,实际参数将值 “传入” 给形式参数,由形式参数参与计算,而非本身参与计算的思想。

数组与数组名。

我们学习过数组的声明:

int data[101]; // 这是一条声明语句,[101]表示数组中含有元素的总个数

这里,我们声明了一个整型数组,由101个整型元素组成,这个“集合”的名字是data
在声明后,要对其中某编号为i的元素进行操作(如访问、赋值等),则使用data[i]。例如:

int main() {
    int data[101];
    data[23] = 101325; // 这是一条赋值语句,[23]表示data的编号为23的元素
    ...
}

这里,我们通过data[23]对编号为23的元素进行赋值,值是101325。由此你变发现中括号在声明和调用时的显著区别:

  • 在声明时,中括号表示数组含有元素的个数,它规定了数组占用空间的大小。
  • 在调用时,中括号表示要访问元素的序号。此时data[23]是一个整型变量,而不是一个大小为23的整型数组。
  • 更进一步说,除了声明语句(如int data[101];)中,带有中括号的表达形式代表数组整体外(因为要确定元素个数),其余时候想要表示整个数组应该用变量名data,而形如data[23]则表示某一个元素。

为什么要强调这一点呢?这是因为当数组做参数的时候尤其重要。
例如,定义一个函数,它的功能是遍历并输出某固定长度为20的数组的全部元素:

#include <stdio.h>

void PrintArray(int array[20]) {
    for (int i = 0; i < 20; i++) {
        printf("%d ", array[i]);
    }
}

int main() {
    int data[20] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20};
    PrintArray(data);
    return 0;
}

这里便清晰地看出,为什么在调用时写PrintArray(data)而不是PrintArray(data[20]),因为要传给函数PrintArray的是整个数组data,而不是第20个元素data[20](更荒谬的是,data[20]并不存在,因为数组data的编号从019)。
而在函数的定义中,参数的声明我们带上了中括号void PrintArray(int array[20]),这是因为我们需要对形式参数进行声明,这属于声明语句而非访问,表示该形式参数array是一个数组。

重要Remark

在学习指针的知识后,你会知道传入数组的工作原理与传入变量的工作原理并不太一样。对于某一变量作参数的情况,例如上面算加法的例子,可以认为把实际参数(如3 518 21x y)的值赋值给了形式参数ab,这是容易理解的。

Tips: 下面的内容有一些难以理解
但对于数组作参数的情况,我们是把这长度为20的数组data整个赋值给长度恰也为20的形式参数array么?并非如此,数组作参数时,向array传递的是data这个数组的0号元素在内存中“存储”的位置。为什么这样可以实现参数的传递呢?因为数组作为一组数据的集合,所有数据是在内存中是连续地放在一起的,知道了0号元素的位置,自然也就知道123、...号元素的位置。

那么这有什么影响?影响在于数组作参数时,形参的数组大小变得不确定。也就是说,既然传递的只是实参数组首元素的内存位置,那么实参数组 “究竟有多大” 的信息在传递中丢失了,也因此我们先前的写法里void PrintArray(int array[20])中的20毫无意义。

回过头来我们这样总结:要让数组作参数,传递给形参数组的是实参数组首元素的位置,形参数组想要访问传入的元素,是拿着“首元素位值”去找第i号元素位置,而并不能知道实际传入的实参数组到底有多大。

我们于是在这里做一点补充:对于数组作参数的函数,既然形参无法知道传入的实参数组的实际大小,那么我们规定下面几种写法等价:

void PrintArray(int arr[]) { ... }
void PrintArray(int arr[20]) { ... }
void PrintArray(int arr[2025]) { ... }
void PrintArray(int *arr) { ... } // 指针写法 (事实上指针变量就是用以存储某元素内存地址的变量)

由于形参无法知道实参数组的实际大小了,为了避免数组越界的问题,在传入数组时,我们一般增加一个整型参数专门记录数组的大小。

#include <stdio.h>

void PrintArray(int array[], int n) {
    for (int i = 0; i < n; i++) {
        printf("%d ", array[i]);
    }
}

int main() {
    int data_1[5] = {5, 4, 3, 2, 1};
    int data_2[20] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20};
    PrintArray(data_1, 5);
    PrintArray(data_2, 20);
    return 0;
}

地址传递(引用传递)与赋值传递

这里与指针的知识强相关。我们考虑上述两个例子:

  • 在变量作参数的情况里,我们说参数传递的过程,是实际参数赋值给形式参数的过程。因此,作实际参数的变量和作形式参数的变量完全是两个独立的不同变量。也就是说,在下面这个例子里:
    #include 
    void func(int a) {
    a = 5;
    }
    int main() {
    int num = 2;
    func(num);
    printf("%d", num);
    return 0;
    } 

    主函数中声明了变量num值为2,调用函数func时,num作实参把值2赋给了形参a,此时numa是完全独立的两个不同变量。函数体中,a的值被重新赋值为5,此时函数体结束,主函数中的num值仍为2,而func中的值为5的形参a被舍弃了。因此输出的结果是2而非5
    这种类型的参数传递称为赋值传递。

  • 而对于数组作参数则不同。我们反复强调了“数组作参数,传给形参的是作实参的数组首元素在内存中的位置”,而“数组数据在内存中是连续存放的,因此形参拿着首元素位置去找其他元素”。因此,对于形参和实参,他们表示的其实是内存中的同一位置存储的数据。因此,考虑下面这段程序:
    #include 
    void func(int arr[]) {
    arr[1] = 3;
    }
    int main() {
    int data[5] = {0, 1, 2, 3, 4};
    func(data);
    printf("%d", data[1]);
    return 0;
    }

    由于arr接收到的是data首元素data[0]在内存中的地址,那么,arr[1]实际是由arr得到的首位地址向后推,找到的第1个元素的地址。也因此,函数funcarr[1]和主函数中data[1]实际上都表示的是同一块内存上的相同数据,那么funcarr[1]的赋值,也就影响了data[1]。此时输出data[1]结果是3,而不是1
    这种类型的参数传递称为地址传递(引用传递)

    如果还没学指针 接下来的内容会更加抽象
    那么对于变量,有没有引用传递的实现方式?是有的,我们想办法让传递参数时,传给形参的同样是实参变量的内存地址就好了好了。在c语言中,对于某一变量val,使用&val就可以表示它在内存中的地址。而什么样的变量存储别的变量的地址呢?这样的变量称作指针变量(Pointer)
    指针变量通过这样的方式声明:

    int main() {
    int* ptr; // 声明一个名为ptr的指针变量,“它存储的内存地址中”存储的值是一个整型变量。
    int val = 5;
    ptr = &val;
    return 0;
    }

    这里我们声明了一个指针变量ptr,并声明了一个整型变量val,最后用&val得到val在内存中的地址,赋值给ptr变量。指针变量必须明确指向位置所存储数据的数据类型,例如这里ptr指向的内存地址,存储的是一个整型变量val,因此ptr是一个整型指针int*

我们这里用&(取地址运算符)建立起了由变量val到地址&val的单向联系,并把地址存在一个指针变量ptr中,那么如果知道一个变量的内存地址ptr = &val;,怎么获取这个位置存储的值呢?这里我们用到取值运算符*。对于一个地址&val(即一个指针变量ptr),用*ptr,即*(&val)就可以获取ptr指向的内存地址中存储的变量值。也就是说:

int main() {
    int val = 5;
    int* ptr = &val;
    printf("%d", *ptr);
    return 0;
}

这里,变量val的值是5,地址是&val并存在指针变量ptr中,我们用*ptr找到指针变量指向的地址&val中存储的值,即为5

虽然略显麻烦,但这个例子便解释了指针工作的原理。我们看看如何利用指针实现地址传递(引用传递)。
我们刚才的例子是这样的:

#include <stdio.h>
void func(int a) {
    a = 5;
}
int main() {
    int num = 2;
    func(num);
    printf("%d", num);
    return 0;
} 

anum是独立存在于函数func和主函数中的两个不同变量
但如果我们函数func的参数改为一个指针ptr,传入的参数改为一个地址&num

#include <stdio.h>
void func(int* ptr) {
    *ptr = 5;
}
int main() {
    int num = 2;
    func(&num);
    printf("%d", num);
    return 0;
} 

这时,我们声明了整型变量num的值为2,存储在内存中的&num位置,我们将这个位置&num作为参数传给指针变量ptr,并在函数func中访问这个地址,修改它存储的值*ptr = 5。由于ptr是指针变量,指向的是主函数中num存储的位置,我们通过寻找该位置,直接修改存储的值,则成功地在func中影响了主函数中num变量的值。最后输出的结果是5而非2。这是与前例显著不同的地方。我们也因此实现了变量的地址传递(引用传递)。


这里是 /* Huajidawang */ 的个人主页