当前位置: 首页 > 知识库问答 >
问题:

添加到“char *”指针 UB 时,它实际上并不指向 char 数组?

颛孙炜
2023-03-14

C 17(expr.add/4)表示:

当将具有整型的表达式加到指针或从指针中减去时,结果为指针操作数的类型。如果表达式P指向具有n个元素的数组对象x的元素x[i],如果为0,则表达式P J和J P(其中J的值为J)指向(可能是假设的)元素x[i J]≤i j(i j)≤n否则,行为是未定义的。同样,表达式P-J指向(可能是假设的)元素x[i−j] 如果为0≤我−j≤n否则,行为是未定义的。

struct Foo {
    float x, y, z;
};

Foo f;
char *p = reinterpret_cast<char*>(&f) + offsetof(Foo, z); // (*)
*reinterpret_cast<float*>(p) = 42.0f;

线标有(*)UB吗?reinterpret_cast

是UB吗?如果不是,为什么不是?


共有3个答案

单于轶
2023-03-14

参见CWG 1314

根据6.9[basic.types]第4段,

类型T的对象的对象表示是类型T的对象占用的N个无符号char对象的序列,其中N等于sizeof(T)。

和4.5【介绍对象】第5段,

普通可复制或标准布局类型的对象(6.9[基本类型])应占据连续的存储字节。

这些段落是否使指针算术(8.7 [expr.add] 第5段)在标准布局对象中明确定义(例如,用于编写自己的memcpy版本?

基本原理(2011年8月):

目前的措辞足够清楚,允许这种用法。

我强烈不同意CWG的说法,即“目前的措辞已经足够清楚”,但尽管如此,这是我们的裁决。

我将 CWG 的响应解释为建议,出于指针算术的目的,将指向无符号 char 的指针转换为可简单复制或标准布局类型的对象的指针,应将其解释为指向无符号 char 数组的指针,其大小等于所讨论的对象的大小。我不知道他们是否打算使用char指针或(截至C 17)std::byte指针也可以工作。(也许如果他们决定真正澄清它,而不是声称现有的措辞足够清晰,那么我就会知道答案。

(另一个问题是,是否需要std::launder来明确定义OP的代码。这里我不讨论这个问题;我认为它值得单独提问。)

韶景曜
2023-03-14

任何不允许使用offsetof的解释都必须是错误的:

#include <assert.h>
#include <stddef.h>
struct S { float a, b, c; };

const size_t idx_S[] = {
    offsetof(struct S, a),
    offsetof(struct S, b),
    offsetof(struct S, c),
};

float read_S(struct S *sp, unsigned int idx)
{
    assert(idx < 3);
    return *(float *)(((char *)sp) + idx_S[idx]); // intended to be valid
}

但是,任何允许跨越显式声明数组末尾的解释也一定是错误的:

#include <assert.h>
#include <stddef.h>
struct S { float a[2]; float b[2]; };

static_assert(offsetof(struct S, b) == sizeof(float)*2,
    "padding between S.a and S.b -- should be impossible");

float read_S(struct S *sp, unsigned int idx)
{
    assert(idx < 4);
    return sp->a[idx]; // undefined behavior if idx >= 2,
                       // reading past end of array
}

我们现在处于一个两难境地,因为C和C标准中的措辞,本来是要不允许第二种情况,也可能不允许第一种情况。

这通常被称为“什么是对象?”问题。自20世纪90年代以来,包括C和C委员会成员在内的人们一直在争论这个问题和相关问题,并且多次尝试修改措辞,据我所知,没有一次成功(从某种意义上说,所有现有的“合理”代码都被渲染为绝对一致,所有现有的“合理”优化仍然被允许)。

(注意:以上所有代码都是用C编写的,以强调这两种语言中都存在相同的问题,并且可以在不使用任何C构造的情况下遇到。)

刘安志
2023-03-14
匿名用户

这一增加是有效的,但我不相信标准能说得足够清楚。引用N4140(大致为C14):

3.9类型[基本类型]

2对于任何简单可复制类型T的对象(基类子对象除外),无论该对象是否持有类型T的有效值,构成该对象的底层字节(1.7)都可以复制到charunsignchar的数组中。42[…]

< sup>42),例如使用库函数(17.6.1.2)< code > STD::memcpy 或< code>std::memmove。

它说“例如”,因为std::memcpystd:∶memmove不是允许复制底层字节的唯一方式。手动逐字节复制的简单for循环也应该有效。

为了使其工作,必须为指向构成对象的原始字节的指针定义加法,并且表达式定义的工作方式,加法的定义不能取决于加法的结果是否随后用于将字节复制到数组中。

这是否意味着这些字节已经形成了一个数组,或者这是否是运算符描述中以某种方式省略的运算符的一般规则的特殊例外,我不清楚(我怀疑是前者),但无论哪种方式都会使你在代码中执行的加法有效。

 类似资料:
  • 在下面给出的代码中,我声明了一个指向int的指针,我们都知道memcpy返回一个指向目标字符串的空指针,所以如果ptr是指向int的指针,那么为什么printf(“%s”,ptr);是完全有效的,ptr毕竟不是指向char的指针。

  • 我正在阅读Bjarne Stroustrup的“使用C进行编程原理和实践”(第二版)。在第660-661页,作者定义了一个函数如下: 稍后在中,函数被称为其中是中,是一个,是一个大小为的数组。 我不明白

  • 我是C编程新手。我知道和数组是不同的。然而,当涉及函数参数时,可以将减为。所以函数声明可能是相同的。 但是,通过查看签名(声明),我如何知道函数是否特别需要数组,而不是? 例如,如果我使用的是库头文件,而函数在下面。我怎么知道要传递哪个? 因为如果函数正在修改参数,并且如果我传递一个,它将得到一个segfault。这是C的,但C也一样吗?

  • 我正在为一个Delphi可执行文件开发一个C++的DLL项目。此可执行文件具有如下结构: 例如: 我试着做了这样的课程: 它给了我这个: 那不是我想要的。 我可以通过使用一个数组来实现我的目标: 但是,我被迫有一个固定的长度,这仍然不是我想要的。 我想了一个肮脏的解决办法: 但要解析、读取和写入它真的很难。 这样的问题有没有干净的解决方案? 顺便说一下,这不是一个XY问题

  • 我是C编程新手。我知道和数组是不同的。然而,当涉及函数参数时,可以将减为。所以函数声明可能是相同的。 但是如何通过查看签名(声明)来知道函数是否特别期望数组与? 例如,如果我使用的是库头文件,函数如下所示。我怎么知道该通过哪一个呢? 因为如果函数正在修改参数,并且如果我传递一个,它将得到一个segfault。这是C的,但C也一样吗?

  • 6. 指向指针的指针与指针数组 指针可以指向基本类型,也可以指向复合类型,因此也可以指向另外一个指针变量,称为指向指针的指针。 int i; int *pi = &i; int **ppi = &pi; 这样定义之后,表达式*ppi取pi的值,表达式**ppi取i的值。请读者自己画图理解i、pi、ppi这三个变量之间的关系。 很自然地,也可以定义指向“指向指针的指针”的指针,但是很少用到: int