ChatGPT解决这个技术问题 Extra ChatGPT

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

尽管我很喜欢 C 和 C++,但在选择以空字符结尾的字符串时,我不禁摸不着头脑:

长度前缀(即 Pascal)字符串在 C 之前存在

长度前缀字符串通过允许恒定时间长度查找使几种算法更快。

长度前缀字符串使得更难导致缓冲区溢出错误。

即使在 32 位机器上,如果您允许字符串为可用内存的大小,则以长度为前缀的字符串仅比以空字符结尾的字符串宽三个字节。在 16 位机器上,这是一个字节。在 64 位机器上,4GB 是一个合理的字符串长度限制,但即使您想将其扩展到机器字的大小,64 位机器通常有足够的内存,这使得额外的 7 个字节排序为空参数。我知道最初的 C 标准是为极其糟糕的机器编写的(就内存而言),但效率的论点并没有在这里卖给我。

几乎所有其他语言(即 Perl、Pascal、Python、Java、C# 等)都使用长度前缀字符串。这些语言通常在字符串操作基准测试中击败 C,因为它们对字符串更有效。

C++ 使用 std::basic_string 模板稍微纠正了这一点,但期望空终止字符串的纯字符数组仍然普遍存在。这也是不完美的,因为它需要堆分配。

以空结尾的字符串必须保留一个字符(即 null),该字符不能存在于字符串中,而以长度为前缀的字符串可以包含嵌入的空值。

其中一些事情比 C 更近一些,所以 C 不知道它们是有道理的。然而,在 C 出现之前,有几个很简单。为什么会选择以空结尾的字符串而不是明显优越的长度前缀?

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

使用空终止字符串的 Concat 需要 O(n + m) 时间复杂度。长度前缀通常只需要 O(m)。

使用空终止字符串的长度需要 O(n) 时间复杂度。长度前缀为 O(1)。

Length 和 concat 是迄今为止最常见的字符串操作。在某些情况下,以空结尾的字符串可能更有效,但这种情况发生的频率要低得多。

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

当您需要切断字符串的开头并需要将其传递给某个方法时。即使您被允许破坏原始字符串,您也不能在恒定时间内使用长度前缀真正做到这一点,因为长度前缀可能需要遵循对齐规则。

在某些情况下,您只是逐个字符地循环遍历字符串,您也许可以节省 CPU 寄存器。请注意,这仅在您没有动态分配字符串的情况下才有效(因为那时您必须释放它,因此必须使用您保存的 CPU 寄存器来保存您最初从 malloc 和朋友那里获得的指针)。

以上都不像长度和连接那样常见。

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

你需要切断字符串的末端

但这一个是不正确的——空终止和长度前缀字符串的时间相同。 (以空结尾的字符串只需在您希望新结尾的位置粘贴一个空值,长度前缀只需从前缀中减去。)

我一直认为编写自己的字符串库是所有 C++ 程序员的必经之路。
现在期待理性的解释是什么意思。我想您接下来会想听听 x86 或 DOS 的基本原理吗?就我而言,最差的技术获胜。每次。和最差的字符串表示。
为什么你声称长度前缀字符串是优越的?毕竟,C 之所以流行,是因为它使用以空字符结尾的字符串,这使它有别于其他语言。
@Daniel:C 之所以流行,是因为它是在冯诺依曼机器上可执行的程序的简单、高效且可移植的表示,并且因为它用于 Unix。这当然不是因为它决定使用以空字符结尾的字符串。如果这是一个好的设计决策,人们会复制它,但他们没有。他们肯定从 C 中复制了几乎所有其他内容。
如果您销毁其中一个字符串,则 Concat 仅具有长度前缀的 O(m) 。否则,同样的速度。 C 字符串最常见的用途(历史上)是打印和扫描。在这两种情况下,空终止更快,因为它节省了一个寄存器。

A
AShelly

horse's mouth

BCPL、B 或 C 语言中没有一个强烈支持字符数据;每个都将字符串视为整数向量,并通过一些约定补充一般规则。在 BCPL 和 B 中,字符串字面量表示静态区域的地址,该地址使用字符串的字符初始化,并打包到单元格中。在 BCPL 中,第一个压缩字节包含字符串中的字符数;在 B 中,没有计数,字符串以特殊字符结尾,B 拼写为 *e。进行此更改的部分原因是为了避免将计数保存在 8 位或 9 位插槽中导致的字符串长度限制,部分原因是根据我们的经验,维护计数似乎不如使用终止符方便。

Dennis M Ritchie,C 语言的开发


另一个相关引用:“......字符串的语义完全包含在管理所有数组的更一般规则中,因此该语言更易于描述......”
C
Community

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

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

鉴于下面的狂风:

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


char *temp = "foo bar"; 是 C 中的有效语句......嘿!那不是一个字符串吗?它不是空终止吗?
@Yanick:这只是告诉编译器创建一个以 null 结尾的 char 数组的便捷方法。它不是“字符串”
@calavera:但它可能只是意味着“使用此字符串内容和两个字节长度前缀创建一个内存缓冲区”,
@Billy:好吧,既然“字符串”实际上只是一个指向 char 的指针,它相当于一个指向字节的指针,那么您怎么知道您正在处理的缓冲区实际上是一个“字符串”?您将需要 char/byte* 以外的新类型来表示这一点。也许是一个结构?
我认为@calavera 是对的,C 没有字符串的数据类型。好的,您可以将字符数组视为字符串,但这并不意味着它始终是字符串(对于字符串,我指的是具有明确含义的字符序列)。二进制文件是一个字符数组,但这些字符对人类没有任何意义。
M
Marco A.

这个问题是作为 Length Prefixed Strings (LPS)zero terminated strings (SZ) 的事情提出的,但主要揭示了长度前缀字符串的好处。这可能看起来势不可挡,但老实说,我们还应该考虑 LPS 的缺点和 SZ 的优点。

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

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

很简单,不需要在语言中引入新概念,char数组/char指针就可以。

核心语言只包含最小的语法糖,将双引号之间的内容转换为一堆字符(实际上是一堆字节)。在某些情况下,它可用于初始化与文本完全无关的事物。例如 xpm 图像文件格式是一个有效的 C 源代码,其中包含编码为字符串的图像数据。

顺便说一句,您可以在字符串文字中添加一个零,编译器也会在文字末尾添加另一个零:“this\0is\0valid\0C”。它是一个字符串吗?还是四弦?或者一堆字节......

平面实现,没有隐藏的间接,没有隐藏的整数。

不涉及隐藏的内存分配(好吧,一些臭名昭著的非标准函数,如 strdup 执行分配,但这主要是问题的根源)。

小型或大型硬件都没有具体问题(想象一下在 8 位微控制器上管理 32 位前缀长度的负担,或者将字符串大小限制为小于 256 字节的限制,这实际上是我在 Turbo Pascal 之前遇到的问题)。

字符串操作的实现只是少数非常简单的库函数

对字符串的主要使用有效:从已知开始顺序读取的常量文本(主要是给用户的消息)。

终止零甚至不是强制性的,所有必要的工具都可以像一堆字节一样操作字符。在 C 中执行数组初始化时,您甚至可以避免使用 NUL 终止符。只需设置正确的尺寸。字符 a[3] = "foo";是有效的 C(不是 C++)并且不会在 a 中放入最后的零。

与 unix 观点“一切都是文件”相一致,包括像标准输入、标准输出这样没有内在长度的“文件”。您应该记住,开放式读写原语是在非常低的级别实现的。它们不是库调用,而是系统调用。相同的 API 用于二进制或文本文件。文件读取原语获取缓冲区地址和大小并返回新大小。您可以使用字符串作为写入的缓冲区。使用另一种字符串表示形式意味着您不能轻易地使用文字字符串作为输出缓冲区,或者在将其转换为 char* 时必须使其具有非常奇怪的行为。即不返回字符串的地址,而是返回实际数据。

非常容易操作从文件就地读取的文本数据,没有无用的缓冲区副本,只需在正确的位置插入零(嗯,现代 C 不是真的,因为双引号字符串现在是 const char 数组,通常保存在不可修改的数据中部分)。

预先添加一些任何大小的 int 值都意味着对齐问题。初始长度应该对齐,但是对于字符数据没有理由这样做(同样,强制对齐字符串会在将它们视为一堆字节时暗示问题)。

对于常量文字字符串 (sizeof),长度在编译时是已知的。那么为什么会有人想将它存储在内存中,并将其添加到实际数据中呢?

在某种程度上,C 与(几乎)其他所有人一样,字符串被视为 char 数组。由于数组长度不由 C 管理,因此也不管理字符串的逻辑长度。唯一令人惊讶的是最后添加了 0 项,但这只是在双引号之间键入字符串时处于核心语言级别。用户可以完美地调用传递长度的字符串操作函数,甚至可以使用普通的 memcopy。 SZ只是一个设施。在大多数其他语言中,数组长度是受管理的,这对字符串来说是相同的逻辑。

在现代无论如何 1 字节字符集是不够的,你经常不得不处理编码的 unicode 字符串,其中字符数与字节数有很大不同。这意味着用户可能想要的不仅仅是“大小”,还有其他信息。保持长度对于这些其他有用的信息没有任何用处(尤其是没有自然的地方来存储它们)。

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

编辑:我最近看了D strings。有趣的是,选择的解决方案既不是大小前缀,也不是零终止。与在 C 中一样,用双引号括起来的文字字符串只是不可变 char 数组的简写,并且该语言还有一个字符串关键字,意思是(不可变 char 数组)。

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

在普通的 char 数组或非文字字符串上,没有最后的零,因此如果程序员想从 D 调用某个 C 函数,则必须将它自己放入。在文字字符串的特定情况下,但是 D 编译器仍然在每个字符串的结尾(以便轻松转换为 C 字符串以便更轻松地调用 C 函数?),但是这个零不是字符串的一部分(D 不计入字符串大小)。

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


...继续...我认为您的一些观点是完全错误的,即“一切都是文件”的论点。文件是顺序访问,C 字符串不是。长度前缀也可以用最少的语法糖来完成。这里唯一合理的论点是试图在小型(即 8 位)硬件上管理 32 位前缀;我认为这可以通过说长度的大小由实现决定来简单地解决。毕竟,这就是 std::basic_string 所做的。
@Billy ONeal:我的答案确实有两个不同的部分。一个是关于“核心 C 语言”的一部分,另一个是关于标准库应该提供什么。关于字符串支持,核心语言中只有一个项:双引号括起来的字节串的含义。对于 C 行为,我并不比你更快乐。我觉得神奇地在每个双闭包字节的末尾添加零就足够糟糕了。当程序员想要而不是隐式的时,我更喜欢最后明确的 \0 。前置长度要糟糕得多。
@Billy ONeal:这不是真的,用途关心什么是核心,什么是库。最大的一点是C用于实现OS。在该级别没有可用的库。 C 也经常用于嵌入式环境或经常有相同限制的编程设备。在许多情况下,Joes's 现在可能根本不应该使用 C:“好吧,你想在控制台上使用它?你有控制台吗?没有?太糟糕了......”
@Billy “好吧,对于实现操作系统的 0.01% 的 C 程序员来说,很好。”其他程序员可以徒步旅行。创建 C 是为了编写操作系统。
为什么?因为它说它是一种通用语言?它是否说明了编写它的人在创建它时正在做什么?在它生命的最初几年里,它是用来做什么的?那么,它说的与我不同的是什么?它是为编写操作系统而创建的通用语言。它否认吗?
N
NotDan

我认为,它有历史原因并发现this in wikipedia

在开发 C(及其衍生语言)时,内存非常有限,因此仅使用一个字节的开销来存储字符串的长度是很有吸引力的。当时唯一流行的替代方法,通常称为“Pascal 字符串”(尽管早期版本的 BASIC 也使用),使用前导字节来存储字符串的长度。这允许字符串包含 NUL 并使得查找长度只需要一次内存访问(O(1)(常数)时间)。但是一个字节将长度限制为 255。这个长度限制比 C 字符串的问题要严格得多,所以 C 字符串通常胜出。


@muntoo 嗯...兼容性?
@muntoo:因为这会破坏大量现有的 C 和 C++ 代码。
@muntoo:范式来来去去,但遗留代码永远存在。任何未来版本的 C 都必须继续支持以 0 结尾的字符串,否则将不得不重写 30 多年的遗留代码(这不会发生)。只要旧方式可用,人们就会继续使用,因为这是他们熟悉的方式。
@muntoo:相信我,有时我希望我能做到。但我仍然更喜欢以 0 结尾的字符串而不是 Pascal 字符串。
谈论遗留问题...... C++ 字符串现在被强制要求以 NUL 结尾。
C
Community

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

首先,让我们考虑一下 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 {
  int len; /* cannot be left implementation-defined */
  char* buf;
}

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

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

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

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


1. +1。 2. 显然,如果语言的默认行为是使用长度前缀来实现的,那么还有其他事情可以让这变得更容易。例如,您在那里的所有演员都会被调用 strlen 和朋友隐藏。至于“留给实现”的问题,您可以说前缀是目标框上的 short 。然后你所有的演员仍然可以工作。 3. 我可以整天想出一些人为的场景,让一个或另一个系统看起来很糟糕。
@Billy 图书馆的事情是真的,除了 C 是为最少或没有图书馆使用而设计的。例如,原型的使用在早期并不常见。说前缀是 short 有效地限制了字符串的大小,这似乎是他们不热衷的一件事。我自己曾使用过 8 位 BASIC 和 Pascal 字符串、固定大小的 COBOL 字符串和类似的东西,很快就成为了无限大小的 C 字符串的忠实粉丝。如今,32 位大小可以处理任何实用的字符串,但是在早期添加这些字节是有问题的。
@Billy:首先,谢谢丹尼尔......你似乎明白我的意思。其次,比利,我认为你仍然没有抓住这里所说的重点。我并不是在争论以字符串数据类型为前缀的长度的利弊。我要说的是,丹尼尔非常清楚地强调的是,在 C 的实现中做出了根本不处理该论点的决定。就基本语言而言,字符串并不存在。如何处理字符串的决定留给了程序员……空终止变得流行起来。
由我+1。我还想补充一件事;您建议的结构错过了迈向真正 string 类型的重要一步:它不知道字符。它是一个“char”数组(机器术语中的“char”是一个字符,就像人类在句子中所说的“单词”一样)。如果您引入了编码的概念,则字符串是一个更高级别的概念,可以 char 的数组之上实现。
@DanielC.Sobral:另外,您提到的结构不需要两次分配。要么像在堆栈上一样使用它(因此只有 buf 需要分配),要么使用 struct string {int len; char buf[]}; 并将整个事物分配给一个分配作为灵活的数组成员,并将其作为 string* 传递。 (或者可以说,struct string {int capacity; int len; char buf[]}; 出于明显的性能原因)
R
R.. GitHub STOP HELPING ICE

显然,为了性能和安全,您在处理字符串时需要保持字符串的长度,而不是重复执行 strlen 或对其进行等效操作。但是,将长度存储在字符串内容之前的固定位置是一个非常糟糕的设计。正如 Jörgen 在对 Sanjit 答案的评论中指出的那样,它排除了将字符串的尾部视为字符串,例如,如果不分配新内存(并导致可能故障和错误处理)。然后当然还有一个问题,没有人可以同意字符串长度字段应该占用多少字节(很多糟糕的“帕斯卡字符串”语言使用 16 位字段,甚至是 24 位字段,这些字段排除了长字符串的处理)。

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


+1。最好有一个标准的地方来存储长度,这样我们这些想要长度前缀的人就不必到处写大量的“胶水代码”。
相对于字符串数据没有可能的标准位置,但是您当然可以使用单独的局部变量(重新计算它而不是在后者不方便且前者不太浪费时传递它)或带有指针的结构到字符串(甚至更好的是,一个标志,指示结构是否“拥有”用于分配目的的指针,或者它是否是对其他地方拥有的字符串的引用。当然,您可以在结构中包含一个灵活的数组成员以灵活分配适合您的结构的字符串。
d
dvhh

考虑到任何语言的汇编内容,尤其是比汇编高出一步的 C(因此继承了许多汇编遗留代码),因此懒惰、注册节俭和可移植性。您会同意,因为 null char 在那些 ASCII 时代将毫无用处,它(并且可能与 EOF control char 一样好)。

让我们看看伪代码

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 计算时),这个“Hack”非常好,并且可以轻松移植到无寄存器处理器。

为了论证起见,我将实现 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 以考虑寄存器长度和位字节序,PASCAL 字符串具有众所周知的 255 字符(0xFF)限制,因为长度存储在 1 个字节(8 位),并且如果您想要更长的字符串(16 位-> 任何内容),则必须考虑代码的一层中的体系结构,这意味着在大多数情况下,如果您想要更长的字符串,则字符串 API 不兼容。

例子:

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

当然,C 也会有它的问题,但是,这里提出的问题对它的影响很小。


@deemoowoor:Concat:O(m+n) 带有 nullterm 字符串,O(n) 在其他任何地方都很常见。长度 O(n) 与 nullterm 字符串,O(1) 其他任何地方。加入:O(n^2) 与 nullterm 字符串,O(n) 其他任何地方。在某些情况下,以 null 结尾的字符串更有效(即,只需将指针加一),但 concat 和 length 是迄今为止最常见的操作(格式化、文件输出、控制台显示等至少需要 length) .如果您缓存长度以摊销 O(n) 您只是表明我的观点是长度应该与字符串一起存储。
我同意在今天的代码中这种类型的字符串效率低下并且容易出错,但是例如控制台显示并不需要知道字符串的长度才能有效地显示它,文件输出并不需要知道字符串长度(仅在旅途中分配簇),并且此时的字符串格式化在大多数情况下是在固定的字符串长度上完成的。无论如何,如果您在 C 中 concat 具有 O(n^2) 复杂度,那么您一定是在编写糟糕的代码,我很确定我可以在 O(n) 复杂度中编写一个
@dvhh:我没有说 n^2 - 我说 m + n - 它仍然是线性的,但是您需要寻找到原始字符串的末尾才能进行连接,而使用长度前缀则不需要寻找是必须的。 (这实际上只是长度需要线性时间的另一个结果)
@Billy ONeal:出于好奇,我对我当前的 C 项目(大约 50000 行代码)进行了 grep 以进行字符串操作函数调用。 strlen 101,strcpy 和变体(strncpy,strlcpy):85(我还有数百个用于消息的文字字符串,隐含副本),strcmp:56,strcat:13(和 6 是连接到零长度字符串以调用 strncat) .我同意长度前缀将加速对 strlen 的调用,但不会加速对 strcpy 或 strcmp 的调用(也许如果 strcmp API 不使用公共前缀)。关于上述评论,最有趣的是 strcat 非常罕见。
@Hurkyl:那不是真的。在空终止的情况下,在每个比较步骤中,您需要让指向字符串(1 个寄存器)的指针加载它指向的字符(2 个寄存器)并与 0(3 个寄存器)进行比较。在长度前缀的情况下,您需要将指向字符串(1 个寄存器)的指针与指向字符串末尾的指针(2 个寄存器)进行比较,并加载它指向的字符(再次是 3 个寄存器)。
J
Jonathan Wood

在许多方面,C 是原始的。我喜欢它。

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

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

回想起来,好像不是那么方便。但我在 80 年代使用汇编语言,当时看起来很方便。我只是认为软件在不断发展,平台和工具不断变得越来越复杂。


我看不到空终止字符串比其他任何东西都更原始。 Pascal 早于 C,它使用长度前缀。当然,它被限制为每个字符串 256 个字符,但在绝大多数情况下,简单地使用 16 位字段就可以解决问题。
它限制字符数的事实正是您在执行此类操作时需要考虑的问题类型。是的,你可以让它更长,但那时字节很重要。对于所有情况,一个 16 位字段是否足够长?来吧,您必须承认空终止在概念上是原始的。
要么限制字符串的长度,要么限制内容(无空字符),要么接受 4 到 8 个字节计数的额外开销。没有免费的午餐。在开始时,以空结尾的字符串非常有意义。在汇编中,我有时会使用字符的最高位来标记字符串的结尾,甚至可以节省一个字节!
没错,马克:没有免费的午餐。这总是一种妥协。这些天来,我们不需要做出同样的妥协。但在当时,这种方法似乎和其他方法一样好。
C
Cristian

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


错误..不,它没有。 C 方法根本不允许将 7 字符长字符串分配给 3 字符长字符串。
@Billy ONeal:为什么不呢?据我所知,在这种情况下,所有字符串都是相同的数据类型(char*),所以长度无关紧要。与帕斯卡不同。但这是 Pascal 的限制,而不是长度前缀字符串的问题。
@Billy:我想你只是重申了克里斯蒂安的观点。 C通过根本不处理这些问题来处理这些问题。您仍在考虑 C 实际上包含字符串的概念。它只是一个指针,所以你可以将它分配给你想要的任何东西。
这就像**矩阵:“没有字符串”。
@calavera:我看不出这能证明什么。您可以使用长度前缀以相同的方式解决它......即根本不允许分配。
P
Pyry Jahkola

不知何故,我理解这个问题意味着 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++ std::string )。
即使你存储了一个长度,你也不应该允许带有嵌入空值的字符串。这是基本常识。如果您的数据中可能包含空值,则永远不应将其与需要字符串的函数一起使用。
@supercat:从安全的角度来看,我欢迎这种冗余。否则,无知(或睡眠不足)的程序员最终会将二进制数据和字符串连接起来,并将它们传递给期望 [null-terminated] 字符串的东西......
@R ..:虽然期望空终止字符串的方法通常期望 char*,但许多不期望空终止的方法也期望 char*。分离类型的一个更重要的好处与 Unicode 行为有关。对于字符串实现而言,维护标志可能是值得的,以判断字符串是否已知包含某些类型的字符,或者已知不包含它们[例如,在已知不包含的百万字符字符串中找到第 999,990 个代码点超出基本多语言平面的任何字符都将快几个数量级......
B
Brangdon

“即使在 32 位机器上,如果您允许字符串是可用内存的大小,则以长度为前缀的字符串仅比以空字符结尾的字符串宽三个字节。”

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

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


我相信我在问题中解决了所有这些问题。是的,在 x64 平台上,32 位前缀不能适合所有可能的字符串。另一方面,您永远不会想要一个像空终止字符串那么大的字符串,因为要执行任何操作,您必须检查所有 40 亿字节以找到几乎所有您可能想要对其执行的操作的结尾。另外,我并不是说空终止的字符串总是邪恶的——如果您正在构建这些块结构中的一个并且您的特定应用程序被这种结构加速,那就去做吧。我只是希望语言的默认行为没有这样做。
我引用了你问题的那一部分,因为在我看来它低估了效率问题。将内存需求增加一倍或四倍(分别在 16 位和 32 位上)可能是很大的性能成本。长字符串可能很慢,但至少它们受到支持并且仍然有效。我的另一点,关于对齐,你根本没有提到。
对齐可以通过指定超出 UCHAR_MAX 的值应该表现得好像使用字节访问和位移位打包和解包来处理。适当设计的字符串类型可以提供基本上与零终止字符串相当的存储效率,同时还允许对缓冲区进行边界检查,而不会产生额外的内存开销(使用前缀中的一位来说明缓冲区是否“满”;如果不是并且最后一个字节非零,该字节将表示剩余空间。如果缓冲区未满且最后一个字节为零,那么最后 256 个字节将未被使用,所以...
...可以在该空间内存储确切数量的未使用字节,而额外的内存成本为零)。使用前缀的成本将被使用 fgets() 等方法的能力所抵消,而不必传递字符串长度(因为缓冲区会知道它们有多大)。
s
supercat

尚未提及的一点:当设计 C 时,有许多机器的“字符”不是 8 位(即使在今天也有 DSP 平台不是)。如果一个人决定字符串是长度前缀的,那么应该使用多少个 'char 的长度前缀?使用两个将对具有 8 位字符和 32 位寻址空间的机器的字符串长度施加人为限制,同时在具有 16 位字符和 16 位寻址空间的机器上浪费空间。

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


前缀可以很容易地具有实现定义的大小,就像 sizeof(char) 一样。
@BillyONEal:sizeof(char) 是一个。总是。前缀可以是实现定义的大小,但这会很尴尬。此外,没有真正的方法可以知道“正确”的尺寸应该是多少。如果一个人持有大量 4 字符的字符串,零填充将产生 25% 的开销,而四字节长度的前缀将产生 100% 的开销。此外,打包和解包四字节长度前缀所花费的时间可能超过扫描零字节的四字节字符串的成本。
是的。你是对的。不过,前缀很容易是 char 以外的东西。任何能使目标平台上的对齐要求得到解决的东西都可以。不过我不会去那里——我已经把这件事吵死了。
假设字符串是以长度为前缀的,可能最明智的做法是使用 size_t 前缀(该死的内存浪费,它是最明智的 --- 允许任何可能长度的字符串适合记忆)。事实上,这就是 D 所做的;数组是 struct { size_t length; T* ptr; },而字符串只是 immutable(char) 的数组。
@TimČas:对不起——我读到你使用“前缀”是指存储在内存中的长度紧接在字符本身之前,因为你说 D 所做的“有点”,我以为你是期望字符串类似于 struct {size_t length; char text[]; }
S
Sonny Saluja

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


嗯?哪些“快速指针操作”不适用于长度前缀?更重要的是,其他使用长度前缀的语言比 C wrt 字符串操作更快。
@billy:使用带有长度前缀的字符串,您不能只取一个字符串指针并向其添加 4,并期望它仍然是有效的字符串,因为它没有长度前缀(无论如何都不是有效的)。
@j_random_hacker:对于 asciiz 字符串(O(m+n) 而不是可能的 O(n)),连接要糟糕得多,并且 concat 比此处列出的任何其他操作都更常见。
有一个很小的操作会因为以空字符结尾的字符串变得更加昂贵:strlen。我会说这有点缺点。
@Billy ONeal:其他人也支持正则表达式。所以呢 ?使用它们的用途库。 C 是关于最大效率和极简主义,而不是包括电池。 C 工具还允许您非常轻松地使用结构实现长度前缀字符串。没有什么能阻止您通过管理自己的长度和字符缓冲区来实现字符串操作程序。这通常是我想要提高效率并使用 C 时所做的事情,而不是调用少数期望在 char 缓冲区末尾为零的函数不是问题。
B
Black

不一定是基本原理,而是长度编码的对立面

就内存而言,某些形式的动态长度编码优于静态长度编码,这完全取决于使用情况。只需查看 UTF-8 即可。它本质上是一个用于编码单个字符的可扩展字符数组。这对每个扩展字节使用一个位。 NUL 终止使用 8 位。我认为长度前缀也可以通过使用 64 位合理地称为无限长度。您多久遇到一次多余的位是决定因素。只有 1 个非常大的字符串?谁在乎您使用的是 8 位还是 64 位?许多小字符串(即英文单词的字符串)?那么您的前缀成本占很大比例。允许节省时间的以长度为前缀的字符串并不是真的。无论您提供的数据是否需要提供长度,您是在编译时进行计数,还是您确实被提供了必须编码为字符串的动态数据。这些大小是在算法中的某个点计算的。可以提供一个单独的变量来存储空终止字符串的大小。这使得关于节省时间的比较毫无意义。一个只是在最后有一个额外的 NUL ......但如果长度编码不包括那个 NUL,那么两者之间实际上没有区别。根本不需要更改算法。只是一个预通行证,您必须自己手动设计,而不是让编译器/运行时为您完成。 C主要是关于手动做事。长度前缀是可选的是一个卖点。我并不总是需要算法的额外信息,因此需要为每个字符串执行此操作,这使得我的预计算+计算时间永远无法低于 O(n)。 (即硬件随机数生成器 1-128。我可以从“无限字符串”中提取。假设它只生成这么快的字符。所以我们的字符串长度一直在变化。但我对数据的使用可能并不关心如何我有很多随机字节。它只想要下一个可用的未使用字节,只要它在请求后就可以得到它。我可以在设备上等待。但我也可以预读一个字符缓冲区。长度比较是不必要的计算浪费。空检查更有效。)长度前缀可以很好地防止缓冲区溢出?对库函数和实现的合理使用也是如此。如果我传入格式错误的数据怎么办?我的缓冲区有 2 个字节长,但我告诉函数它是 7 个字节!例如:如果 gets() 旨在用于已知数据,它可能已经进行了内部缓冲区检查,以测试已编译的缓冲区和 malloc() 调用,并且仍然遵循规范。如果它打算用作未知 STDIN 到达未知缓冲区的管道,那么显然人们无法知道缓冲区大小,这意味着长度 arg 是没有意义的,你需要其他东西,比如金丝雀检查。就此而言,您不能对某些流和输入进行长度前缀,您就是不能。这意味着长度检查必须内置到算法中,而不是打字系统的神奇部分。 TL;DR NUL 终止永远不必是不安全的,它只是通过滥用而以这种方式结束。 counter-counter 点:NUL 终止对二进制文件很烦人。你要么需要在这里做长度前缀,要么以某种方式转换 NUL 字节:转义码、范围重新映射等......这当然意味着更多的内存使用/减少的信息/更多的操作每字节。长度前缀在这里主要赢得了这场战争。转换的唯一好处是不需要编写额外的函数来覆盖长度前缀字符串。这意味着在您更优化的 sub-O(n) 例程中,您可以让它们自动充当其 O(n) 等价物,而无需添加更多代码。当然,缺点是在 NUL 重字符串上使用时会浪费时间/内存/压缩。根据您最终复制多少库以对二进制数据进行操作,仅使用长度前缀字符串可能是有意义的。也就是说,也可以对长度前缀字符串做同样的事情...... -1 长度可能意味着 NUL 终止,您可以在长度终止的内部使用 NUL 终止的字符串。 Concat:“O(n + m)vs O(m)”我假设您将m称为连接后字符串的总长度,因为它们都必须具有最少的操作数(您不能只添加-on 到字符串 1,如果你必须重新分配怎么办?)。而且我假设 n 是由于预先计算而不再需要进行的大量操作。如果是这样,那么答案很简单:预计算。如果你坚持你总是有足够的内存而不需要重新分配,这是大 O 表示法的基础,那么答案就更简单了:对分配的内存进行二进制搜索以查找字符串 1 的结尾,显然有一个字符串 1 之后的大量无限零让我们不必担心重新分配。在那里,很容易得到 n 到 log(n) 我几乎没有尝试过。如果你回想一下,log(n) 在真实计算机上基本上只有 64 大,这基本上就像说 O(64+m),本质上是 O(m)。 (是的,该逻辑已被用于今天使用的真实数据结构的运行时分析。这不是我头脑中的胡说八道。) Concat()/Len() 再次:Memoize 结果。简单的。如果可能/必要,将所有计算转换为预计算。这是一个算法决定。这不是语言的强制约束。使用 NUL 终止时,字符串后缀传递更容易/可能。根据长度前缀的实现方式,它可能对原始字符串具有破坏性,有时甚至不可能。需要一个副本并通过 O(n) 而不是 O(1)。与长度前缀相比,NUL 终止的参数传递/取消引用较少。显然是因为您传递的信息较少。如果您不需要长度,那么这可以节省大量空间并允许优化。你可以作弊。它实际上只是一个指针。谁说你必须把它当作一个字符串来读?如果您想将其读取为单个字符或浮点数怎么办?如果您想做相反的事情并将浮点数作为字符串读取怎么办?如果你小心,你可以用 NUL 终止来做到这一点。你不能用长度前缀来做到这一点,它是一种通常与指针明显不同的数据类型。您很可能必须逐字节构建字符串并获取长度。当然,如果你想要一个像整个浮点数这样的东西(可能里面有一个 NUL),你无论如何都必须逐字节读取,但细节留给你决定。

TL;DR 你在使用二进制数据吗?如果否,则 NUL 终止允许更多的算法自由。如果是,那么代码数量与速度/内存/压缩是您主要关心的问题。两种方法或记忆的混合可能是最好的。


9 有点离谱/错误表示。长度前缀没有这个问题。 Lenth 作为单独的变量传递。我们在谈论pre-fiix,但我被带走了。仍然是一件好事,所以我会把它留在那里。 :d
s
supercat

许多围绕 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-作为对某些操作的轻微速度损失和对较长字符串的一点额外开销的交换,可以让处理字符串的方法直接接受指向字符串的指针、边界检查的字符串缓冲区或识别另一个字符串的子字符串的数据结构。像“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 没有实现任何这样的东西,但这很可能是因为他们不想在字符串处理上花费太多精力——即使在今天,许多语言似乎都相当乏力。


没有什么可以阻止 char* arr 指向 struct { int length; char characters[ANYSIZE_ARRAY] }; 或类似形式的结构,该结构仍可作为单个参数传递。
@BillyONeal:这种方法有两个问题:(1)它只允许将字符串作为一个整体传递,而目前的方法还允许传递字符串的尾部; (2) 与小字符串一起使用时会浪费大量空间。如果 K&R 想花一些时间在字符串上,他们本可以让事情变得更加健壮,但我不认为他们打算在十年后使用他们的新语言,更不用说四十年了。
关于调用约定的这一点只是一个与现实无关的故事……这不是设计中的考虑因素。基于寄存器的调用约定已经被“发明”了。此外,诸如两个指针之类的方法不是一种选择,因为结构不是一流的……只有原语是可分配或可传递的;结构复制直到 UNIX V7 才出现。需要 memcpy (它也不存在)只是为了复制一个字符串指针是一个笑话。如果您假装语言设计,请尝试编写完整的程序,而不仅仅是孤立的功能。
“这很可能是因为他们不想在字符串处理上花费太多精力”——废话;早期 UNIX 的整个应用领域都是字符串处理。如果不是因为它,我们永远不会听说它。
'我不认为“char 缓冲区以包含长度的 int 开头”更神奇' - 如果您要让 str[n] 引用正确的 char。这些是讨论这个问题的人们没有考虑的事情。
B
BenK

根据 this blog post 中的 Joel Spolsky,

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

在这里看到所有其他答案后,我确信即使这是真的,这只是 C 具有以空字符结尾的“字符串”的部分原因。那篇文章很好地说明了字符串之类的简单事物实际上是多么困难。


听着,我在很多方面都尊重乔尔;但这是他在猜测的地方。 Hans Passant 的答案直接来自 C 的发明者。
是的,但如果斯波尔斯基所说的是真的,那么这将是他们所指的“方便”的一部分。这就是我包含这个答案的部分原因。
AFAIK .ASCIZ 只是一个用于构建字节序列的汇编语句,然后是 0。这只是意味着 零终止字符串 在当时是一个成熟的概念。它确实意味着以零结尾的字符串与 PDP-* 的体系结构有关,除了您可以编写由 MOVB(复制一个字节)和 BNE(分支如果复制的最后一个字节不为零)。
它应该表明 C 是陈旧、松散、破旧的语言。
M
Mr. Boy

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

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

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


P
PMar

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

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

将此与 NUL 终止算法进行对比:

扫描两个字符序列,减去匹配索引处的字符[请注意,使用移动指针处理得更好]。当差值非零时停止,返回差值。注意:如果一个字符串是另一个字符串的正确前缀,则减法中的一个字符将为 NUL,即零,并且比较自然会停在那里。如果差异为零,则仅检查任一字符是否为 NUL。如果是,则返回零,否则继续下一个字符。

NUL 终止的情况更简单,并且很容易通过双指针扫描有效地实现。以长度为前缀的情况至少做同样多的工作,几乎总是更多。如果您的算法必须进行大量字符串比较[例如编译器!],则以 NUL 结尾的情况胜出。如今,这可能不那么重要,但回到过去,哎呀。


k
kkaaii

gcc 接受以下代码:

字符 s[4] = "abcd";

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


@Adrian W。这是有效的C。精确长度的字符串是特殊的,并且 NUL 被省略了。这通常是一种不明智的做法,但在填充使用 FourCC“字符串”的标头结构等情况下可能很有用。
你说的对。这是有效的 C,将编译并按照 kkaaii 描述的方式运行。否决票(不是我的......)的原因可能是这个答案没有以任何方式回答 OP 的问题。
u
user13947194

我认为更好的问题是为什么你认为 C 欠你什么? C 旨在为您提供所需的一切,仅此而已。您需要摆脱语言必须为您提供一切的心态。或者只是继续使用您的高级语言,这些语言将为您提供 String、Calendar、Containers 的奢华;在 Java 的情况下,你会得到一件事,以吨计。多种类型的字符串,多种类型的 unordered_map(s)。

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

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

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

最后,这是一句俗语;任务的正确数据结构。如果您发现需要知道字符串的大小比什么都重要;很好地使用允许您以最佳方式执行此操作的字符串结构。但不要声称该操作对每个人的使用比其他任何操作都多。比如,为什么知道字符串的大小比读取它的内容更重要。我发现读取字符串的内容是我最常做的事情,所以我使用空终止字符串而不是 std::string;这在 GCC 编译器上为我节省了 5 个指针。如果我什至可以保存 2 个指针,那就太好了。


没有数十亿美元的单字节错误不会使 C 成为“臃肿”的语言。