V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Distributions
Ubuntu
Fedora
CentOS
中文资源站
网易开源镜像站
kgdb00
V2EX  ›  Linux

gcc 为什么连这种代码都能编译通过?

  •  
  •   kgdb00 · 2022-06-18 11:14:39 +08:00 · 7426 次点击
    这是一个创建于 915 天前的主题,其中的信息可能已经有所发展或是发生改变。
    #include <stdio.h>
    
    int main(int argc, char *argv[])
    {
    	char *s = argv[argc-1];
    	printf("%s\n", s);
    
    	char *s2 = argc[argv-1];
    	printf("%s\n", s2);
    
    	return 0;
    }
    

    打印最后一个参数,上面的写法是正确的,问题是下面的写法也能编译通过,而且打印输出和上面的写法一样,不明白为什么编译器允许 argc[argv-1] 这种写法。

    42 条回复    2022-07-05 20:36:31 +08:00
    realradiolover
        1
    realradiolover  
       2022-06-18 11:19:31 +08:00
    管理指针是程序员的责任
    ecloud
        2
    ecloud  
       2022-06-18 11:25:56 +08:00
    x[y]应该等价于 (x 地址数+y*x 定义的位长)的地址
    那么这里是不是可以理解 argc 也是个地址,这个传进来的所谓 int 并不在堆里?
    kgdb00
        3
    kgdb00  
    OP
       2022-06-18 11:26:45 +08:00
    @realradiolover 问题是把 int 类型直接当成数组用,不符合 c 语言的语法规范啊
    XiLingHost
        4
    XiLingHost  
       2022-06-18 11:27:48 +08:00
    @kgdb00 实际上数组是个语法糖,本质是指针操作
    stein42
        5
    stein42  
       2022-06-18 11:28:52 +08:00   ❤️ 10
    a[b] 等价于 *(a + b)。
    C 语言标准就是这样规定的。
    https://en.cppreference.com/w/c/language/operator_member_access
    msg7086
        6
    msg7086  
       2022-06-18 11:29:40 +08:00 via Android   ❤️ 5
    a[b] 就是 *(a+b)。
    加法的两端是可以交换的。所以 a[b]就是 b[a]。
    hardwork
        7
    hardwork  
       2022-06-18 11:30:41 +08:00 via Android
    a[3]等价于 3[a],c 语言混乱编码里经常看到,最终都是*(a+3)的意思,至于是不是符合语言规范就不知道了,反正 gcc 可以通过
    adoal
        8
    adoal  
       2022-06-18 11:30:49 +08:00 via iPhone
    什么类型不类型的,C 又不是 Haskell
    ecloud
        9
    ecloud  
       2022-06-18 11:32:34 +08:00
    这么看起来这段内存是这样的:
    argc | argv[0] | argv[1] | ...
    argv-1 的地址就相当于 argc 的地址
    你的 argc 是不是等于 1 呢?你试试它等于 3 的时候是不是就显示不全 argv[0]了
    Caturra
        10
    Caturra  
       2022-06-18 11:34:18 +08:00
    我没看过 C 标准,但是
    可不可以这么写是标准负责的事情
    要不要这么写是程序员负责的事情
    kgdb00
        11
    kgdb00  
    OP
       2022-06-18 11:36:53 +08:00
    @ecloud 不管 argc 等于几,两种写法的输出都是一样的。
    knir
        12
    knir  
       2022-06-18 11:41:12 +08:00   ❤️ 3
    翻了一下 C 标准,6.2.5.1 Array subscripting
    1: One of the expressions shall have type ‘‘pointer to complete object type’’, the other expression shall have integer type, and the result has type ‘‘type’’.
    2: ... The definition of the subscript operator [] is that E1[E2] is identical to (*((E1)+(E2))) ...
    kgdb00
        13
    kgdb00  
    OP
       2022-06-18 11:44:39 +08:00
    @knir 感谢,Array subscripting 是在 6.5.2.1 节
    xiri
        14
    xiri  
       2022-06-18 11:48:12 +08:00 via Android
    @ecloud 没有你想的那种巧合,楼上已经讲的很清楚了,因为 a[b]的本质还是指针操作,有:a[b]=*(a+b)=*(b+a)=b[a]
    qbqbqbqb
        15
    qbqbqbqb  
       2022-06-18 12:19:17 +08:00
    首先很显然 (argv-1)[argc] 和 argv[argc-1]访问的是同一个东西。然后根据 C 语言数组下标操作的定义,(argv-1)[argc]和 argc[argv-1]又是等价的。
    chronosphere
        16
    chronosphere  
       2022-06-18 12:30:13 +08:00 via Android
    @knir 这种下标运算的写法和结果和汇编语言里的指针寻址很像,可能 C 语言就照搬过来了吧。把 argc 对应的二进制数据看作相应的内存地址,就好理解一些了
    zyeros1991
        17
    zyeros1991  
       2022-06-18 13:54:13 +08:00
    跟我一起背:指针是合法的整形,整形是合法的指针
    geelaw
        18
    geelaw  
       2022-06-18 14:14:32 +08:00   ❤️ 1
    argv[argc-1] 是正确的,这不必多说。

    argc[argv-1] 是语法正确的,楼上已经解释过了。然而这段代码是错误的(不可移植),见 C89 2.1.2.2 和 3.3.6 。

    2.1.2.2 规定了 int main(int argc, char *argv[]) 的实参含义,满足该要求的实现里,argv 可以是一个长度为 (argc + 1) 的数组,此时依 3.3.6 对指针加减一个整数定义,(argv - 1) 是 undefined behavior ,因为它不能指向 argv 数组的任何一个元素,也不能指向 argv 数组最后一个元素之后的位置。该 undefined behavior 不要求算出的指针被解引用。

    注意 argc[argv-1] 等同于 *(argc + (argv - 1)),因此是错误的。但 *(argc + argv - 1) 就不是错误的了(除非 argc 等于 0 )。
    evan1024
        19
    evan1024  
       2022-06-18 19:20:12 +08:00 via Android
    换 rust😏
    mingl0280
        20
    mingl0280  
       2022-06-18 23:42:08 +08:00
    @geelaw 你这个理解是错误的,C99 6.5.6 (8):When an expression that has integer type is added to or subtracted from a pointer, the result has the type of the pointer operand. In other words, if the expression P points to
    the i-th element of an array object, the expressions (P)+N (equivalently, N+(P)) and (P)-N (where N has the value n) point to, respectively, the i+n-th and i−n-th elements of the array object, provided they exist.
    mingl0280
        21
    mingl0280  
       2022-06-18 23:45:46 +08:00   ❤️ 1
    因为这就是标准定义:
    When an expression that has integer type is added to or subtracted from a pointer, the result has the type of the pointer operand. If the pointer operand points to an element of an array object, and the array is large enough, the result points to an element offset from the original element such that the difference of the subscripts of the resulting and original array elements equals the integer expression. In other words, if the expression P points to the i-th element of an array object, the expressions (P)+N (equivalently, N+(P)) and (P)-N (where N has the value n) point to, respectively, the i+n-th and i−n-th elements of the array object, provided they exist. Moreover, if the expression P points to the last element of an array object, the expression (P)+1 points one past the last element of the array object, and if the expression Q points one past the last element of an array object, the expression (Q)-1 points to the last element of the array object. If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined. If the result points one past the last element of the array object, it shall not be used as the operand of a unary * operator that is evaluated.
    注意 18 楼的理解是错误的。
    mingl0280
        22
    mingl0280  
       2022-06-18 23:46:31 +08:00
    @kgdb00 C 标准就这么规定的,不是什么“不符合 C 语言的语法规范”
    xfriday
        23
    xfriday  
       2022-06-19 01:08:17 +08:00
    @xiri 虽然我知道为什么它们相等,也知道符合规范,但是不妨碍我认为这规范就是坨 shi ,这坨 shi 除了玩花样以外没有带来任何用处,a[b]=*(a+b)=*(b+a)=b[a] 这个等式只在 a 和 b 当中有 1 个是可以用下标访问的,1 个是整数才成立; 2 个都是可以用下标访问的或都是整数就过不了,这规范跟个八股文似的
    xfriday
        24
    xfriday  
       2022-06-19 01:10:51 +08:00
    这坨 shi 就像说 a ÷ b = b ÷ a 一样
    geelaw
        25
    geelaw  
       2022-06-19 01:55:14 +08:00
    @mingl0280 #20 #21 我帮你节选了你需要关注的部分:

    If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined.

    现在你能理解 #18 的意思了吗?
    MrKrabs
        26
    MrKrabs  
       2022-06-19 02:20:57 +08:00
    意义不明反直觉
    mingl0280
        27
    mingl0280  
       2022-06-19 05:32:51 +08:00
    @geelaw 我节选的段落里面已经非常清楚地表述了求值顺序:先使用 N+(P)(同样地,N-(P)或(P)-N )求得 i+(n-th)或 i-(n-th)个元素,然后判断操作数和结果指针是否越界,如果其中**一个**没有越界,则结果**不**应该溢出;否则,结果未定义。
    根据这个手册的内容,首先,P 是 argv ,N 是-1 ,先求得 argv-1 的指针,该指针没有越过 argv 的最后一个项,因此值合法且指向数组最后一项;然后,argc[...]等价于(...)[argc],该顺序下不存在未定义行为,不存在代码错误。
    现在你告诉我,18 楼理解哪里正确了?
    你截取的部分只能证明你连这段话都没看懂。
    mingl0280
        28
    mingl0280  
       2022-06-19 05:42:01 +08:00
    @xfriday 这其实是标准那边应该是想把 a[N] 同等于 N[a]搞出来的交换律,数学上加法和乘法符合交换律,除法和减法不符合交换律,一样的道理。
    AX5N
        29
    AX5N  
       2022-06-19 07:25:40 +08:00
    @xfriday 与其说是“玩花样”,倒不如说是“不玩花样”才导致了这种灵活性。
    geelaw
        30
    geelaw  
       2022-06-19 11:09:35 +08:00   ❤️ 1
    @mingl0280 #27 您的翻译是错误的,但您的错误不止于此。

    >如果其中**一个**没有越界,则结果**不**应该溢出

    If BOTH the pointer operand AND the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined.

    这句话的意思是:如果指针运算数和结果都指向同一个数组对象里的元素或该数组对象最后一个元素之后的位置,求它的值无溢出;否则,行为无定义。

    假设 (P)+N 或 N+(P) 或 (P)-N 里的 P 指向数组里的下标是 i 的元素(如果是最后一个元素之后的位置,则令 i=M 为数组里的长度),令 i+N 或 i-N 为 j (取决于运算),这个定义要求 0 <= i <= M 且 0 <= j <= M ,否则行为未定义。

    以上是英语和汉语的问题。下面是数学问题

    > P 是 argv ,N 是-1 ,先求得 argv-1 的指针,该指针没有越过 argv 的最后一个项,因此值合法且指向数组最后一项

    #18 已经说明 argv 可以指向某数组的第一个元素(即下标是 0 的元素),此时 argv-1 是“第一个元素之前的位置”(这个概念只存在于你我的想象中,不存在于 C 语言里),您怎么会认为 argv-1 是该数组的最后一个元素呢?
    Kasumi20
        31
    Kasumi20  
       2022-06-19 11:14:37 +08:00
    a[b] 应该等价于 *(a + b*sizeof(*a))。
    mingl0280
        32
    mingl0280  
       2022-06-19 12:11:40 +08:00
    @geelaw 我回去重新想了下这里问题出在哪。我之前那个说法应该是错误的。
    现在我还有一个比较坑爹的说法。
    argc[argv-1] = *((argc) + *((argv) - (1)))
    此处 argv 地址为 0x00000000
    那么第一次运算的 ptr 指向 0x0-4 = FFFC ,此时 ptr 能否解引用?应该是可以的吧。
    第二次运算的 ptr 为 FFFC + argc ,又回来了。(因为 argc 至少为 1 )
    *((argc) + argv) 的地址就不会越过 0 ,这个结果就是合法的。
    这是另一个想法。
    geelaw
        33
    geelaw  
       2022-06-19 12:55:52 +08:00
    @mingl0280 #32 我觉得您开始混淆应然和实然的问题了,从 #18 以来的问题并不考虑 argc[argv-1] 在最近常见的电脑的常见操作系统的常见编译器上是否对应 argv[argc-1]。

    > argc[argv-1] = *((argc) + *((argv) - (1)))

    这个想法也是错误的,argc[argv-1] 等价于 *(argc + (argv - 1)),改成 *(argc + *(argv - 1)) 是完全不同的意思,后者等价于 argc[argv[-1]],很明显也是不可移植代码。

    认为指针的运算等于“地址数值”的运算也是错误的,更不能认为 p - 1 + 1 等同于 p ,最简单的例子:

    int a[1] = {};
    int *p = a - 1 + 1;

    这段代码里 p 的初始化表达式蕴含着未定义行为,因为 a - 1 是不存在的概念。改成下面这样就没问题了:

    int a[1] = {};
    int *p1 = a + 1 - 1;
    int *p2 = a - (-1) + (-1);
    int *p3 = (int *)((uintptr_t)a - sizeof(int) + sizeof(int));

    前两个版本的运算从来没有离开 a 的元素或元素之后的位置,第三个版本里,无符号数的加减运算无溢出,并且 uintptr_t 和指针之间的转换保证数值上的返程关系。
    2NUT
        34
    2NUT  
       2022-06-19 14:07:33 +08:00
    [] 下标标记 只是 指针加 的语法糖
    phithon
        35
    phithon  
       2022-06-19 14:46:45 +08:00
    众所周知,C 是弱类型语言
    zxCoder
        36
    zxCoder  
       2022-06-19 17:22:12 +08:00
    羡慕 C 程序员的大脑
    LANB0
        37
    LANB0  
       2022-06-20 10:42:40 +08:00
    指针啊,C 语言里一切变量函数皆指针,给足了程序员发挥空间。而不是像很多高级语言,给足了语法糖大礼包,程序员只是个两点一线的工蜂。
    FrankHB
        38
    FrankHB  
       2022-06-24 21:40:57 +08:00
    @LANB0 你在说啥?函数是什么鬼指针,你 sizeof 试试?
    又有几个语言有比 C 的得 [] 更加标准的源语言层面的纯语法糖而不是混了坨 unspecified 没法一一映射实现的?
    FrankHB
        39
    FrankHB  
       2022-06-24 21:48:29 +08:00
    好像没人直接回答标题的字面问题。

    WG14 N2176
    5.1.1.3 Diagnostics
    1 A conforming implementation shall produce at least one diagnostic message (identified in an implementation-defined manner) if a preprocessing translation unit or translation unit contains a violation of any syntax rule or constraint, even if the behavior is also explicitly specified as undefined or implementation-defined. Diagnostic messages need not be produced in other circumstances. 9
    9) The intent is that an implementation should identify the nature of, and where possible localize, each violation. Of course, an implementation is free to produce any number of diagnostics as long as a valid program is still correctly translated. It may also successfully translate an invalid program.

    注意最后一句话。
    LANB0
        40
    LANB0  
       2022-06-27 10:24:05 +08:00
    @FrankHB sizeof 可以用于函数名称?不如打印函数名称和函数名称取地址的 16 进制看看是不是一个东西?[]作为指针的另一种写法也好意思叫语法糖了?作为一根指针行天下的 C 程序员,题主的问题还需要去翻一堆标准文档?和下面这个宏有什么区别?
    define offsetof(TYPE, MEMBER) ((int) &((TYPE *)0)->MEMBER)
    FrankHB
        41
    FrankHB  
       2022-06-30 08:06:12 +08:00
    @LANB0 所以不是你说的函数是指针么。如果是语法糖,那么只要没语法错误就能够直接替换。实际呢?
    所谓语法糖就是能只需要遵从语法规则,在早期替换而不引起含义改变的严格意义上的“另一种写法”。任何引起语义规则介入影响正确性的,任何多引入未指定行为的,诸如此类,都不配叫“语法”糖。望周知。
    kkzxak47
        42
    kkzxak47  
       2022-07-05 20:36:31 +08:00
    已经不是语法和语法糖的范畴了,是 syntactic poison 哈哈哈
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1235 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 18:05 · PVG 02:05 · LAX 10:05 · JFK 13:05
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.