V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
herozem
V2EX  ›  C

C 语言是没有传址的, 对吗?

  •  
  •   herozem · 2015-03-15 13:25:01 +08:00 · 3492 次点击
    这是一个创建于 3528 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近编写C程序发现一个问题, 传统教材里说的传址, 实际上并没有这回事, 是吗? 比如说

    void print(int sum);
    
    调用函数的时候函数内的sum是实参的一份复制(int sum = 实参), 但实际上, 即便是传递指针:
    
    void print(int *sum);
    
    也只是把指针复制了一份, 因此可以改变指针所指向的内容, 但是并不能改变作为实参的指针本身的内容, 是吗?例如:
    
    #include <stdio.h>
    #include <stdlib.h>
    
    typedef struct Node {
      int num;
      struct Node *next;
    } Node;
    
    void init(Node *p)
    {
      p = malloc(sizeof(Node)); /* Header */
      p->num = 1;
      p->next = NULL;
    }
    
    int main(void)
    {
      Node *p;
      init(p);
      printf("%d\n", p->num);
    
      return 0;
    }
    

    这一段程序如果编译运行的话, 会core dumped, 就是因为在main函数执行完init(p)的时候init函数内的p是实参p的一份拷贝, 所以改变的实际上是拷贝版本, 最终导致printf的时候访问到了非法地址而造成的。这是我的猜想, 是这样子的吗?

    33 条回复    2015-09-15 22:55:34 +08:00
    canautumn
        1
    canautumn  
       2015-03-15 13:42:27 +08:00 via iPhone
    是的,应该让init把p传回,再赋给main的p。
    noark9
        2
    noark9  
       2015-03-15 13:44:25 +08:00
    C里面只有值传递,所谓的指针,其实也就是把指针的值传递给了函数,然后函数去修改指针值里面的内容,上面例子要这么写

    init(Node **p)
    {
    *p = xxxx;
    }

    调用的时候

    Node *p;
    init(&p);

    p是一个指向Node的指针,其实也就是一个变量而已
    传入&p也就是为了能够在函数中修改p这个变量里面的值
    只是这个值是一个地址
    riaqn
        3
    riaqn  
       2015-03-15 13:45:35 +08:00
    对, 所以最好是这样:
    Node *node_new();
    或者这样
    int node_init(Node *p);
    zkd8907
        4
    zkd8907  
       2015-03-15 13:52:13 +08:00   ❤️ 1
    我这里倒是没有出现core dumped的情况。不过在C/C++中,所有的函数调用实际上都是“值传递”这种理解是不太准确的。
    假定你是在Win32平台上写的Code,在Win32汇编中需要调用函数之前,需要把函数的参数按次序压入栈中,在调用函数时,再依次取出。INVOKE函数以后父方法的堆栈空间会<strong>被保留</strong>,此时执行栈会进入函数中的堆栈空间。
    其实这个这保留是比较容易理解的,如果没有这个特性的话,大部分的递归函数都会出现问题。
    假定楼主的方法init有两个参数,`init(*p, 0)`等价于`invoke init, *p, 0`等价于
    `PUSH 0
    PUSH *p
    CALL init`
    到了init方法以后,编译器会帮你再把*p和0给POP出来,此时和*p和0这两个值已经是参数的副本,所有对*p的操作只能影响到当前执行栈内。
    执行完init方法以后,当用户再返回父函数时,子函数中的*p就没有办法再操作到,这时候实际上子函数中的Node对象已经出现了内存泄露。
    AsterN
        5
    AsterN  
       2015-03-15 13:58:06 +08:00
    你的理解是真确的,指针传址是传的指针指向的地址值(其实也是一个整数),因此实质上和整数参数没有区别。对于你的实例代码,可以传指针的指针来完成。

    void init(Node **p)
    {
    (*p) = malloc(sizeof(Node)); /* Header */
    (*p)->num = 1;
    (*p)->next = NULL;
    }

    int main(void)
    {
    Node *p;
    init(&p);
    printf("%d\n", p->num);

    return 0;
    }
    msg7086
        6
    msg7086  
       2015-03-15 14:03:05 +08:00 via iPhone   ❤️ 1
    传址,就是指传指针地址。
    FrankHB
        7
    FrankHB  
       2015-03-15 14:30:06 +08:00   ❤️ 1
    标题的说法正确。
    @zkd8907 有两个重要的明显错误。
    1.把具体语言指定的程序的语义和语言的实现的操作语义混为一谈。
    这里说的Win32 code和C没有必然联系。一个C编译器需要遵守什么样的ABI生成代码,本质上就不是C语言管得了的。
    ISO C和ISO C++都以抽象机模型规范程序的行为。基于可观察行为的等价规则(C++专门称为as-if rule),一个C或C++实现(一般就是优化编译器)有权变换底层实现中的子例程调用来体现函数的语义(C++还有另外一条例外规则,和这里问题没什么关系,略)。显然,这不表示两者是一回事。这里的C编译器生成的代码是汇编或者其它语言,不再是C语言程序。
    2.把C和C++混为一谈。
    事实上,C的函数参数就只能CBV,也就是“传值”。而C++的引用类型参数是被CBR的,并非“传值”(虽然所谓“传址”的说法也不确切,引用是否占用存储是unspecified,在具体实现以上根本就无所谓“引用的地址”)。
    zkd8907
        8
    zkd8907  
       2015-03-15 14:31:57 +08:00
    @FrankHB 感谢指正
    herozem
        9
    herozem  
    OP
       2015-03-15 14:51:22 +08:00
    onemoo
        10
    onemoo  
       2015-03-15 14:55:19 +08:00
    LZ你的理解没问题。
    C语言只有语义上的“传地址”,C++增加了&(reference),可视为实现了语法层面的传地址。
    想要实现你的逻辑,就按照 2L 和 5L 的做法。

    还有,也对于4L中的解释:“到了init方法以后,编译器会帮你再把*p和0给POP出来”,这是stdcall调用约定,在Win32 API中使用。如果你在Unix/Linux等使用cdecl调用约定的系统中,会看到压栈的参数是由调用者清理的。
    lsylsy2
        11
    lsylsy2  
       2015-03-15 15:03:44 +08:00
    C语言只有语义上的“传地址”,C++增加了&(reference),可视为实现了语法层面的传地址。

    然后LZ有兴趣了解汇编基础的话,会发现不管怎样,都是复制了一份指针过去……(在寄存器里)
    invite
        12
    invite  
       2015-03-15 15:11:33 +08:00
    LZ 应该用汇编去理解。 C只是一种语言。
    wsxyeah
        13
    wsxyeah  
       2015-03-15 15:15:22 +08:00
    是的。用C++的时候遇到这个坑,C++可以用引用“&”解决。
    bestsanmao
        14
    bestsanmao  
       2015-03-15 15:28:54 +08:00
    传**p就可以了
    Reficul
        15
    Reficul  
       2015-03-15 15:32:58 +08:00 via Android
    是的,你要改指针所指向的地址你得用一个指针指向你要改的那个指针。。。。
    像我这样的白痴用多了这样的变量就容易乱了😂😯
    wizardforcel
        16
    wizardforcel  
       2015-03-15 15:59:30 +08:00 via Android
    没有传引用,但是可以传指针。引用传参是套着普通变量外衣的指针。
    exiahan
        17
    exiahan  
       2015-03-15 16:33:08 +08:00 via Android
    可以使用指向指针的指针,这样就能修改指针本身的值。但是这玩意用多了估计会很乱吧
    icedx
        18
    icedx  
       2015-03-15 16:50:48 +08:00 via Android
    因为init 没返回 所以改变的是局部的变量
    mringg
        19
    mringg  
       2015-03-15 16:55:49 +08:00 via Android
    理解的挺透彻了
    nooblee
        20
    nooblee  
       2015-03-15 17:15:15 +08:00
    一切都是数字
    zhicheng
        21
    zhicheng  
       2015-03-15 17:16:16 +08:00 via Android
    指针是一个“类型”,它的值是它指向的内存地址。你的代码之所以是错的,是因为你改变的是指针的值(地址的值),而不是指针(地址)指向的内存。如果你实在理解不了,就定义一个 ptr_t (typedef void * ptr_t)。遇到指针的参数用这个。
    herozem
        22
    herozem  
    OP
       2015-03-15 19:07:10 +08:00
    @Reficul 经常乱+1

    @exiahan 是, 指来指去有点晕
    herozem
        23
    herozem  
    OP
       2015-03-15 19:11:47 +08:00
    但是现在有这么一个新的问题:

    ```c
    void printlist(listnode *list)
    {
    listnode *item = list;

    while (item->next != NULL) {
    printf("num = %d =>", item->num);
    item = item->next;
    }
    }
    ```

    传入的list是这样子的:

    -------------- ----------------
    (list)--->| 1 | ----|------>| 100 | -----|------> NULL
    --------------- ----------------

    运行printlist也会coredump, 这里有什么不妥吗?
    xieyudi1990
        24
    xieyudi1990  
       2015-03-15 19:32:39 +08:00 via iPhone
    @herozem 你没有检查item. 遍历到最后一个node之后, item可能无效. 然后cpu用这个可能无效的地址加上结构体的偏移产生一个无效的有效地址去访存, 然后mmu就抱怨了, 指令在完成时产生异常.

    跟着执行流想想就知道问题在哪, 看不出来大不了调汇编.
    MntCw
        25
    MntCw  
       2015-03-15 21:09:10 +08:00
    你传的是一个地址的值,但是你实际要传的是指向这地址的指针。
    choury
        26
    choury  
       2015-03-15 21:59:48 +08:00
    @herozem 请提高自己的debug能力
    gerorim
        27
    gerorim  
       2015-03-15 23:30:45 +08:00
    geeti
        28
    geeti  
       2015-03-16 05:47:10 +08:00
    需要**p
    毕业时候看了Google推荐的面试教材,interview exposed,才考虑到这个问题。
    blackworld
        29
    blackworld  
       2015-03-16 23:32:46 +08:00
    @geeti 为什么需要**P?
    typedef struct Node {
    int num;
    struct Node *next;
    } Node;
    这个里面的 “struct Node *next”
    这句话怎么理解比较好?
    blackworld
        30
    blackworld  
       2015-03-16 23:34:33 +08:00
    @noark9
    为什么需要**P?
    typedef struct Node {
    int num;
    struct Node *next;
    } Node;
    这个里面的 “struct Node *next”
    这句话怎么理解比较好?
    Reficul
        31
    Reficul  
       2015-03-17 00:51:30 +08:00 via Android
    @blackworld
    把一个指针传给函数里面之后,其实是把这个指针指向的地址赋值给了函数里的另一个变量。不管你怎么修改他,原本函数外的那个指针的值是不会变得,所以还是指着原来的地方。你要修改指针指向的目标,那你得传入函数的是一个指向该指针的指针,传入的是需要修改的指针的地址。
    其实和函数传入a这个变量,不能修改他一样,要修改他需要传入a的地址。

    那个typedef struct Node {} Node;是定义一个叫Node的结构体,然后再typedef成一个叫Node的类型。两个Node不是一个概念。在结构体里面那一行就是定义一个Node类型的结构体指针。不能直接Node *next;貌似是因为在这里面的时候,typedef还没被执行,还不是一个类型,要用他只能叫他全名-结构体Node。

    说的比较乱,很多自己的感受,不一定正确。
    geeti
        32
    geeti  
       2015-03-17 02:58:48 +08:00
    @blackworld 说啥也没用,自己写个最简单的code,然后根据地址一步一步跳着看就明白了
    noark9
        33
    noark9  
       2015-09-15 22:55:33 +08:00
    @blackworld
    为什么是 **p ,因为你需要修改的是 *p 所以要取 *p 的地址的值给函数去修改
    比如,你要修改一个 int 的时候,传递的方法应该是 fun (int *p )
    那你要修改一个 int *,那么传递的方法自然是 fun (int **p )

    struct Node *next
    可以认为,*next 是一个 struct Node ,也就是一个 Node 的结构体(注意前面写的是 *next )
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2665 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 21ms · UTC 10:24 · PVG 18:24 · LAX 02:24 · JFK 05:24
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.