为什么以null结尾的字符串?或者:以空值终止与字符+长度存储

Ima*_*ist 29 c string algorithm performance null-terminated

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

struct String
{
    char* characters;
    size_t length;
};
Run Code Online (Sandbox Code Playgroud)

因此,我必须花费大量时间在我的解释器中手动处理这种字符串,因为C不包含对它的内置支持.我考虑过切换到简单的以null结尾的字符串只是为了符合底层C,但似乎有很多理由不:

如果使用"length"而不是查找null,则内置边界检查.

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

你必须做额外的事情来处理以null结尾的字符串中间的空字符.

以空值终止的字符串与Unicode处理不佳.

非空终止字符串可以实习更多,即"Hello,world"和"Hello"的字符可以存储在同一个地方,只是具有不同的长度.使用以null结尾的字符串无法做到这一点.

字符串切片(注意:字符串在我的语言中是不可变的).显然,第二较慢(和更容易出错:考虑增加的错误检查beginend对两种功能).

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;
}
Run Code Online (Sandbox Code Playgroud)

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

所以我的问题是:我错过了无效的空终止有什么好处吗?

wei*_*ure 30

从乔尔回到基础:

为什么C字符串以这种方式工作?这是因为发明了UNIX和C编程语言的PDP-7微处理器具有ASCIZ字符串类型.ASCIZ的意思是"最后用Z(零)的ASCII".

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

  • Denis Ritchie的观点有所不同.BCPL具有长度+内容表示,长度包含在一个字节中.乙切换到终止的字符串"部分,以避免造成保持的计数在一个8或9比特时隙的字符串的长度的限制,部分是因为维持计数似乎,在我们的经验,比使用终止子不太方便." (C语言的发展,http://cm.bell-labs.com/cm/cs/who/dmr/chist.pdf) (19认同)

Dan*_*ker 17

通常的解决方案是同时执行这两项操作 - 保持长度并保持空终止符.这不是额外的工作,意味着你总是准备将字符串传递给任何函数.

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

  • 这是大多数事情的作用!你甚至不必一直维护null终止符 - 只要你需要,就可以执行`str [len] ='\ 0'`.这就是`std :: string :: c_str``通常在C++中的作用. (3认同)

sha*_*oth 7

一个好处是,对于null-termination,以null结尾的字符串的任何尾部也是以null结尾的字符串.如果你需要将一个以第N个字符开头的子字符串(假设没有缓冲区溢出)传递给某个字符串处理函数 - 没问题,只需在那里传递一个被覆盖的地址.以其他方式存储大小时,您需要构造一个新字符串.

  • 对于我对结构的处理,这并没有什么不同:`struct String new; new.characters = old.characters + offset; new.length = old.length - offset;`这是一些记账,但出现了什么,5条指令?如果您需要对字符串的开头而不是结尾做一些事情,那么这与差异相比似乎微不足道. (3认同)

caf*_*caf 6

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

while (*s)
{
    *s = toupper(*s);
    s++;
}
Run Code Online (Sandbox Code Playgroud)

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

while (i < s.length)
{
    s.data[i] = toupper(s.data[i]);
    i++;
}
Run Code Online (Sandbox Code Playgroud)

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

s_end = s + length;
while (s < s_end)
{
    *s = toupper(*s);
    s++;
}
Run Code Online (Sandbox Code Playgroud)

当CPU寄存器是稀缺资源(并且编译器在分配它们时更糟糕)时,这很重要.现在,不是那么多.

  • "当CPU寄存器是稀缺资源时" - 寄存器仍然是x86和x64上的稀缺资源. (4认同)

Jas*_*ams 5

长度也存在问题.

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

  • 每次更改字符串时都必须更新长度,这样就可以全面降低性能.

  • 使用NUL终止的字符串,您仍然可以使用长度或存储指向最后一个字符的指针,因此如果您正在进行大量的字符串操作,您仍然可以使字符串与长度相等.

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

  • @Jimmy我的问题是:在这样的嵌入式系统上,为什么要使用字符串?我认为当我进行机器人编程时,我什至没有使用过“char”。我能想到的唯一例子是,如果你正在为 LED 显示屏编程(比如那些滚动文本的东西或软饮料机上的东西),但那里的功能非常简单,我仍然很难想象额外的 3 个字节这是一个问题(4 字节 int - 1 字节,因为您不必存储空字符)。 (4认同)

Nic*_*son 5

略微偏离主题,但有一种更有效的方式来做长度前缀的字符串比你描述的方式.创建这样的结构(在C99及以上版本中有效):

struct String 
{
  size_t length;
  char characters[0];
}
Run Code Online (Sandbox Code Playgroud)

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

mystr = malloc(sizeof(String) + strlen(cstring))
Run Code Online (Sandbox Code Playgroud)

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

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