尽管我很喜欢 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 和朋友那里获得的指针)。
以上都不像长度和连接那样常见。
在下面的答案中还有一个断言:
你需要切断字符串的末端
但这一个是不正确的——空终止和长度前缀字符串的时间相同。 (以空结尾的字符串只需在您希望新结尾的位置粘贴一个空值,长度前缀只需从前缀中减去。)
BCPL、B 或 C 语言中没有一个强烈支持字符数据;每个都将字符串视为整数向量,并通过一些约定补充一般规则。在 BCPL 和 B 中,字符串字面量表示静态区域的地址,该地址使用字符串的字符初始化,并打包到单元格中。在 BCPL 中,第一个压缩字节包含字符串中的字符数;在 B 中,没有计数,字符串以特殊字符结尾,B 拼写为 *e。进行此更改的部分原因是为了避免将计数保存在 8 位或 9 位插槽中导致的字符串长度限制,部分原因是根据我们的经验,维护计数似乎不如使用终止符方便。
Dennis M Ritchie,C 语言的开发
语言中没有字符串。 C 中的“字符串”只是一个指向 char 的指针。所以也许你问错了问题。
“省略字符串类型的理由是什么”可能更相关。对此我要指出,C 不是面向对象的语言,只有基本的值类型。字符串是一个更高级别的概念,必须以某种方式组合其他类型的值来实现。 C 处于较低的抽象级别。
鉴于下面的狂风:
我只想指出,我并不是要说这是一个愚蠢或糟糕的问题,或者表示字符串的 C 方式是最佳选择。我试图澄清如果考虑到 C 没有将字符串作为数据类型与字节数组区分开来的事实,这个问题会更简洁。鉴于当今计算机的处理和存储能力,这是最佳选择吗?可能不是。但事后诸葛亮总是 20/20 之类的 :)
char *temp = "foo bar";
是 C 中的有效语句......嘿!那不是一个字符串吗?它不是空终止吗?
这个问题是作为 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兼容的)。
std::basic_string
所做的。
\0
。前置长度要糟糕得多。
我认为,它有历史原因并发现this in wikipedia:
在开发 C(及其衍生语言)时,内存非常有限,因此仅使用一个字节的开销来存储字符串的长度是很有吸引力的。当时唯一流行的替代方法,通常称为“Pascal 字符串”(尽管早期版本的 BASIC 也使用),使用前导字节来存储字符串的长度。这允许字符串包含 NUL 并使得查找长度只需要一次内存访问(O(1)(常数)时间)。但是一个字节将长度限制为 255。这个长度限制比 C 字符串的问题要严格得多,所以 C 字符串通常胜出。
Calavera 是 right,但由于人们似乎不明白他的意思,我将提供一些代码示例。
首先,让我们考虑一下 C 是什么:一种简单的语言,所有代码都可以直接翻译成机器语言。所有类型都适合寄存器和堆栈,并且它不需要操作系统或大型运行时库来运行,因为它旨在编写这些东西(考虑到那里,这是一个非常适合的任务甚至到今天都不是可能的竞争对手)。
如果 C 有一个 string
类型,例如 int
或 char
,它将是一个不适合寄存器或堆栈的类型,并且需要内存分配(及其所有支持基础结构)以任何方式处理。所有这些都违背了 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 中所谓的 “字符串” 仅出于一个目的而创建:在编写用于文本终端的操作系统的上下文中显示消息。而且,为此,空终止就足够了。
strlen
和朋友隐藏。至于“留给实现”的问题,您可以说前缀是目标框上的 short
。然后你所有的演员仍然可以工作。 3. 我可以整天想出一些人为的场景,让一个或另一个系统看起来很糟糕。
short
有效地限制了字符串的大小,这似乎是他们不热衷的一件事。我自己曾使用过 8 位 BASIC 和 Pascal 字符串、固定大小的 COBOL 字符串和类似的东西,很快就成为了无限大小的 C 字符串的忠实粉丝。如今,32 位大小可以处理任何实用的字符串,但是在早期添加这些字节是有问题的。
string
类型的重要一步:它不知道字符。它是一个“char”数组(机器术语中的“char”是一个字符,就像人类在句子中所说的“单词”一样)。如果您引入了编码的概念,则字符串是一个更高级别的概念,可以在 char
的数组之上实现。
buf
需要分配),要么使用 struct string {int len; char buf[]};
并将整个事物分配给一个分配作为灵活的数组成员,并将其作为 string*
传递。 (或者可以说,struct string {int capacity; int len; char buf[]};
出于明显的性能原因)
显然,为了性能和安全,您在处理字符串时需要保持字符串的长度,而不是重复执行 strlen
或对其进行等效操作。但是,将长度存储在字符串内容之前的固定位置是一个非常糟糕的设计。正如 Jörgen 在对 Sanjit 答案的评论中指出的那样,它排除了将字符串的尾部视为字符串,例如,如果不分配新内存(并导致可能故障和错误处理)。然后当然还有一个问题,没有人可以同意字符串长度字段应该占用多少字节(很多糟糕的“帕斯卡字符串”语言使用 16 位字段,甚至是 24 位字段,这些字段排除了长字符串的处理)。
让程序员选择是否/在哪里/如何存储长度的设计更加灵活和强大。但是,程序员当然必须聪明。 C 用崩溃、停止或让敌人生根的程序来惩罚愚蠢。
考虑到任何语言的汇编内容,尤其是比汇编高出一步的 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 也会有它的问题,但是,这里提出的问题对它的影响很小。
O(m+n)
带有 nullterm 字符串,O(n)
在其他任何地方都很常见。长度 O(n)
与 nullterm 字符串,O(1)
其他任何地方。加入:O(n^2)
与 nullterm 字符串,O(n)
其他任何地方。在某些情况下,以 null 结尾的字符串更有效(即,只需将指针加一),但 concat 和 length 是迄今为止最常见的操作(格式化、文件输出、控制台显示等至少需要 length) .如果您缓存长度以摊销 O(n)
您只是表明我的观点是长度应该与字符串一起存储。
在许多方面,C 是原始的。我喜欢它。
它比汇编语言高出一步,使用更容易编写和维护的语言为您提供几乎相同的性能。
空终止符很简单,不需要语言的特殊支持。
回想起来,好像不是那么方便。但我在 80 年代使用汇编语言,当时看起来很方便。我只是认为软件在不断发展,平台和工具不断变得越来越复杂。
假设 C 以 Pascal 方式实现字符串,通过在字符串前面加上长度: 7 字符长字符串与 3 字符字符串相同的 DATA TYPE 吗?如果答案是肯定的,那么当我将前者分配给后者时,编译器应该生成什么样的代码呢?字符串应该被截断,还是自动调整大小?如果调整大小,该操作是否应该受到锁的保护以使其线程安全? 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"
那样简单的代码,如我的示例显示。
char*
,但许多不期望空终止的方法也期望 char*
。分离类型的一个更重要的好处与 Unicode 行为有关。对于字符串实现而言,维护标志可能是值得的,以判断字符串是否已知包含某些类型的字符,或者已知不包含它们[例如,在已知不包含的百万字符字符串中找到第 999,990 个代码点超出基本多语言平面的任何字符都将快几个数量级......
“即使在 32 位机器上,如果您允许字符串是可用内存的大小,则以长度为前缀的字符串仅比以空字符结尾的字符串宽三个字节。”
首先,额外的 3 个字节对于短字符串来说可能是相当大的开销。特别是,一个零长度的字符串现在占用了 4 倍的内存。我们中的一些人正在使用 64 位机器,因此我们要么需要 8 个字节来存储零长度字符串,要么字符串格式无法处理平台支持的最长字符串。
还可能需要处理对齐问题。假设我有一个包含 7 个字符串的内存块,例如“solo\0second\0\0four\0five\0\0seventh”。第二个字符串从偏移量 5 开始。硬件可能要求 32 位整数在 4 的倍数的地址处对齐,因此您必须添加填充,从而进一步增加开销。相比之下,C 表示非常节省内存。 (内存效率很好;例如,它有助于缓存性能。)
尚未提及的一点:当设计 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)
一样。
sizeof(char)
是一个。总是。前缀可以是实现定义的大小,但这会很尴尬。此外,没有真正的方法可以知道“正确”的尺寸应该是多少。如果一个人持有大量 4 字符的字符串,零填充将产生 25% 的开销,而四字节长度的前缀将产生 100% 的开销。此外,打包和解包四字节长度前缀所花费的时间可能超过扫描零字节的四字节字符串的成本。
size_t
前缀(该死的内存浪费,它会是最明智的 --- 允许任何可能长度的字符串适合记忆)。事实上,这就是 D 所做的种;数组是 struct { size_t length; T* ptr; }
,而字符串只是 immutable(char)
的数组。
struct {size_t length; char text[]; }
空终止允许基于快速指针的操作。
strlen
。我会说这有点缺点。
不一定是基本原理,而是长度编码的对立面
就内存而言,某些形式的动态长度编码优于静态长度编码,这完全取决于使用情况。只需查看 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 终止允许更多的算法自由。如果是,那么代码数量与速度/内存/压缩是您主要关心的问题。两种方法或记忆的混合可能是最好的。
许多围绕 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 返回的字符串的生命周期将受到 s
和 src
的限制,后者更短(这就是该方法需要传入 inf
的原因——如果它是本地的,它当方法返回时会死掉)。
在内存成本方面,最多 64 字节的字符串和缓冲区将有一个字节的开销(与以零结尾的字符串相同);更长的字符串会稍微多一些(两个字节之间允许的开销量和所需的最大值是否是时间/空间权衡)。长度/模式字节的特殊值将用于指示字符串函数被赋予包含标志字节、指针和缓冲区长度的结构(然后可以任意索引到任何其他字符串)。
当然,K&R 没有实现任何这样的东西,但这很可能是因为他们不想在字符串处理上花费太多精力——即使在今天,许多语言似乎都相当乏力。
char* arr
指向 struct { int length; char characters[ANYSIZE_ARRAY] };
或类似形式的结构,该结构仍可作为单个参数传递。
str[n]
引用正确的 char。这些是讨论这个问题的人们没有考虑的事情。
根据 this blog post 中的 Joel Spolsky,
这是因为发明了 UNIX 和 C 编程语言的 PDP-7 微处理器具有 ASCIZ 字符串类型。 ASCIZ 的意思是“以 Z(零)结尾的 ASCII”。
在这里看到所有其他答案后,我确信即使这是真的,这只是 C 具有以空字符结尾的“字符串”的部分原因。那篇文章很好地说明了字符串之类的简单事物实际上是多么困难。
.ASCIZ
只是一个用于构建字节序列的汇编语句,然后是 0
。这只是意味着 零终止字符串 在当时是一个成熟的概念。它确实不意味着以零结尾的字符串与 PDP-* 的体系结构有关,除了您可以编写由 MOVB
(复制一个字节)和 BNE
(分支如果复制的最后一个字节不为零)。
我不买“C 没有字符串”的答案。诚然,C 不支持内置的高级类型,但您仍然可以在 C 中表示数据结构,这就是字符串。字符串只是 C 中的指针这一事实并不意味着前 N 个字节不能作为长度具有特殊含义。
Windows/COM 开发人员将非常熟悉 BSTR
类型,它完全像这样 - 一个以长度为前缀的 C 字符串,其中实际的字符数据不是从字节 0 开始的。
因此,使用空终止的决定似乎只是人们喜欢的,而不是语言的必要性。
与长度前缀相比,NUL 终止的一个优点是字符串比较的简单性,我还没有看到任何人提到这一点。考虑返回小于、等于或大于的带符号结果的比较标准。对于长度前缀,算法必须遵循以下几行:
比较两个长度;记录较小的值,并注意它们是否相等(最后一步可能推迟到第 3 步)。扫描两个字符序列,减去匹配索引处的字符(或使用双指针扫描)。当差异不为零时停止,返回差异,或者当扫描的字符数等于较小的长度时。当达到较小的长度时,一个字符串是另一个字符串的前缀。根据哪个更短返回负值或正值,如果长度相等,则返回零。
将此与 NUL 终止算法进行对比:
扫描两个字符序列,减去匹配索引处的字符[请注意,使用移动指针处理得更好]。当差值非零时停止,返回差值。注意:如果一个字符串是另一个字符串的正确前缀,则减法中的一个字符将为 NUL,即零,并且比较自然会停在那里。如果差异为零,则仅检查任一字符是否为 NUL。如果是,则返回零,否则继续下一个字符。
NUL 终止的情况更简单,并且很容易通过双指针扫描有效地实现。以长度为前缀的情况至少做同样多的工作,几乎总是更多。如果您的算法必须进行大量字符串比较[例如编译器!],则以 NUL 结尾的情况胜出。如今,这可能不那么重要,但回到过去,哎呀。
gcc 接受以下代码:
字符 s[4] = "abcd";
如果我们将 is 视为字符数组而不是字符串,那也没关系。也就是说,我们可以使用 s[0]、s[1]、s[2] 和 s[3],甚至使用 memcpy(dest, s, 4) 来访问它。但是当我们尝试使用 puts(s) 或者更糟糕的是 strcpy(dest, s) 时,我们会得到混乱的字符。
我认为更好的问题是为什么你认为 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 个指针,那就太好了。