访问同一内存位置两次,是否属于未定义行为(UB)?

5
这个主题中,最受欢迎的答案得到了很多赞和奖励。它提出了以下算法:
void RemoveSpaces(char* source)
{
  char* i = source;
  char* j = source;
  while(*j != 0)
  {
    *i = *j++;         // UB?
    if(*i != ' ')
      i++;
  }
  *i = 0;
}

我的第一反应是,这段代码会引发未定义的行为,因为 ij 指向同一内存位置,表达式 *i = *j++; 将访问同一变量两次,而不是用于确定要存储什么,中间没有序列点。尽管它们是两个不同的变量,但最初它们指向同一内存位置。

然而,我不确定,在实践中,两个非序列化访问同一内存位置如何会造成任何伤害。

我是否正确地陈述了这是未定义的行为?如果是,那么有没有任何依赖于此类 UB 的示例可以导致有害行为?


编辑

将此标记为 UB 的 C 标准的相关部分是:

C99 6.5

在前一个和后一个序列点之间,对象通过计算表达式的值最多被修改一次其存储的值。此外,只能读取先前的值以确定要存储的值。

C11 6.5

如果标量对象上的副作用相对于同一标量对象上的不同副作用或使用相同标量对象的值计算是无序的,则行为未定义。如果表达式的子表达式有多个允许的排序方式,则如果在任何排序中发生这样的无序副作用,则行为未定义。

文本的实际含义应该在标准的两个版本中相同,但我认为 C99 文本更易于阅读和理解。


5
@NatashaDutta 不是这样的。 - Iharob Al Asimi
3
请仔细阅读C标准中的相关部分,你可以读取一个对象来确定新值。如果你出于其他目的读取一个对象,则将导致未定义的行为。请注意不要改变原文的意思。 - gnasher729
3
抱歉,我到现在还有点困惑,那么我的理解是,我可以说 *j(与 *i 相同)已被读取并分配给 *i,而 *j 的递增是在赋值完成后进行的,因此这是一种定义良好的行为。我理解得对吗? - Natasha Dutta
3
请注意运算符优先级,递增操作作用的是j(指针地址)而不是*j(指针所指向的内容)。 - Lundin
2
@Lundin 是的,完全正确。我的错。 - Natasha Dutta
显示剩余3条评论
4个回答

6

如果没有中间的序列点,访问相同对象两次会出现两种未定义行为的情况:

  1. If the modify the same object twice. For example

    int x = (*p = 1, 1) + (*p = 2, 100);
    

    Obviously you wouldn't know whether *p is 1 or 2 after this, but the wording in the C standard says that it is undefined behaviour, even if you write

    int x = (*p = 1, 1) + (*p = 1, 100);
    

    so storing the same value twice doesn't save you.

  2. If you modify the object, but also read it without using the value read to determine the new value of the object. That means

    *p = *p + 1; 
    

这是没问题的,因为你读取了 *p,修改了 *p,但是你又读取了 *p 以确定存储在 * 中的值。


我非常喜欢这个答案,比起我的答案来说,虽然我的答案并没有错,但是这个答案解释得更好。我想我的问题理解可能不是100%清晰,否则即使英语不是我的母语,我也会表达得更好。 - Iharob Al Asimi
那么你的意思是,代码是UB,因为2)吗?它读取j并在同一表达式中将j增加1,这不是为了确定要存储在i中的值。 - Lundin

3

这里没有未定义的行为(这甚至是惯用的C语言写法),因为:

  • *i 只被修改了一次(在 *i = 中)
  • j 只被修改了一次(在 *j++ 中)

当然在发布的代码中,ij 可以指向同一位置(并且在第一次通过时确实会这样),但是……它们仍然是不同的变量。所以在 *i = *j++; 这一行中:

  • 地址被读入两个指针(ij)中
  • 先前的值被读取(*j++)并用于确定要存储的值
  • 只有 j 指针被修改
  • 源通过未修改的指针进行修改

它绝对不是未定义的行为。


以下代码会导致未定义的行为:

*i = *j++ + *j++;  // UB j modified twice
i = i++ + j;       // UB i modified twice

但是对于发布的代码,source被修改了两次...你是说变量名决定了UB,而不是内存访问吗? - Lundin
1
@Lundin:不,源代码只修改了一次。另一个变化在j指针上。但请看我的编辑。 - Serge Ballesta
抱歉,未更改,已访问,用于除了确定要存储的值之外的其他目的。 - Lundin
@Lundin: 不是的。该值存储在 *i 中(假设它是源)。而 *i 的先前值仅用于确定要存储的值。地址被用于第二个访问。*i = *(j++) 会导致 UB,因为这里源 将被修改两次。 - Serge Ballesta
这仍然会让我们使用指针变量(源地址)被访问三次。首先是为了确定要存储的值,然后被访问以确定要将其存储在哪里,最后增加。因此,_pointer_的值被修改一次,但也被用于与计算要存储的值(j+1)无关的两个其他目的。 - Lundin
1
@Lundin:不,你必须考虑变量。变量j只被访问一次。*i = *j + *j++;是未定义行为,因为j被访问了两次,并且其中一个没有用于确定要存储的值。 - Serge Ballesta

0

我认为这不会导致未定义行为。在我看来,这就像说

int k=0;
k=k; //useless but does no harm

将数据从内存中读取然后写入相同的位置不会造成任何损害。


但是如果 k = k++ 呢?这与原帖中的问题相关。 - Santosh A

0

分解表达式*i = *j++。三个运算符的优先级顺序为:++(后增量)最高,然后是运算符*(指针解引用),=最低。

因此,j++将首先被评估(结果等于j,并具有增加j的效果)。因此,该表达式等同于

 temp = j++;
 *i = *temp;

这里的temp是编译器生成的指针临时变量。这两个表达式都没有未定义行为,这意味着原始表达式也没有未定义行为。


1
我包含了临时变量,因为这是后置递增的语义。只要它产生相同的净效果,编译器可以重新排序和消除临时变量,就像你所描述的那样。但这并不是必需的。 - Peter
是的,因此没有任何保证会创建临时变量。如果有这样的保证,在这种情况下,代码肯定不会有害的行为。 - Lundin
1
你没有理解重点。我并不是说会创建一个临时变量。我是说编译器需要产生与生成临时变量相同的净效果(即*i*jj的更改)。 - Peter
注意:你的论点会得出 *i = *i++ + *j++ 不是未定义行为的结论...但实际上它确实是!首先计算 i + j,然后对 j 进行后缀递增,但没有规定 i 的递增和对 *i 的赋值哪个先执行。这两个操作不是有序的 - Serge Ballesta
不是真的。我对所询问的表达式进行了具体分析。我没有发表一般性的概括性陈述,暗示适用于任何其他表达式。 - Peter
显示剩余2条评论

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