在C语言中,把指向数组的指针作为指向UB的指针传递吗?

niknxzdl  于 2023-08-03  发布在  其他
关注(0)|答案(3)|浏览(119)

我有这样一个代码:

#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";呢?行为几乎是一样的。。奇怪

ktca8awb

ktca8awb1#

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

*b = *b + 1;

字符串
因为你通过一个不兼容的指针类型(除了char *)派生一个对象。关于允许取消引用的规则在第6.5p7节中列出:
对象的储存值只能由具有下列其中一种型别的左值运算式存取:

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

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

h79rfbju

h79rfbju2#

一个数组不是一个指针,因此当你用func(&b)传递给func时,一个指向数组的指针不是一个指向指针的指针。它是一个指向数组的指针,这是一种不常见的类型,在将数组的数组传递给函数时会产生(int b[10][10]定义了int的数组的数组)。
传递&bfunc涉及到指针类型之间的转换,这是C标准允许的,但程序员应该小心:如果配置正确,编译器将发出警告:gccclang推荐使用-Wall -Werror
关于未定义的行为本身:将&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表达式

对象的储存值只能由具有下列其中一种型别的左值运算式存取:

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

因此,在*b = *b + 1中取消引用b具有未定义的行为。例如,您可以尝试调用func(&(b+1))来检查未定义的行为是否更明显(程序可能会因总线错误而退出)。
还要注意,printf需要void *作为%p,因此b&b[0]必须转换为(void *),以避免另外两个未定义行为的示例。

yhxst69z

yhxst69z3#

表达式&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;


将递增所获得的指针,该指针现在将指向数组的第二个元素。

相关问题