尽管我喜欢C和C ++,但我还是忍不住在选择空终止字符串时不知所措:
-
在C之前存在长度前缀(即Pascal)字符串
-
通过允许恒定时间长度查找,长度前缀字符串使得几种算法更快。
-
长度前缀字符串使得更容易导致缓冲区溢出错误。
-
即使在32位机器上,如果允许字符串为可用内存的大小,则长度前缀字符串仅比空终止字符串宽三个字节。在16位机器上,这是一个字节。在64位机器上,4GB是一个合理的字符串长度限制,但即使你想将它扩展到机器字的大小,64位机器通常有足够的内存使额外的七个字节排序为null参数。我知道最初的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和朋友那里获得的指针)。
以上都不像长度和连续那样常见。
在下面的答案中还有一个断言:
但这个不正确 - 它与null终止和长度前缀字符串的时间相同。 (Null终止字符串只是在你希望新结束的地方粘贴一个空值,长度前缀只是从前缀中减去。)
-
我一直以为所有C ++程序员编写自己的字符串库都是一种成文。
-
@Juliet:大声笑 - 这是真的。但这并不意味着他们应该在生产代码中实际使用他们的字符串库。我会坚持使用标准位TYVM :)
-
@Juliet:那么你开始想知道如果你需要关心它依赖的每个库的不同字符串实现,你的应用程序会是什么样子。
-
这是关于现在期待理性解释的内容。我想你会想听听下一个x86或DOS的理由吗?就我而言,最糟糕的技术获胜。每次。最糟糕的字符串表示。
-
@jalf:1。x86赢了,因为它更便宜,不是因为任何技术原因。 (但这是另一个论点)2。长度前缀赢得了无处不在但是C.不要看(哈!)这是如何赢得空终止。
-
即使是使用C构建的大型系统也经常创建自己的字符串数据结构,该结构存储字节旁边的长度,并围绕它构建一个操作库。以NT NT的UNICODE_STRING为例。
-
@Billy:因为C成了一种可笑的流行语言? ;)
-
我在2003年写过这篇文章,并支持我当时所说的话。
-
Bjarne Stroustrup遗憾地看不到未来。
-
你为什么声称长度前缀字符串优越?毕竟,C变得流行,因为它使用了以空字符结尾的字符串,这使得它与其他语言区别开来。
-
@Daniel:C开始流行,因为它是在Von Neumann机器上可执行的程序的简单,高效和可移植的表示,并且因为它用于Unix。当然不是因为它决定使用空终止字符串。如果这是一个很好的设计决定,那么人们就会复制它,而他们却没有。他们当然从C复制了其他所有东西。
-
我不明白为什么使用长度前缀而不是空终止导致"混乱的语义"。在这两种情况下,你都有一大块字节。如果你想谈论C#/ Java,它会自动执行字符串实习,那么你可能会有一个参数....
-
@Juliet。错误。每个C ++程序员的通过仪式都是他们自己的智能指针库。字符串库是课外的。
-
你需要的时候哪里是圣战标签?
-
长度前缀不是"字节块"的一部分,除非您的代码处理它(在常量使用情况下这将非常慢)。它是一个特定于机器的(大小,字节序,对齐要求等)数据对象,它使得字符串需要大量的序列化来存储在文件中,通过网络传输等等。看看你看到多少个新手发送(机器特定的)二进制数据通过SO上的电线,想象如果字符串包含二进制数据会有多糟糕...
-
@calavera:哈哈 - 如果人们真的试图攻击上面的点,圣战也不算太糟糕。这是"它必须是正确的答案,因为C做到了"答案非常烦人。无论任何给定的系统有多好,都会有一些糟糕的部分。只是希望他们能够意识到完全有可能扯掉C的一个属性,就像C本身一样。喜欢一种语言并不意味着你喜欢一切。 (同样适用于任何"它必须是答案,因为X做到了",用"C"代替X,"Linus",)
-
由于一个问题,我无法相信我今天达到了上限。 (好吧,20分来自答案,但是geez!)
-
我不敢相信我的回答是为了我的回答,考虑到被投票的次数:P
-
每个人都这样做。你不打算退出吗?或者你是一只鸡?
-
Why would null terminated strings have been chosen instead of the obviously superior length prefixing?老实说,我不知道前缀字符串的长度是多少obviously superior。两种选择都存在明显的缺点和优点,因此像superior这样的词是没有意义的。
-
@trithithis:在我看来,这是一个乞求问题的例子。
-
@Billy嗯,"它一定是错的,因为其他人没有这样做"的问题也很烦人。你已经提供了没有更好的事实,事实上,使用空终止字符串有很多东西更容易。而且你没有提供证据证明C没有胜出,因为它使用了空终止字符串。这就是问题所在:这个问题是纯粹的火焰和猜测,我从来没有在Stack Overflow上看到一个不值得关闭的问题。
-
@Daniel:没有事实?我想我在问题中列出了很多内容。
-
@Daniel:我已经编辑了一下这个问题。更好?
-
@Billy没有事实。 1.年龄!=更好。反之亦然。 3.不正确,在管理自己的内存时,任何一种方式都可能有缓冲区溢出。这只是一种防御,而不是一种优势。 5.人气!=更好。 6.不相关 - 在C之前C ++不存在.7。不相关 - C可以处理空值很小的内存缓冲区,C字符串用于在屏幕上显示内容,而null不是图形字符。因此,没有事实表明大小前缀字符串优于空终止字符串。
-
@Daniel:1。我从不打算说年龄意味着更好 - 更多的意思是说长度前缀不会过时C,因此可以在设计时考虑。我相信我通过编辑更好地证实了这一点。但是C的标准库不能。任何C库也不能期望普通的"C字符串"。因此,如果您正在阅读一些应该包含字符串的磁盘格式,并且有人在那里放置了一个空值,那么您的程序就会受到影响。这种"只适用于其他语言"没有任何困难。
-
如果你销毁其中一个字符串,Concat只有O(m),带有长度前缀。否则,速度相同。最常见的用途C(历史上)是打印和扫描。在这两种情况下,空终止更快,因为它节省了一个寄存器。
-
@Daniel:strcat摧毁其中一个字符串。
-
@Billy对不起,但是我的标准C库有一堆以"mem"开头的函数,这些函数都是基于大小的。它们中的任何一个都不是新增的。
-
@Daniel:你的观点?
-
@Billy你说"C的标准库不能",但它可以。
-
@Daniel:但他们没有。
-
@Billy以什么方式不可能使用mem *函数来处理其中包含空值的东西?我当然使用过,如果你曾经使用过Unix,那么你肯定会使用已经利用它的代码。当然,您无法打印它,因为空字符不可打印。但你可以任何你想要的方式操纵它。这是手册页的标题:"bcmp(3),bcopy(3),bzero(3),memccpy(3),memchr(3),memcmp(3),memcpy(3),memmove(3),memset (3) - 字节串操作"。这些(和其他)函数都没有给出关于null的函数,那么问题是什么呢?
-
这是有史以来最愚蠢的火焰战争。我对这两种风格都没有偏好,但比利的愚蠢坚持让我走向了空终止的字符串。 (另外当确定字符串的长度是字符串最常见的操作之一时?Concat,当然。输出,长度......没有。即使考虑连续,m + n - > 2max(m,n) - > 2n - > n - >没什么区别。)
-
此外,我处理的大多数字符串都是常量。即使在考虑非常数时,你也永远不会注意到速度之间的差异。如果你这样做,那么你无论如何都使用了错误的数据类型。你甚至提到the efficiency argument doesn't sell me here,所以你对字符串算法效率的论证不会让我(即使我认为它很重要)。
-
如果我可以把自己放在任何地方,那么当K& R定义为C.几乎每个安全漏洞都是字符串或缓冲区溢出。如果只有它们包含长度前缀字符串/数组,并使用相应的语言结构来操作它们。 (然后我会拜访哥白尼;告诉他这是椭圆形,而不是伟大的贝壳。在这两种情况下,人们都可以免于数十年的痛苦。)
-
@trithithis:我必须指出,虽然我在谈论算法方面的效率差异,但在内存差异方面只有3个字节。首先是一个大问题。第二是小奶酪。至于"愚蠢的坚持",如果你能解释我是多么愚蠢而不是叫我愚蠢,也许你会被认为更认真。
-
如果您还记得Microsoft的汇编程序(MASM),它会使用$终止的字符串。所以终结者(或缺少终结者)是语言作者的任意选择。
-
@TimBray:恕我直言,Java应该包含一个string原语,它可以是一个32位的opaque类型,可以保存对Java对象的引用,但不一定要这样做。我一直在玩C ++中为RAM限制系统实现垃圾收集的字符串/数组池,其中每个字符串引用将占用池外2个字节,内部2-3个字节; 32字节以下的字符串长度有一个额外的开销字节,而较长的字符串会增加一点。存储在池中的引用数组每个字符串只需要两个字节。
-
@TimBray:我唯一一次看到零终止字符串是有利的,就是将一个字符串按值传递给一个方法,该方法在返回后不需要使用该字符串。在所有其他上下文中,需要使用长度未知的字符串的代码需要以某种方式跟踪分配的内存块的长度,如果它必须这样做,则null终止符并不真正占用很多。
-
"我在2003年写过这篇文章并支持我当时所说的话。" - 这可能是有史以来最糟糕的文章。 strncpy是"最佳实践"?好悲伤。
-
由于性能原因,提醒C避免自动执行计算。因此,您必须使用\0以与您必须相同的方式存储自己的长度。在我看来,在我自己的每个字符串的末尾维护一个\0比在开头维护一个数字要容易得多,并跟踪它后面的数据量,如果数量发生变化,我还必须更改数。
-
@Zaibis:您需要更改null终止符的位置,这在功能上是相同的转换。
-
"长度和连续是迄今为止最常见的字符串操作。" [引证需要]我发现除了字符串之外还存储长度的代码(作为前缀或描述符块)往往会使用很多长度。但是使用零终止字符串的代码通常根本不关心长度。并且concat仍然是O(n + m),因为除了特殊情况之外,首先必须将原始字符串复制到足够大的缓冲区以包含两者。我并不反对预先计算的字符串,但这个问题会产生许多偏见答案的假设。
-
"长度[ - ]前缀字符串只比空终止字符串宽三个字节"加上一些用于对齐的填充,因为您可能希望计数对齐。在堆上分配的以零结尾的字符串也将对齐,但字符串文字可以在编译和链接时打包,因此没有对齐开销。
-
"......所有其他语言......"用C语写成
-
@purec:这并不意味着他们使用NTCTS。 (反正这不是真的)
从马的嘴里
None of BCPL, B, or C supports
character data strongly in the
language; each treats strings much
like vectors of integers and
supplements general rules by a few
conventions. In both BCPL and B a
string literal denotes the address of
a static area initialized with the
characters of the string, packed into
cells. In BCPL, the first packed byte
contains the number of characters in
the string; in B, there is no count
and strings are terminated by a
special character, which B spelled
*e. This change was made partially
to avoid the limitation on the length
of a string caused by holding the
count in an 8- or 9-bit slot, and
partly because maintaining the count
seemed, in our experience, less
convenient than using a terminator.
Dennis M Ritchie,C语言的发展 sub>
-
另一个相关的引用:"...字符串的语义完全归入管理所有数组的更一般规则,因此语言更容易描述......"
C没有字符串作为语言的一部分。 C中的'string'只是指向char的指针。所以也许你问的是错误的问题。
"遗漏字符串类型的理由是什么"可能更相关。为此,我要指出C不是面向对象的语言,只有基本的值类型。字符串是更高级别的概念,必须通过某种方式组合其他类型的值来实现。 C处于较低的抽象层次。
鉴于下面肆虐的狂风:
我只想指出,我并不是说这是一个愚蠢或糟糕的问题,或者表示字符串的C方式是最佳选择。我试图澄清,如果考虑到C没有将字符串作为数据类型与字节数组区分开的机制,那么问题会更简洁。鉴于当今计算机的处理能力和内存能力,这是最佳选择吗?可能不是。但后见之明总是20/20和所有:)
-
@calavera这不是一个错误的问题。 condiser it asciiz类型,或以null结尾的char数组。
-
char *temp ="foo bar";是C中的有效声明...嘿!这不是一个字符串?是不是空终止了?
-
@Yanick:这只是告诉编译器在末尾创建一个null数组的方便的方法。它不是'字符串'
-
@calavera:但它可能只是简单地意味着"用这个字符串内容和两个字节长度的前缀创建一个内存缓冲区",
-
@calavera,"字符串"根据定义是"符号的线性序列",并不一定是数据类型。为方便起见,它是由更高语言级别的类型。在C中,这是一个字符串,在C#中它是另一回事。问题是关于C字符串,就是这样;指向线性字符序列的指针,后跟\0 char。
-
它可能不是一个字符串对象,就像人们在C ++中想到的那样,但它根据定义是一个C字符串。不要试图否认它。
-
1)C有字符串。 2)C字符串不是类型,它们被定义为char或wchar_t数组,最后只包含一个空字符。 3)你所说的没有意义。为什么"str"以null结尾但不是大小前缀?
-
@Billy:好吧,因为'string'实际上只是一个指向char的指针,它相当于指向byte的指针,你怎么知道你正在处理的缓冲区真的是一个'字符串'?你需要一个除char / byte *之外的新类型来表示这一点。也许是一个结构?
-
@calavera:你不会。但你真的不知道用C字符串。有人可以随时通过非空终止缓冲区。
-
chill y'all :)我只是指出,为了让字符串对机器语言本身有意义,必须有一些方法来确定一个类型是否实际上是一个字符串。语言作者显然选择采用最简单,最节省内存的方法之一。不可否认的是,不理想且仍然模棱两可,但只是将长度预先设置在字节数组的前面并不能解决问题,除非您知道所有字节数组都以它们的长度为前缀。
-
@Billy:见上面的评论。我承认它并不明确,但它不像简单地将长度预先设置为字节数组那样模棱两可。
-
我认为@calavera是对的,C没有字符串的数据类型。好吧,你可以考虑像字符串一样的字符数组,但这并不意味着它总是一个字符串(对于字符串我的意思是一系列具有明确含义的字符)。二进制文件是一个字符数组,但这些字符对人类来说并不意味着什么。
-
@Yanick:绝对不真实。"字符串"一词的定义随着上下文而变化。我是在数据类型的上下文中发言。
-
@tiftik:你问的是正确的问题。
-
字符串不需要是人类可读的 -"在计算机科学中,字符串是任何有限的字符序列(即字母,数字,符号和标点符号)。"
-
@Billy:即使我们将两种方法(前置长度和附加null)结合起来,仍然必须遍历生成的char / byte数组,以确定字节是否实际表示字符串。因此,这种方法最初效率较低,但一旦我们有点验证我们正在处理字符串,那么效率会更高。使用现有类型处理字符串的唯一明确方法是创建一个表示字符串的结构,不允许像char* myStr ="Hello World";这样的语句,但只能strStruct* str ="hello world";
-
@jweyrich:你是对的,我的意思是,没有人会读取二进制文件并将其内容放在字符串变量中,对吧?好吧,我不会这样做,但我不知道c ++。
-
@calvera:你为什么需要横切字符串?我见过的大多数C都不会检查它得到的缓冲区是否为空终止;如果它不是null终止,则会导致崩溃。 (因为一般来说,不可能检测到这种故障)
-
@Billy:是的,这正是为什么没有将字符串类型集成到语言中的原因。以及为什么像strcpy_s之类的函数已经取代了旧版本。
-
@Mark Ransom:你错了,不要告诉我该怎么做。如果您对定义非常肯定,请尝试以下操作:int* str ="this is just bytes, i have no idea what a string is";右侧的部分是字符串文字。 C不知道字符串类型是什么,它只知道如何将字符串文字分配给指针。
-
@Yanick Rochon:char a[4] ="toto";也是一个有效的C语句,但在这种情况下,"toto"可能是一个字符串但不是零终止(C和C ++之间大部分被忽略的小差异之一)。
-
@calavera:对不起,打算早点告诉你+1。 @BlackBear:我不明白为什么长度前缀会阻止在字符串变量中存储二进制数据。我也没有看到为什么空终止也是如此。人们一直使用char *指向普通字节,这是完全合理的。
-
仅仅因为C没有字符串类型并不意味着它没有字符串。它有一个明确定义的约定,可以追溯到语言的开头,通过字符串文字支持语言。任何以其他方式提出要求的企图都只是过分迂腐。
-
@Mark:你迂腐。
-
@Billy:谢谢,我感谢我们可以讨论这个并且不同意而不诉诸于告诉对方该做什么或做什么。 :)
-
@Billy:我不想这么说。我只是说我同意@Calavera:"好吧,因为'string'实际上只是一个指向char的指针,它相当于一个指向byte的指针,你怎么知道你正在处理的缓冲区真正意图是一个'字符串'?"
-
@BlackBear:你没有。至少,不是在C.
-
来自我的+1。意识到char的数组与字符串不相同(例如,因为没有编码的概念)是一个关键的洞察力。
-
@BillyONeal:关于你的char *声明;人们可能会这样做,但这不是那么糟糕的做法吗?默认情况下,它假定为unsigned char。如果他们真的在处理signed char的系统上处理二进制数据而不是ASCII字符串,则会出现问题。
-
@mrduclaw:不,它真的不会假设有关签名或未签名的字符。如果您实际上从未通过char类型实际访问数据,那么缓冲区的实际类型并不重要。 (处理从文件或其他地方加载的不透明数据时,这是一种常见模式)
-
'但它可以简单地意味着"用这个字符串内容创建一个内存缓冲区和两个字节长度的前缀"' - 不,它不能有,因为这会将temp的第n个字符放在temp [n + 2] ],这是一种在编程语言中存在的恶劣内容。使用以NULL结尾的字符串的另一个明显原因是,您可以指向字符串...这就是C语言中的字符串处理总是在具有高效索引操作的机器和编译器出现之前完成的。
-
"字符串"由C标准明确定义为(基本上)以空值终止的字符序列。
-
有许多其他非对象的oriendted语言,仍然有字符串支持
-
@JimBalter:如果有一个指向第一个字符的指针,并指定如果前面的字符不大于UCHAR_MAX / 2,则表示长度;否则,如果之前的字符不大于UCHAR_MAX / 2,则长度为p [-2] *(CHAR_MAX / 2 + 1)+ p [-1]等,最多可达到前面所需的字节数?
-
@supercat你的疯子想法有很多问题,例如无法附加到字符串......即使将这样的东西复制到现有的缓冲区也是不可能的。并且由于你的cockamamie方案需要至少2个字节的长度,它将不如在原始C机器上简单地使用固定的16位前面的长度。当然,你最糟糕的想法仍然是不允许指针进入遍历它的字符串 - 你要回复的评论的第二点。
-
@JimBalter:对于最多127个字节的字符串,需要一个字节长度,对于最多为16383个字节的字符串需要两个字节,对于最多为2,097,151个字节的字符串需要三个字符串等。当分配一定大小的字符串缓冲区时,请保留适当数量的字符串空间的长度。如果一个方法被告知缓冲区有32768字节的空间,它将有权假设指针前面的三个字节可用。
-
@JimBalter:轻微纠正:指定更改字符串长度的所有代码必须以与旧格式相同的格式写入新长度。要分配32768字节的字符串,char *s=malloc(32768+3)+3; s[-1]=0x80; s[-2]=0x80; s[-3]=0x00. Given a pointer to a string, one could find the allocation base via char p = s; do {--p;} while(* p& 0x80);`。实际上,曾经可以通过要求所有*可写字符串前面有两个可变长度的数字来增强这个想法 - 较近的一个是当前长度而另一个是分配的长度。以这种方式...
-
...可以有效地防止缓冲区溢出,而无需代码手动跟踪缓冲区长度。
-
@supercat"它需要一个字节的长度" - 抱歉,我误读了你的设计。但我不会再花时间在这个愚蠢而毫无意义的想法上。如果你想设计一个使用它的新老式PL,那就去吧。
-
"C中的'字符串'只是指向char的指针......"它不是(指针);它是0终止的char数组。
-
@alk:C中的数组只是一个指针!据我所知,array[3]实际上是在幕后做*(array + 3)。 (当然,忽略像ASLR之类的东西。)我实际上看到人们使用指针操作迭代字符串。
该问题被称为Length Prefixed Strings (LPS) vs zero terminated strings (SZ),但主要是暴露长度前缀字符串的好处。 这可能看起来势不可挡,但说实话,我们也应该考虑LPS的缺点和SZ的优势。
据我所知,这个问题甚至可以被理解为一种偏见的方式来问"零终止字符串的优点是什么?"。
Zero Terminated Strings的优点(我看到):
非常简单,无需在语言中引入新概念,char
数组/字符指针可以做到。
核心语言只包含最少的语法糖转换
双引号之间的东西
一堆字符(真的是一堆
字节)。在某些情况下,它可以使用
完全初始化事物
与文字无关。例如xpm
图像文件格式是有效的C源
包含编码为的图像数据
串。
顺便说一句,你可以在字符串文字中加零,编译器会
还要在文字的末尾添加另一个:"this\0is\0valid\0C"。
它是一个字符串?还是四串?或者一堆字节......
平面实现,没有隐藏的间接,没有隐藏的整数。
没有隐藏的内存分配(好吧,一些臭名昭着的非
标准函数如strdup
执行分配,但这主要是
问题的根源)。
对于小型或大型硬件没有具体问题(想象一下
在8上管理32位前缀长度
位微控制器,或
限制字符串大小的限制
小于256字节,这是我实际上与Turbo Pascal之前的问题)。
字符串操作的实现只是少数几个
非常简单的库函数
主要使用字符串的效率:常量文本读取
从已知的开始顺序
(主要是给用户的消息)。
终止零甚至不是强制性的,所有必要的工具
操纵像一堆的字符
字节可用。表演时
在C中进行数组初始化,你可以
甚至避免使用NUL终结器。只是
设置合适的尺寸。 char a[3] =
"foo";是有效的C(不是C ++)和
不会把最后的零放在一个。
与unix观点一致的"一切都是文件",包括
没有固有长度的"文件"
像stdin,stdout。您应该记住实现了开放的读写原语
处于非常低的水平。它们不是库调用,而是系统调用。并使用相同的API
用于二进制或文本文件。文件读取原语获取缓冲区地址和大小并返回
新的尺寸。您可以使用字符串作为缓冲区来编写。使用另一种字符串
表示意味着你不能轻易地使用文字字符串作为输出的缓冲区,或
将它投射到char*时,你必须使它有一个非常奇怪的行为。亦即
不返回字符串的地址,而是返回实际数据。
非常容易操作从文件中就地读取的文本数据,而无需无用的缓冲区副本,
只需在正确的位置插入零(嗯,不是真正的现代C,因为双引号字符串现在通常保存在不可修改的数据段中的const char数组)。
预先设置一些任何大小的int值都意味着对齐问题。最初的
长度应该对齐,但没有理由为字符数据(和
再一次,强制对齐字符串会在将它们视为一堆时意味着问题
字节)。
在编译时已知长度为常量文字字符串(sizeof)。那么为什么呢
有人想将它存储在内存中,并将其添加到实际数据中吗?
在某种程度上C正在(几乎)所有其他人,字符串被视为char数组。由于数组长度不由C管理,因此对于字符串不管理逻辑长度。唯一令人惊讶的是,最后添加了0项,但在双引号之间键入字符串时,这只是核心语言级别。用户可以完美地调用字符串操作函数传递长度,甚至可以使用普通的memcopy。 SZ只是一个设施。在大多数其他语言中,管理数组长度,对于字符串来说,逻辑是相同的。
在现代,无论如何1字节字符集是不够的,你经常需要处理编码的unicode字符串,其中字符的数量与字节数非常不同。这意味着用户可能想要的不仅仅是"大小",还有其他信息。对于这些其他有用的信息,保持长度不使用任何东西(特别是没有自然存储它们的地方)。
也就是说,在标准C字符串确实效率低下的罕见情况下,无需抱怨。 Libs可用。如果我跟着这个趋势,我应该抱怨,标准C不包括任何正则表达式的支持功能......但真的每个人都知道,因为可用于这一目的的库它不是一个真正的问题。因此,当需要字符串操作效率时,为什么不使用像bstring这样的库?甚至是C ++字符串?
编辑:我最近看了D弦。有趣的是,选择的解决方案既不是大小前缀,也不是零终止。如C,包括在双引号的文字串是不可变的字符数组只是短针,和语言也有一个字符串的关键字意味着(不可变的字符数组)。
但是D阵列比C阵列更丰富。在静态数组的情况下,长度在运行时是已知的,因此不需要存储长度。编译器在编译时有它。对于动态数组,长度可用,但D文档没有说明它的保存位置。据我们所知,编译器可以选择将其保存在某个寄存器中,或者存储在远离字符数据的某些变量中。
在普通的char数组或非文字字符串上没有最终的零,因此如果他想从D调用一些C函数,程序员必须自己设置。在文字字符串的特殊情况下,但D编译器仍然在每个字符串的结束(以方便投给C字符串让用户轻松调用C函数?),但这种零不是字符串的一部分(d不会字符串大小算的话)。
唯一令我失望的是字符串应该是utf-8,但是长度显然仍然会返回一些字节(至少在我的编译器gdc上是这样),即使使用多字节字符也是如此。我不清楚它是编译器错误还是目的。 (好吧,我可能已经知道发生了什么。要对D编译器说你的源码使用utf-8你必须在开头放一些愚蠢的字节顺序标记。我写傻了因为我知道不是编辑那样做,特别是对于UTF- 8应该是ASCII兼容的)。
好。
-
@kriss:非常好的答案。我感谢其他人承认原始问题有一些编辑,并不是它看起来的样子。
-
@kriss:我的问题是"为什么选择空终止字符串"。我知道有更好的方法来处理使用库的事情。但是,每当你转向图书馆解决方案来解决这样的问题时,你所获得的大部分内容都会因使用代码将库粘合到现有代码而丢失。鉴于标准使用空终止字符串,这就是你所坚持的。 (有时我仍然需要编写这种胶水,因为现有的代码不支持i18n GRRR)。另外,我认为你的几个要点同样适用于长度前缀(即库函数)。
-
......续......我认为你的一些观点是完全错误的,即"一切都是文件"的论点。文件是顺序访问,C字符串不是。长度前缀也可以用最少的语法糖来完成。这里唯一合理的论点是试图在小(即8位)硬件上管理32位前缀;我认为可以简单地通过说长度的大小由实现来毕竟,这就是std::basic_string的作用。
-
@Billy ONeal:我的答案中确实有两个不同的部分。一个是关于"核心C语言"的一部分,另一个是关于标准库应该提供什么。关于字符串支持,核心语言中只有一个项目:双引号括起来的字节串的含义。我对C行为并不比你更开心。我觉得奇怪的是,在每个双关闭的结尾处添加零是封闭的字节串已经足够糟糕了。当程序员想要而不是隐式的时候,我更喜欢和显式的\0。前置长度要差得多。
-
@kriss:该语言的用户不关心核心语言定义的内容与标准库定义的内容。 (一般来说)所有joe C程序员关心的是"我在这里有字符串,我希望它在控制台上"......并且那些接受空终止字符串的函数我断言空终止字符串是一个设计错误。我通过指出C(和C ++)是唯一(流行)使用它们的地方来支持它。我不明白为什么在"" s内的字符数据上添加长度前缀比null更具侵入性。
-
@Billy ONeal:这不是真的,用途关心什么是核心,什么是库。最重要的一点是当C用于实现OS时。在那个级别没有可用的库。 C也常用于嵌入式上下文或编程设备,在这些设备中,您通常具有相同的限制。在许多情况下,乔斯现在可能根本不应该使用C:"好吧,你想要它在控制台上吗?你有控制台吗?没有?太糟糕了......"
-
@kriss:嗯,对于.01%的C程序员实现操作系统,很好。我会坚持其他99.9%。也就是说,因为没有标准库的C不是C.当我在谈论C时,我说的是标准C,而不是用于引导OS的一些限制版本。
-
@Billy ONeal:您还应该考虑C和C ++是用于实现OS内核的唯一语言。你只需要有一个意思来初始化一堆字节。 c字符串很简单。如果你将双引号的含义更改为一些前置字符串(不是库中唯一的部分)...你必须找到另一个平均值,因为初始化常量字节文字很容易。顺便说一句,您可以使用常量字符串文字的sizeof。它在编译时就知道了,为什么要在任何地方使用它?
-
@kriss:我不是建议改变C.我问为什么C做出了最初做出的决定。那里有区别。仅仅因为使用有限形式的C来实现操作系统,并不意味着99%的语言用户正在这样做。 C是通用编程语言,并且通用意味着它不会做出妥协以使特定任务(即OS)更容易。
-
@Billy ONeal:C选择允许轻松实现前置长度行为(嘿,其他语言的库主要使用C编写)。另一种方式是不可能的。如果语言不包含任何定义一堆字节的方法,那么你就注定要失败,有些事情是无法完成的。
-
@kriss:确实包含了char myBunchOfBytes[] = {'a', 'b', 'c'};
-
@Billy ONeal:C的另一个非常相似的方面是数组的长度。我真的相信这里的设计选择是一样的。大多数lnaguage存储数组长度somwhere。 C做出了另一个选择。这是设计错误吗?
-
@Billy ONeal:你在开玩笑吧?长度很重要!
-
@kriss:你说"如果语言没有提供任何方式" - 我说有办法。而且有。 ""语法未设计,也不打算用作将字节插入程序的随机方法。它旨在用于人类可读的字符串,如果您获得原始(或ANSI版本)K& R C书籍的副本,您可以清楚地看到它,这是它曾经使用过的唯一内容。
-
@Billy ONeal:f = open({'m', 'y', ' ', 'p', 'a', 't', 'h'}, flags);
-
@kriss:并且没有理由open不接受长度前缀字符串,在这种情况下,您只需使用已经使用的普通""语法。而且,open不是C函数,它是POSIX系统调用。
-
@Billy ONeal:现在我们应该在内核级强制使用size-prepending字符串?因为那是开放的。据我所知,内核不会调用很多C字符串操作库...
-
@kriss:就像你现在强制执行null终止一样。你没有。
-
@Billy ONeal:存在使用大小的系统调用,并且选择的约定(以及历史上它是C约定)是将字符串数据和长度作为单独的参数传递,因为它更多。大小前置的字符串只是将这两个信息粘合在一起,并不是必需的。在需要时传递长度,在没有时传递长度。
-
@kriss:我知道有一个C大会的历史。我的问题是,"为什么那个会议首先出现在那里?"。因为如果C的定义不同,那么系统调用也会有不同的定义。就"必要"而言,你需要某种方式让函数告诉字符串结尾的位置。有几种方法可以做到这一点。一个是长度前缀,一个是空终止,一个是指向范围的开始和结束的指针。你确实在上面的代码中将两条信息传递给open - null告诉它字符串结束的位置。
-
@Billy ONeal:我不明白你以前的评论。也许这就是我使用的强制执行这个词含糊不清。我的意思是你应该改变非常低级的内核APIS,甚至是像open这样的函数的API,其中字符串长度不是必需的,并且不会提供任何性能。然后选择一个API,在其中为其提供异构数据(int和char数组)。它看起来不是一个好的设计选择。
-
@Billy ONeal:另一个简单的选择确实是将两个参数传递给open(),比如数据和长度。现在你必须使用寄存器来存储长度,但你仍然需要读取字符来使用它们,也许它们可以将它们与文件系统中的条目进行比较。这样效率较低,因为您使用两个寄存器而不是一个寄存器。在这种情况下,将终结器放入串中更为经济。我很确定如果仔细检查系统调用,对于所有涉及零终止字符串作为输入的情况都是如此。
-
@kriss:你正在用C来实现API应该是什么。如果它是"每个人都做的标准事情"使用长度前缀,那么那就是你要使用的。它不会想"我传递两个值",它是"我正在传递一个字符串"。不要忘记C早于POSIX,这是定义您正在谈论的大多数系统调用的标准。而且你不会在速度/寄存器分配参数上说服我,因为其他语言,尽管在平均情况下比C慢,但比C w.r.t快得多。字符串操作。
-
@kriss:我已经完成了对此的争论。如果这是一个很好的设计决策,那么就会有其他编程语言会复制这种行为。 (他们从C中复制了大多数其他行为 - 必须有一个很好的理由让这一点离开)
-
@Billy"好吧,对于.01%的C程序员实现操作系统,很好。"其他程序员可以加息。创建C是为了编写操作系统。
-
@Daniel:我的K& R C书不同意你的看法。
-
为什么?因为它说它是一种通用语言?是否说出创作它的人在创作时做了什么?它生命的最初几年用了什么?那么,它说不同意我的意思是什么?它是为编写操作系统而创建的通用语言。它否认了吗?
-
来自我的+1;我不太同意你的所有观点,但我很欣赏你实际上已经付出了努力并列出了一些支持空终止字符串的意见。
-
strdup是标准化功能。它不在C规范中,但它在POSIX规范中。
-
@dreamlax:是的。没错,但POSIX不是C,除此之外不是重点。我只是指出除了显式的alloc之外所有隐藏malloc的函数都可能引入很难找到错误(使用C ++库时问题通常比C语言更糟)。作为个人经历,我失去了几个星期,指出了来自strdup的内存泄漏......我可能对这些事情变得过于谨慎。
-
@Daniel:不,它不否认它。但是,它确实定义了一个标准库,并假定该语言的用户可以访问该标准库。它对操作系统一无所知。
-
@Billy我还在等着听K& R所说的与C编写操作系统相矛盾的说法。实际上,你找不到,因为C是为编写操作系统而创建的。 K& R的C编程语言只是一本书,用于教人们如何编写语言,这是在语言创建之后写的。你甚至懒得去争论是否创建了C来编写一个操作系统 - 一个众所周知的事实 - 并且完全无视这种设计后果,这是完全可笑的。
-
@Daniel:当然不是以牺牲语言的所有其他可能用途为代价来编写操作系统。它被创建为一种系统编程语言,可用于编写操作系统。它不是为了编写操作系统而创建的,因为如果这是真的,它就不是系统编程语言。
-
@BillyONeal"C编程语言是在20世纪70年代早期设计的,作为新兴Unix操作系统的系统实现语言。" - 所以Dennis Ritchie在cm.bell-labs.com/who/dmr/chist.html这里说是丹尼尔最初的说法,你说K& R不同意:"C是为编写操作系统而创建的。"显而易见的事实是,丹尼尔是对的,你错了。
-
@BillyONeal至于为什么字符串是NUL终止的:主要是a)所以字符串的第一个字符是str [0],而不是str [1]或str [2]或str [4],这取决于字符串的长度长度和b)使得字符串可以被指针遍历。这些原因与C的设计的其他方面有关。
-
@Jim:我已经描述了一个长度为前缀的字符串设计,它允许str[0]保留字符串的第一个字符。
-
@BillyONeal你的"设计"等同于在语言中添加一个字符串类型......字符串不能简单地是一个字符数组,其中一个字符意思是"终结符"。它会使语言变得更加复杂,并且正如已经指出的那样,需要更多的空间和寄存器,这对于pdp-7和pdp-11来说是非常宝贵的。整个事情没有实际意义,因为很久以前就选择了设计并且无法改变。如果你有理由相信他们犯了一个错误,请继续这样做,但这不是一个真正有效的问题。
-
@Jim:不,我不建议为此添加单独的类型。它不会使用更多的寄存器,我已经过去了。至于问题的有效性,至少有160人不同意你的意见。
-
我认为你在所有这三点上都是错误的,很不幸的是,很多人浪费了太多时间在一些不可能发生的事情上,包括我自己。完了,走吧。
-
"我不建议为此添加一个单独的类型" - 我将再次注意到这是不可能的; MSFT的CString使用的隐藏长度前缀需要一个新类型(结构和运算符重载),两个指针,一个到开头,一个到最后,也需要一个结构......这些必须是原始的用于处理字符串文字的语言。
-
"如果这是一个很好的设计决策,那么会有其他编程语言会复制这种行为。(他们从C中复制了大多数其他行为 - 必须有一个很好的理由让这一点离开)" - - 这是如此愚蠢和在理智上不诚实。所有这些其他语言的字符串类型都是该语言中的原语,并且它们没有C最初定位的PDP-7的内存限制。
-
@JimBalter:如果字符串文字产生指向前缀字符串的指针,并且许多方法希望接收指向前缀字符串或struct {char kind; char *data; int length; int avail;}的指针(接收指针的函数可以查看第一个字节,看看它是带有前缀的字符串还是字符串信息结构)然后程序员可以跟踪哪些指针适合传递给这样的方法,什么指针不是。适当的字符串类型会更好,但不是绝对必要的。
-
@supercat我已经解释了为什么C文字不能表示为没有类型和运算符重载的前缀字符串... str [n]产生错误的字符。让编译器在前缀的负地址偏移处分配空间是一场噩梦。并且只有NUL终止的字符串提供指向字符串的指针指向字符串本身...早期C字符串处理的基本特征。在任何情况下,NUL终止的字符串对于C语言来说都不是一个糟糕的设计决策。这就是我对这种愚蠢的说法。
-
好吧,还有一件事:指向字符串开头和结尾的指针也会提供指向字符串的指针指向字符串本身,但它们意味着每个字符串多3个字节,更多时间绕过两个指针而不是一个指针,并且内置在字符串类型中,因为早期的C甚至没有在语言中内置结构复制。
-
@JimBalter:很少有人抱怨几乎所有理想的malloc实现都返回一个指向数据的指针,该指针以分配的大小为前缀(内存块前缀的确切格式各不相同,但要求free能够释放没有被告知其大小的内存块意味着必须可以从指向块本身的指针找到大小。 C使用指针的一般方式是对20世纪70年代的合理的程序员 - 努力与内存的权衡;这并不意味着它对具有基本+索引寻址模式和基于寄存器的参数传递的处理器进行合理的权衡。
-
malloc管理堆!这不会暴露给程序员,也不需要编译器支持如果每个字符串在其地址之前都有字节的方式。你对malloc的提及是愚蠢的,在理智上是不诚实的。这不是关于人们"抱怨"的内容,而是关于使用它需要什么...... malloc的存储长度(实际上是指向下一个块的指针)除了malloc之外不需要任何其他任何东西。"对于20世纪70年代来说,这是一个明智的程序员 - 努力与记忆的权衡" - 这是这里的主题!再见。
-
@kriss:使用UTF-8长度的字节数是唯一有意义的。它们的核心是uint8[]。你可能在代码点方面有长度,但这对你没有帮助 - 毕竟,在某些情况下,必须将多个代码点组合成一个字形(此算法可能取决于Unicode版本,所以...)。在大多数情况下(例如,连接,写入控制台/流/文件,...),您需要字节大小,而不是代码点1。您想要代码点的唯一地方是直接处理高级字符。
-
@kriss :(续)......如果你正在处理高级角色,你需要开始考虑Unicode版本和文化差异。你看,没有一个规范可以处理后者 - 它需要很多特定领域的知识,而且如果语法改变就容易发生变化[是的,它甚至人为地发生;例如德国,1996年],或者应该解决处理中的一些错误。那么你需要一个像HarfBuzz这样的库。 (旁注:事实上,这否定了UCS-4 / UTF-32的绝大部分优势)
-
@Tim?as:嗯,字节数通常是重要的,但不完全是唯一有意义的事情。在我的应用程序中,我执行字形渲染。在这种情况下,我必须从我的字符串中找到字形定义,并使用codepoint找到正确的字形(当然,我必须知道当前的警察)。对于这样的用例,字符长度非常有意义。组合代码点不会成为单个字形,我只需要绘制两个字形。
-
@Tim?as:另一个用例,我也有。我正在将数据从一个系统转码到另一个系统,有时候会将UTF-8转换为UTF-16。知道字符长度有助于调整目标缓冲区内存的大小(好吧,老实说,在那个用例中,我不想要字符长度,我只想要UTF-16等效长度的某些UTF-8字符串。因为在那种情况下我真的使用UTF-16的子集而没有扩展字符字符长度对我来说已经足够了)。
-
@kriss:在第一种情况下,代码点仍然不够---你需要处理组合字符。如果你不这样做,你无论如何都在迭代它(不做随机访问),为什么它重要?至于转码,当然,但这是一个非常具体的用例,而绝大多数都需要字节长度,而不是代码点。
-
@Tim?as:组合字符没有什么特别之处,它们至少按照我正在做的方式被字形度量考虑在内。在其他用例(口头文字?)中会有所不同。我不必知道读者是否将结果理解为一个或几个字符。我同意在这种情况下我不会随机访问。字符长度本身并不是非常有用。但很容易按字符迭代,而不是按字节迭代。实际上,知道字节长度只对复制完整的字符串很有用,因此我只是希望得到它。我真的不在乎长度。
-
@kriss:对于"字形度量"的非常宽泛的定义,是的。查看OpenType字符组合的东西 - 只是基本指标是不够的。并且您可以轻松地按字符迭代而无需.lengthInCharacters。实际上,高级语言已经以这种方式进行迭代。至于字节长度与字符相比的有用性,你忘记了偏移和这样的工作对字符串切片很好(与代码点相同的问题)。
-
@kriss(续)基本上,你想要字节长度而不是字符(并且列表不完整):字符串连接(这包括:打印到控制台,写入文件,字符串操作),字符串搜索(它比每个字符串更快) -character [无论如何都将转换为每字节],并具有相同的限制),字符串副本(无论是切片还是不切片;再次,比每个字符更快),等等。当然,搜索可以做得更好,但是为了改进,你需要超越每个代码点访问,并处理组合字符。
我认为,它有历史原因,并在维基百科中发现:
At the time C (and the languages that
it was derived from) were developed,
memory was extremely limited, so using
only one byte of overhead to store the
length of a string was attractive. The
only popular alternative at that time,
usually called a"Pascal string"
(though also used by early versions of
BASIC), used a leading byte to store
the length of the string. This allows
the string to contain NUL and made
finding the length need only one
memory access (O(1) (constant) time).
But one byte limits the length to 255.
This length limitation was far more
restrictive than the problems with the
C string, so the C string in general
won out.
-
但那是很久以前的事了!为什么标准没有改变,所以字符串有一个4字节的"Pascal头"?
-
@muntoo嗯...兼容性?
-
@muntoo:因为这会破坏现有C和C ++代码的数量。
-
BASIC的字符串有一个4字节的标题;一个2字节的数据类型和一个2字节(无符号整数)的数据长度...但我们不是在谈论Pascal或BASIC字符串,所以不要试图改变世界:)
-
@muntoo:Paradigms来去匆匆,但遗留代码是永恒的。任何未来的C版本都必须继续支持以0结尾的字符串,否则将需要重写30多年的遗留代码(这不会发生)。只要旧方法可用,这就是人们将继续使用的方式,因为这是他们熟悉的方式。
-
@John烧掉所有遗留代码。 (将其打印出来然后刻录。);)
-
@muntoo:相信我,有时我希望我能。但是我仍然喜欢使用以0开头的字符串而不是Pascal字符串。
-
谈论遗产...... C ++字符串现在被强制终止NUL。
Calavera是对的,但由于人们似乎没有明白他的观点,我将提供一些代码示例。
首先,让我们考虑一下C是什么:一种简单的语言,所有代码都可以直接翻译成机器语言。所有类型都适合寄存器和堆栈,并且它不需要运行操作系统或大型运行时库,因为它是为了编写这些东西(一个非常适合的任务,考虑到那里甚至不是今天的竞争对手)。
如果C具有string类型,如int或char,则它将是一个不适合寄存器或堆栈的类型,并且需要内存分配(及其所有支持基础结构)以任何方式处理。所有这些都违背了C的基本原则。
所以,C中的字符串是:
那么,让我们假设这是长度前缀的。让我们编写代码来连接两个字符串:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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;
} |
另一种方法是使用结构来定义字符串:
1 2 3 4
| struct {
int len; /* cannot be left implementation-defined */
char* buf;
} |
此时,所有字符串操作都需要进行两次分配,实际上,这意味着您将通过库来对其进行任何处理。
有趣的是......结构就像在C中存在!它们不会用于日常向用户处理显示消息。
所以,这是Calavera正在制作的观点:C中没有字符串类型。要对它做任何事情,你必须使用指针并将其解码为指向两种不同类型的指针,然后它变得非常相关是字符串的大小,不能只是保留为"实现定义"。
现在,C无论如何都可以处理内存,库中的mem函数(在中,甚至!)提供了处理内存所需的所有工具,作为一对指针和大小。 C语言中的所谓"字符串"仅用于一个目的:在写入用于文本终端的操作系统的上下文中显示消息。而且,为此,空终止就足够了。
-
1. +1。 2.显然,如果语言的默认行为是使用长度前缀进行的,那么还有其他事情可以使这更容易。例如,通过调用strlen和朋友来隐藏所有你的演员阵容。至于"将其留给实现"的问题,你可以说前缀是目标框上的short。然后你所有的铸造仍然可以工作。我可以整天想出一些人为的场景,让一个或另一个系统看起来很糟糕。
-
@Billy图书馆的事情是真的,除了C的设计是为了最少或没有库使用。例如,原型的使用在早期并不常见。说前缀是short有效地限制了字符串的大小,这似乎是他们不热衷的一件事。我自己,使用8位BASIC和Pascal字符串,固定大小的COBOL字符串和类似的东西,迅速成为无限大小C字符串的巨大粉丝。如今,32位大小将处理任何实际的字符串,但在早期添加这些字节是有问题的。
-
@Billy:首先,谢谢丹尼尔......你似乎明白我的目标是什么。第二,比利,我想你仍然错过了这里的观点。我不是在争论字符串数据类型的长度前缀的优缺点。我所说的,以及丹尼尔非常清楚地强调的是,在执行C时做出的决定根本就没有处理这个论点。就基本语言而言,字符串不存在。关于如何处理字符串的决定留给程序员......并且null终止变得流行。
-
+1由我。还有一件事我想补充一点;你提出的结构错过了迈向真正的string类型的重要一步:它不知道字符。它是一个"char"数组(机器术语中的"char"就像人类在句子中称之为"单词"一样多的字符)。字符串是一个更高级别的概念,如果您引入了编码概念,它可以在char数组之上实现。
-
@Frerich虽然现在可能是真的,但是在创建C时的char非常像一个角色。直到最近,i18n的努力才改变了"角色"的含义。
-
@DanielC.Sobral:另外,你提到的结构不需要两次分配。要么在堆栈上使用它(因此只有buf需要分配),要么使用struct string {int len; char buf[]};并将整个事物分配为灵活的数组成员,并将其作为string*传递。 (或者可以说,struct string {int capacity; int len; char buf[]};出于明显的性能原因)
显然,为了性能和安全性,您需要在使用它时保持字符串的长度,而不是重复执行strlen或等效字符串。但是,将长度存储在字符串内容之前的固定位置是一个非常糟糕的设计。正如J?rgen在对Sanjit的回答的评论中所指出的那样,它排除了将字符串的尾部视为字符串,例如,如果没有分配新的内存(并且产生新的内存),就会使很多常见操作如path_to_filename或filename_to_extension成为可能失败和错误处理的可能性)。然后当然存在这样的问题:没有人能够同意字符串长度字段应该占用多少字节(大量不好的"Pascal字符串"语言使用16位字段甚至24位字段来排除长字符串的处理)。
C让程序员选择是否/何处/如何存储长度的设计更加灵活和强大。但当然程序员必须聪明。 C惩罚愚蠢的程序崩溃,停止,或给你的敌人根。
-
是的,最重要的一点可能是内存分配。
-
+1。尽管有一个标准的地方存储长度会很好,所以我们这些想要长度前缀的人不必在任何地方写出大量的"胶水代码"。
-
相对于字符串数据没有可能的标准位置,但是你当然可以使用一个单独的局部变量(重新计算它而不是在后者不方便且前者不太浪费时传递它)或带有指针的结构到字符串(更好的是,一个标志,指示结构是否"拥有"指针用于分配目的,或者它是否是对其他地方拥有的字符串的引用。当然,您可以在结构中包含一个灵活的数组成员,以便灵活分配适合你的结构字符串。
懒惰,注册节俭和可移植性考虑到任何语言的汇编,特别是C比汇编高出一步(因此继承了许多汇编遗留代码)。
您会同意,因为null char在那些ASCII天中是无用的,它(可能和EOF控件字符一样好)。
让我们看看伪代码
1 2 3 4 5
| 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
1 2 3 4 5 6
| 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个常见的字符串操作
1 2 3 4 5
| stringLength(string)
pointer=addressOf(string)
while(string[pointer]!=CONTROL_CHAR) do
increment pointer
return pointer-addressOf(string) |
复杂度O(n)其中大多数情况下PASCAL字符串是O(1),因为字符串的长度预先设置为字符串结构(这也意味着此操作必须在较早阶段进行)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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位计算机上读取,懒惰程序会认为你的4字节是字符串的长度然后分配那么多的内存然后尝试读取那么多字节。
另一种情况是PPC 32字节字符串读取(小端)到x86(大端),当然如果你不知道一个是由另一个写,那就会有麻烦。
1字节长度(0x00000001)将变为16777216(0x0100000),读取1字节字符串为16 MB。
当然你会说人们应该就一个标准达成一致,但即使是16位的unicode也只能得到很少的大字节。
当然C也有它的问题,但是受到这里提出的问题的影响很小。
-
那么为什么C的字符串操作效率低于其他地方呢?
-
@Billy ONeal:你能用比其他地方更低的效率来定义你的意思吗?
-
效率是非常主观的
-
@Billy ONeal,这里的效率是多少? C字符串操作与它可以获得的效率(在内存,编译代码复杂性方面)一样高效。是什么让你认为C字符串操作效率较低?
-
@deemoowoor:Concat:O(m+n),其中包含nullterm字符串,其他地方都是O(n)。长度O(n),其中包含nullterm字符串,其他地方都是O(1)。加入:O(n^2),其中包含nullterm字符串,O(n)。在某些情况下,空终止字符串更有效(即只添加一个指针的情况),但concat和length是迄今为止最常见的操作(格式,文件输出,控制台显示等等至少需要长度) 。如果你缓存长度以分摊O(n),你只是指出长度应该与字符串一起存储。
-
我同意在今天的代码中这种类型的字符串是低效的并且容易出错,但是例如Console显示实际上不必知道字符串的长度以便有效地显示它,文件输出实际上不需要知道字符串length(仅在运行时分配集群),此时字符串格式化在大多数情况下是在固定的字符串长度上完成的。无论如何,如果你在C中的concat具有O(n ^ 2)复杂度,你必须编写错误的代码,我很确定我可以在O(n)复杂度中写一个
-
@dvhh:我没有说n ^ 2 - 我说m + n - 它仍然是线性的,但是你需要寻找到原始字符串的末尾才能进行连接,而使用长度前缀不要求是必须的。 (这实际上只是长度需要线性时间的另一个结果)
-
@Billy ONeal:仅凭好奇心,我就当前的C项目(约50000行代码)进行了字符串操作函数调用。 strlen 101,strcpy和variants(strncpy,strlcpy):85(我还有数百个文字字符串用于消息,隐含副本),strcmp:56,strcat:13(和6是连接到零长度字符串以调用strncat) 。我同意长度前缀将加速调用strlen,但不是strcpy或strcmp(可能如果strcmp API不使用公共前缀)。关于上述评论最有趣的事情是strcat是非常罕见的。
-
@Billy:这个帖子的重点是O表示法中隐藏的常量。迭代一个长度为前缀的字符串的字符需要一个额外的寄存器,当迭代一个以null结尾的字符串的字符时,这是不需要的,这意味着你要用字符串做的任何事情必须用少一个寄存器来实现,这会影响性能。为了帮助您理解,当我第一次学习汇编语言时,它是一个微处理器,您基本上只有三个寄存器可用。
-
@Hurkyl:那不是真的。在null终止的情况下,在每个比较步骤中,您需要指向字符串(1寄存器)的指针加载它指向的字符(2个寄存器)并与0(3个寄存器)进行比较。在长度前缀的情况下,您需要将指向字符串(1寄存器)的指针与指向字符串末尾的指针(2个寄存器)进行比较,然后加载它指向的字符(再次输入3个寄存器)。
-
@Billy:某些架构具有立即寻址模式,因此您无需将0加载到寄存器中。某些体系结构具有始终为零的特殊寄存器。加载角色时,某些体系结构将设置零标记,因此您甚至不必进行测试。一些架构具有"如果为零的分支"指令。即使您不在任何这些体系结构中,也可以在测试后释放寄存器,这与长度前缀版本不同,后者要求您将字符串结尾指针保持在寄存器中(或者从内存中重新加载,我想)。
-
@kriss:我猜strcat是罕见的,因为它是一个设计不佳的方法。如果它接受指向每个字符串的开头和已分配空间的结尾的指针,并返回指向写入的零字节的指针,则可以安全有效地使用它,而无需事先找到字符串长度。但是,实际上,安全有效地使用strcat通常需要知道字符串和缓冲区的长度,并且在知道这些事情的情况下,memcpy通常会更有效。
-
@supercat:关于strcat(确实设计得很差)的一个有趣的事实是,一些现代编译器现在可以正确地优化它,并且不会一次又一次地计算一些隐藏的strlen。
-
@kriss:在思考字符串时,我发现自己认为知道C将继续被用作内存大小超过四个演出,我可以设计一个在K& R时代非常实用的字符串类型(也许甚至比z字符串还要多,因为z字符串通常需要一个或两个单独的整数来跟踪字符串长度和/或缓冲区长度,但今天仍然实用。另一方面,不难相信有人试图设计没有这种预知的前缀字符串可能已经实现了它们......
-
...以这种方式使得编写可移植代码变得困难,或者可以使字符串超过255或65535字节。我不确定当前或现在哪种前缀样式的混合是最优的,但允许例如固定大小的字符串0-127字节,带有一个字节的前缀,可变长度缓冲区最多4095字节,带有一个双字节前缀,或者带有((sizeof size_t)+1)字节前缀等等更大的缓冲区作为一些"间接指针"类型似乎是一个实际的混合。使代码紧凑和便携的关键是使用......
-
...标准库方法,用于将字符串指针转换为标识缓冲区位置,缓冲区长度和字符串长度的结构,以及更新存储字符串的长度。然后,用户代码可以主要传递字符串指针并隐式地传递字符串和缓冲区长度。此外,如果strcat或sprintf等方法的目标是堆分配的字符串,则该方法可以根据需要自动调整分配,这是现在不可能的。
-
@supercat:你所描述的内容与C ++ String类非常相似。我没有看到任何禁止通过标准库调用来编写某些C等价物的内容(最好是内部因素,因为速度通常是个问题),包括例如printf和scanf周长。当然,这与字节数组之类的字符串的C视图完全不同,其中双引号仅仅是语法糖并且会引发许多问题:这些字符串不能与中性内存操作原语一起使用。
-
@kriss:库方法的行为是根据char的零终止序列严格定义的,因此必须使用具有其他名称的方法。使用备用类型的最大困难是字符串文字和使用字符串的库方法,但其主要目的在于其他地方(例如fopen)。可以编写一个宏来允许例如ShortString(fred,"My name is Fred");产生union { struct { char header; char dat[15];} STRINGREF stringref; } fred = {{30,"My name is Fred"}};,同样MedString,LongString等......
-
...或者编写一种方法,如果它是一个零终止的话,可以将长度为前缀的字符串转换为char*(默认情况下除了最短的字符串之外的所有字符串都是零终止,或者可能全部字符串零终止并忍受浪费的空间)但这远不如能够使用内联字符串文字那么好。
-
@kriss:另外,我认为我的方法与C ++ String类略有不同,因为大多数字符串缓冲区变量将在声明区域中使用MedString(george,255);声明[声明一个具有双字节前缀的字符串缓冲区,并且255个字符]。对于非常早期的C,在可执行区域中将需要单独的InitMedString(george);步骤[目前C编译器将允许char george[256]; strcpy(george,whatever);而不预先初始化George,但在我的设计下,正常的字符串复制方法将在继续之前检查目标长度。
-
@kriss:C ++ String类的大多数实现主要用于堆存储的可变长度字符串,这些字符串在大多数情况下比预先分配的固定长度字符串缓冲区更昂贵。我的方法与C的不同之处在于,我将使用字符串数据包含当前和最大长度,以便对短,中和长字符串有效。
-
@supercat:不是,请看一些实现。短字符串使用基于短栈的缓冲区(无堆分配),只有当它们变大时才使用堆。但随意提供您作为库的想法的实际实现。通常只有在我们了解细节时才会出现问题,而不是整体设计。
-
我知道短字符串优化,但它要求全局的所有字符串实例具有相同数量的"就地"分配;没有办法将变量声明为"就地"保留127个字符。至于发布实现,我应该在哪里做到最好?
在许多方面,C是原始的。我喜欢它。
它比汇编语言高出一步,使用更易于编写和维护的语言为您提供几乎相同的性能。
null终止符很简单,不需要语言的特殊支持。
回想起来,它似乎并不方便。但是我在80年代使用汇编语言,当时看起来非常方便。我只是认为软件在不断发展,平台和工具不断变得越来越复杂。
-
我没有看到关于null终止字符串的原始内容比其他任何东西都要原始。 Pascal早于C,它使用长度前缀。当然,每个字符串限制为256个字符,但在绝大多数情况下,只使用16位字段就可以解决问题。
-
它限制字符数的事实正是你在做类似事情时需要考虑的问题类型。是的,你可以把它做得更长,但那时候字节很重要。对于所有情况,16位字段是否足够长?来吧,你必须承认null-terminate在概念上是原始的。
-
要么限制字符串的长度要么限制内容(没有空字符),要么接受4到8字节计数的额外开销。没有免费的午餐。在开始时,空终止字符串非常有意义。在汇编中,我有时会使用字符的顶部位来标记字符串的结尾,甚至可以节省一个字节!
-
马克:没有免费的午餐。这总是妥协。如今,我们不需要做出同样的妥协。但那时候,这种方法似乎和其他方法一样好。
假设C实现了Pascal方式的字符串,通过长度为它们加前缀:是一个7字符长的字符串,相同的DATA TYPE是3-char字符串?如果答案是肯定的,那么当我将前者分配给后者时,编译器应该生成什么样的代码?字符串应该被截断,还是自动调整大小?如果调整大小,该操作是否应该通过锁保护以使其线程安全? C方法方面解决了所有这些问题,不管你喜欢与否:)
-
呃..不,没有。 C方法不允许将7 char长字符串分配给3 char长字符串。
-
@Billy ONeal:为什么不呢?据我所知,在这种情况下,所有字符串都是相同的数据类型(char *),因此长度无关紧要。与Pascal不同。但这是Pascal的限制,而不是长度前缀字符串的问题。
-
@Billy:我想你刚刚重申了Cristian的观点。 C根本不处理这些问题来处理这些问题。你仍然在考虑C实际上包含一个字符串的概念。它只是一个指针,所以你可以将它分配给你想要的任何东西。
-
这就像**矩阵:"没有字符串"。
-
@calavera:我不知道这是怎么回事。您可以使用长度前缀以相同的方式解决它...即根本不允许分配。
不知怎的,我理解这个问题暗示C中没有编译器支持长度前缀字符串。下面的例子显示,至少你可以启动自己的C字符串库,其中字符串长度在编译时计算,使用如下结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #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
", string1.n, string1.p); /* prints:"6 Hello!" */
printf("%d %s
", string2.n, string2.p); /* prints:"48 Allows" */
return 0;
} |
但是,这不会带来任何问题,因为您需要特别小心何时专门释放该字符串指针以及何时静态分配(文字char数组)。
编辑:作为一个更直接的问题答案,我认为这是C可以支持字符串长度可用(作为编译时常量)的方式,如果你需要它,但如果你想使用它仍然没有内存开销只有指针和零终止。
当然,似乎使用零终止字符串是推荐的做法,因为标准库通常不会将字符串长度作为参数,并且因为提取长度不像char * s ="abc"那样简单,因为我的例子显示。
-
问题是库不知道你的struct的存在,并且仍然错误地处理嵌入空值之类的东西。而且,这并没有真正回答我提出的问题。
-
确实如此。因此,更大的问题是没有更好的标准方法来提供带有字符串参数的接口,而不是普通的旧的零终止字符串。我仍然声称,有些库支持指针 - 长度对的输入(好吧,至少你可以用它们构造一个C ++ std :: string)。
-
即使存储长度,也不应允许包含嵌入空值的字符串。这是基本的常识。如果您的数据中可能包含空值,则不应将其与期望字符串的函数一起使用。
-
@R ..:许多应用程序需要传递任意长度的字节序列。与数据无关的字符串类型通常可以很好地用于此目的。虽然有些人可能称之为"滥用",但语言设计者会认为包含数据不可知字符串类型和可变大小的二进制数据类型都是冗余的。
-
@supercat:从安全的角度来看,我欢迎这种冗余。否则无知(或睡眠不足)程序员最终会连接二进制数据和字符串,并将它们传递给期望[以null结尾]字符串的东西......
-
@R ..:虽然期望以null结尾的字符串的方法通常期望char*,但是许多不期望空终止的方法也期望char*。分离类型的一个更重要的好处是与Unicode行为有关。字符串实现可能值得维护字符串,以确定字符串是否包含某些类型的字符,或者已知不包含它们[例如在一个百万字符的字符串中查找第999,990个代码点,该字符串已知不包含基本多语言平面之外的任何字符,将会快几个数量级...
-
...而不是找到可能包含此类字符的字符串的第999,990个代码点]。但是,对于用于保存打包二进制数据的字符串,这样的标记将毫无用处。此外,通常需要使用与其内部存储不同的编码来序列化字符串,但二进制数据通常应以内容无关的方式进行序列化。太糟糕了,Java和.NET都不包含"blob"类型。
"Even on a 32 bit machine, if you allow the string to be the size of available memory, a length prefixed string is only three bytes wider than a null terminated string."
首先,对于短字符串,额外的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()等方法的能力所抵消,而不必传递字符串长度(因为缓冲区会知道它们有多大)。
空终止允许基于快速指针的操作。
-
咦?什么"快速指针操作"不适用于长度前缀?更重要的是,使用长度前缀的其他语言比C w.r.t更快。字符串操作。
-
@billy:对于长度为前缀的字符串,你不能只取一个字符串指针并向它添加4,并期望它仍然是一个有效的字符串,因为它没有长度前缀(无论如何都不是有效的)。
-
@Jorgen:好的,所以你不能快速切断开头。但是你可以毫无困难地完成其他任务(即交换,转移,记忆,memmove等)。
-
@Billy ONeal:所有这些操作(交换,memcpy,memmove)对于ASCIIZ字符串具有相同的时间复杂度。不确定你的意思是"转移"。 ASCIIZ字符串的唯一字符串修改时间复杂度较差的是删除后缀,对于长度为前缀的字符串,该后缀为O(1)。
-
@j_random_hacker:asciiz字符串(O(m + n)而不是潜在的O(n))的连接更糟糕,而concat比这里列出的任何其他操作更常见。
-
@Billy:要在O(n)时间内使concat成为可能,你也需要存储保留内存的大小。它也不是渐近的复杂性,因为对于大n,你必须重新分配整个字符串。
-
有一个很小的操作,使用以null结尾的字符串变得更加昂贵:strlen。我会说这有点不对劲。
-
@ybungalobill:我的观点是其他人都使用长度前缀,而其他人在字符串操作时比C更快,尽管在其他任何事情上都比较慢。我不太关心理论上的复杂性,我更关心典型的程序使用。
-
@Billy ONeal:其他人也支持正则表达式。所以呢 ?使用它们所针对的库。 C是最大效率和极简主义,不包括电池。 C工具还允许您非常轻松地使用结构实现长度前缀字符串。并且没有任何事情禁止您通过管理自己的长度和char缓冲区来实现字符串操作程序。这通常是我想要效率并使用C时所做的事情,而不是在char缓冲区结束时调用少数期望为零的函数不是问题。
-
@kriss:虽然标准行为有很多话要说。库将需要字符串的"标准"接口,因此如果您编写自己的结构/库,最终会编写大量的粘合剂。
-
@Jorgen:在C中,如果你接受一个字符串指针并向其添加4,如果原始字符串的长度小于4,则会得到一个无效的字符串。所以,不,指针数学不是保证正确的操作。
-
谁在乎标准行为?如果你害怕你会搞砸,只需在它上面写一个包装结构(mystring)。
-
@Mike:当然,图4是一个随意的数字。假设您在字符串中找到了位置4的最后一个,您只需要文件名,而不是整个路径。在那种情况下,filename = path + 4。在将其传递给需要字符串的另一个函数之前,您不必创建新字符串。这是我的观点。 :)
-
只要您的算法不假设它总是找到。也就是说,其他几个路径操作(提取文件路径,提取文件名sans类型后缀)仍然需要复制。我认为通过缓冲区溢出攻击可以通过文件名推送整个4 MiB程序是不够的,但是对于单长度字节系统,无论什么都会有255字节限制。
还有一点尚未提及:当设计C时,有许多机器的'char'不是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%的开销。此外,打包和解包四字节长度前缀所花费的时间可能超过扫描零字节的4字节字符串的成本。
-
没错。你是对的。除了char之外,前缀可能很容易。任何可以使目标平台上的对齐要求得以解决的问题都可以。我不打算去那里 - 我已经把这个想到了死。
-
假设字符串是长度前缀的,那么可能要做的最好的事情是size_t前缀(内存浪费被诅咒,这将是最安全的---允许任何可能长度的字符串可能适合内存)。事实上,这就是D的作用;数组是struct { size_t length; T* ptr; },字符串只是immutable(char)的数组。
-
@Tim?as:除非字符串需要字对齐,否则在许多平台上使用短字符串的成本将由打包和解包长度的要求所主导;我真的不认为这是实际的。如果想要字符串是与内容无关的任意大小的字节数组,我认为最好将长度与指向字符数据的指针分开,并且有一种语言允许为文字字符串获取两条信息。
-
@supercat:我对你的意思感到困惑;上面的D实现有一个单独的指针,你可以通过str.ptr访问它。结构本身按值传递(用C表示:它是ARRAY(char) foo;而不是ARRAY(char)* foo;)。
-
@Tim?as:对不起 - 我读到你使用"前缀"作为指存储在字符本身前面的内存中的长度,因为你说"有点"D做什么,我以为你期待字符串成为某种东西喜欢struct {size_t length; char text[]; }
-
@supercat:啊,不;它是一个char *,由于各种原因,包括切片。例如,您可以这样做:str[5..10],它计算一个新数组{ .length = 10 - 5; .ptr = old_ptr + 5; }。这也适用于"plain"T*指针,用于从指针转换回数组:ptr[0..len]
-
@Tim?as:这是一个很好的格式,但我不会将它称为"长度前缀"。我将描述为使用"长度前缀"(例如,经典的Macintosh OS,或PC上的Turbo Pascal)的系统存储紧接在字符串文本之前的长度。
-
@supercat:的确如此。因此,"种类" - 不完全相同,但实现(大多数;实际上多一点)相同的效果。
-
@supercat"sizeof(char)就是一个。总是",是吗?我敢肯定我在2000年代使用了16位DSP,其中sizeof(char)== 2。您可能会认为它不符合某些标准,但现实世界中存在非标准编译器。
-
@Neil:我使用的是一个16位TI DSP,其中sizeof (int)是一个,但是sizeof (char)是除了一个以外的任何值的任何编译器都是简单的。如果p和q是指向T[]的连续元素的指针,则q-p将为1,但(char*)q - (char*)p将等于sizeof (T)。这两者都能成功的唯一方法是sizeof (char)是一个。
不是基本原理,而是长度编码的对应点
就内存而言,某些形式的动态长度编码优于静态长度编码,这完全取决于使用情况。只需看看UTF-8就可以获得证明。它本质上是一个可扩展的字符数组,用于编码单个字符。这对每个扩展字节使用一位。 NUL终止使用8位。我认为长度前缀可以通过使用64位合理地称为无限长度。你经常看到额外比特的情况是决定因素。只有1个极大的字符串?谁在乎你是使用8位还是64位?很多小字符串(即英文单词串)?那么你的前缀成本很高。
长度为前缀的字符串可以节省时间并不是真的。无论您提供的数据是否需要提供长度,您在编译时计算,或者您真正提供的动态数据必须编码为字符串。这些大小是在算法中的某个点计算的。可以提供用于存储空终止字符串大小的单独变量。这使得时间节省的比较没有实际意义。一个人在最后只有一个额外的NUL ...但是如果长度编码不包括那个NUL那么两者之间几乎没有区别。根本不需要算法更改。只是一个预先通过,你必须手动设计自己,而不是让编译器/运行时为你做。 C主要是关于手动操作。
长度前缀是可选的是卖点。我并不总是需要算法的额外信息,因此需要为每个字符串执行此操作,这使得我的预计算+计算时间永远不会低于O(n)。 (即硬件随机数生成器1-128。我可以从"无限字符串"中拉出来。假设它只生成字符这么快。所以我们的字符串长度一直在变化。但是我对数据的使用可能并不关心如何我有很多随机字节。只要它能在请求后得到它就想要下一个可用的未使用字节。我可以在设备上等待。但是我也可以预先读取一个字符缓冲区。长度比较是不必要的计算浪费。空检查更有效。)
长度前缀是防止缓冲区溢出的好方法吗?因此,图书馆功能和实施的理智使用。如果我传入格式错误的数据怎么办?我的缓冲区是2个字节长,但我告诉它的功能是7!例如:如果要在已知数据上使用gets(),它可能会进行内部缓冲区检查,测试编译缓冲区和malloc()调用并仍然遵循规范。如果它被用作未知STDIN的管道到达未知缓冲区那么显然人们无法知道缓冲区大小,这意味着长度arg是没有意义的,你需要其他东西,如金丝雀检查。就此而言,你不能为某些流和输入加上前缀,你就是不能。这意味着长度检查必须内置到算法中,而不是打字系统的神奇部分。 TL; DR NUL终止永远不必是不安全的,它只是通过滥用而以这种方式结束。
反计数点:NUL终止对二进制很烦人。你需要在这里做长度前缀或者以某种方式转换NUL字节:转义码,范围重新映射等......这当然意味着更多内存使用/减少信息/更多操作每字节。长度前缀主要在这里赢得战争。转换的唯一好处是不必编写额外的函数来覆盖长度前缀字符串。这意味着在您更优化的子O(n)例程上,您可以让它们自动充当其O(n)等价物,而无需添加更多代码。当然,在NUL重弦上使用时,下侧是时间/内存/压缩浪费。根据您最终复制以对二进制数据进行操作的库的数量,仅使用长度前缀字符串可能是有意义的。也就是说,也可以使用长度前缀字符串执行相同操作... -1长度可能意味着NUL终止,并且您可以在长度终止内使用NUL终止的字符串。
Concat:"O(n + m)vs O(m)"我假设你将m作为连接后字符串的总长度,因为它们都必须使操作数最小(你不能只是-on到string 1,如果你需要realloc怎么办?)。而且我假设n是一个神秘的操作量,因为预先计算而不再需要做。如果是这样,那么答案很简单:预先计算。如果你坚持要求你总是有足够的内存来不需要重新分配,这就是big-O表示法的基础,那么答案就更简单了:对分配的内存进行二进制搜索以获得字符串1的结尾,显然有一个字符串1之后的大量无限零值为我们不担心realloc。在那里,很容易得到n到log(n),我几乎没有尝试过。如果你记得log(n)在实际计算机上基本上只有64那么大,这基本上就像是说O(64 + m),基本上是O(m)。 (是的,逻辑已被用于今天使用中的实际数据结构的运行时分析。这并不是我头脑中的废话。)
Concat()/ Len()再次 sub>:记住结果。简单。如果可能/必要,将所有计算变为预计算。这是一个算法决策。它不是语言的强制约束。
NUL终止时,字符串后缀传递更容易/可能。根据length-prefix的实现方式,它可能对原始字符串具有破坏性,有时甚至无法实现。要求复制并传递O(n)而不是O(1)。
对于NUL终止和长度前缀,参数传递/解引用较少。显然是因为你传递的信息较少。如果您不需要长度,那么这将节省大量空间并允许优化。
你可以作弊。它真的只是一个指针。谁说你必须把它读作一个字符串?如果您想将其作为单个字符或浮点数读取,该怎么办?如果你想做相反的事情并将浮点数作为字符串读取怎么办?如果你小心,你可以通过NUL终止来做到这一点。你不能用length-prefix做到这一点,它是一种与指针明显不同的数据类型。你很可能必须逐字节地构建一个字符串并获得长度。当然如果你想要一个像整个浮点数(可能里面有一个NUL)的东西,你还是必须逐字节读取,但是细节留待你决定。
TL; DR您使用的是二进制数据吗?如果不是,那么NUL终止允许更多的算法自由。如果是,那么代码数量与速度/内存/压缩是您主要关注的问题。两种方法或记忆的混合可能是最好的。
好。
-
9有点偏离基础/错误代表。长度预修复没有这个问题。 Lenth作为一个单独的变量传递。我们谈论的是pre-fiix但是我被带走了。考虑还是一件好事,所以我会留在那里。 :d
围绕C的许多设计决策源于这样一个事实:当它最初实现时,参数传递有点昂贵。给出了例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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);
} |
与
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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"这样的函数看起来像[现代语法]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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&amp; R strcat方法略大,但它将支持边界检查,而K&amp; R方法则不支持。此外,与当前方法不同,可以容易地连接任意子串,例如,
1 2 3 4 5 6 7 8
| /* 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&amp; R没有实现任何这样的东西,但这很可能是因为他们不想在字符串处理方面花费太多精力 - 即使在今天,许多语言看起来都很贫乏。
-
没有什么可以阻止char* arr指向struct { int length; char characters[ANYSIZE_ARRAY] };或类似形式的结构,它仍然可以作为单个参数传递。
-
@BillyONeal:这种方法有两个问题:(1)它只允许传递字符串作为一个整体,而本方法也允许传递一个字符串的尾部; (2)使用小弦时会浪费大量空间。如果K&amp; R希望花一些时间在字符串上,他们本可以使事情变得更加强大,但我认为他们并不打算在十年之后使用他们的新语言,更不用说四十年了。
-
@BillyONeal:我概述了我认为可能是C中字符串的最佳设计(如果不介意通过预处理器运行代码将字符串文字转换为适当的 - 那么嵌入式系统可能仍然是一个很好的设计 - 前缀字符数组。
-
字符串信息结构在例子中看起来有些神奇。它会依赖于一些全局隐藏数组还是char *会有一个完整的意义变化(即:只指向数组的指针,不再有意义作为指向单个char的指针?)。目前还不清楚新catr实施的建议是什么。你能详细说明一下吗?
-
@kriss:不需要魔法,依赖于实现的行为,全局变量等。每个字符串标头的第一个字节将标识目标为零长度字符串,1-63字节字符串,1-63字节缓冲区,其中至少一个字节为空,较长的字符串或缓冲区将使用更多字节存储其长度,或struct SUBSTRING_INFO { unsigned char mflag; struct SUBSTRING_INFO inf; } [如果后者,mflag将设置为标识SUBSTRING_INFO的字节值]。
-
@kriss:用于存储大小的最后一个字节将使用一位来指示目标是字符串还是非完整缓冲区。对于非完整缓冲区,最后一个字节将指示是否有1-255个字节空闲,或者前一个字节是否表示空闲空间。 STRING_INFO结构将是char *dat; stringsize_t length,buffsize;。 get_string_info函数将查看目标并适当地加载STRING_INFO的值(如果目标是SUBSTRING_INFO,它将从中复制值)。
-
@kriss:可移植性与速度之间会有一些权衡,但即使是完全可移植的版本,在大多数使用场景中也可能胜过零终止字符串;虽然strcpy()不需要事先知道字符串的长度,但它在短字符串上有优势,它需要在每个字节上花费比memcpy更长的时间 - 有时候要长得多。我猜我的库的一个版本仅限于INT_MAX字节的字符串[无论发生在给定平台上的什么],并且为该大小定制,将胜过...
-
...几乎在任何平台上都有零终止的C字符串,几乎所有涉及十几个字符的操作都是[盈亏平衡点取决于操作]。知道是否有一个好地方发布它?这里有点偏离主题。
-
@supercat:我明白了,但我不确定是否同意。这确实可以在语言中,但不是char *,而是完全不同的类型。这可能是某种预定义的结构(让我们称之为"字符串")。实际上它确实可以是双引号而不是char *之间的类型。你的建议强烈让我想起Pascal Strings。如果Pascal还在那里,那么就不应该让它们以这种方式发展。
-
@supercat:我明白了,但我不确定是否同意。这确实可以在语言中,但不是char *,而是完全不同的类型。这可能是某种预定义的结构(让我们称之为"字符串")。实际上它确实可以是双引号而不是char *之间的类型。你的建议强烈让我想起Pascal Strings。如果Pascal还在那里,那么就不应该让它们以这种方式发展。
-
@kriss:我假设unsigned char*而不是char*指向字符串标题。从语言的角度来看,可以说字符串文字将具有两种表示形式,具体取决于长度。对于长度UCHAR_MAX/4,存储长度后跟文本;返回指向长度的指针。对于更长的长度,分配一个对齐的unsigned,后跟一个UCHAR_MAX字节,然后是文本;返回指向UCHAR_MAX字节的指针。因此,任何字符串文字都会产生指向值0-UCHAR_MAX/4-1或UCHAR_MAX的指针。
-
@kriss:我不认为"char缓冲区以包含长度的int开头"比"使用此特殊字符终止char缓冲区的结尾"更神奇
-
@BillyONeal:您如何看待在字符串缓冲区的开头使用字节或int +字节来指示其长度以及它是否为固定字符串,可填充的可调整大小的字符串缓冲区或可调整大小的概念字符串缓冲区小于满?可以使用不同的标志字节值来启用多个大小的长度指示符,但是将四个甚至八个字节添加到64个字符的字符串将是轻微的开销,相比之下,甚至将两个额外的字节添加到四个字符的字符串。
-
@supercat:我认为这样的设计在1975年因代码大小原因而不实用。现在?不知道。需要基准测试才能
-
@BillyONeal:决定速度,数据大小和代码大小之间的最佳权衡将是棘手的(并且有可能在1975年尝试这样做的人会实现难以修复的架构限制),但我会认为一个好的紧密编写的字符串库可以减少整体应用程序代码和数据大小,即使在1975年,也可以避免应用程序分别跟踪缓冲区长度,字符串长度和字符串内容,以及允许对子字符串进行有效操作(而不仅仅是尾巴)。
-
@supercat:唯一的麻烦就是打破unsigned char *或char *的语义。这两个都可以指向一个char或几个连续的char,我们习惯称之为"字符串"。这就是为什么需要新的专用字符串类型的真正原因。
-
@BillyONeal:当然要么添加一个前缀(一个字节或稍微复杂一点的supercat建议),要么在我们使用双引号语法时添加一个特殊的结束字节都是神奇的行为。我们都知道第一个行为是由Pascal选择的,而第二个行为是由C选择的。唯一真正的一点是,如果我们选择带前缀的第一个选项,它不再是char *,而是一个稍微复杂的对象,最好描述一下通过C语言的结构。当然,这个结构可以预定义为"string"并添加库函数。不需要为此打破C类型系统。
-
@kriss:选择第二个选项的主要好处是,如果一个指针指向一个已知包含至少n个字符的字符串,则可以很容易地获得指向包含超过第n个字符的部分的字符串的指针。另一方面,如果一个人使用前缀并保留一些保留的值,并且愿意在从字符串访问字符之前使用子程序调用,那么可以获得许多能力,包括将引用传递给a的任意部分的能力。字符串[不只是尾巴],边界检查等。有这样的语言功能......
-
...会有所帮助,因为声明string[23] foo;可以允许编译器不仅为foo分配24个字节,而且初始化第一个字以便将其标识为空的23字节缓冲区。否则,使用边界检查的缓冲区要求用户代码使用单独的方法"将字符串存储到已知足够大的单元化缓冲区"和"将字符串存储到边界检查缓冲区中" - 有点令人讨厌 - - 或者在使用之前初始化缓冲区的宏。尽管如此,我认为努力节省几个字节是非常不幸的......
-
...几十年后,PDP系列硬件在不再真正节省太多的平台上持续存在[事实上,最终会在任何想要安全的代码中增加额外成本]。
-
关于调用约定的这一点是一个与现实无关的正常故事......它不是设计中的考虑因素。基于注册的呼叫约定已经"发明"了。此外,诸如两个指针之类的方法不是一个选项,因为结构不是第一类......只有原语是可分配的或可通过的;结构复制直到UNIX V7才到达。只需复制字符串指针需要memcpy(也不存在)是一个笑话。如果你假装语言设计,尝试编写一个完整的程序,而不仅仅是孤立的函数。
-
"这很可能是因为他们不想在字符串处理上花费太多精力" - 胡说八道;早期UNIX的整个应用程序域都是字符串处理。如果不是那样的话,我们就再也听不到了。
-
'我不认为'char缓冲区以一个包含长度的int开头"更为神奇" - 如果你要使str[n]引用正确的字符。这些是人们讨论这个问题时没有想到的事情。
-
@JimBalter:正如我所描述的那样C将真正需要进行字符串处理的工作方法是请求结构分配,然后是在其中声明的最后一个类型或数组的额外n元素。然后可以声明任何这些类型的struct TINYSTR { unsigned char head; char dat[0];} struct MEDSTR { unsigned int head; char dat[0];}和struct LONGSTR {unsigned long head; char dat[0];}以及struct ISTRING {char *ptr; unsigned int length; unsigned int alloc; unsigned head; char dat[0];}, and given an initialized variable v`,将v.dat传递给字符串方法。
-
@JimBalter:支持VLA很容易[不要打扰拒绝大小为零的数组,并允许一个语法来请求大于正常的分配]并且可以省去代码不得不在缺乏支持的情况下捣乱。初始化缓冲区的头一次将消除将缓冲区大小传递给字符串处理方法的任何进一步需求。想要传递字符串缓冲区(不仅仅是尾部)的任意部分的代码可以创建一个ISTRING并将指针传递给它的dat[]字段。无论如何,最重要的观察......
-
... K&amp; R是基于PDP系列的指令集上的C设计,其中访问指针比索引到数组便宜,并且将指针传递到数组比分别传递base和index便宜。在许多平台上,这两个假设都没有。传递base + index意味着可以使用边界检查或不使用边界检查,而单独传递指针可以消除这种可能性。就个人而言,我更愿意选择绑定检查数组,而不是决定任何错误的数组访问只会有不可覆盖的UB。
-
K&amp; R没有在PDP指令集上建立C;里奇以印刷品的形式反驳了这一谣言。无论如何它是无关紧要的,因为这个问题是关于为什么C设计使用NUL终止的字符串,以及OP重复的harebrained声称这是一个"劣等"设计。上面的其他评论也无关紧要,尤其是关于VLA的评论。现在,SO明智地告诉我们避免扩展讨论......
-
@JimBalter:你会否认C及其库的设计主要取决于*dest++ = *src++;会比dest[i]=src[i];更快的想法吗?我的答案第一部分的主要内容 - 如果你能指出我的历史参考资料来帮助我解决任何不太好的错误 - 是C是围绕将指针传递到数组中间的概念设计的,没有任何手段让接收者知道它们出现的阵列的任何信息,而这又是指针访问比索引访问更快的动机。你不同意?
据Joel Spolsky在这篇博文中说,
It's because the PDP-7 microprocessor, on which UNIX and the C programming language were invented, had an ASCIZ string type. ASCIZ meant"ASCII with a Z (zero) at the end."
在看到这里的所有其他答案之后,我确信即使这是真的,它也只是C具有空终止"字符串"的部分原因。关于字符串之类的简单事物实际上是多么简单,这个帖子非常有启发性。
-
看,我尊重Joel很多事情;但这是他猜测的东西。 Hans Passant的回答直接来自C的发明者。
-
是的,但如果Spolsky所说的是真的,那么它将成为他们所指的"便利"的一部分。这就是为什么我把这个答案包括在内的原因。
-
AFAIK .ASCIZ只是一个用于构建字节序列的汇编语句,后跟0。它只是意味着零终止字符串是当时完善的概念。这并不意味着零终止字符串与PDP- *的体系结构有关,除了您可以编写由MOVB(复制字节)和BNE组成的紧密循环(如果复制的最后一个字节不是,则为分支)零)。
-
它假设显示C是陈旧,松弛,破旧的语言。
gcc接受以下代码:
char s [4] ="abcd";
如果我们把它当作一个字符数组而不是字符串就可以了。也就是说,我们可以用s [0],s [1],s [2]和s [3]访问它,甚至可以用memcpy(dest,s,4)访问它。但是当我们尝试使用puts(s)时,我们会变得混乱,或者更糟糕的是使用strcpy(dest,s)。
-
这是错的。 "abcd"需要五个字节(因为尾随零字节)并且不适合char[4]。