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

Posted

技术标签:

【中文标题】我是不是正确理解了 C 中的按值传递和按引用传递?【英文标题】:Am I Correctly Understanding Pass By Value and Pass By Reference in C?我是否正确理解了 C 中的按值传递和按引用传递? 【发布时间】:2020-04-30 06:38:32 【问题描述】:

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

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 的特定内存位置.

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

【问题讨论】:

set_int中的参数是传值的,也叫“传值”。该函数不修改变量“a”。 set_array 中的参数通过引用传递,因为数组的名称计算为指向数组第一个元素的指针。这意味着 b[0] 被函数修改了。 【参考方案1】:

根据定义,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;

【讨论】:

值得明确指出,使用数组语法声明函数参数实际上将参数的类型声明为相应的指针类型,而不是实际作为数组。这个重要的微妙之处可能会令人困惑,因此我认为将参数声明为具有完整数组类型的形式很糟糕。 set_array(b) 通过引用传递b。数组b 未通过。传递了对它的引用。不要将 C++ 使用“reference”作为特定语言功能的名称这一事实与“reference”是用于描述这一点的英文单词这一事实混淆。指针是指它指向的对象。 set_array(b)void set_array(int array[4]) … 具有通过引用传递的特性和行为。该机制在标准中被指定为转换和对指针的调整这一事实并不能否定它是传递引用的实现这一事实。 @EricPostpischil “通过引用传递 b”意味着您可以在 set_array 中执行 b = something else; 并让它影响 main 中的 b,但事实并非如此。 @EricPostpischil Pass-by-reference 在 C 中不存在,Java 或 C# 中也不存在。它们是按值传递的,您传递指针的。参见 C 语言:***.com/a/2229510/12624874。这只是传递引用的模拟。 ……指的是对象,也就是指针。 它实际上是传递引用的一种实现。 它只是通过转换为指针而不是通过其他一些内置语言特性来实现的。您引用的评论继续说“这将被称为'C 风格的传递引用。'”【参考方案2】:

数组表达式在 C 中是特殊的。

6.3.2.1 左值、数组和函数指示符 ... 3 除非它是 sizeof 运算符、_Alignof 运算符或 一元 & 运算符,或者是用于初始化数组的字符串文字,具有 type ‘‘array of type’’ 被转换为类型为 ‘‘pointer to type’’ 的表达式,它指向 到数组对象的初始元素并且不是左值。如果数组对象有 注册存储类,行为未定义。

C 2011 online draft

当你打电话时

set_array(b);

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

6.7.6.3 函数声明符(包括原型) ... 7 将参数声明为“类型数组”应调整为“限定指针” type'',其中类型限定符(如果有)是在 [] 的 数组类型推导。如果关键字static 也出现在[] 的 数组类型推导,然后对函数的每次调用,对应的值 实际参数应提供对数组的第一个元素的访问,其中至少有 由大小表达式指定的元素。

同上。

基本上,在函数定义中,任何声明为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 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 元素(不是字节!)该地址并尊重结果。如果a 是数组表达式,则在完成此计算之前首先将其转换为指针表达式。这就是为什么您可以访问b 的元素通过 array - 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)中,函数定义中的形式参数和函数调用中的实际参数都指定同一个对象(在“占用内存并且可以存储值”的意义,而不是面向对象的“类的实例”意义),因此在这些系统中,对形式参数的任何更改都会反映在实际参数中(导致关于Hacker Test, "你有没有改变过 4 的值?无意间?用 Fortran 以外的语言?")

【讨论】:

以上是关于我是不是正确理解了 C 中的按值传递和按引用传递?的主要内容,如果未能解决你的问题,请参考以下文章

理解按值传递和按引用传递

C/C++按值传递和按地址传递

java的按值传递与按引用传递

按引用传递然后复制和按值传递在功能上是不是不同?

js按值传递和按引用传递

在 C# 中将 REF 和 OUT 关键字与按引用传递和按值传递一起使用