指针表达式何时“基于”另一个指针?

3
在 C 语言标准的 第 6.7.3.1 节 中涉及到 restrict 关键字,其中写道:
  1. 令 D 为一个普通标识符的声明,它提供了一种将对象 P 指定为类型 T 的 restrict 限定指针的方法。

  2. ...

  3. 在接下来的内容中,如果指针表达式 E 基于对象 P,则在 B 的执行过程中某个顺序点之前修改 P 以指向其原先所指向的数组对象的副本会改变 E 的值。

我不明白这段文字是什么意思 - 字面上:

  • P 是谁说它指向了“数组对象的副本”?
  • 为什么 P “曾经”指向任何东西?也就是说,谁说我们更改了它的值?
  • 假设 E 是一个具有局部作用域的指针。为什么修改除 E 指针本身之外的任何指针表达式都会“改变 E 的值”?它可能只会改变 E 所指向的值。对吗?

有人能帮我解释这段文字吗,使其更加易懂?

(灵感来自此答案


它假设您正在修改P,这意味着您已更改其值。 - Barmar
第3个脚注试图澄清第137个注释。 - Barmar
@Barmar:修改P本身,还是修改P所指向的值? - einpoklum
1
它说“修改P指向一个副本”。这意味着修改P,而不是它所指向的值。 - Barmar
4个回答

1

谁说过P指向“数组对象的副本”?

指针算术是在指向数组元素的指针的基础上定义的(参见C 2018 6.5.6 8和9)。为此,单个对象被视为一个元素的数组。因此,每当我们有任何非空对象指针时,在这个模型中,它都指向一个数组。

为什么P“以前”指向任何东西?也就是说,谁说我们改变了它的值?

你引用的文本是在说“为了确定E是否基于P,让我们假设复制P所指向的数组,并将指针分配给复制品中相应位置的P。”因此,你引用的文本是说我们正在改变P的值,然后我们将比较E的值与此更改和没有更改的值。

假设E是本地范围的指针。为什么修改E指针本身之外的任何指针表达式会“改变E的值”?它可能会改变E所指的值。对吗?

对象和值没有作用域。标识符有作用域。但是让我们考虑一个具有块级作用域的标识符:

// P is a pointer into A.
// S is the size of A.
// A is the start of an array not contained in any other array.
void foo(char *P, size_t S, char *A)
{
    void *E = P+2;
}

举例来说,假设 P 的值为 0x1004,A 的值为 0x1000。那么 E 是否基于 P?根据上述情况,E 的值是 0x1006。现在假设我们在定义 E 之前考虑这段代码:
    char *N = malloc(S);
    memcpy(N, A, S);
    P = P - A + N;

假设malloc返回0x2000。那么E的值将是多少?它将是0x2006。这与0x1006不同。因此,E基于P
另一方面,考虑以下内容:
void foo(char **P, size_t S, char **A)
{
    #if OnOrOff
        char *N = malloc(S);
        memcpy(N, A, S);
        P = P - A + N;
    #endif
    char **E = P[3];
}

现在,无论 OnOrOff 是 true 还是 false,E 的值会改变吗?不会,它将接收到作为引用元素的 A 的值,直接或通过复制。 P 可能指向 AN,但这并不影响 E 的值。 因此,这个 E 不是基于 P

我认为你触及了标准中语言的意图,但实际上标准措辞不可行,导致一些对象与其无关的其他对象“基于”它们,并且在某些情况下,它们并不基于从中复制其值的指针。 - supercat

1
"基于"的定义旨在定义指针之间的传递关系,但其实际措辞会产生一个无法工作的定义,就我所知,它与任何实际的编译器行为都不匹配。更简单的做法是按照以下规则进行传递(这似乎是编译器所做的):如果 *p 是类型为 T* 的指针,则以下指针“基于”p:
- p + (intExpr) 或 p - (intExpr) - (otherType*)p - &*p - &p->structMemberofNonArrayType 或 &p->unionMemberofNonArrayType - p->structMemberofArrayType 或 p->unionMemberofArrayType - &p[intExpr] - 任何基于上述任何内容的指针
我认为标准并没有明确说明 (someType*)someIntegerFunction((uintptr_t)p),编译器编写者也不清楚。
请注意,通过上述表达式之一(除了涉及到uintptr_t的转换)导出的任何q都将独立于p所持有的地址,因此(char*)p(char*)q之间的差异将是独立于p所持有的地址。

顺便提一下,这里有一个问题较多的特殊情况的示例:

int test1(int * restrict p1, int * restrict p2, int n)
{
    int *restrict p3 = p1+n;
    // How would p4 and p5 be affected if p3 were replaced
    // with a pointer to a copy here?
    int *p4 = p3;
    if (p3 != p1) p4=p1;
    int *p5 = p2 + (p3 == p1);
    *p3 = 1;
    *p5 = 2;
    return *p4;
}

使用基于另一个指针的传递方式形成指针,如果 n 为零,则 p4 显然将基于 p3。然而,指针 p5 不会派生自 p3,因为没有“基于”步骤序列可以推导出其值。

尝试将标准中给定的规则应用于 n==0 情况,通过将 p3 替换为数组副本的指针来不会影响 p4 的值,但会影响 p5 的值。这意味着 p4 不是基于 p3,但 p5 却是,以某种方式。

我认为这样的结果毫无意义,我想标准的作者们也是这样认为的,但它遵循了标准所给出的措辞。


@einpoklum:有限的操作可以创建具有所需特征的指针,我认为我没有遗漏任何一个。 - supercat
@einpoklum:我想我错过了一个指针可以“基于”另一个的方式,而我的描述并未涵盖这一点。但是,由于它是无法使用的,而且gcc和clang都不承认它,因此我认为我的描述更准确地描述了编译器实际执行的操作。[顺便说一句,给定int x,y; int restrict *p;,如果p等于x并被替换为其内容的副本,则p == x?&x:y的值将受到影响。] - supercat
@einpoklum:我认为标准的作者们知道他们想要什么概念,并试图用一般性的术语来描述它,但承认他们的描述在某些边角情况下会失败。 "数组的副本"旨在处理这些边角情况,但未能修复所有问题。措辞可能可以说(char*)p - (char*)e的值不受替换p与其副本的影响,但实际上并非如此,我认为即使是这样也可能会对通过uintptr_tintptr_t进行的操作造成问题。 - supercat
1
@einpoklum:对你的问题直接的文字回答是“不行。没有人能够帮助你将委员会所写的内容解释为一条精确规则,使其比书面文本更有意义,因为如果用于此目的,该文本将毫无意义。”我认为描述规则预计要表达的内容可能更有用,但如果你只想要一个直接回答你的问题,那么“不”应该就足够了。 - supercat
1
如果使用memcpy来复制指针,则其行为类似于将指针转换为整数(或实际上是整数序列),然后稍后从整数序列形成指针。任何单一的规则都会将某些构造定义为UB,这些构造对某些程序类型很有用,或者禁止某些对其他类型程序有用的优化。对于大多数目的而言,使得从整数形成指针的任何操作都将所形成的指针视为基于转换为整数的任何指针应该是可以的... - supercat
显示剩余8条评论

0

第三点可以用代码表达(大致如下):

#define E(P)  ( (P) + 1 )   // put the expression you want to test here

extern T obj;    // T is some type
T copy = obj;

if ( E(&obj) != E(&copy) )
    printf("the expression in the macro is based on P")

标准中使用的正式语言定义允许E是非确定性的和其他病态情况(例如(P) + rand() % 5),而我的示例则不允许。

标准版本是我们比较在相同上下文中E(&obj)的结果与E(&copy)的结果。


在大多数情况下,如果E是从P线性派生出来的,那么以任何方式更改P都会更改E。而在大多数情况下,如果不是这样,将P指向数据的副本也不会更改E。线性派生是一种易于识别的传递属性,因为除了函数调用、整数到指针转换或?:的结果之外,每个指针值都是从恰好一个其他指针值线性派生出来的。标准中给出的“正式定义”与“E是从P线性派生出来的”相符,除了在它几乎没用的困难情况下。 - supercat

0

在阅读了几篇评论以及@EricPostpischil的回答后,我试图综合出更清晰但略长的措辞来澄清事情并回答所提出的问题。

原始文本:

在接下来的内容中,如果指针表达式E基于对象P,则在B执行期间的某个序列点(在评估E之前)将修改P以指向数组对象的副本,这将改变E的值。

澄清后的文本:

在接下来的内容中,如果在评估E之前更改P(有一定限制),则称指针表达式E基于对象P会导致E评估为不同的值。这些限制是:

  • 平凡的合理性限制:必须在序列点上修改P。
  • P只能被修改为指向与其最初指向相同的副本。
    (由于通常可以认为指针总是指向数组对象 - 因此P只能设置为指向该数组对象的副本)。

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