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


281

尽管我热爱C和C ++,但我还是忍不住选择以null结尾的字符串:

  • 在C之前存在长度前缀(即Pascal)的字符串
  • 长度前缀的字符串允许进行恒定的时间长度查找,从而使几种算法更快。
  • 前缀字符串的长度使导致缓冲区溢出错误更加困难。
  • 即使在32位计算机上,如果允许字符串成为可用内存的大小,则带前缀前缀的字符串也只比以空终止的字符串宽3个字节。在16位计算机上,这是一个字节。在64位计算机上,4GB是一个合理的字符串长度限制,但是即使您希望将其扩展为机器字的大小,64位计算机通常也具有足够的内存,使得多余的7个字节属于null参数。我知道原始的C标准是为极差的机器(就内存而言)编写的,但是效率论点在这里并没有卖给我。
  • 几乎所有其他语言(例如Perl,Pascal,Python,Java,C#等)都使用长度前缀的字符串。这些语言通常在字符串操作基准中胜过C,因为它们在使用字符串时效率更高。
  • C ++使用std::basic_string模板对此进行了一些纠正,但是期望以null终止的字符串的纯字符数组仍然很普遍。这也是不完美的,因为它需要堆分配。
  • 空终止的字符串必须保留一个字符(即null),该字符不能存在于字符串中,而长度前缀的字符串可以包含嵌入的null。

这些事情中,有几件事比C揭露的要新,因此C不必了解它们是有道理的。但是,在C出现之前,有几个很简单。为什么会选择空终止的字符串而不是明显更好的长度前缀?

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

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

从以下答案中可以看出,在某些情况下以null终止的字符串更有效:

  • 当您需要切断字符串的开头并将其传递给某种方法时。即使允许销毁原始字符串,也不能真正在恒定时间内使用长度前缀来完成此操作,因为长度前缀可能需要遵循对齐规则。
  • 在某些情况下,您只是逐个字符地遍历字符串,则可以保存CPU寄存器。请注意,这仅在您没有动态分配字符串的情况下才有效(因为您必须释放它,必须使用保存的CPU寄存器保存最初从malloc和friends获得的指针)。

以上都不是最常见的长度和连贯性。

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

  • 您需要切断字符串的结尾

但这是不正确的-空终止和长度为前缀的字符串的时间相同。(以NULL结尾的字符串只是在您希望新的结尾处保留一个NULL,长度前缀只是从前缀中减去。)


110
我一直认为,所有C ++程序员编写自己的字符串库都是一个礼拜式。
朱丽叶2010年

31
现在期待合理的解释是什么?我想您接下来想听听x86或DOS的原理吗?就我而言,最糟糕的技术胜出。每次。和最差的字符串表示形式。
jalf

4
为什么您认为长度前缀字符串更好?毕竟,C之所以流行,是因为它使用了以null终止的字符串,这使其与其他语言区分开来。
Daniel C. Sobral

44
@Daniel:C之所以流行是因为C是在Von Neumann机器上可执行的程序的一种简单,高效且可移植的表示形式,并且因为它用于Unix。当然不是因为它决定使用以null终止的字符串。如果这是一个好的设计决定,那么人们会复制它,而没有复制。他们已经肯定复制几乎一切从C
比利·奥尼尔

4
如果销毁其中一个字符串,则Concat只有带长度前缀的O(m)。否则,速度相同。(历史上)C字符串最常见的用途是打印和扫描。在这两种情况下,空终止都更快,因为它节省了一个寄存器。
Daniel C. Sobral 2010年

Answers:


195

马口中

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

Dennis M Ritchie,C语言开发


12
另一个相关的报价:“ ...字符串的语义已完全由控制所有数组的更通用规则所包含,因此该语言更易于描述...”
AShelly

151

C没有字符串作为语言的一部分。C语言中的“字符串”只是指向char的指针。所以也许您问的是错误的问题。

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

鉴于下面的狂风:

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


29
char *temp = "foo bar";是C语言中的有效语句...嘿!那不是字符串吗?它不是以null结尾吗?
Yanick Rochon 2010年

56
@Yanick:这只是告诉编译器创建一个char数组的末尾的便捷方法。它不是“字符串”
Robert S Ciaccio

28
@calavera:但它可能只是简单地意味着“创建这个字符串内容的内存缓冲区,两个字节长的前缀”
比利奥尼尔

14
@Billy:好吧,既然“字符串”实际上只是一个指向char的指针,相当于一个指向字节的指针,那么您如何知道正在处理的缓冲区实际上是一个“字符串”呢?您需要除char / byte *以外的其他新类型来表示此内容。也许一个结构?
罗伯特·西亚乔

27
我认为@calavera是正确的,C没有字符串的数据类型。好的,您可以将字符数组视为一个字符串,但这并不意味着它总是一个字符串(对于字符串,我指的是具有确定含义的字符序列)。二进制文件是一个字符数组,但是这些字符对人类没有任何意义。
BlackBear 2010年

106

这个问题是作为Length Prefixed Strings (LPS)vs 问题提出zero terminated strings (SZ)的,但是大多数都暴露了以长度为前缀的字符串的好处。这看起来似乎是压倒性的,但是说实话,我们还应该考虑LPS的缺点和SZ的优点。

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

零终止字符串的优点(我知道):

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

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

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

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

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

唯一令我有些失望的是,字符串应该是utf-8,但即使使用多字节字符,长度显然仍会返回一定数量的字节(至少在我的编译器gdc上是正确的)。我不清楚这是编译器错误还是故意的。(好吧,我可能已经发现了发生的情况。要对D编译器说您的源使用utf-8,您必须在开始时放置一些愚蠢的字节序标记。我之所以写愚蠢的代码是因为我知道不是编辑器这样做的,尤其是对于UTF- 8,应该是ASCII兼容的)。


7
...继续...我认为您的几点完全是错误的,即“一切都是文件”参数。文件是顺序访问,C字符串不是。长度前缀也可以用最少的语法糖来完成。这里唯一合理的论据是试图在小型(即8位)硬件上管理32位前缀。我认为可以通过说长度的大小由实现方式来解决。毕竟是std::basic_string这样。
Billy ONeal 2010年

3
@Billy ONeal:我的回答确实有两个不同的部分。一个是关于“核心C语言”的一部分,另一个是关于标准库应该提供的内容。关于字符串支持,核心语言中只有一项:用双引号引起来的一堆字节的含义。对于C行为,我真的不比你高兴。我感到神奇的是,在每个双精度结尾处关闭封闭的字节串的零都足够糟糕。\0最后,当程序员希望使用它时,我希望它是显式的,而不是隐式的。前置长度要差得多。
克里斯,

2
@Billy ONeal:事实并非如此,使用者关心的是核心和库。最大的一点是使用C来实现OS。在该级别上没有可用的库。C也经常用在嵌入式上下文中或用于您经常受到相同限制的编程设备。在许多情况下,乔斯(Joes's)现在可能根本不使用C:“好吧,您想要在控制台上使用它吗?您有控制台吗?否?太糟糕了……”
kriss 2010年

5
@Billy“好吧,对于实现操作系统的0.01%的C程序员来说,这很好。” 其他程序员可以加息。创建C来编写操作系统。
Daniel C. Sobral

5
为什么?因为它说这是通用语言?它说出创作它的人在做什么吗?它生命的最初几年使用了什么?那么,这表示我不同意什么呢?它是编写操作系统的通用语言。它否认吗?
Daniel C. Sobral 2010年

61

我认为,这是有历史原因的,并在Wikipedia中发现了这一点

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


2
@muntoo嗯...兼容性吗?
khachik 2010年

19
@muntoo:因为那样会破坏大量现有的C和C ++代码。
Billy ONeal 2010年

10
@muntoo:范式来了又去,但是遗留代码是永远的。任何将来的C版本都必须继续支持以0结尾的字符串,否则必须重写30年以上的旧代码(这将不会发生)。只要可以使用旧的方式,那就是人们将继续使用的方式,因为这是他们所熟悉的方式。
约翰·波德

8
@muntoo:相信我,有时候我希望我可以。但是我仍然更喜欢0终止的字符串而不是Pascal字符串。
John Bode 2010年

2
谈论传统... C ++字符串现在必须以NUL终止。
Jim Balter'3

32

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

首先,让我们考虑一下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语言中创建所谓的“字符串”仅出于一个目的:在编写用于文本终端的操作系统的上下文中显示消息。而且,为此,空终止就足够了。


2
1. +1。2.显然,如果语言的默认行为是使用长度前缀完成的,那么还有其他事情可以使之更容易。例如,您在那里的所有演员表都将被呼叫strlen和朋友隐藏。至于“将其留给实现”的问题,您可以说前缀是short目标框中的a。这样,您的所有投射仍将起作用。3.我可以整天想出一些使一个系统或另一个系统看起来很糟糕的假想方案。
Billy ONeal 2010年

5
@Billy库事实是足够正确的,除了事实是C是为最少或根本没有库使用而设计的。例如,原型的使用在早期并不常见。说前缀short有效地限制了字符串的大小,这似乎是他们并不热衷的一件事。我自己使用8位BASIC和Pascal字符串,固定大小的COBOL字符串以及类似的东西,很快成为了无限大小的C字符串的忠实拥护者。如今,32位大小可以处理任何实际的字符串,但是尽早添加这些字节是有问题的。
Daniel C. Sobral 2010年

1
@Billy:首先,谢谢Daniel……您似乎了解我的意思。其次,比利,我认为您仍然没有抓住这里要提出的要点。我不是在争论用字符串数据类型的长度作为前缀的利弊。我想说,什么丹尼尔非常清楚地强调,是有在执行C作出无法处理的参数决定在所有。就基本语言而言,字符串不存在。决定如何处理字符串的决定权留给了程序员……空终止变得很流行。
罗伯特·西亚乔

1
我+1了。我还要补充一件事;正如您所建议的那样,结构错过了向真实string类型迈出的重要一步:它不了解字符。它是“字符”的数组(机器术语中的“字符”既是字符,又是“单词”,人类在句子中将其称为单词)。字符串是一个高级概念,如果您引入了编码概念,则可以在数组的顶部实现char
Frerich Raabe

2
@ DanielC.Sobral:另外,您提到的结构不需要两个分配。可以像在堆栈上那样使用它(因此只buf需要分配),也可以使用struct string {int len; char buf[]};和分配具有分配的整个对象作为灵活的数组成员,然后将其作为传递string*。(或者说是struct string {int capacity; int len; char buf[]};出于明显的性能原因)
Mooing Duck

20

显然,为了提高性能和安全性,您将需要在使用字符串时保持字符串的长度,而不是重复执行字符串strlen或等效操作。但是,将长度存储在字符串内容之前的固定位置是非常糟糕的设计。正如约尔根(Jörgen)在对Sanjit答案的评论中所指出的那样,它排除了将字符串的尾部视为字符串的可能性,例如,这使得很多常见的操作(如不分配新内存,就可能path_to_filenamefilename_to_extension不可能)(并导致失败和错误处理) 。然后,当然存在一个问题,没人能同意字符串长度字段应占用多少字节(大量错误的“ Pascal字符串”

C让程序员选择是否/在何处/如何存储长度的设计更加灵活和强大。但是,当然,程序员必须很聪明。C会通过崩溃,崩溃或停顿敌人的程序来惩罚愚蠢的人。


+1。不过,最好有一个标准的位置来存储长度,以便我们这些想要长度前缀之类的人不必到处都写很多“胶水代码”。
Billy ONeal 2010年

2
相对于字符串数据而言,没有可能的标准位置,但是您当然可以使用单独的局部变量(重新计算而不是在后者不方便且前者不太浪费时传递它)或带有指针的结构到字符串(甚至更好的是,一个标志,指示结构是“拥有”指针用于分配目的还是对其他位置拥有的字符串的引用。当然,您可以在结构中包括一个灵活的数组成员,以便灵活分配的字符串时,它适合你的结构
R.,GitHub上停止帮助ICE

13

考虑到任何语言的汇编语言,尤其是C,它比汇编语言高出一步(因此,它继承了许多汇编遗留代码),从而使惰性,寄存器的节俭性和可移植性得以考虑。您会同意,因为空字符在那些ASCII时代是没用的(它可能与EOF控件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 char(0xFF),因为该长度存储在1个字节(8位)中),并且您想要一个更长的字符串(16位->任何东西),则必须在代码的一层中考虑架构,这在大多数情况下意味着如果您想要更长的字符串,则字符串API不兼容。

例:

一个文件在8位计算机上用您的前置字符串api编写,然后必须在32位计算机上读取,这个懒惰的程序会认为您的4bytes是字符串的长度,然后分配那么多内存然后尝试读取那么多字节。另一种情况是将PPC 32字节字符串read(little endian)读到x86(big endian)上,当然,如果您不知道一个是由另一个写的,那会很麻烦。1字节长度(0x00000001)将变为16777216(0x0100000),即16 MB,可读取1个字节的字符串。当然,您会说人们应该在一个标准上达成共识,但是即使16位unicode的字节序也很少而又很大。

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


2
@deemoowoor:Concat:O(m+n)带有nullterm字符串,O(n)在其他地方都很典型。O(n)带nullterm字符串的长度,O(1)在其他任何地方。连接:O(n^2)使用nullterm字符串,O(n)在其他任何地方。在某些情况下,以null终止的字符串更有效(即,只需在指针的情况下加一个),但是concat和length是迄今为止最常见的操作(格式化,文件输出,控制台显示等至少需要长度) 。如果您缓存长度以摊销,则O(n)您只是指出长度应与字符串一起存储。
Billy ONeal 2010年

1
我同意在今天的代码中,这种类型的字符串效率低下并且容易出错,但是例如,控制台显示并不需要真正知道字符串的长度就可以有效地显示它,文件输出实际上并不需要了解字符串长度(仅在旅途中分配群集),此时,在大多数情况下,字符串格式化是在固定的字符串长度上完成的。无论如何,如果您在C中进行concat操作具有O(n ^ 2)复杂度,那么您肯定会编写错误的代码,我很确定我可以以O(n)复杂度编写一个
dvhh 2010年

1
@dvhh:我没有说n ^ 2-我说的是m + n-它仍然是线性的,但是您需要搜索到原始字符串的末尾才能进行连接,而使用长度前缀则不能搜索是必须的。(这实际上只是长度需要线性时间的另一个结果)
Billy ONeal 2010年

1
@Billy ONeal:出于好奇,我在当前的C项目(大约50000行代码)中做了一个grep用于字符串操作函数调用。strlen 101,strcpy和变体(strncpy,strlcpy):85(我也有数百个用于消息的文字字符串,隐含副本),strcmp:56,strcat:13(和6是零长度字符串的串联,以调用strncat) 。我同意前缀的长度将加快对strlen的调用,但不会加快对strcpy或strcmp的调用(如果strcmp API不使用公共前缀)。关于以上注释,最有趣的是strcat非常罕见。
克里斯,2010年

1
@supercat:不是,请看一些实现。短字符串使用基于短堆栈的缓冲区(无堆分配),只有当它们变大时才使用堆。但是,请随意提供您的想法作为库的实际实现。通常,只有当我们了解细节时才会出现问题,而不是在总体设计中。
克里斯(Kriss)

9

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

这是比汇编语言更高的一步,它为您提供了与更易于编写和维护的语言几乎相同的性能。

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

往回看,似乎并不方便。但是我早在80年代就使用汇编语言,这在当时似乎非常方便。我只是认为软件在不断发展,平台和工具也越来越复杂。


我没有看到关于null终止字符串的原始内容。Pascal早于C,并且使用长度前缀。当然,每个字符串的长度限制为256个字符,但是仅使用16位字段就可以在绝大多数情况下解决该问题。
Billy ONeal 2010年

限制字符数的事实恰恰是您在执行此类操作时需要考虑的问题类型。是的,您可以延长它的长度,但那时字节很重要。而且16位字段对于所有情况都足够长吗?来吧,您必须承认null终止在概念上是原始的。
乔纳森·伍德

10
您可以限制字符串的长度,也可以限制内容(没有空字符),或者接受4到8个字节计数的额外开销。没有免费的午餐。在开始时,以空终止的字符串很有意义。在汇编中,有时我会使用字符的最高位来标记字符串的结尾,甚至节省一个字节!
马克·兰瑟姆

是的,马克:没有免费的午餐。这始终是一个折衷方案。如今,我们无需做出任何妥协。但是在那时,这种方法似乎和其他方法一样好。
乔纳森·伍德

8

假设C暂时以Pascal方式为字符串加上长度前缀:7个字符长的字符串与3个字符的字符串是否具有相同的数据类型?如果答案是肯定的,那么当我将前者分配给后者时,编译器应生成哪种代码?字符串应该被截断还是自动调整大小?如果调整大小,该操作是否应受锁保护以使其线程安全?C方法解决了所有这些问题,无论您是否喜欢:)


2
嗯..不,不是。C方法根本不允许将7个字符长的字符串分配给3个字符长的字符串。
Billy ONeal 2010年

@Billy ONeal:为什么不呢?据我了解,在这种情况下,所有字符串都是相同的数据类型(char *),因此长度无关紧要。与Pascal不同。但这是Pascal的局限性,而不是长度前缀的字符串有问题。
奥利弗·梅森

4
@比利:我想你只是重申了克里斯蒂安的观点。C根本不处理这些问题。您仍在考虑C实际上包含字符串的概念。它只是一个指针,因此您可以将其分配给所需的任何对象。
罗伯特·西亚乔

2
就像**矩阵:“没有字符串”。
罗伯特·西亚乔

1
@calavera:我不知道这怎么证明。您可以使用长度前缀以相同的方式解决它...即完全不允许分配。
Billy ONeal 2010年

8

我以某种方式理解这个问题,意味着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"像我的示例所示那样简单。


问题在于库不知道您的结构的存在,并且仍会错误地处理诸如嵌入null之类的事情。另外,这并不能真正回答我提出的问题。
Billy ONeal 2010年

1
确实如此。因此,更大的问题是,没有比普通的旧的零终止字符串更好的标准方法来为接口提供字符串参数了。我仍要声明,有些库支持以指针长度对的形式进行馈送(嗯,至少您可以使用它们来构造C ++ std :: string)。
Pyry Jahkola 2010年

2
即使存储了长度,也绝不允许包含嵌入式null的字符串。这是基本常识。如果数据中可能包含空值,则永远不要将其与需要字符串的函数一起使用。
R .. GitHub停止帮助ICE,2010年

1
@supercat:从安全的角度来看,我欢迎这种冗余。否则,无知的(或睡眠不足的)程序员最终会连接二进制数据和字符串,并将它们传递到需要[以null结尾的]字符串的事物中……
R .. GitHub停止帮助ICE

1
@R ..:虽然期望以null终止的字符串的方法通常期望a char*,但是许多不期望以null终止的方法也期望char*。分离类型的更大好处是与Unicode行为有关。字符串实现可能需要维护标志,以了解是否已知字符串包含某些种类的字符,或者已知不包含这些字符的标志[例如,在不包含任何百万字符的字符串中找到第999,990个代码点基本多语言平面之外的任何字符都将更快数量级……
超级猫

6

“即使在32位计算机上,如果允许字符串成为可用内存的大小,则带前缀的长度字符串也只比以空终止的字符串宽3个字节。”

首先,额外的3个字节对于短字符串可能是相当大的开销。特别是,零长度的字符串现在占用的内存是原来的4倍。我们中有些人使用的是64位计算机,因此我们要么需要8个字节来存储长度为零的字符串,要么字符串格式不能满足平台支持的最长字符串。

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


我相信我已经解决了所有这些问题。是的,在x64平台上,32位前缀不能容纳所有可能的字符串。另一方面,您永远不希望这样的字符串大到以null结尾的字符串,因为要做任何事情,您都必须检查所有40亿字节,以查找几乎所有您想对其执行的操作的结尾。而且,我并不是说以null结尾的字符串总是邪恶的-如果您正在构建这些块结构之一,并且特定的应用程序通过这种结构加速了,那就去吧。我只是希望该语言的默认行为不会那样做。
Billy ONeal 2012年

2
我引用了您问题的那一部分,因为我认为它低估了效率问题。将内存需求加倍或翻倍(分别在16位和32位上)可能会增加性能成本。长字符串可能很慢,但是至少它们受支持并且仍然可以使用。关于对齐的另一点,您完全没有提及。
布兰登2012年

可以通过指定超出UCHAR_MAX的值表现为使用字节访问和移位来进行打包和解压缩来处理对齐。适当设计的字符串类型可以提供与零终止字符串基本相当的存储效率,同时还允许对缓冲区进行边界检查,而没有额外的内存开销(在前缀中使用一位表示缓冲区是否已“满”;如果可以)不是,并且最后一个字节不为零,则该字节将表示剩余空间。如果缓冲区未满且最后一个字节为零,则最后256个字节将不被使用,所以...
supercat

...一个可以在该空间中存储未使用字节的准确数量,而额外的存储成本为零)。使用前缀的成本将因使用fgets()之类的方法而无需传递字符串长度而抵消(因为缓冲区会知道它们的大小)。
2015年

4

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


5
??哪些“快速指针操作”不适用于长度前缀?更重要的是,使用长度前缀的其他语言比C wrt字符串处理要快。
Billy ONeal 2010年

12
@billy:使用带有长度前缀的字符串,您不能只获取一个字符串指针并将其添加4,并期望它仍然是有效的字符串,因为它没有长度前缀(无论如何都无效)。
约尔根·西格瓦德森(JörgenSigvardsson)2010年

3
@j_random_hacker:对于asciiz字符串(O(m + n)而不是可能的O(n)),连接的情况要差得多,并且concat比此处列出的任何其他操作都更常见。
Billy ONeal 2010年

3
有变成与null结尾的字符串更加昂贵的tiiny小操作:strlen。我会说这是一个缺点。
jalf

10
@Billy ONeal:其他所有人也都支持正则表达式。所以呢 ?使用其用途的库。C代表最大效率和极简主义,不包括电池。C工具还允许您非常轻松地使用结构来实现Length Prefixed字符串。而且没有什么可以阻止您通过管理自己的长度和char缓冲区来实现字符串处理程序的。当我想要效率并使用C时,通常这就是我要做的,而不是调用少数几个在char缓冲区末尾期望为零的函数,这不是问题。
kriss

4

尚未提到的一点是:在设计C时,有很多机器的'char'不是八位(即使在今天,也有DSP平台没有)。如果确定字符串是长度前缀的,那么一个人应该使用多少个'char's长度前缀?对于具有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%的开销。此外,打包和解压缩四字节长度前缀所花费的时间可能会超过扫描四字节字符串以寻找零字节的开销。
超级猫

1
是的。你是对的。前缀可以很容易地不是char。可以解决目标平台上的对齐要求的任何事情都可以。不过我不会去那里了-我已经把这个争论死了。
Billy ONeal,2012年

假设字符串以长度为前缀,那么最明智的做法可能是添加一个size_t前缀(该死的内存浪费,这是最明智的-允许任何长度的字符串都可能适合内存)。事实上,这是一种什么d做; 数组是struct { size_t length; T* ptr; },字符串只是的数组immutable(char)
2015年

@TimČas:除非要求字符串按字对齐,否则在许多平台上使用短字符串的成本将由打包和解包长度的要求所决定;我真的不认为这是可行的。如果想让字符串成为内容无关的任意大小的字节数组,我认为最好将长度与指向字符数据的指针分开,并使用一种语言允许为文字字符串获取两条信息。
supercat

2

关于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-为了以某些速度降低速度以及在较长的字符串上花费一点额外的开销,可以使用与字符串一起使用的方法直接接受指向字符串的指针,边界检查的字符串缓冲区或标识另一个字符串的子字符串的数据结构。像“ 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] };仍然可以作为单个参数传递的表单或类似结构的结构。
Billy ONeal,2015年

@BillyONeal:该方法存在两个问题:(1)仅允许整体传递字符串,而本方法还允许传递字符串的尾部;(2)与小琴弦一起使用会浪费大量空间。如果K&R想花一些时间在字符串上,它们本来可以使事情变得更加健壮,但是我不认为他们打算十年后使用新语言,而不会使用四十多种。
超级猫

1
关于调用约定的这一点只是个简单的故事,与现实无关……这不是设计中的考虑因素。基于寄存器的调用约定已经被“发明”了。同样,诸如两个指针之类的方法也不是一种选择,因为结构不是一流的…… 只有原语才是可分配的或可传递的。直到UNIX V7才进行结构复制。只需要memcpy(它也不存在)来复制字符串指针是一个笑话。如果您要假装语言设计,请尝试编写一个完整的程序,而不仅仅是隔离的功能。
Jim Balter 2015年

1
“这很可能是因为他们不想花很多精力在字符串处理上” –废话;早期UNIX的整个应用程序域都是字符串处理。如果不是那件事,我们将永远不会听说。
Jim Balter

1
“我不认为“ char缓冲区以包含长度的int开头”更具魔力”,这是如果您要str[n]引用正确的char。这些是讨论此问题的人们所没有想到的事情。
Jim Balter 2015年

2

根据Joel Spolsky在此博客文章中所说,

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

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


2
看,我尊重乔尔的很多事情;但这是他在猜测的东西。Hans Passant的答案直接来自C的发明者。
Billy ONeal

1
是的,但是如果Spolsky所说的话是真的,那将是他们所指的“便利”的一部分。这就是为什么我包含此答案的部分原因。
2016年

AFAIK .ASCIZ只是一个汇编语句,用于构建字节序列,后跟0。这只是意味着零终止字符串在当时是一个公认的概念。它并不意味着零个终止字符串是一些相关的PDP-*的结构,不同之处在于你可以写由紧密循环MOVB(复制字节)和BNE(分支如果复制的最后一个字节不为0)。
阿德里安W

它似乎表明C是古老的,松弛的,残缺的语言。
purec

2

不一定是基本原理而是对长度编码的对策

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

  2. 带有前缀的字符串可以节省时间,这不是一件真事。无论是需要提供提供的数据来提供长度,还是在编译时进行计数,还是真正为您提供必须编码为字符串的动态数据。这些大小是在算法中的某个点计算的。一个单独的变量来存储一个空终止字符串的大小可以被提供。这使得节省时间的比较变得毫无意义。一个在结尾处只是有一个额外的NUL ...但是,如果长度编码不包括该NUL,则两者之间实际上没有区别。根本不需要算法更改。只是预传递,您必须手动设计自己,而不需要编译器/运行时为您完成。C主要是关于手动做事。

  3. 长度前缀是可选的是卖点。我并不总是需要算法的额外信息,因此需要为每个字符串执行此操作,因此我的precompute + compute时间永远无法降至O(n)以下。(即硬件随机数生成器1-128。我可以从“无限字符串”中提取。假设它生成的字符是如此之快。因此我们的字符串长度一直在变化。但是我对数据的使用可能并不在乎我有很多随机字节,它只想要一个下一个可用的未使用字节,它可以在请求后尽快获得它。不必要的计算浪费。空检查更有效。)

  4. 长度前缀可以防止缓冲区溢出吗?合理使用库函数和实现也是如此。如果我输入格式错误的数据怎么办?我的缓冲区长2个字节,但我告诉函数是7个字节!例如:如果打算将gets()用于已知数据,则可以进行内部缓冲区检查,以测试编译后的缓冲区和malloc()通话,并且仍然遵循规范。如果将其用作未知STDIN到达未知缓冲区的管道,那么很明显,除了缓冲大小之外,其他人都无法知道,这意味着长度arg是没有意义的,在这里您还需要诸如金丝雀检查之类的东西。因此,您不能为某些流和输入添加长度前缀,只是不能。这意味着长度检查必须内置在算法中,而不是打字系统不可思议的部分。TL; DR NUL终止永远不必是不安全的,它只是通过滥用而结束了。

  5. 反点: NUL终止令人讨厌。您要么需要在此处进行长度前缀,要么以某种方式转换NUL字节:转义码,范围重新映射等...这当然意味着更多的内存使用/减少的信息/每个字节更多的操作。长度前缀在这里主要是赢得战争。转换的唯一好处是不必编写其他函数即可覆盖长度前缀字符串。这意味着在更优化的sub-O(n)例程上,您可以让它们自动充当其O(n)等效项,而无需添加更多代码。当在NUL重弦上使用时,缺点当然是时间/内存/压缩浪费。根据您最终要复制多少库来对二进制数据进行操作,仅使用长度前缀字符串可能有意义。也就是说,长度前缀字符串也可以做同样的事情... -1长度可能意味着NUL终止,并且您可以在长度终止内使用NUL终止的字符串。

  6. Concat:“ O(n + m)vs O(m)”我假设您将m表示为连接后字符串的总长度,因为它们都必须具有最小数量的操作(您不能只作固定操作-on到字符串1,如果必须重新分配呢?)。而且我假设n是由于预先计算而不再需要执行的神话操作。如果是这样,那么答案很简单:预先计算。如果您坚持要始终有足够的内存来不需要重新分配,这就是big-O表示法的基础,那么答案就更简单了:在已分配的内存中对字符串1的末尾进行二进制搜索,显然有很大的空间字符串1之后的无限零样本,我们不必担心重新分配。在那里,很容易将n记录到log(n)中,而我几乎没有尝试过。如果您回想起log(n),那么在实际计算机上,log(n)基本上只能是64,这就像说O(64 + m),本质上就是O(m)。(是的,该逻辑已用于当今正在使用的真实数据结构的运行时分析中。这不是胡说八道。)

  7. 再次 Concat()/ Len():记忆结果。简单。如果可能/必要,将所有计算转换为预计算。这是一个算法决策。这不是语言的强制约束。

  8. 通过NUL终止,字符串后缀的传递更加容易/可行。根据长度前缀的实现方式,它可能对原始字符串具有破坏性,有时甚至是不可能的。需要复制并传递O(n)而不是O(1)。

  9. 与长度前缀相比,NUL终止的参数传递/取消引用要少。显然是因为您传递的信息较少。如果您不需要长度,则可以节省大量空间并进行优化。

  10. 你可以作弊。它实际上只是一个指针。谁说您必须将其读取为字符串?如果要以单个字符或浮点数形式阅读该怎么办?如果您想做相反的事情并将浮点数读取为字符串怎么办?如果您小心一点,可以使用NUL端接进行操作。您不能使用长度前缀来做到这一点,它是一种与指针通常明显不同的数据类型。您很可能必须逐字节构建字符串并获取长度。当然,如果您想要一个像整个浮点数(里面可能有一个NUL)之类的东西,则无论如何都必须逐字节读取,但是细节由您决定。

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


9有点离谱/代表错误。长度前缀没有这个问题。Lenth 作为单独的变量传递。我们当时在谈论Fiix,但我被带走了。考虑仍然是一件好事,所以我将其保留。:d
黑色

1

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

Windows / COM开发人员将非常熟悉BSTR这种确切的类型-长度为C的字符串,其中实际字符数据不以字节0开始。

因此,看来使用零终止的决定只是人们喜欢的,而不是该语言的必要性。


-3

gcc接受以下代码:

char s [4] =“ abcd”;

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


@AdrianW。这是有效的C。精确长度的字符串是特殊情况,并且省略了NUL。这通常是不明智的做法,但是在诸如填充使用FourCC“字符串”的标头结构的情况下很有用。
凯文·蒂伯多

你是对的。这是有效的C,将按照kkaaii所述进行编译和运行。拒绝投票(不是我的...)的原因可能是该答案不能以任何方式回答OP的问题。
阿德里安W
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.