空终止字符串的基本原理是什么?

尽管我很喜欢C和c++,但我还是忍不住对空结尾字符串的选择抓耳挠脑:

  • 长度前缀(即Pascal)字符串在C
  • 长度前缀字符串允许常量时间长度查找,从而使几种算法更快。
  • 带长度前缀的字符串更难以引起缓冲区溢出错误。
  • 即使在32位机器上,如果允许字符串的大小与可用内存相同,那么带前缀的长度字符串只比以空结尾的字符串宽3个字节。在16位机器上,这是一个单字节。在64位计算机上,4GB是一个合理的字符串长度限制,但即使您想将其扩展到机器字的大小,64位计算机通常有足够的内存,因此额外的7个字节有点像空参数。我知道最初的C标准是为极其糟糕的机器(就内存而言)编写的,但这里的效率论点并不能说服我。
  • 几乎所有其他语言(即Perl, Pascal, Python, Java, c#等)都使用长度前缀字符串。这些语言通常在字符串操作基准测试中胜过C语言,因为它们处理字符串的效率更高。
  • c++用std::basic_string模板纠正了这一点,但期望空结束字符串的普通字符数组仍然普遍存在。这也是不完美的,因为它需要堆分配。
  • 以空结尾的字符串必须保留一个字符(即Null),该字符不能存在于字符串中,而以长度为前缀的字符串可以包含嵌入的Null。

其中一些东西比C语言出现得更晚,所以C语言不知道它们是有道理的。然而,在C语言出现之前,有些语言就已经很简单了。为什么会选择空终止字符串,而不是明显更好的长度前缀?

编辑:由于有些人在我上面的效率点上要求事实(并且不喜欢我已经提供的那些),它们源于以下几件事:

  • 使用空结尾字符串的Concat需要O(n + m)时间复杂度。长度前缀通常只需要O(m)。
  • 使用空结尾字符串的长度需要O(n)时间复杂度。长度前缀为O(1)。
  • Length和concat是迄今为止最常见的字符串操作。在一些情况下,以空结尾的字符串可能更有效,但这种情况发生的频率要低得多。

从下面的答案,这些是一些情况下,空终止字符串更有效:

  • 当你需要切断字符串的开头并需要将它传递给某个方法时。你不能用长度前缀在常数时间内做到这一点,即使你被允许销毁原始字符串,因为长度前缀可能需要遵循对齐规则。
  • 在某些情况下,你只是一个字符一个字符地循环字符串,你可能能够保存一个CPU寄存器。注意,这只在没有动态分配字符串的情况下才有效(因为这样你就必须释放它,必须使用你保存的CPU寄存器来保存你最初从malloc和朋友那里得到的指针)。

上面这些词都没有length和concat常见。

下面的答案中还有一个断言:

  • 你需要把绳子的一端剪掉

但这个是不正确的——对于以null结尾的字符串和有长度前缀的字符串,它的时间是相同的。(以Null结尾的字符串只是在你想要的新结尾的地方插入一个Null,长度前缀只是从前缀中减去。)

30944 次浏览

C语言中没有字符串。C语言中的“string”只是一个指向char的指针。所以也许你问错问题了。

省略字符串类型的基本原理是什么?可能更相关。对此,我要指出C不是面向对象的语言,只有基本的值类型。字符串是一个更高级别的概念,必须以某种方式组合其他类型的值来实现。C处于较低的抽象级别。

鉴于下面的狂风暴雨

我只是想指出,我并不是想说这是一个愚蠢或糟糕的问题,或者C语言表示字符串的方式是最好的选择。我试图澄清的是,如果考虑到C语言没有区分字符串作为数据类型与字节数组的机制这一事实,那么这个问题就会更简洁。考虑到今天计算机的处理和存储能力,这是最好的选择吗?可能不会。但事后诸葛总是20/20之类的。

我认为,它有历史原因,并发现这在维基百科上:

在当时C(和语言 它源自于)被开发出来, 内存非常有限,所以使用 只需要一个字节的开销来存储 绳子的长度很吸引人。的 当时唯一流行的选择, 通常称为帕斯卡字符串 (尽管早期版本的 BASIC),使用前导字节进行存储 弦的长度。这允许 包含NUL和made的字符串 求长度只需要一个 内存访问(O(1)(常量)时间)。 但是一个字节限制长度为255。 这个长度限制远不止这些 限制性比问题用的多 C字符串,一般来说是C字符串 胜出。< / p >

空终止允许基于快速指针的操作。

马的嘴

BCPL、B、C均不支持 中的字符数据强 语言;每个都很重视字符串 比如整数向量和 对一般规则稍加补充 约定。在BCPL和B a 字符串字面值表示的地址 属性初始化的静态区域 字符的字符串,打包成 细胞。在BCPL中,第一个打包字节 中包含的字符数 字符串;在B中没有计数 字符串以a结尾 特殊字符,由B拼写 *e。这个变化是局部的 避免了对长度的限制 引起的字符串 在8位或9位槽中计数,并且 部分原因是为了维持人口数量 根据我们的经验,似乎更少

Dennis M Ritchie, C语言的开发

显然,为了性能和安全,你会想要在处理字符串时保持它的长度,而不是重复地对它执行strlen或等价操作。然而,将长度存储在字符串内容之前的固定位置是一种非常糟糕的设计。正如Jörgen在对Sanjit的回答的评论中指出的那样,它排除了将字符串的尾部作为字符串处理,例如,这使得许多常见的操作,如path_to_filenamefilename_to_extension不可能在不分配新内存的情况下进行(并导致失败和错误处理的可能性)。当然,还有一个问题,没有人能就字符串长度字段应该占用多少字节达成一致(许多糟糕的“Pascal字符串”语言使用16位字段甚至24位字段,这妨碍了长字符串的处理)。

C语言让程序员选择是否/在哪里/如何存储长度的设计更加灵活和强大。当然,程序员必须聪明。C语言惩罚愚蠢的程序崩溃,慢慢停止,或者让你的敌人扎根。

在很多方面,C语言是原始的。我很喜欢。

它比汇编语言高了一步,用一种更容易编写和维护的语言提供了几乎相同的性能。

空结束符很简单,不需要语言的特殊支持。

现在回想起来,似乎并不是那么方便。但我在80年代使用汇编语言,当时它似乎非常方便。我只是认为软件在不断地发展,平台和工具也在不断地变得越来越复杂。

这个问题是作为Length Prefixed Strings (LPS) vs zero terminated strings (SZ)的问题提出的,但主要暴露了长度前缀字符串的好处。这似乎有些势不可挡,但老实说,我们也应该考虑到LPS的缺点和SZ的优点。

在我看来,这个问题甚至可以被理解为一种带有偏见的提问方式:“零终止字符串的优势是什么?”

零终止字符串的优点(我看到了):

    很简单,不需要在语言中引入新的概念,char 数组/char指针可以做。
  • 核心语言只包含最小的语法糖来转换 在双引号和a之间 一堆字符(真的是一堆 字节)。在某些情况下可以使用它 完全初始化 与文本无关。例如xpm image文件格式是一个有效的C源代码 ,其中包含编码为 李字符串。< / > 顺便说一下,你可以在字符串文字中放入一个0,编译器会 只需要在字面量的末尾添加另一个:"this\0is\0valid\0C"。 是字符串吗?还是四根弦?或者一串字节…
  • 扁平化实现,没有隐藏的间接,没有隐藏的整数。
  • 没有涉及隐藏内存分配(好吧,一些臭名昭著的非 标准函数,如strdup 执行分配,但主要是这样 A source of problem).
  • 对于小型或大型硬件没有特定的问题(想象一下负担到 在8上管理32位前缀长度 位微控制器,或 限制字符串大小的限制 小于256字节,这是一个问题,我实际上有Turbo Pascal亿年前)
  • 实现的字符串操作只是屈指可数的 非常简单的库函数
  • 高效主要用于字符串:常量文本读取 顺序地从一个已知的开始 (主要是给用户的消息).
  • 终止0甚至不是强制性的,所有必要的工具 像一堆 字节可用。当执行 在C中初始化数组,你可以 甚至避免使用NUL终止符。只是 设置正确的大小。char a[3] = 是有效的C(不是c++)和 不会把最后一个0放在a。
  • 符合Unix的观点"一切都是文件",包括 没有固有长度的“文件” 比如stdin, stdout。您应该记住,开放读写原语是实现的 在一个非常低的水平。它们不是库调用,而是系统调用。使用相同的API 用于二进制或文本文件。文件读取原语获取缓冲区地址和大小并返回 新尺寸。你可以使用字符串作为写入缓冲区。使用另一种字符串 表示法意味着您不能轻易地使用文字字符串作为输出的缓冲区,或者 当将它强制转换为char*时,你必须让它有一个非常奇怪的行为。即 不返回字符串的地址,而是返回实际的数据
  • 非常容易操作的文本数据从文件中读取,没有无用的缓冲区拷贝, 只需在正确的位置插入0(好吧,在现代C中并不是这样,因为双引号字符串是const char数组,现在通常保存在不可修改的数据段中)
  • 在某些int值前加上任何大小的值都意味着对齐问题。最初的 长度应该对齐,但没有理由这样做的字符数据(和 再次强调,当将字符串视为一串字符串时,强制字符串对齐将意味着问题 李字节)。< / > 对于常量字面值字符串(sizeof),
  • 长度在编译时已知。那么为什么
  • . .
  • 在某种程度上,C语言和(几乎)其他语言一样,字符串被视为char数组。由于数组长度不受C语言的管理,它的逻辑长度也不受字符串的管理。唯一令人惊讶的是在末尾添加了0项,但这只是在双引号之间键入字符串时的核心语言级别。用户可以完美地调用传递长度的字符串操作函数,甚至使用普通的memcopy代替。深圳只是一个设施。在大多数其他语言中,数组长度是受管理的,这在逻辑上与字符串相同。
  • 在现代,无论如何1字节字符集是不够的,你经常不得不处理编码的unicode字符串,其中字符的数量与字节的数量有很大的不同。这意味着用户可能想要的不仅仅是“大小”,还包括其他信息。保持长度不考虑其他有用的信息(特别是没有自然的地方存储它们)。

也就是说,在标准C字符串确实效率低下的罕见情况下,没有必要抱怨。图书馆是可用的。如果我遵循这个趋势,我应该抱怨标准C不包括任何正则表达式支持函数……但实际上每个人都知道这不是一个真正的问题,因为有库可以用于此目的。因此,当需要提高字符串操作效率时,为什么不使用这样的库呢?或者甚至是c++字符串?

编辑:我最近看了一下D弦。有趣的是,所选择的解决方案既不是大小前缀,也不是零终止。与C语言一样,双引号括起来的字面值字符串只是不可变字符数组的简写,并且该语言也有一个字符串关键字表示(不可变字符数组)。

但是D数组比C数组丰富得多。在静态数组的情况下,长度在运行时是已知的,因此不需要存储长度。编译器在编译时拥有它。在动态数组的情况下,长度是可用的,但D文档没有说明它保存在哪里。就我们所知,编译器可以选择将它保存在某个寄存器中,或者存储在远离字符数据的某个变量中。

正常char数组或非字符串没有最终为零,因此程序员必须把它本身如果他想叫一些C函数从D .字符串字面量的具体情况,然而D编译器仍然把零在每个字符串(允许容易把C字符串容易调用C函数?),但这零不是字符串的一部分(D不计算字符串大小)。

唯一让我有点失望的是,字符串应该是utf-8,但长度显然仍然返回一些字节(至少在我的编译器gdc上是真的),即使使用多字节字符。我不清楚这是编译器错误还是故意的。(好吧,我大概已经知道发生了什么事。要对D编译器说你的源代码使用utf-8,你必须在开始时放一些愚蠢的字节顺序标记。我写愚蠢,因为我知道没有编辑器这样做,特别是UTF-8,应该是ASCII兼容的)。

假设C以Pascal的方式实现字符串,通过前缀长度:7字符长字符串与3字符字符串的数据类型相同吗?如果答案是肯定的,那么当我将前者分配给后者时,编译器应该生成什么样的代码?字符串应该被截断,还是自动调整大小?如果调整大小,该操作是否应该被锁保护以使其线程安全?不管你喜不喜欢,C语言的方法回避了所有这些问题。

考虑到任何语言的汇编核心,尤其是C语言,它比汇编高出一步(因此继承了大量汇编遗留代码),懒惰、寄存器节俭和可移植性。 你会同意null字符在那些ASCII的日子里是无用的,它(可能和EOF控件字符一样好)

让我们看看伪代码

function readString(string) // 1 parameter: 1 register or 1 stact entries
pointer=addressOf(string)
while(string[pointer]!=CONTROL_CHAR) do
read(string[pointer])
increment pointer

共使用1个寄存器

案例2

 function readString(length,string) // 2 parameters: 2 register used or 2 stack entries
pointer=addressOf(string)
while(length>0) do
read(string[pointer])
increment pointer
decrement length

共使用2个寄存器

这在当时似乎是短视的,但考虑到代码和寄存器的节俭(这在当时是PREMIUM,那时你知道,他们使用穿孔卡)。因此,更快(当处理器速度可以以kHz计),这个“黑客”是相当不错的,可轻松移植到无寄存器处理器。

为了便于讨论,我将实现2个常见的字符串操作

stringLength(string)
pointer=addressOf(string)
while(string[pointer]!=CONTROL_CHAR) do
increment pointer
return pointer-addressOf(string)

复杂度O(n),在大多数情况下PASCAL字符串是O(1),因为字符串的长度是预先挂起的字符串结构(这也意味着该操作必须在更早的阶段进行)。

concatString(string1,string2)
length1=stringLength(string1)
length2=stringLength(string2)
string3=allocate(string1+string2)
pointer1=addressOf(string1)
pointer3=addressOf(string3)
while(string1[pointer1]!=CONTROL_CHAR) do
string3[pointer3]=string1[pointer1]
increment pointer3
increment pointer1
pointer2=addressOf(string2)
while(string2[pointer2]!=CONTROL_CHAR) do
string3[pointer3]=string2[pointer2]
increment pointer3
increment pointer1
return string3

复杂度O(n)和预先设置字符串长度不会改变操作的复杂性,而我承认它会减少3倍的时间。

另一方面,如果你使用PASCAL字符串将不得不重新设计您的API来考虑在长度和bit-endianness注册,帕斯卡字符串的众所周知的限制255字符(0 xff)因为中存储的长度是1个字节(8位),而且你想要更长的字符串(16位- >任何)你必须考虑在一层的架构代码,这意味着在大多数情况下不相容的字符串API如果你想要更长的字符串。

例子:

一个文件是用你的前置字符串api在8位计算机上写的,然后必须在32位计算机上读取,惰性程序会做什么呢?考虑到你的4字节是字符串的长度,然后分配大量内存,然后尝试读取这么多字节。 另一种情况是PPC 32字节字符串读取(小端序)到x86(大端序),当然,如果你不知道一个是由另一个写的,就会有麻烦。 1字节长度(0x00000001)将变成16777216 (0x0100000),对于读取1字节的字符串是16mb。 当然,你会说人们应该同意一个标准,但即使是16位unicode也有大有小

当然,C也有它的问题,但它不会受到这里提出的问题的影响。

不知怎的,我把这个问题理解为C中没有编译器支持以长度为前缀的字符串。下面的例子显示,至少你可以开始你自己的C字符串库,其中字符串长度在编译时计算,使用这样的构造:

#define PREFIX_STR(s) ((prefix_str_t){ sizeof(s)-1, (s) })


typedef struct { int n; char * p; } prefix_str_t;


int main() {
prefix_str_t string1, string2;


string1 = PREFIX_STR("Hello!");
string2 = PREFIX_STR("Allows \0 chars (even if printf directly doesn't)");


printf("%d %s\n", string1.n, string1.p); /* prints: "6 Hello!" */
printf("%d %s\n", string2.n, string2.p); /* prints: "48 Allows " */


return 0;
}

然而,这不会没有问题,因为你需要小心什么时候专门释放字符串指针和什么时候静态分配它(字面char数组)。

编辑:作为对这个问题更直接的回答,我的观点是,这是C既可以支持可用的字符串长度(作为编译时间常量)的方式,如果你需要它,但如果你只想使用指针和零终止,仍然没有内存开销。

当然,使用以零结尾的字符串似乎是推荐的做法,因为标准库一般不接受字符串长度作为参数,而且提取长度的代码不像char * s = "abc"那样简单,正如我的例子所示。

制的正确的,但由于人们似乎不明白他的意思,我将提供一些代码示例。

首先,让我们考虑一下C是什么:一种简单的语言,其中所有代码都可以直接转换为机器语言。所有类型都适合寄存器和堆栈,并且它不需要一个操作系统或一个大的运行时库来运行,因为它是为了这些东西(这是一个非常适合的任务,考虑到今天甚至没有一个可能的竞争对手)。

如果C语言有一个string类型,比如intchar,它将是一个不适合寄存器或堆栈的类型,并且需要以任何方式处理内存分配(及其所有支持的基础设施)。所有这些都违背了C语言的基本原则。

因此,C语言中的字符串是:

char s*;

那么,我们假设这是有长度前缀的。让我们编写代码来连接两个字符串:

char* concat(char* s1, char* s2)
{
/* What? What is the type of the length of the string? */
int l1 = *(int*) s1;
/* How much? How much must I skip? */
char *s1s = s1 + sizeof(int);
int l2 = *(int*) s2;
char *s2s = s2 + sizeof(int);
int l3 = l1 + l2;
char *s3 = (char*) malloc(l3 + sizeof(int));
char *s3s = s3 + sizeof(int);
memcpy(s3s, s1s, l1);
memcpy(s3s + l1, s2s, l2);
*(int*) s3 = l3;
return s3;
}

另一种方法是使用struct来定义字符串:

struct {
int len; /* cannot be left implementation-defined */
char* buf;
}

此时,所有的字符串操作都需要进行两次分配,这实际上意味着您将通过一个库来进行任何处理。

有趣的是……像这样的结构体存在于C!它们只是不用于日常显示消息给用户处理。

所以,这里是Calavera的观点:C语言中没有字符串类型。要对它做任何事情,你必须获取一个指针,并将其解码为指向两个不同类型的指针,然后字符串的大小就变得非常相关,而不能仅仅是“实现定义”。

现在,C 可以以任何方式处理内存,而库中的mem函数(甚至在<string.h>中!)提供了将内存作为一对指针和大小来处理所需的所有工具。C语言中所谓的“弦”只用于一个目的:在为文本终端编写操作系统的上下文中显示消息。因此,空终止就足够了。

还有一点没有提到:当C语言被设计出来的时候,有很多机器的“char”不是8位的(即使是今天的DSP平台也不是8位的)。如果一个人决定字符串是长度前缀,应该使用多少'char'的长度前缀?使用two会人为地限制具有8位字符和32位寻址空间的机器的字符串长度,而在具有16位字符和16位寻址空间的机器上浪费空间。

如果一个人想要有效地存储任意长度的字符串,如果'char'总是8位,他可以——在速度和代码大小上付出一些代价——定义一个方案,一个以偶数为前缀的字符串N将是N/2字节长,一个以奇数为前缀的字符串N和一个偶数为前缀的字符串M(向后读)可以是((N-1) + M*char_max)/2,等等,并要求任何声称提供一定空间来保存字符串的缓冲区必须允许在该空间之前有足够的字节来处理最大长度。事实上,'char'并不总是8位,然而,这将使这样的方案复杂化,因为保存字符串长度所需的'char'的数量取决于CPU架构。

即使在32位机器上,如果允许字符串的大小与可用内存相同,带前缀的长度字符串也只比以空结尾的字符串宽3个字节。

首先,对于短字符串来说,额外的3个字节可能是相当大的开销。具体来说,零长度字符串现在占用的内存是原来的4倍。我们中的一些人正在使用64位机器,因此我们要么需要8个字节来存储零长度的字符串,要么字符串格式无法处理平台支持的最长字符串。

可能还需要处理对齐问题。假设我有一个包含7个字符串的内存块,比如“solo\0second\0\0four\0five\0\0seventh”。第二个字符串从偏移量5开始。硬件可能要求32位整数以4的倍数的地址对齐,因此您必须添加填充,从而进一步增加开销。相比之下,C表示非常节省内存。(内存效率很好;例如,它有助于缓存性能。)

围绕C语言的许多设计决策都源于这样一个事实:在最初实现C语言时,参数传递的代价有些昂贵。如果在两者之间作选择。

void add_element_to_next(arr, offset)
char[] arr;
int offset;
{
arr[offset] += arr[offset+1];
}


char array[40];


void test()
{
for (i=0; i<39; i++)
add_element_to_next(array, i);
}

void add_element_to_next(ptr)
char *p;
{
p[0]+=p[1];
}


char array[40];


void test()
{
int i;
for (i=0; i<39; i++)
add_element_to_next(arr+i);
}

后者会稍微便宜一点(因此是首选),因为它只需要传递一个参数而不是两个。如果被调用的方法不需要知道数组的基址,也不需要知道其中的索引,那么将这两个值组合在一起传递一个指针比分别传递值要便宜。

虽然C语言可以有许多合理的方法来编码字符串长度,但当时发明的方法都需要能够处理部分字符串的函数,以接受字符串的基址和所需的索引作为两个单独的参数。使用零字节终止可以避免这种要求。尽管在当今的机器上,其他方法会更好(现代编译器经常在寄存器中传递参数,memcpy可以以strcpy()的等等物无法实现的方式进行优化),但足够多的生产代码使用零字节终止字符串,很难更改为其他任何东西。

PS——为了在某些操作上稍微降低速度,以及在较长的字符串上稍微增加一点额外开销,可以让处理字符串的方法接受直接指向字符串的指针、bounds-checked字符串缓冲区或标识另一个字符串的子字符串的数据结构。像“strcat”这样的函数看起来像[现代语法]

void strcat(unsigned char *dest, unsigned char *src)
{
struct STRING_INFO d,s;
str_size_t copy_length;


get_string_info(&d, dest);
get_string_info(&s, src);
if (d.si_buff_size > d.si_length) // Destination is resizable buffer
{
copy_length = d.si_buff_size - d.si_length;
if (s.src_length < copy_length)
copy_length = s.src_length;
memcpy(d.buff + d.si_length, s.buff, copy_length);
d.si_length += copy_length;
update_string_length(&d);
}
}

比k&r strcat方法大一点,但它支持边界检查,而k&r方法不支持。此外,与当前的方法不同,它可以轻松地连接任意子字符串,例如。

/* Concatenate 10th through 24th characters from src to dest */


void catpart(unsigned char *dest, unsigned char *src)
{
struct SUBSTRING_INFO *inf;
src = temp_substring(&inf, src, 10, 24);
strcat(dest, src);
}

注意,由temp_substring返回的字符串的生命周期将受到ssrc的生命周期的限制,后者更短(这就是为什么该方法需要传入inf——如果它是本地的,它将在方法返回时死亡)。

就内存成本而言,64字节以内的字符串和缓冲区将有一个字节的开销(与以零结尾的字符串相同);较长的字符串会有稍多一点的开销(是否允许两个字节之间的开销和所需的最大开销将是时间/空间的权衡)。长度/模式字节的特殊值将用于指示字符串函数被赋予一个包含标志字节、指针和缓冲区长度的结构(然后可以任意索引到任何其他字符串)。

当然,K&R并没有实现任何这样的东西,但这很可能是因为他们不想在字符串处理上花费太多精力——即使在今天,许多语言在这方面似乎都相当缺乏。

根据Joel Spolsky在这篇博文中的说法,

这是因为发明了UNIX和C编程语言的PDP-7微处理器有一个ascii字符串类型。ASCIZ的意思是“以Z(零)结尾的ASCII”。

在看到这里所有其他的答案后,我相信即使这是真的,这也只是C具有以空结束的“字符串”的部分原因。这篇文章很有启发性,因为像字符串这样简单的东西实际上是相当困难的。

GCC接受以下代码:

Char s[4] = "abcd";

如果我们把is当作字符数组,而不是字符串数组,这是可以的。也就是说,我们可以使用s[0], s[1], s[2]和s[3],甚至使用memcpy(dest, s, 4)访问它。但是当我们尝试使用put (s)时,我们会得到混乱的字符,或者更糟糕的是使用strcpy(dest, s)。

不是一个基本原理必然,而是一个长度编码的对位

  1. 就内存而言,某些形式的动态长度编码优于静态长度编码,这完全取决于使用情况。看看UTF-8就知道了。它本质上是一个用于编码单个字符的可扩展字符数组。这对每个扩展的字节使用一个位。NUL终止使用8位。长度前缀我认为可以合理地称为无限长度,以及使用64位。你是否经常碰到你多余的比特是决定因素。只有一根非常大的弦?谁在乎你使用的是8位还是64位呢?许多小字符串(即英语单词字符串)?那么你的前缀成本占很大比例。

  2. 长度前缀字符串允许节省时间是不是一个真实的东西。无论您所提供的数据需要提供长度,您都是在编译时计数,或者您实际上是在提供必须编码为字符串的动态数据。这些大小是在算法的某个点上计算出来的。可以提供一个单独的变量来存储以空结尾的字符串 。这使得时间节省的比较变得毫无意义。一个只是在结尾多了一个NUL…但如果长度编码不包括NUL,那么两者之间就没有区别了。根本不需要改变算法。这只是一个预先传递,你必须自己手动设计,而不是让编译器/运行时为你做。C语言主要是关于手动操作。

  3. 长度前缀可选是一个卖点。我并不总是需要一个算法的额外信息,所以被要求为每个字符串做这件事,使我的预计算+计算时间永远无法低于O(n)。(即硬件随机数发生器1-128。我可以从“无限的弦”中拉。假设它生成字符的速度只有这么快。所以弦的长度一直在变化。但我对数据的使用可能并不关心我有多少随机字节。它只是希望在请求后得到下一个可用的未使用字节。我可能在等设备。但我也可以有一个预读字符缓冲区。长度比较是一种不必要的计算浪费。空检查更有效。)

  4. 长度前缀是防止缓冲区溢出的好方法?库函数和实现的合理使用也是如此。如果我传入畸形数据呢?我的缓冲区是2字节长,但我告诉函数它是7!如果gets()打算用于已知数据,它可以有一个内部缓冲区检查,测试编译缓冲区和malloc()调用,仍然遵循spec。如果它打算用作未知STDIN到达未知缓冲区的管道,那么显然不能知道缓冲区大小,这意味着长度参数是毫无意义的,你需要在这里像金丝金丝猴检查一样的东西。就此而言,你不能给某些流和输入加上长度前缀,你就是不能。这意味着长度检查必须内置到算法中,而不是输入系统的神奇部分。TL;DR null -terminated从来不是不安全的,它只是由于滥用而以这种方式结束。

  5. counter-counter point: null - terminate是恼人的二进制。你要么需要在这里做长度前缀,要么以某种方式转换NUL字节:转义码,范围重映射,等等…这当然意味着更多的内存使用/减少的信息/每个字节更多的操作。长度前缀在这里赢得了胜利。转换的唯一优点是不需要编写额外的函数来覆盖长度前缀字符串。这意味着在你优化的子O(n)例程中,你可以让它们自动充当它们的O(n)等效物,而无需添加更多代码。当然,缺点是在NUL重字符串上使用时,会浪费时间/内存/压缩。根据操作二进制数据所复制的库的大小,只使用长度前缀字符串可能是有意义的。也就是说,人们也可以对长度前缀字符串做同样的事情……-1 length可以表示以null结尾,你可以在length-terminated中使用以null结尾的字符串

  6. Concat: "O(n+m) vs O(m)"我假设你引用m作为连接后字符串的总长度,因为它们都必须有最少的操作次数(你不能只是附加到字符串1,如果你必须realloc怎么办?)我假设n是一个神奇的运算量因为有了预计算你不再需要做这些运算。如果是这样,那么答案很简单:预先计算。如果你坚持你总是有足够的内存不需要realloc,这是大o符号的基础,那么答案就更简单了:在字符串1的末尾对分配的内存进行二分搜索,显然在字符串1之后有大量的无限零,我们不用担心realloc。在这里,很容易得到n到log(n)我几乎没有尝试。如果你回想一下,log(n)在真正的计算机上只等于64,也就是O(64+m)也就是O(m)(是的,该逻辑已用于当前使用的real数据结构的运行时分析。这不是我脑子里蹦出来的废话。)

  7. Concat()/Len() again: Memoize results。一件容易的事。如果可能/必要,将所有计算转换为预计算。这是一个算法决策。这不是语言的强制约束。

  8. 字符串后缀传递更容易/可能与NUL终止。取决于长度前缀是如何实现的,它可以破坏原始字符串,有时甚至不可能。需要复制并传递O(n)而不是O(1)。

  9. 以null结尾的参数传递/去引用比以长度前缀结尾的参数传递/去引用少。显然是因为你传递的信息少了。如果您不需要长度,那么这将节省大量的占用空间并允许优化。

  10. 你可以作弊。它实际上只是一个指针。谁说你必须把它当作字符串来读?如果你想把它读成单个字符或浮点数呢?如果你想做相反的事情,把浮点数读成字符串呢?如果你小心的话,你可以用null终止来实现。你不能用length-prefix这样做,它是一种与指针明显不同的数据类型。您很可能必须逐字节构建字符串并获取长度。当然,如果你想要一个 whole float(可能有一个NUL在里面),你必须一个字节一个字节地读取,但细节留给你决定。

你使用二进制数据吗?如果不是,那么null终止允许更多的算法自由。如果是,那么代码数量vs速度/内存/压缩是你的主要关注点。两种方法的混合或记忆可能是最好的。

我不相信“C没有字符串”的答案。没错,C语言不支持内置的高级类型,但你仍然可以用C语言表示数据结构,这就是字符串。在C语言中,字符串只是一个指针,但这并不意味着前N个字节不能作为长度具有特殊意义。

Windows/COM开发人员将非常熟悉BSTR类型,即像这样的完全 -一个有长度前缀的C字符串,其中实际的字符数据不是从字节0开始。

因此,使用空终止符的决定似乎只是人们喜欢的,而不是语言的必要。

与长度前缀相比,null终止的一个优点是字符串比较的简单性,这一点我没有看到任何人提到过。考虑比较标准,它返回小于、等于或大于的有符号结果。对于长度前缀,算法必须遵循以下几行:

  1. 比较这两个长度;记录较小的数字,并注意它们是否相等(最后一步可能推迟到步骤3)。
  2. 扫描两个字符序列,在匹配的索引处减去字符(或使用双指针扫描)。当差值不为零时停止,返回差值,或者当扫描的字符数等于较小的长度时停止。
  3. 当达到较小的长度时,一个字符串是另一个字符串的前缀。返回负数或正数,根据谁更短,或零,如果长度相等。

将其与null终止算法进行对比:

  1. 扫描两个字符序列,在匹配的索引处减去字符[注意,移动指针处理得更好]。当差值非零时停止,返回差值。注意:如果一个字符串是另一个字符串的PROPER前缀,减法中的一个字符将为NUL,即零,比较将自然地停止在那里。
  2. 如果差值为零,-only then-检查是否有字符为NUL。如果是,则返回0,否则继续到下一个字符。

以null结尾的情况更简单,并且非常容易用双指针扫描高效地实现。带长度前缀的大小写至少做同样多的工作,几乎总是更多。如果你的算法必须做大量的字符串比较[e。编译器!],以null结尾的情况胜出。现在,这可能不那么重要了,但在过去,是的。

我觉得更好的问题是你为什么觉得C欠你什么?C语言的设计是为了满足你的需要,仅此而已。你需要摆脱那种认为语言必须为你提供一切的心态。或者只是继续使用你的高级语言,这将给你奢侈的字符串,日历,容器;而在Java中,你会得到一种千变万化的东西。多个类型字符串,多个类型的unordered_map(s)。

这对你来说太糟糕了,这不是C的目的。C并不是被设计成一种从大头针到锚的臃肿语言。相反,您必须依赖第三方库或您自己的库。没有什么比创建一个包含字符串及其大小的简单结构体更容易的了。

struct String
{
const char *s;
size_t len;
};

你知道问题出在哪里。它不标准。另一种语言可能决定将len组织在字符串之前。另一种语言可能决定使用指针来代替结束。另一个人可能决定使用六个指针来提高String的效率。然而,null结尾的字符串是字符串的最标准格式;你可以用它来与任何语言进行交互。甚至Java JNI也使用以空结尾的字符串。

最后,这是一个常见的说法;任务的正确数据结构。如果你发现需要知道字符串的大小比其他任何事情都重要;我们使用一个字符串结构,它能让你做到最优。但不要声称这个操作对每个人都是最常用的。比如,为什么知道字符串的大小比读取它的内容更重要。我发现,读取字符串的内容是我主要做的,所以我使用null终止的字符串,而不是std::string;这让我在GCC编译器上节省了5个指针。如果我能保存两个指针,那就很好了。