在C语言中,将指向数组的指针作为指向指针的指针传递是否会导致未定义行为?

4
我有这样的代码:
#include <stdlib.h>
#include <stdio.h>

void func(int **b)
{
    printf("b = %p\n", b); // 0x7ffe76932330
    *b = *b + 1;
}

int main(void)
{
    int b[10] = {0};

    printf("b = %p\n", &b[0]); // 0x7ffe76932330
    printf("%d\n", b[0]);      // 0

    func(&b);

    printf("%d\n", b[0]); // 4
    return 0;
}

这段代码是否有未定义行为(UB)?在我看来是有的,至少因为不同类型没有明确的转换 int (*)[10] != int **

另外,如果我有 char b[] = "some string"; 会怎么样呢?行为几乎相同......奇怪。


我相信传递数组不会引起未定义行为,但是对*b的重新赋值几乎可以确定是的。 - Mooing Duck
我相信传递数组不会导致未定义行为,但是几乎可以肯定重新赋值*b会导致未定义行为。 - Mooing Duck
我相信传递数组不会导致未定义行为,但是对*b的重新赋值几乎肯定会导致未定义行为。 - undefined
3个回答

5
将指针本身传递并不一定是未定义行为,但随后使用转换后的指针则是。
C语言允许从一个对象类型转换到另一个对象类型,并在C标准的6.2.3.2p7节中有所记录:
引用如下: “可以将指向对象类型的指针转换为指向不同对象类型的指针。如果结果指针对于引用类型没有正确对齐,则行为是未定义的。否则,当再次转换回来时,结果应与原始指针相等。当将指向对象的指针转换为指向字符类型的指针时,结果指向对象的最低地址字节。对结果的连续增加,直到对象的大小,将产生指向对象剩余字节的指针。”
因此,假设没有对齐问题(即数组在64位系统上以8字节偏移开始),只要将int (*)[10]传递给期望int **的函数是允许的,尽管大多数编译器会警告转换不兼容的指针类型。
未定义行为发生在这里:
*b = *b + 1;

因为您正在通过一个不兼容的指针类型(而不是char *)对对象进行解引用。关于您被允许解引用的规则在第6.5p7节中列出:

一个对象只能通过具有以下类型之一的lvalue表达式来访问其存储值:

  • 与对象的有效类型兼容的类型
  • 与对象的有效类型兼容的类型的限定版本
  • 与对象的有效类型相应的有符号或无符号类型
  • 与对象的有效类型的限定版本的相应有符号或无符号类型
  • 包含上述类型之一的聚合或联合类型(包括递归地作为子聚合或包含的联合的成员),或
  • 字符类型。

int (*)[10]解引用为int **不满足上述任何条件,因此*b是未定义行为。


谢谢回答!所以,解引用不兼容的类型是未定义行为(UB),除了你提到的规则外。如果是 char b[] = "some string" 这样的情况,它不会是 UB,对吗? - k1r1t0
1
@k1r1t0 仍然是UB(未定义行为),因为char (*)[12]char **不兼容。 - dbush
1
@k1r1t0 仍然是UB(未定义行为),因为char (*)[12]char **不兼容。 - dbush
1
@k1r1t0 仍然是UB,因为char (*)[12]char **不兼容。 - undefined
@chqrlie 只有在存在对齐问题的情况下,才会出现我之前提到的假设。 - dbush
显示剩余10条评论

2
一个数组不是一个指针,因此当你将指向数组的指针传递给func时,使用func(&b)的指针不是指向指针的指针。它是一个指向数组的指针,这是一种不寻常的类型,当将数组的数组传递给函数时产生(int b[10][10]定义了一个由int组成的数组的数组)。
&b传递给func涉及指针类型之间的转换,这是C标准允许的,但程序员应该小心:如果正确配置,编译器会发出警告:-Wall -Werror建议在gccclang中使用。
关于未定义行为本身:你将&b传递给func,期望得到一个int **类型。编译器会将&b的类型,即int(*)[10]转换为可能具有不同对齐要求的int **类型。实际上,b按照int的宽度(通常为4字节)对齐,而int *可能需要8字节的对齐,这在大多数64位系统上是这样的。
C23标准将此转换定义为具有未定义行为:

6.3.2.3 指针

对象类型的指针可以转换为不同对象类型的指针。如果结果指针对于引用的类型没有正确对齐,则行为是未定义的。

因此,标准将这种转换描述为具有未定义行为
如果int *int具有相同的对齐要求,例如在32位系统上,当将&b传递给func时,您不会遇到未定义的行为,但是在评估表达式*b = *b + 1;时会遇到未定义的行为,因为:

6.5 表达式

一个对象只能通过以下类型之一的lvalue表达式访问其存储值:

  • 与对象的有效类型兼容的类型
  • 与对象的有效类型兼容的类型的限定版本
  • 与对象的有效类型相应的有符号或无符号类型
  • 与对象的有效类型的限定版本相应的有符号或无符号类型
  • 包括上述类型之一的聚合或联合类型(包括递归地作为子聚合或包含的联合的成员),或
  • 字符类型。
因此,在*b = *b + 1中对b进行解引用会导致未定义的行为。为了说明这一点,你可以尝试调用func(&(b+1))来检查未定义行为是否更加明显(程序可能会因总线错误而退出)。
另外,请注意printf期望一个void *作为%p的参数,所以b&b[0]必须被强制转换为(void *),以避免出现另外两个未定义行为的实例。

其实,你是对的,但问题不在于如何使其正常工作,而是关于未定义行为本身,即C标准对此的规定。 - k1r1t0
你的改写完全改变了 *b = *b + 1; 的意义。 - Ben Voigt
@BenVoigt:我更新了答案:一开始我忽略了问题的法律解释性质 :) - chqrlie
@k1r1t0: 对不起,我一开始忽视了这个问题的“法律巫师”性质 :) - chqrlie
@k1r1t0: 不好意思,我最初忽略了问题的法律性质 :) - chqrlie
显示剩余5条评论

1
表达式 &b 的类型是 int ( * )[10]。从类型 int ( * )[10] 的指针到类型 int ** 的指针没有隐式转换。因此,编译器应该对这个语句发出一个错误信息。
func(&b);

但是即使你将参数表达式写成这样:
func( (int ** )&b);

尽管解引用获得的指针表达式可能会导致未定义的行为。也就是说,在函数调用中使用的表达式&b具有与数组第一个元素的地址值相同的地址值。
因此,在函数内部,表达式*b将产生传递数组的第一个元素的值(例如,如果sizeof( int * )等于sizeof( int ),当两者都等于4时)或传递数组的前两个元素的组合值(例如,如果sizeof( int * )等于2 * sizeof( int ),当指针大小等于8且整数大小等于4时)。
也就是说,表达式*b将不包含有效的地址。
因此,这个语句:
*b = *b + 1;

这个没有意义。在提供的例子中,由于初始数组是零初始化的,表达式*b可能会产生一个空指针。你可以在函数内部测试这个表达式,例如以下方式。
printf( "*b == NULL is %s\n", *b == NULL ? "true" : "false" );

同样的问题也会出现在字符数组中:
char b[] = "some string";

如果你会以同样的方式使用它。
相反,你可以写成这样:
int b[10] = {0};

int *pb = b;

func( &pb );

在这种情况下,函数内的表达式*b将指向传递数组的第一个元素,并且此语句:
*b = *b + 1;

将增加获取的指针,该指针现在将指向数组的第二个元素。


数组内容被初始化为零,但是数组的地址不为零——*b 不会是一个空指针。 - Ben Voigt
数组内容被初始化为零,但是数组的地址不为零——*b不会是空指针。 - Ben Voigt
@BenVoigt 数组的地址是函数内的表达式b。表达式*b读取数组元素的值。你尝试过我展示的函数内的printf调用了吗? - Vlad from Moscow
@BenVoigt 特别为您提供一个演示程序。#include <stdio.h>void func(int b) { printf(“ b == NULL is%s\n”, b == NULL?“true”:“false”); } int main(void) { int b [10] = {0}; func((int **)&b); } 程序输出是 * b == NULL is true所以在对我的回答进行负评之前,执行我回答中显示的printf语句可能会对您有所帮助。:) - Vlad from Moscow
@BenVoigt 在我的回答中清楚地写着,表达式 *b 会将数组元素的值或值作为地址获取,从而得到一个无效的地址,程序也证明了这一点。所以表达式 *b 可能是一个空指针。有什么不清楚的吗? - Vlad from Moscow
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接