如何修复 strcpy 以便检测重叠字符串

Posted

技术标签:

【中文标题】如何修复 strcpy 以便检测重叠字符串【英文标题】:How to fix strcpy so that it detects overlapping strings 【发布时间】:2011-11-17 15:52:27 【问题描述】:

在一次采访中,我被要求编写strcpy 的实现,然后对其进行修复,以便正确处理重叠字符串。我的实现如下,非常幼稚。我该如何解决它:

    它检测重叠的字符串并 检测后,我们如何处理重叠并继续?

char* my_strcpy(char *a, char *b) 

     if (a == NULL || b == NULL) 
         return NULL;
     
     if (a > b) 
         //we have an overlap?
         return NULL;
     
     char *n = a;

     while (*b != '\0') 
         *a = *b;
         a++;
         b++;
     
     *a = '\0';
     return n;


int main(int argc, char *argv[])

    char str1[] = "wazzupdude";
    char *after_cpy = my_strcpy(str1 + 2, str1);
    return 0;


编辑:

因此,基于 @Secure 的 答案的一种可能实现是:

char* my_strcpy(char *a, char *b) 

    if (a == NULL || b == NULL) 
        return NULL;
    

    memmove(a, b, strlen(b) + 1);
    return a;

如果我们不依赖memmove,那么

char* my_strcpy(char *a, char *b) 

    if (a == NULL || b == NULL) 
        return NULL;
    

    if (a == b) 
        return a;
    

    // case1: b is placed further in the memory
    if ( a <= b && a + strlen(a) > b ) 
        char *n = a;

        while(*b != '\0') 
            *a = *b;
            a++; b++;
        
        *a = '\0';
        return n;
    

    // case 2: a is further in memory
    else if ( b <= a && b + strlen(b) > a )  
        char *src = b + strlen(b) - 1; // src points to end of b
        char *dest = a;

        while(src != b) 
            *dest = *src;
            dest--; src--;  // not sure about this..
        
        *a = '\0';
        return a;
    

【问题讨论】:

a &gt; b 应该如何“检测重叠”?它只是测试两个地址。 您可以进行两次复制:首先复制到本地缓冲区,没有重叠的机会,然后从本地缓冲区复制到目的地。 @pmg:你可以,但是必须允许my_strcpy 使 ENOMEM 失败。 @Steve:对——“天下没有免费的午餐”;尽管一开始就做两份副本与 免费午餐 相去甚远 :-) 关于你的编辑,作为面试官,我的下一个问题是:你为什么不依赖 memmove,而是用一个单行来代替一个不可维护的指针处理混乱? 【参考方案1】:
if a > b; then
    copy a from the beginning
else if a < b; then
    copy a from the ending
else // a == b
    do nothing

你可以参考memmove中的一个implementation,和我说的很像。

【讨论】:

【参考方案2】:

没有可移植的方法来检测这一点。您必须进行指针比较,并且这些仅在同一个对象中定义。 IE。如果两个字符串不重叠并且实际上是不同的对象,那么指针比较会给您未定义的行为。

我会让标准库处理这个问题,使用memmove(a, b, strlen(b) + 1)

编辑:

正如 Steve Jessop 在 cmets 中指出的那样,在这种情况下,实际上有一种可移植但缓慢的方法来检测重叠。比较 b 中的每个地址与 a 的第一个和最后一个地址是否相等。与== 的相等比较总是明确定义的。

所以你有这样的东西:

l = strlen(b);
isoverlap = 0;
for (i = 0; i <= l; i++)

    if ((b + i == a) || (b + i == a + l))        
    
        isoverlap = 1;
        break;
    

编辑 2:案例 2 的可视化

你有类似下面的数组和指针:

S t r i n g 0 _ _ _ _ _ _ _
^       ^
|       |
b       a

请注意,b + strlen(b) 导致指向终止 \0 的指针。从后面开始,否则您需要额外处理边缘情况。在那里设置指针是有效的,你不能取消引用它们。

src = b + strlen(b) + 1;
dst = a + strlen(b) + 1;

S t r i n g 0 _ _ _ _ _ _ _
^       ^     ^       ^  
|       |     |       |
b       a     src     dst

现在是复制 \0 的复制循环。

while (src > b)

    src--; dst--;
    *dst = *src;

第一步给出了这个:

src--; dst--;

S t r i n g 0 _ _ _ _ _ _ _
^       ^   ^       ^  
|       |   |       |
b       a   src     dst

*dst = *src;

S t r i n g 0 _ _ _ 0 _ _ _
^       ^   ^       ^  
|       |   |       |
b       a   src     dst

以此类推,直到src 最终等于b

S t r i S t r i n g 0 _ _ _
^       ^              
|       |            
b       a          
src     dst

如果你想要它更hackish,你可以进一步压缩它,但我不建议这样做:

while (src > b)
    *(--dst) = *(--src);

【讨论】:

不能便携检测重叠是不正确的。有一种令人震惊的低效方法。这是给memmove的,但我相信它可以适应strcpy:***.com/questions/4023320/… "你必须做指针比较,这些只在同一个对象中定义。" -如何检查两个指针​​是否指向同一个对象(本例中为数组) @Steve Jessop:正确,但在这种情况下它不能被便携检测,因为你只有两个指针,但没有它们指向的数组边界(起始地址和大小)。 @user639309:它必须作为已知的前提条件给出。否则,如果它们不是,则您无法在不调用未定义行为的情况下检查它,除非史蒂夫指出数组边界是已知的。然后您可以检查一个数组中的每个可能的地址是否等于另一个指针。 OTOH,如果您知道 both 数组的边界,您可以简单地检查它们是否相等。带有 == 的指针的相等性检查始终是明确定义的,相同对象规则仅适用于小于或大于检查。 @Secure:但正如你自己所说,strlen(b)+1 给了你这个大小。如果调用者首先做了一些无效的事情,它只会出错,例如如果a 没有指向足够大的缓冲区,但这不是我们的错。【参考方案3】:

如果您希望字符串重叠,您可以使用 memmove()。

char* my_strcpy(char *a, char *b)

    memmove(a, b, strlen(b) + 1);
    return a;

【讨论】:

考虑到一个字符 == 一个字节。我将 strlen(b) + 1 更改为 ( strlen(b) + 1 ) * sizeof( char ) sizeof(char) 总是正好是一个字节。 是的,但是 memmove 需要字节,而不是字符,即使它们偶然具有相同的大小。无论如何,我只是说“我愿意”。 @Baltasarq "memmove 需要字节,而不是字符" 具有误导性。 memmove() 期望字符大小,在 C 中,“字节”和字符具有相同的大小。 "memmove函数将s2指向的对象中的n个字符复制到s1指向的对象中。C11 "7.24.2.2"【参考方案4】:
if (a>= b && a <= b+strlen(b))) || (b+strlen(b) >= a && b+strlen(b) <= a + strlen(b))

(*) 你应该缓存 strlen(b) 以提高性能

它的作用: 检查a+len [address of a + extra len bytes] 是否在字符串内,或者a [address of a] 在字符串内,这些是字符串重叠的唯一可能性。

【讨论】:

【参考方案5】:

我在最近的一次采访中被问到这个问题。我们不必“检测”重叠。我们可以写strcpy 以处理重叠地址。关键是从源字符串的末尾而不是从开头复制。

这是一个快速代码。

void str_copy(const char *src, char *dst) 

    /* error checks */

    int i = strlen(a); /* may have to account for null character */

    while(i >= 0) 
    
        dst[i] = src[i];  
        i--; 
    

编辑:这仅在 a b,从头开始复制。

【讨论】:

如果字符串重叠,问题仍然存在。与memcpy 一样,您应该从头或尾复制,具体取决于要复制的目标地址是比源地址低还是高。 1) 代码无法编译。 2) 建议重新设计答案/代码以使用src dest,而不是a b。 3) strlen() 返回类型size_t,但随后size_t i 导致while(i&gt;=0) 测试出现问题,这始终是正确的。【参考方案6】:

注意:这里,b 是源字符串的地址,a 是目标地址。

a &gt; b 不一定会有重叠。如果

(a <= b && a+strlen(a) >= b) || (b <= a && b+strlen(b) >= a)

那么你就有了重叠。

但是,除了为了采访而检测重叠之外,a &gt; b 应该可以很好地用于strcpy。思路是这样的:

如果b 放在内存中更远的位置(b &gt; a),那么您通常可以将b 复制到a 中。 b 的部分内容将被覆盖,但您已经超过了该部分。

如果a在内存中的位置更远(a &gt; b),这意味着可能通过写在a的第一个位置,你已经覆盖了@987654335中的一个位置@ 具有更高的索引。在这种情况下,您应该向相反的方向复制。所以不要从索引0复制到strlen(b)-1,你应该从strlen(b)-1复制到0

如果您对这有什么帮助感到困惑,请在纸上绘制两个重叠的数组,并尝试从数组的开头复制一次,从结尾复制一次。在 a &gt; ba &lt; b 的情况下尝试使用重叠数组。

注意,如果a == b,你不需要实际复制任何东西,你可以直接返回。

编辑:我不确定,但阅读其他解决方案,似乎这个答案可能不是完全可移植的。小心那个。

【讨论】:

如果a==b,您甚至可以直接返回:-) strcpy 采用指向非易失性的指针,因此不需要实际接触内存。也就是说,不值得添加代码来优化这种荒谬的情况。 @chux,您是否考虑了终止 NUL?【参考方案7】:

如果这两个字符串重叠,那么,在复制时,您会遇到原始的 ab 指针。

假设strcpy(a, b)大致意思是ab的位置。

你只需要保存b原来的位置,在复制的时候检查你没有到达。此外,如果您已到达该位置,请不要写尾随零。

 char* my_strcpy(char *a, const char *b)
 

    if ( a == NULL
      || b == NULL )
    
        return NULL;
    

    char *n = a;
    const char * oldB = b;

    while( *b != '\0'
       &&  a != oldB )
    
        *a = *b;
        a++;
        b++;
    

    if ( a != oldB ) 
        *a = '\0';
    

    return n;
 

这个算法只是停止复制。也许您想做其他事情,例如标记错误条件,或者在前一个位置添加一个字符串结尾标记(尽管静默失败(就像算法目前所做的那样)不是最好的选择)。

希望这会有所帮助。

【讨论】:

【参考方案8】:

即使不使用关系指针比较、memmove 或等效项,也可以编写 strcpy 的版本,在不重叠的情况下将作为 strlenmemcpy 执行,并且作为重叠情况下的自上而下的副本。关键是要利用这样一个事实,即如果读取目标的第一个字节然后用零替换,则在源上调用strlen并将返回的值添加到源指针将产生一个合法的指针,它将等于在“麻烦的重叠”情况下目的地的开始。如果源和目标是不同的对象,则可以安全地计算“源加 strlen”指针并观察到不等于目标。

如果将字符串长度添加到源指针产生目标指针,则将零字节替换为较早读取的值并在目标上调用 strlen 将允许代码确定源和目标字符串的结束地址.此外,源字符串的长度将指示指针之间的距离。如果这个值很大(可能大于 16 左右),代码可以有效地将“移动”操作细分为自上而下的 memcpy 操作序列。否则,可以使用自上而下的单字节复制操作循环复制字符串,或者使用“memcpy 到源到缓冲区”/“memcpy 缓冲区到目标”操作的序列[如果大型 memcpy 的每字节成本小于单个字符复制循环的一半,使用约 256 字节的缓冲区可能是一个有用的优化]。

【讨论】:

【参考方案9】:

这个 SO 条目已经很老了,但我目前正在处理一段旧代码,它使用 strcpy() 复制重叠的字符串。日志输出中缺少字符。我决定使用以下紧凑的解决方案,将char 复制到char

static char *overlapped_strcpy(char *dest, const char *src)

  char *dst = dest;

  if (dest == NULL || src == NULL || dest == src)
    return dest;

  do 
    *dst++ = *src;
   while (*src++);

  return dest;


编辑:

正如@Gerhardh 指出的那样,上面的代码只有在dest &lt;= src 时才有效(我只需要解决这个问题)。对于dest &gt; src 的情况,情况更复杂。但是,正如其他答案已经提到的那样,从后面复制会导致成功。例如:

if (dest <= src) 
  /* do the above */
 else 
  int i = (int)strlen(src);
  while (i >= 0) 
    dst[i] = src[i];
    i--;
  

【讨论】:

这如何解决重叠部分?假设strlen(src) == 20dest=src+5 @Gerhardh 我只需要解决strcpy(posPtr, posPtr+2);。反之则一团糟。 检测和处理混乱将是这种功能的重点。 ;)

以上是关于如何修复 strcpy 以便检测重叠字符串的主要内容,如果未能解决你的问题,请参考以下文章

如何修复 g++ 内存范围重叠?

如何strcpy并返回复制的字符数?

如何检测两个正则表达式在它们可以匹配的字符串中是不是重叠?

memcpy在C语言中的含义,与strcpy区别

码海拾遗:strcpy()strncpy()和strcpy_s()区别

strcpy和strncpy用法和区别