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

2015-03-15 13:25:01 +08:00
 herozem

最近编写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的时候访问到了非法地址而造成的。这是我的猜想, 是这样子的吗?

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

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

调用的时候

Node *p;
init(&p);

p是一个指向Node的指针,其实也就是一个变量而已
传入&p也就是为了能够在函数中修改p这个变量里面的值
只是这个值是一个地址
riaqn
2015-03-15 13:45:35 +08:00
对, 所以最好是这样:
Node *node_new();
或者这样
int node_init(Node *p);
zkd8907
2015-03-15 13:52:13 +08:00
我这里倒是没有出现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
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
2015-03-15 14:03:05 +08:00
传址,就是指传指针地址。
FrankHB
2015-03-15 14:30:06 +08:00
标题的说法正确。
@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
2015-03-15 14:31:57 +08:00
@FrankHB 感谢指正
herozem
2015-03-15 14:51:22 +08:00
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
2015-03-15 15:03:44 +08:00
C语言只有语义上的“传地址”,C++增加了&(reference),可视为实现了语法层面的传地址。

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

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/177016

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX