"函数的所有参数都按值传递#34;在C中,关于在C ++中通过引用传递的混淆

时间:2017-01-01 03:18:36

标签: c++ pointers

"函数的所有参数都按值"

传递

这出现在书籍"Pointers on C" by Kenneth A Reek.

即使形式参数是指针,我也能理解指针是如何通过值传递的。本质上,传递指针的副本,使用该指针可以访问指向的元素。我可以通过以下代码验证这一点。

//Code snippet 1
int main(void)
{
    int x = 10;//&x here is d9fccc
    pass_pointer(&x); 
    //Some other stuff
}

void pass_pointer(int *c) {
    //&c here is d9fbdc [note, this is different from &x in main]
    //c correctly returns d9fccc [i.e., c = &x]
    //Some other stuff
}

到目前为止,非常好。

然而,当我尝试通过引用传递C ++经典时​​,我无法弄清楚"函数的所有参数是如何通过值"传递的。可能是真的。

请考虑以下代码。

    //Code snippet 2
    int main(void)
    {
        int x = 10;//&x here is d9fccc
        pass_by_reference(x); 
        //Some other stuff
    }

    void pass_by_reference(int &c) {
        //&c here returns d9fccc [note, this is same as &x in main]
        //Some other stuff
    }

我在缠绕头时遇到的问题如下:

  1. 在代码段1中,很明显发生了复制。基本上,隐含地执行int * c =& x。因此,c和x是两个不同的变量,它们都指向碰巧保存整数的相同地址。但是,代码片段2中的复制发生在哪里?
  2. 如果代码片段2中没有发生复制,那么可以概括一下,只要将实际参数传递给函数,编译器就会将形式参数设为左值,将实际参数设为右值?也就是说,在代码片段2中,这意味着int& c = x;我之所以希望这是真的,是因为这也涵盖了代码片段1中发生显式复制的情况。
  3. 代码片段对堆栈的影响有何含义?如果我理解正确,在代码片段1中,当输入pass_pointer()时,c(其地址为d9fbdc)被压入堆栈。但是代码片段2呢? x本身是否被推入堆栈?

4 个答案:

答案 0 :(得分:3)

C和C ++中的逻辑不同。在C中,所有参数都按值传递。在C ++中,所有未通过引用传递的参数都按值传递。

在C和C ++中,可以通过传递指针和解除引用指针来模拟引用传递的行为。

void pass_pointer(int *p)
{
    printf("%d\n", *p);
    *p = 42;
}

p本身是按值传递的,但此代码有效地使用*p作为参考,并允许打印任何p点的值,然后更改为{ {1}}。请注意,此ASSUMES 42指的是有效的*p(或等效地,int指向有效的p)。如果int没有引用有效的*p(例如intp,或者指的是就程序而言并不存在的事情)然后NULL的两个用法都给出了未定义的行为(这意味着任何事情都可能发生,但是在打印垃圾值和程序崩溃之间,常见的实际结果会有所不同)。

*cp都是有效实体(指针的值,以及它指向的值)。忘记*p是使用指针时程序员错误的常见原因,并且可能导致非常难以找到和纠正的隐秘错误。

以上示例在C和C ++中的作用相同。

在C ++中,C语言中不支持替代方法 - 通过引用传递。

*

这与void pass_reference(int &r) { printf("%d\n", r); r = 42; } 示例相同,但有一些关键变化。

  • pass_pointer()通过引用传递。代码中没有指针(尽管编译器可能会发出相同的目标代码,并且像幕后指针一样处理引用,这是编译器实现的细节,而不是C ++程序员需要关心的事情。)
  • 不允许解除引用r - 在此函数中使用r将无法编译。因此,可以避免忘记*r的所有可能错误。
  • *(通过语言规则)保证是有效的参考。使用r没有引入未定义的行为。因此,在r使用之前,pass_reference()无需检查r是否为有效参考。
  • 没有引用"引用"在C ++中(而在C中有指针指针)。

答案 1 :(得分:3)

  

然而,当我尝试通过引用传递C ++经典时​​,我无法弄清楚"函数的所有参数是如何通过值"传递的。可能是真的。

这种说法有点误导。通过引用传递的语义本身就不同于传递值,您应该将它们视为单独的事物(即使编译器最终以类似方式实现它们)。 C和C ++是完全不同的语言,这是一个很好的例子。

一旦你理解了参考的目的是什么,事情就开始变得有意义了。引用某些东西是一种为它创建别名的方法,换句话说,只是用不同的名称来引用它。例如:

 int a = 5;
 int &b = a;

这使b成为a的别名。也就是说,b是你给a之类的一个不同的名字,但它们是同一个东西。这就是为什么您无法重新分配b来引用其他内容的原因。就其生命周期而言,它将是变量a的同义词。

这与指针本质上是不同的。对于前者指针中的上述代码类似于:

 int a = 5;
 int *b = &a;  

但您可以重新指定b指向其他内容并充当"引荐来源"相反;这是C ++引用无法做到的。

因此,当您在C ++中看到引用传递时,它意味着一些不同的东西;你只想让参数作为它所引用的对象的同义词名称。尽管可以使用指针模拟这种行为(在幕后它就是这种情况),但使用指针和使用引用将意味着你想要做的事情不同。

在C中,您不能选择通过引用传递内容,以便 使用指针模拟行为;语言不够丰富,不具备引用语义。在C ++中,引用传递是它自己的事情,你应该这样想。

示例:

这是我编写的一些代码和程序集输出,这是编译器为计算机运行所产生的代码。

#include <iostream>

using namespace std;

void print_struct(my_struct &s);    // implemented elsewhere


struct my_struct {
    int arr[10000];
};


int main() {
    my_struct s;
    for(size_t i = 0; i < 10000; ++i) {     // fill array of struct
        s.arr[i] = i;
    }
    my_struct &s_ref = s;                   // print reference to s
    print_struct(s_ref);
    return 0;
}

下面是装配输出没有任何优化(O0)。注意s的地址如何存储在堆栈中。这是作为print_struct的参数传递的,只是s的地址。

  000000000040085a <main>:
  40085a: 55                    push   %rbp
  40085b: 48 89 e5              mov    %rsp,%rbp
  40085e: 48 81 ec 50 9c 00 00  sub    $0x9c50,%rsp           
  400865: 48 c7 85 b0 63 ff ff  movq   $0x0,-0x9c50(%rbp)          # the counter i is stored at -0x9c50  
  40086c: 00 00 00 00
  400870: eb 1f                 jmp    400891 <main+0x37>
  400872: 48 8b 85 b0 63 ff ff  mov    -0x9c50(%rbp),%rax   
  400879: 89 c2                 mov    %eax,%edx
  40087b: 48 8b 85 b0 63 ff ff  mov    -0x9c50(%rbp),%rax
  400882: 89 94 85 c0 63 ff ff  mov    %edx,-0x9c40(%rbp,%rax,4)   # struct s starts at -0x9c40
  400889: 48 83 85 b0 63 ff ff  addq   $0x1,-0x9c50(%rbp)
  400890: 01
  400891: 48 81 bd b0 63 ff ff  cmpq   $0x270f,-0x9c50(%rbp)
  400898: 0f 27 00 00
  40089c: 76 d4                 jbe    400872 <main+0x18>
  40089e: 48 8d 85 c0 63 ff ff  lea    -0x9c40(%rbp),%rax
  4008a5: 48 89 85 b8 63 ff ff  mov    %rax,-0x9c48(%rbp)           # stores address of s at -0x9c48 which is what s_ref is
  4008ac: 48 8b 85 b8 63 ff ff  mov    -0x9c48(%rbp),%rax
  4008b3: 48 89 c7              mov    %rax,%rdi                    # the address stored at -0x9c48 is passed as parameter to print_struct method
  4008b6: e8 72 ff ff ff        callq  40082d <_Z12print_structR9my_struct>
  4008bb: b8 00 00 00 00        mov    $0x0,%eax
  4008c0: c9                    leaveq
  4008c1: c3                    req

通过一些基本的典型优化,组件如下所示。请注意,在这种情况下,引用永远不会存储在堆栈中。

000000000040099a <main>:
40099a: 48 81 ec 48 9c 00 00    sub    $0x9c48,%rsp               # s is stored at 0x9c48
4009a1: b8 00 00 00 00          mov    $0x0,%eax                  # counter of for loop stored in register
4009a6: eb 07                   jmp    4009af <main+0x15>
4009a8: 89 04 84                mov    %eax,(%rsp,%rax,4)
4009ab: 48 83 c0 01             add    $0x1,%rax
4009af: 48 3d 0f 27 00 00       cmp    $0x270f,%rax
4009b5: 76 f1                   jbe    4009a8 <main+0xe>
4009b7: 48 89 e7                mov    %rsp,%rdi                  # doesn't put address of s onto stack. Just passes the address straight as a parameter to print_struct
4009ba: e8 42 ff ff ff          callq  400901 <_Z12print_structR9my_struct>
4009bf: b8 00 00 00 00          mov    $0x0,%eax
4009c4: 48 81 c4 48 9c 00 00    add    $0x9c48,%rsp
4009cb: c3                      retq

答案 2 :(得分:2)

您正在考虑2种不同语言的2种不同情况。

在C 函数的所有参数都按值传递

通过引用传递只是对象的相同引用。任何变化都反映在被叫方。

  

在通过引用传递(也称为传递地址)时,副本   存储实际参数的地址。使用时通过引用传递   您正在更改客户端程序传入的参数。

我们也可以在传值环境中以某种形式模仿传递(不完全)。

在指针的情况下你需要有2个内存访问。但是如果引用它会被自动解除引用。

您正在等待听到的一件关键事情......

如果您看一下gcc生成的汇编,那么您将看到在引用的情况下也传递指针。但唯一的问题就是我们直接访问它而不是通过其他变量(如C指向函数的指针传递)

如果你愿意,可以亲身经历(如果你不相信我)

编写一个c ++代码,只需使用指针,也可以通过引用传递。 然后使用gcc -S progname.c并检查progname.s文件......您将看到存储的地址。

答案 3 :(得分:2)

理解参考文献的最佳方法是理解两个规则:

  1. 引用与指针相同,除了:

  2. 每次使用引用时,它都会被*运算符自动解除引用,作为使用它之前的第一个业务顺序。

  3. 嗯,还有第三条规则:

    1. 构建引用时,如果地址,&,运算符应用于您创建引用的任何内容,那就是。
    2. ......但这对于本次讨论而言并不重要。无论如何,前两条规则是最重要的规则。如果你记住这些规则,那么你可以考虑:

      int &c
      

      与以下内容相同:

      int *c
      

      除非每次在任何上下文中使用此c,否则它会自动解除引用,就像您编写(*c)一样使用,所以在以下表达式中使用:

      int a = b + c;
      

      如果c是引用,则相当于int a = b + (*c);

      如果你做了这个心理调整 - 用指针替换一个引用变量,并在实际使用的任何地方打一个dereference操作符 - 你应该能够自己找出你自己的问题的答案,并理解当引用传递给函数时,它们通过值传递,就像其他所有传递一样。在这里,他们没有什么不同。