【问题标题】:Subtraction of char* in implementation of strlen()strlen() 实现中 char* 的减法
【发布时间】:2020-10-14 05:03:45
【问题描述】:

我正在研究 C 中 strlen() 函数的实现。我需要了解它在我的一项任务中的工作原理。

#define ALIGN (sizeof(size_t))
#define ONES ((size_t)-1/UCHAR_MAX)
#define HIGHS (ONES * (UCHAR_MAX/2+1))
#define HASZERO(x) ((x)-ONES & ~(x) & HIGHS)

size_t strlen(const char *s)
{
    const char *a = s;
    const size_t *w;
    for (; (uintptr_t)s % ALIGN; s++) if (!*s) return s-a;
    for (w = (const void *)s; !HASZERO(*w); w++);
    for (s = (const void *)w; *s; s++);
    return s-a;
}

我不明白 char* 的减法在“return s-a”语句中的作用。

这是 musl 的 strlen 实现。 glibc 的 strlen() 实现也使用了这种 char* 减法。

【问题讨论】:

  • !*s 标记字符串的结尾(char 是字节零)。 s-a 返回结束地址(最后一个非 nul 字符位置 + 1,其中 s 当前是)减去开始地址(a 是提供给函数的初始 s 的副本),因此长度字符串。

标签: c char libraries subtraction strlen


【解决方案1】:

用cmets注解的代码说明:

size_t strlen(const char *s)
{
    const char *a = s;      // store a copy pointing at the start of the original        
    const size_t *w;
    for (; (uintptr_t)s % ALIGN; s++) // in case of misalignment, look for first aligned address
      if (!*s) return s-a; // if we encounter \0 while doing so, return the string length
    for (w = (const void *)s; !HASZERO(*w); w++); // work with word-sized chunks and do lookup
    for (s = (const void *)w; *s; s++); // find the exact location of \0 in the final word
    return s-a; // end minus beginning = length
}

关于 C 语言兼容性的注意事项:

  • w = (const void *)s 依赖非标准扩展,*w 调用未定义行为。这是库代码,因此有时可能会使用特定设置进行编译,例如 -fno-strict-aliasing

  • s-a 实际上是ptrdiff_t 类型,而不是size_t。因此,可能需要强制转换来消除编译器警告。

  • size_t 不一定是实现的最大对齐类型,它可能比这更大。我相信用于 32 位及更高版本的最正确类型是uint_fast32_t。编译器/lib 应该将此类型设为 32 位或 64 位,具体取决于 32/64 位 CPU 上的实际速度。

  • 像这样的库实现有时会读取超出传递字符串末尾的字大小的块。这假设如果字符串没有以对齐的地址结尾,那么无害的填充字节将在那里存在并可以访问。这绝不是由 C 标准保证的(这样做是数组越界访问 UB),但可能由本地实现保证。

在不影响性能的情况下,应该可以将这段代码整理成更易读和自我记录的东西。我们可以解决上述一些问题。也许类似于(未经测试/基准测试):

#include <stdint.h>
#include <limits.h>

#define ONES ((uint_fast32_t)-1/UCHAR_MAX)
#define HIGHS (ONES * (UCHAR_MAX/2+1))
#define HASZERO(x) ((x)-ONES & ~(x) & HIGHS)

size_t strlen (const char* s)
{
  const char* begin = s;
  const char* end   = s;

  for (; (uintptr_t)end % _Alignof(uint_fast32_t); end++)
  {
    if (*end == '\0') 
    {
      return (size_t)(end - begin);
    }
  }
  
  const uint_fast32_t* word;
  for (word = (const void*)end; !HASZERO(*word); word++)
  {}
  
  for (end = (const void*)word; end != '\0'; end++)
  {}
  
  return (size_t)(end - begin);
}

【讨论】:

  • 另一个严重的问题是,如果数组未在单词边界处结束,单词搜索循环将读取到数组边界之外。
  • @user694733 是的。库实现通常期望在字符串结尾之后出现无害的填充字节。语言标准根本无法保证这一点。我也可以为此添加注释。
  • 查找 '\0' 的确切值的 for 循环初始化 end = (const void*) 字的值。我们是否应该使用 const char* 代替 const void*。你能解释一下为什么在这种情况下使用 void* 有效吗?
  • @ShubhamSondhi 演员表用于从 32 位指针类型到 8 位指针类型。它们不是兼容的类型,因此需要强制转换。通常也很可疑,但这是库代码。当然,您也可以转换为 (const char*),因为 void* 与所有指针类型兼容,所以没有区别。
  • @ShubhamSondhi 我想原作者认为用类似的方式编写两行 for (w = (const void *)s;...for (s = (const void *)w; ... 看起来很漂亮。没有太多理由解释为什么原始代码是这样编写的。
【解决方案2】:

假设您有字符串"Hello world"。此字符串作为数组存储在您的计算机内存中,并以特殊的“null”字符 ('\0') 终止。

数组看起来像这样:

+-----+-----+-----+------+-----+-----+-----+-----+- --+-----+------+ | 'H' | 'e' | 'l' | 'l' | 'o' | ' ' | 'w' | 'o' | 'l' | 'd' | '\0' | +-----+-----+-----+------+-----+-----+-----+-----+- --+-----+------+

当调用此函数时(如在strlen("Hello world") 中),s 将指向数组中的第一个字符。 a的初始化也会让它指向数组的第一个字符。

三个循环修改了s,使其指向终止的空字符。

如果我们再次显示数组,但现在使用指针,它将是这样的:

+-----+-----+-----+------+-----+-----+-----+-----+- --+-----+------+ | 'H' | 'e' | 'l' | 'l' | 'o' | ' ' | 'w' | 'o' | 'l' | 'd' | '\0' | +-----+-----+-----+------+-----+-----+-----+-----+- --+-----+------+ ^ ^ | | 作为

s - a 所做的是计算两个指针sa 的差值(在数组元素中)。这个差异将是10,它是字符串的长度(不包括空终止符)。

【讨论】:

    猜你喜欢
    • 2011-10-14
    • 2023-03-14
    • 2012-08-01
    • 2014-08-05
    • 1970-01-01
    • 2016-04-11
    • 2010-12-16
    • 2018-11-12
    • 2023-03-14
    相关资源
    最近更新 更多