35

我正在用 C 编写语言解释器,我的string类型包含一个length属性,如下所示:

struct String
{
    char* characters;
    size_t length;
};

正因为如此,我不得不花很多时间在我的解释器中手动处理这种字符串,因为 C 不包含对它的内置支持。我考虑过切换到简单的以 null 结尾的字符串以符合底层 C,但似乎有很多理由不这样做:

如果您使用“长度”而不是查找空值,则内置边界检查。

您必须遍历整个字符串才能找到它的长度。

您必须做一些额外的事情来处理以空字符结尾的字符串中间的空字符。

以空字符结尾的字符串无法很好地处理 Unicode。

非空结尾的字符串可以实习更多,即“Hello, world”和“Hello”的字符可以存储在同一个地方,只是长度不同。这不能用空终止的字符串来完成。

字符串切片(注意:字符串在我的语言中是不可变的)。begin显然,第二个速度较慢(并且更容易出错:考虑end为两个函数添加错误检查)。

struct String slice(struct String in, size_t begin, size_t end)
{
    struct String out;
    out.characters = in.characters + begin;
    out.length = end - begin;

    return out;
}

char* slice(char* in, size_t begin, size_t end)
{
    char* out = malloc(end - begin + 1);

    for(int i = 0; i < end - begin; i++)
        out[i] = in[i + begin];

    out[end - begin] = '\0';

    return out;
}

毕竟,我的想法不再是我是否应该使用以空字符结尾的字符串:我正在考虑为什么 C 使用它们!

所以我的问题是:我缺少的空终止有什么好处吗?

4

10 回答 10

33

从乔尔回归基础

为什么 C 字符串会这样工作?这是因为发明了 UNIX 和 C 编程语言的 PDP-7 微处理器具有 ASCIZ 字符串类型。ASCIZ 的意思是“以 Z(零)结尾的 ASCII”。

这是存储字符串的唯一方法吗?不,事实上,这是存储字符串的最糟糕的方式之一。对于非平凡的程序、API、操作系统、类库,你应该避免像瘟疫一样的 ASCIZ 字符串。

于 2009-08-10T06:15:08.570 回答
19

通常的解决方案是两者都做 - 保持长度并保持空终止符。这并没有太多额外的工作,这意味着您随时可以将字符串传递给任何函数。

空终止字符串通常会消耗性能,原因很明显,发现长度所花费的时间取决于长度。从好的方面来说,它们是在 C 中表示字符串的标准方式,所以如果你想使用大多数 C 库,你别无选择,只能支持它们。

于 2009-08-10T06:12:14.470 回答
9

以 nul 结尾的字符串的一个优点是,如果您逐个字符地遍历字符串,则只需要保留一个指针来寻址该字符串:

while (*s)
{
    *s = toupper(*s);
    s++;
}

而对于没有标记的字符串,您需要保留两位状态:指针和索引:

while (i < s.length)
{
    s.data[i] = toupper(s.data[i]);
    i++;
}

...或当前指针和限制:

s_end = s + length;
while (s < s_end)
{
    *s = toupper(*s);
    s++;
}

当 CPU 寄存器是稀缺资源时(编译器在分配它们方面做得更差),这一点很重要。现在,没有那么多。

于 2009-08-10T06:45:34.390 回答
8

长度也有问题。

  • 长度需要额外的存储空间(现在不是这样的问题,而是 30 年前的一个重要因素)。

  • 每次更改字符串时,都必须更新长度,因此您会全面降低性能。

  • 使用以 NUL 结尾的字符串,您仍然可以使用长度或存储指向最后一个字符的指针,因此,如果您正在执行大量字符串操作,您仍然可以与字符串长度相同的性能。

  • NUL 终止的字符串要简单得多 - NUL 终止符只是strcat确定字符串结尾之类的方法使用的约定。因此,您可以将它们存储在常规 char 数组中,而不必使用结构。

于 2009-08-10T06:14:20.270 回答
7

一个好处是,使用 null 终止,以 null 结尾的字符串的任何尾部也是以 null 结尾的字符串。如果您需要将一个以第 N 个字符开头的子字符串(前提是没有缓冲区溢出)传递给某个字符串处理函数 - 没问题,只需将偏移地址传递给那里。当以其他方式存储大小时,您需要构造一个新字符串。

于 2009-08-10T06:09:01.123 回答
6

有点题外话,但是有一种比您描述的方式更有效的方法来处理以长度为前缀的字符串。创建这样的结构(在 C99 及更高版本中有效):

struct String 
{
  size_t length;
  char characters[0];
}

这将创建一个结构,该结构在开始时具有长度,'characters' 元素可用作 char*,就像您使用当前结构一样。但是,不同之处在于您只能在堆上为每个字符串分配一个项目,而不是两个。像这样分配你的字符串:

mystr = malloc(sizeof(String) + strlen(cstring))

例如 - 结构的长度(只是 size_t)加上足够的空间来放置实际的字符串。

如果您不想使用 C99,您也可以使用“char characters[1]”来执行此操作,并从要分配的字符串长度中减去 1。

于 2009-08-20T20:13:46.597 回答
4

只是抛出一些假设:

  • 没有办法获得空终止字符串的“错误”实现。然而,标准化的结构可能具有特定于供应商的实现。
  • 不需要结构。可以说,空终止字符串是“内置”的,因为它是 char* 的一种特殊情况。
于 2009-08-10T06:14:38.210 回答
2

尽管在大多数情况下我更喜欢 array + len 方法,但使用 null 终止是有正当理由的。

以 32 位系统为例。

存储一个 7 字节的字符串
char * + size_t + 8 bytes = 19 bytes

存储一个 7 字节的 null-term 字符串
char * + 8 = 16 字节。

空项数组不需要像您的字符串那样是不可变的。我可以通过简单地放置一个空字符来愉快地截断 c 字符串。如果您编写代码,则需要创建一个新字符串,其中涉及分配内存。

根据字符串的使用,您的字符串将永远无法与 c-strings 的性能相匹配,而不是您的字符串。

于 2009-08-10T06:14:04.937 回答
2

绝对正确的是,0 终止是一种在类型检查和部分操作性能方面很差的方法。此页面上的答案已经总结了它的起源和用途。

我喜欢 Delphi 存储字符串的方式。我相信它在(可变长度)字符串之前保持长度/最大长度。这样,字符串可以为空终止以实现兼容性。

我对您的机制的担忧: - 附加指针 - 您的语言核心部分中的不变性 si;通常字符串类型不是不可变的,所以如果你重新考虑它会很困难。您需要实现“更改时创建副本”机制 - 使用 malloc(效率很低,但可能只是为了方便而包含在此处?)

祝你好运; 编写自己的解释器对于主要理解编程语言的语法和句法非常有教育意义!(至少,它适合我)

于 2009-08-10T09:53:19.610 回答
0

我认为主要原因是标准没有具体说明除字符以外的任何类型的大小。但是 sizeof(char) = 1 这对于字符串大小来说绝对是不够的。

于 2009-08-10T06:07:18.710 回答