指针的 memcpy 与赋值相同吗?

Is memcpy of a pointer the same as assignment?

简介:这个问题是我的 C 和 C++(和 C/C++ 公共子集)问题集的一部分,关于 指针对象严格相同的情况允许按字节表示具有不同的 "values",即对某些操作表现不同(包括在一个对象上具有定义的行为而在另一个对象上具有未定义的行为)。

之后,这里是关于指针语义的问题,希望能解决问题:

这个程序在所有情况下都有效吗?唯一有趣的部分是 "pa1 == pb" 分支。

#include <stdio.h>
#include <string.h>

int main() {
    int a[1] = { 0 }, *pa1 = &a[0] + 1, b = 1, *pb = &b;
    if (memcmp (&pa1, &pb, sizeof pa1) == 0) {
        int *p;
        printf ("pa1 == pb\n"); // interesting part
        memcpy (&p, &pa1, sizeof p); // make a copy of the representation
        memcpy (&pa1, &p, sizeof p); // pa1 is a copy of the bytes of pa1 now
        // and the bytes of pa1 happens to be the bytes of pb 
        *pa1 = 2; // does pa1 legally point to b?
    }
    else {
        printf ("pa1 != pb\n"); // failed experiment, nothing to see
        pa1 = &a[0]; // ensure well defined behavior in printf
    }
    printf ("b = %d *pa1 = %d\n", b, *pa1);
    return 0;
 }

我想要一个基于标准引语的答案。

编辑

应大家的要求,我想知道的是:

这里假设某个超过结束指针的指针碰巧不小心指向了另一个对象;我怎样才能使用结束指针之后的这样一个对象来访问另一个对象?

我有权做任何事情,除了使用其他对象地址的副本。 (这是一个理解C中指针的游戏。)

IOW,我试着像黑手党一样回收脏钱。但是我通过提取它的值表示来回收一个脏指针。然后它看起来像干净的钱,我的意思是指针。没人能分辨出来吧?

指针只是一个无符号整数,其值是内存中某个位置的地址。覆盖指针变量的内容与覆盖普通 int 变量的内容没有什么不同。

所以是的,做例如memcpy (&p, &pa1, sizeof p) 等同于赋值 p = pa1,但效率可能较低。


让我们换个方式试试:

你有 pa1 指向某个对象(或者更确切地说,指向某个对象之外的对象),然后你有指针 &pa1 指向变量 pa1 (即其中变量 pa1 位于内存中)。

图形上看起来像这样:

+------+     +-----+     +-------+
| &pa1 | --> | pa1 | --> | &a[1] |
+------+     +-----+     +-------+

[注:&a[0] + 1等同于&a[1]]

   *pa1 = 2; // does pa1 legally point to b?

不,pa1指向b纯属巧合。请注意,程序必须在编译时符合,指针在运行时恰好具有相同的值并不重要。

Nobody can tell the difference, no?

编译器优化器可以区分! 编译器优化器可以(通过代码的静态分析)看到 b 并且永远不会通过 "legal" 指针访问,因此它假定将 b 保存在寄存器中是安全的。这个决定是在编译时做出的。

底线:

"Legal"指针是通过赋值或者内存拷贝的方式从一个合法的指针中得到的指针。您还可以使用指针算法获得 "legal" 指针,前提是生成的指针在 assigned/copied 来自的 array/memory 块的合法范围内。如果指针运算的结果恰好指向另一个内存块中的有效地址,那么使用这样的指针仍然是UB。

另请注意,仅当两个指针指向相同的 array/memory 块时,指针比较才有效。

编辑:

哪里出错了?

标准规定越界访问数组会导致未定义的行为。您通过一个指针获取越界地址,复制它然后取消引用它。

标准规定,越界指针可能与指向恰好在内存中相邻的另一个对象的指针比较相等(6.5.9 pt 6)。然而,即使它们比较相等,但在语义上它们并不指向同一个对象。

在你的例子中,你不比较指针,而是比较它们的位模式。没关系。指针 pa1 仍然被认为是指向数组末尾的指针。

请注意,如果您将 memcpy 替换为您自己编写的某个函数,编译器将不知道 pa1 的值是什么,但它仍然可以静态地确定它不能包含 "legally" 获得了 &b.

的副本

因此,在这种情况下,允许编译器优化器优化 b 的 read/store。

is a pointer's semantic "value" (its behavior according to the specification) determined only by its numerical value (the numerical address it contains), for a pointer of a given type?

没有。该标准推断,有效指针只能使用寻址运算符 (&)、通过复制另一个有效指针或通过 in/decreasing 数组边界内的指针从对象中获取。作为一种特殊情况,超出数组末尾的指针是有效的,但不能取消引用它们。这可能看起来有点严格,但如果没有它,优化的可能性将受到限制。

if not, it is possible to copy only the physical address contained in a pointer while leaving out the associated semantic?

不,至少不是以可移植到任何平台的方式。在许多实现中,指针值就是地址。语义在生成的代码中。

您已经证明它似乎适用于特定的实现。这并不意味着它一般有效。事实上,这是一种未定义的行为,其中一种可能的结果恰好是 "seems to work".

如果,我们回到 MS-DOS 时代,我们有近指针(相对于特定段)和远指针(包含段和偏移量)。

大数组往往分配在自己的段中,只有偏移量用作指针。编译器已经知道哪个段包含特定数组,因此它可以将指针与适当的段寄存器组合起来。

在那种情况下,您可以有两个具有相同位模式的指针,其中一个指针指向数组段 (pa),另一个指针指向堆栈段 (pb ).指针比较相等,但仍然指向不同的东西。

更糟的是,具有 segment:offset 对的远指针可以由重叠的段组成,因此 不同的 位模式仍然指向相同的物理内存地址.例如 0100:02100120:0010.

的地址相同

C 和 C++ 语言的设计使其可以工作。这就是为什么我们有规则比较指针只在同一个数组内工作(给出总顺序),并且指针可能不会指向同一个东西,即使它们包含相同的位模式。

未定义的行为:n 部分的戏剧。

Compiler1 和 Compiler2 进入,阶段正确。

int a[1] = { 0 }, *pa1 = &a[0] + 1, b = 1, *pb = &b;

[Compiler1] Hello, a, pa1, b, pb. How very nice to make your acquaintance. Now you just sit right there, we're going to look through the rest of the code to see if we can allocate you some nice stack space.

Compiler1 浏览了剩余的代码,偶尔皱着眉头在纸上做一些标记。 Compiler2 挖起鼻子盯着 window.

[Compiler1] Well, I'm afraid, b, that I have decided to optimize you out. I simply couldn't detect somewhere which modified your memory. Maybe your programmer did some tricks with Undefined Behaviour to work around this, but I'm allowed to assume that there is no such UB present. I'm sorry.

退出 b,被熊追赶。

[Compiler2] Wait! Hold on a second there, b. I couldn't be bothered optimizing this code, so I've decided to give you a nice cosy space over there on the stack.

b高兴得跳起来,一变身就被鼻魔杀死

[Narrator] Thus ends the sad, sad tale of variable b. The moral of this story is that one can never rely on undefined behaviour.

在 C99 之前,实现应该表现得好像任何类型的每个变量的值都存储了一个 unsigned char 值序列;如果检查相同类型的两个变量的底层表示并发现它们相等,这意味着除非未定义行为已经 已经 发生,否则它们的值通常是相等的并且可以互换。有几个地方有点模棱两可,例如给出

char *p,*q;
p = malloc(1);
free(p);
q = malloc(1);
if (!memcmp(&p, &q, sizeof p))
  p[0] = 1;

C 的每个版本都非常清楚 q 可能等于也可能不等于 p,如果 q 不等于 p,代码应该期望在写入 p[0] 时可能发生任何事情。虽然 C89 标准没有明确指出,如果写入 p 等同于写入 q,则实现可能只有 p 按位比较等于 q,这种行为通常由完全封装在 unsigned char 值序列中的变量模型暗示。

C99 添加了一些变量可能比较按位相等但不相等的情况。考虑一下,例如:

extern int doSomething(char *p1, char *p2);
int act1(char * restrict p1, char * restrict p2)
  { return doSomething(p1,p2); }
int act2(char * restrict p)
  { return doSomething(p,p); }
int x[4];
int act3a(void) { return act1(x,x); }
int act3b(void) { return act2(x); }
int act3c(void) { return doSomething(x,x); }

调用 act3aact3bact3c 将导致 doSomething() 被调用,两个指针比较等于 x,但如果调用通过 act3a,在 doSomething 中写入的 x 的任何元素都必须使用 x 独占访问,独占使用 p1,或独占使用 p2.如果通过 act3b 调用,该方法将获得使用 p1 编写元素并通过 p2 访问它们的自由,反之亦然。如果通过 act3c 访问,该方法可以互换使用 p1p2xp1p2 的二进制表示中没有任何内容表明它们是否可以与 x 互换使用,但允许编译器在 doSomething 内内联扩展 act1act2 并且这些扩展的行为根据允许和禁止的指针访问而有所不同。

问题是:

Is this program valid in all cases?

答案是"no, it is not"。


程序中唯一有趣的部分是 if 语句保护的块内发生的事情。保证控制表达式的真实性有些困难,因此我通过将变量移至全局范围对其进行了一些修改。同样的问题仍然存在:这个程序是否总是有效的:

#include <stdio.h>
#include <string.h>

static int a[1] = { 2 };
static int b = 1;
static int *pa1 = &a[0] + 1;
static int *pb = &b;

int main(void) {
    if (memcmp (&pa1, &pb, sizeof pa1) == 0) {
        int *p;
        printf ("pa1 == pb\n"); // interesting part
        memcpy (&p, &pa1, sizeof p); // make a copy of the representation
        memcpy (&pa1, &p, sizeof p); // pa1 is a copy of the bytes of pa1 now
        // and the bytes of pa1 happens to be the bytes of pb 
        *pa1 = 2; // does pa1 legally point to b?
    }
}

现在我的编译器上的保护表达式为真(当然,通过让它们具有静态存储持续时间,编译器无法真正证明它们在此期间没有被其他东西修改...)

指针 pa1 刚好指向数组 a 的末尾,并且是一个有效的指针,但不能取消引用,即 *pa1 具有未定义的行为,因为价值。现在的情况是,将 this 值复制到 p 并再次复制回来会使指针 valid.

答案是否定的,这仍然无效,但标准本身并未明确说明。委员会对 C standard defect report DR 260 的回应是这样说的:

If two objects have identical bit-pattern representations and their types are the same they may still compare as unequal (for example if one object has an indeterminate value) and if one is an indeterminate value attempting to read such an object invokes undefined behavior. Implementations are permitted to track the origins of a bit-pattern and treat those representing an indeterminate value as distinct from those representing a determined value. They may also treat pointers based on different origins as distinct even though they are bitwise identical.

即你甚至不能得出这样的结论,如果 pa1pb 是相同类型的指针并且 memcmp (&pa1, &pb, sizeof pa1) == 0 是真的那么它也是必要的 pa1 == pb,更不用说复制位模式了指向另一个对象并再次返回的不可引用指针 pa1 将使 pa1 有效。

响应继续:

Note that using assignment or bitwise copying via memcpy or memmove of a determinate value makes the destination acquire the same determinate value.

即它确认 memcpy (&p, &pa1, sizeof p); 将导致 p 获得与 pa1 相同的值,而 .

之前没有

这不仅仅是一个理论问题 - 众所周知,编译器会跟踪指针来源。例如 the GCC manual 表示

When casting from pointer to integer and back again, the resulting pointer must reference the same object as the original pointer, otherwise the behavior is undefined. That is, one may not use integer arithmetic to avoid the undefined behavior of pointer arithmetic as proscribed in C99 and C11 6.5.6/8.

即程序是否写成:

int a[1] = { 0 }, *pa1 = &a[0] + 1, b = 1, *pb = &b;
if (memcmp (&pa1, &pb, sizeof pa1) == 0) {
    uintptr_t tmp = (uintptr_t)&a[0]; // pointer to a[0]
    tmp += sizeof (a[0]); // value of address to a[1]
    pa1 = (int *)tmp;
    *pa1 = 2; // pa1 still would have the bit pattern of pb,
              // hold a valid pointer just past the end of array a,
              // but not legally point to pb
}

GCC 手册指出这个 明确不合法

没有。鉴于 memcmp() 的任何特定结果,我们甚至无法推断此代码的任一分支都有效。即使指针相同,您与 memcmp() 比较的对象表示也可能不同,即使对象表示匹配,指针也可能不同。 (自从我最初发布后,我改变了主意。)

您尝试将数组末尾后一位的地址与数组外对象的地址进行比较。标准(n1548 草案的 §6.5.8.5,强调已添加)是这样说的:

When two pointers are compared, the result depends on the relative locations in the address space of the objects pointed to. If two pointers to object types both point to the same object, or both point one past the last element of the same array object, they compare equal. If the objects pointed to are members of the same aggregate object, pointers to structure members declared later compare greater than pointers to members declared earlier in the structure, and pointers to array elements with larger subscript values compare greater than pointers to elements of the same array with lower subscript values. All pointers to members of the same union object compare equal. If the expression P points to an element of an array object and the expression Q points to the last element of the same array object, the pointer expression Q+1 compares greater than P. In all other cases, the behavior is undefined.

在附录 J 中重复了此警告,即比较指针的结果未定义。

还有未定义的行为:

An object which has been modified is accessed through a restrict qualified pointer to a const-qualified type, or through a restrict-qualified pointer and another pointer that are not both based on the same object

但是,您程序中的 none 个指针是受限制的。你也不做非法指针运算。

您尝试使用 memcmp() 来绕过这个未定义的行为。规范的相关部分 (§7.23.4.1) 说:

The memcmp function compares the first n characters of the object pointed to by s1 to the first n characters of the object pointed to by s2.

因此,memcmp() 比较对象表示的位。 pa1pb 的位在某些实现上已经相同,但在其他实现上则不同。

本标准§6.2.6.1做出如下保证:

Two values (other than NaNs) with the same object representation compare equal, but values that compare equal may have different object representations.

指针值比较相等是什么意思? §6.5.9.6 告诉我们:

Two pointers compare equal if and only if both are null pointers, both are pointers to the same object (including a pointer to an object and a subobject at its beginning) or function, both are pointers to one past the last element of the same array object, or one is a pointer to one past the end of one array object and the other is a pointer to the start of a different array object that happens to immediately follow the first array object in the address space.

我认为最后一个条款是决定性因素。不仅比较相等的两个指针可以具有不同的对象表示,而且具有相同对象表示的两个指针如果其中一个是尾数指针(如 &a[0]+1 而另一个是指针)则可能不等价数组外的对象,如 &b。这正是这里的情况。

我说不,不求助于UB tarpit。来自以下代码:

extern int f(int x[3], int y[4]);

....
    int   a[7];
    return f(a, a) + f(a+4, a+3);
...

C 标准不应阻止我编写执行边界检查的编译器;有几个可用。边界检查编译器必须通过使用边界信息 (*) 扩充指针来 加厚 指针。所以当我们到达 f():

....
    if (x == y) {
....

F() 会对 C 相等的概念感兴趣,即它们是否指向相同的位置,而不是它们具有相同的类型。如果您对此不满意,假设 f() 调用了 g(int *s, int *t),并且它包含一个类似的测试。编译器将执行比较而不比较 fat.

指针大小 sizeof(int *),必须包含 fat,因此两个指针的 memcmp 也会比较它,从而提供与比较不同的结果.

  • = 是的,您可以将此类信息存储在动态关联数组中;这可能会导致程序因资源不足而中止,并可能引入 memcpy、alloc 和 free 的跟踪问题。

PS: 我们应该为肚脐凝视引入一个新标签吗?

题主的理解是:

Is memcpy of a pointer the same as assignment?

我的回答是,是的。

memcpy 基本上是对没有内存对齐要求的可变长度数据的优化赋值。它与以下内容几乎相同:

void slow_memcpy(void * target, void * src, int len) {
  char * t = target;
  char * s = src;
  for (int i = 0; i < len; ++i)
  {
    t[i] = s[i];
  }
}

is a pointer's semantic "value" (its behavior according to the specification) determined only by its numerical value (the numerical address it contains), for a pointer of a given type?

是的。 C 没有隐藏的数据字段,因此指针的行为完全取决于它的数字数据内容。

但是,指针算法由编译器解析并取决于指针的类型。

A char * str 指针算法将使用 char 单位(即,str[1]str[0] 相差一个 char),而 int * p_num 指针算法将使用 int 单位(即 p_num[1]p_num[0] 相差一个 int)。

Are two pointers with identical bit patterns allowed to have different behavior? (edit)

是也不是。

它们指向内存中的相同位置,在这个意义上它们是相同的。

但是,指针解析可能取决于指针的类型。

例如,通过取消引用 uint8_t *,(通常)仅从内存中读取 8 位。但是,当取消引用 uint64_t * 时,会从内存地址读取 64 位。

另一个区别是指针算法,如上所述。

但是,当使用 memcpymemcmp 等函数时,指针的行为相同。


那为什么大家都说"No"?

嗯,那是因为您示例中的代码没有反映标题中的问题。代码的行为是未定义的,正如许多答案所清楚解释的那样。

(编辑):

代码问题与实际问题关系不大。

例如,考虑以下行:

int a[1] = { 0 }, *pa1 = &a[0] + 1, b = 1, *pb = &b;

在这种情况下,pa 指向 a[1],这是越界的。

这几乎将代码抛入了未定义的行为领域,这让许多答案偏离了实际问题。