我是否正确理解 C 中的按值传递和按引用传递?

Am I Correctly Understanding Pass By Value and Pass By Reference in C?

在我的一个课程教程视频中,我遇到过按值传递和按引用传递的情况。在他给我们的例子中,他首先承认代码写得不好,然后让我们找出到底要打印什么。相关代码如下:

void set_array(int array[4]);
void set_int(int x);

int main(void){
    int a = 10; 
    int b[4] = {0, 1, 2, 3);
    set_int(a);
    set_array(b);
    printf("%d %d\n", a, b[0]);
}

void set_array(int array[4]){
    array[0] = 22;
}

void set_int(int x){
    x = 22;
}

我想确定的是,在最初得出错误的输出后,我理解为什么要打印正确的输出“10, 22”。

打印10的值是因为在调用set_int函数时,它的参数是一个变量,意味着可以传递任何变量。它不仅限于 set_int 函数中定义的值 22。

打印22的值是因为在调用set_array函数时,它的参数是一个数组,意味着只能传递原始值,因为它指向特定的内存位置存储值 22 的位置。

我是否误解了正在发生的事情,我是否遗漏了任何关键点?

根据定义,C 中的函数调用中没有按引用传递。您的两个示例都使用按值传递。但是,根据使用情况,结果会有所不同(即,我们可以模拟按引用传递的行为)。

  • 在第一种情况下,变量a是按值传递的,因此不能从函数调用中更改它。

  • 在第二种情况下,变量b是按值传递的,因此它也不能从函数调用中更改。

    但是,在第二种情况下,会出现一个特殊的现象。传递的参数 b 是一个数组。一个数组,虽然作为函数参数传递(在许多其他用法中),但会衰减到指向数组第一个元素的指针。所以本质上,您传递的是一个指针,并且可以修改指针指向的内存地址处的值(但不能修改指针本身)。这就是为什么 set_array() 函数的变化在 main().

    中持续存在的原因

    换句话说,第二种情况相当于

    void set_pointer(int *ptr);
    void set_int(int x);
    
    int main(void){
        int a = 10; 
        int b[4] = {0, 1, 2, 3);
        int * p = &(b[0]);         // address of the first element
        set_int(a);
        set_pointer(p);           // pass the pointer
        printf("%d %d\n", a, b[0]);
    }
    
    void set_array(int *ptr){
        *ptr = 22;                 // operate on pointer
    }
    
    void set_int(int x){
        x = 22;
    }
    

数组表达式在 C 中很特殊。

6.3.2.1 Lvalues, arrays, and function designators
...
3 Except when it is the operand of the sizeof operator, the _Alignof operator, or the unary & operator, or is a string literal used to initialize an array, an expression that has type ‘‘array of type’’ is converted to an expression with type ‘‘pointer to type’’ that points to the initial element of the array object and is not an lvalue. If the array object has register storage class, the behavior is undefined.

C 2011 online draft

当你打电话时

set_array(b);

表达式 b 从类型“int 的 4 元素数组”转换为 "pointer to int"(int * ),表达式的值为b[0]的地址。因此,set_array接收到的是一个指针值(&b[0]),而不是一个数组。

6.7.6.3 Function declarators (including prototypes)
...
7 A declaration of a parameter as ‘‘array of type’’ shall be adjusted to ‘‘qualified pointer to type’’, where the type qualifiers (if any) are those specified within the [ and ] of the array type derivation. If the keyword static also appears within the [ and ] of the array type derivation, then for each call to the function, the value of the corresponding actual argument shall provide access to the first element of an array with at least as many elements as specified by the size expression.

同上

基本上,在函数定义中,声明为 T a[N]T a[] 的任何参数都应解释为 T *a - IOW,参数被视为指针,而不是数组.您的函数定义

void set_array(int array[4]){
    array[0] = 22;
}

被视为已写入

void set_array(int *array){
    array[0] = 22;
}

所以在 C 中,数组有点像但不是真的通过引用传递。真正发生的是指向数组第一个元素的指针通过值传递 set_array 中的 array 参数指定内存中与 b 不同的对象,因此您对 array 本身 所做的任何更改都没有对 b 的影响(IOW,您可以将新值分配给 array 并让它指向不同的对象,但此分配根本不会影响 b)。相反,您正在做的是访问 b through array.

的元素

图片形式:

       +---+
    b: |   | b[0] <---+
       +---+          |
       |   | b[1]     |
       +---+          |
       |   | b[2]     |
       +---+          |
       |   | b[3]     |
       +---+          |
        ...           |
       +---+          |
array: |   | ---------+
       +---+

一个实际结果是 sizeof array 产生 指针 类型 int * 的大小,而不是数组的大小,不像 sizeof b。这意味着您无法使用 sizeof array / sizeof array[0] 技巧计算数组中元素的数量。当你将一个数组作为参数传递给一个函数时,你还应该将数组的大小(即元素的数量)作为一个单独的参数传递除非数组包含一个井-定义的标记值(如字符串中的 0 终止符)。

记住表达式 a[i] 定义*(a + i) - 给定起始地址 a,偏移量 i elements(不是字节!)来自该地址并尊重结果。如果 a 是数组表达式,则在完成此计算之前首先将其转换为指针表达式。这就是为什么您可以通过 array 访问 b 的元素 - array 只是存储 地址 b 的第一个元素。

在C中,所有函数参数都是按值传递的——函数定义中的形式参数和函数调用中的实际参数引用内存中的不同对象,并且实际参数的值被复制到形式参数。对形式论证的更改不会反映在实际论证中。

我们通过将指针传递给实际参数并写入取消引用的指针来伪造传递引用语义:

void foo( T *ptr )
{
  *ptr = new_value();  // write a new value to the thing ptr points to
}

void bar( void )
{
  T var;
  foo( &var ); // have foo write a new value to var
}

IOW,写入 *ptr 与写入 var 相同。写入 ptr,OTOH,在 foo 之外没有影响。

在真正的引用传递系统(比如 Fortran)中,函数定义中的形式参数和函数调用中的实际参数都指定同一个对象(在 "thing that takes up memory and can store values" 意义上,而不是面向对象的 "instance of a class" 意义),因此在这些系统中,对形式论证 的任何更改都会 反映在实际论证中(导致关于 Hacker Test, "Have you ever changed the value of 4? Unintentionally? In a language other than Fortran?")