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

Bil*_*eal 272 c c++ string null-terminated

尽管我喜欢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终止字符串只是在你希望新结束的地方粘贴一个空值,长度前缀只是从前缀中减去.)

Han*_*ant 191

马的嘴里

BCPL,B或C都不支持该语言中的字符数据; 每个字符串都像整数向量一样对待字符串,并通过一些约定来补充一般规则.在BCPL和B中,字符串文字表示用字符串字符初始化的静态区域的地址,打包到单元格中.在BCPL中,第一个打包字节包含字符串中的字符数; 在B中,没有计数,字符串由特殊字符终止,B拼写 *e.这种改变部分是为了避免因在8位或9位插槽中保持计数而导致的字符串长度限制,部分原因是根据我们的经验,保持计数似乎不如使用终结器.

Dennis M Ritchie,C语言的发展

  • 另一个相关的引用:"...字符串的语义完全归入管理所有数组的更一般规则,因此语言更容易描述......" (11认同)

Rob*_*cio 151

C没有字符串作为语言的一部分.C中的'string'只是指向char的指针.所以也许你问的是错误的问题.

"遗漏字符串类型的理由是什么"可能更具相关性.为此,我要指出C不是面向对象的语言,只有基本的值类型.字符串是更高级别的概念,必须通过某种方式组合其他类型的值来实现.C处于较低的抽象层次.

鉴于下面肆虐的狂风:

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

  • @Yanick:这只是告诉编译器在末尾创建一个null数组的方便的方法.它不是'字符串' (56认同)
  • `char*temp ="foo bar";`是C中的有效语句...嘿!这不是一个字符串?是不是空终止了? (28认同)
  • 我认为@calavera是对的,C没有字符串的数据类型.好吧,你可以考虑像字符串一样的字符数组,但这并不意味着它总是一个字符串(对于字符串我的意思是一系列具有明确含义的字符).二进制文件是一个字符数组,但这些字符对人类来说并不意味着什么. (27认同)
  • @calavera:但它可能只是简单地意味着"用这个字符串内容和两个字节长度的前缀创建一个内存缓冲区", (26认同)
  • @Billy:好吧,因为'string'实际上只是一个指向char的指针,它相当于指向byte的指针,你怎么知道你正在处理的缓冲区真的是一个'字符串'?你需要一个除char/byte*之外的新类型来表示这一点.也许是一个结构? (12认同)
  • 它可能不是一个字符串对象,就像人们在C++中想到的那样,但它根据定义*是一个C字符串.不要试图否认它. (4认同)
  • 1)C有字符串.2)C字符串不是类型,它们被定义为char或wchar_t数组,最后只包含一个空字符.3)你所说的没有意义.为什么"str"以null结尾但不是大小前缀? (4认同)
  • 字符串不需要是人类可读的 - "在计算机科学中,字符串是任何有限的字符序列(即字母,数字,符号和标点符号)." (4认同)
  • @Mark Ransom:你错了,不要告诉我该怎么做.如果你对你的定义如此肯定,试试这个:`int*str ="这只是字节,我不知道字符串是什么";`右边的部分是**字符串文字**.C不知道字符串类型是什么,它只知道如何将字符串文字分配给指针. (4认同)
  • 仅仅因为C没有字符串类型并不意味着它没有字符串.它有一个明确定义的约定,可以追溯到语言的开头,通过字符串文字支持语言.任何以其他方式提出要求的企图都只是过分迂腐. (4认同)
  • @Mark:你迂腐. (4认同)
  • 来自我的+1.实现`char`数组与字符串不一样(例如,因为没有编码的概念)是一个关键的洞察力. (4认同)
  • "字符串"由C标准明确定义为(基本上)以空值终止的字符序列. (4认同)
  • @Yanick Rochon:`char a [4] ="toto";`也是一个有效的C语句,但在这种情况下,"toto"可能是一个字符串但不是零终止(C和C之间大部分被忽略的小差异之一) C++). (3认同)
  • @calavera,"字符串"根据定义是"符号的线性序列",并不一定是数据类型.为方便起见,它是由更高语言级别的类型.在C中,这是一个字符串,在C#中它是另一回事.问题是关于C字符串,就是这样; 一个指向线性字符序列的指针,后跟一个`\ 0`字符. (2认同)
  • @calavera:你不会.但你真的不知道用C字符串.有人可以随时通过非空终止缓冲区. (2认同)
  • @Yanick:绝对不真实。“字符串”这个词的定义随着上下文的变化而变化。我是在数据类型的背景下谈论的。 (2认同)
  • @jweyrich:你是对的,我的意思是,没有人会读取二进制文件并将其内容放在字符串变量中,对吧?好吧,我不会这样做,但我不知道c ++. (2认同)
  • “*C 中的‘字符串’只是一个指向 char 的指针...*”它不是 (*指针*);它是一个以“0”结尾的“char”的***数组***。 (2认同)

kri*_*iss 101

这个问题被视为Length Prefixed Strings (LPS)vs的zero terminated strings (SZ)事情,但主要是暴露长度前​​缀字符串的好处.这可能看起来势不可挡,但说实话,我们也应该考虑LPS的缺点和SZ的优势.

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

Zero Terminated Strings的优点(我看到):

  • 非常简单,无需在语言中引入新概念,char数组/ 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兼容的).

  • ......续......我认为你的几个观点是完全错误的,即"一切都是文件"的论点.文件是顺序访问,C字符串不是.长度前缀也可以用最少的语法糖来完成.这里唯一合理的论点是试图在小(即8位)硬件上管理32位前缀; 我认为可以简单地通过说长度的大小由实现来确定.毕竟,这就是`std :: basic_string`的作用. (6认同)
  • @Billy"好吧,对于.01%的C程序员实现操作系统,很好." 其他程序员可以加息.创建C是为了编写操作系统. (5认同)
  • 为什么?因为它说它是一种通用语言?是否说出创作它的人在创作时做了什么?它生命的最初几年用了什么?那么,它说不同意我的意思是什么?它是一种通用语言_created来编写操作系统_.它否认了吗? (5认同)
  • @Billy ONeal:我的答案中确实有两个不同的部分.一个是关于"核心C语言"的一部分,另一个是关于标准库应该提供什么.关于字符串支持,核心语言只有**一个**项:双引号括起来的字节串的含义.我对C行为并不比你更开心.我觉得奇怪的是,在每个双关闭的结尾处添加零是封闭的字节串已经足够糟糕了.当程序员想要而不是隐含的那个时,我更喜欢和显式的`\ 0`.前置长度要差得多. (3认同)
  • @BillyONeal至于为什么字符串以NUL结尾:主要是a)使字符串的第一个字符位于str [0],而不是str [1]或str [2]或str [4],具体取决于字符串的长度长度和 b) 以便可以通过指针遍历字符串。这些原因与C的设计的其他方面有关。 (3认同)
  • @Billy ONeal:事实并非如此,使用者关心的是核心和库。最大的一点是使用C来实现OS。在该级别上,没有可用的库。C也经常用于嵌入式环境中或用于您经常受到相同限制的编程设备。在许多情况下,Joes's可能现在根本不应该使用C:“好,您要在控制台上使用它吗?您有控制台吗?否?太糟糕了……” (2认同)
  • @Billy ONeal:C 选择允许轻松实现前置长度行为(嘿,其他语言的库大多使用 C 编写)。反过来是不可能的。如果该语言不包含**任何**方式来定义一堆字节,那么你就注定了,有些事情是无法完成的。 (2认同)
  • @Billy,我仍在等待听听 K&R 所说的“与 C 是为了编写操作系统而创建的相矛盾”的说法。事实上,你不会发现这一点,因为 C 是为了编写操作系统而创建的。K&R 的 C 编程语言只是一本教人们如何用它编程的书,是在该语言创建多年后编写的。你甚至费心去争论 C 是否是为了编写操作系统而创建的——这是众所周知的事实——这绝对是可笑的,而试图忽视这一点作为设计后果是非常愚蠢的。 (2认同)
  • @Daniel:当然不是以牺牲语言的所有其他可能用途为代价来编写操作系统.它被创建为一种系统编程语言,可用于编写操作系统.它不是为了编写操作系统而创建的,因为如果这是真的,它就不是系统编程语言. (2认同)
  • @BillyONeal “C 编程语言是在 20 世纪 70 年代初设计的,作为新生 Unix 操作系统的系统实现语言。”——Dennis Ritchie 在 http://cm.bell-labs.com/who/dmr/chist 如此说道。 .html 这是 Daniel 最初的主张,你说 K&R 不同意:“C 是为了编写操作系统而创建的。” 显而易见的事实是丹尼尔是对的而你是错的。 (2认同)
  • “如果这是一个好的设计决策,那么就会有其他编程语言复制该行为。(他们复制了 C 语言中的几乎所有其他行为 - 必须有一个非常好的理由来忽略这一点)” - - 这太愚蠢了,而且在智力上不诚实。所有这些其他语言都有*字符串类型*,它是语言中的原语,并且它们没有 C 最初针对的 PDP-7 的内存限制。 (2认同)
  • malloc 管理堆!这不会暴露给程序员,也不需要编译器支持,就像每个字符串在其地址之前都有字节一样。您对 malloc 的引用是愚蠢的并且在智力上是不诚实的。这不是关于人们“抱怨”什么,而是关于使用它需要什么...... malloc 的存储长度(实际上,指向下一个块的指针)不需要 malloc 之外的任何东西。“这是 20 世纪 70 年代程序员工作量与内存的明智权衡”——这就是这里的主题!再见。 (2认同)

kha*_*hik 60

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

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

  • @muntoo:因为这会破坏现有C和C++代码的数量. (19认同)
  • @muntoo:Paradigms来去匆匆,但遗留代码是永恒的.任何未来的C版本都必须继续支持以0结尾的字符串,否则将需要重写30多年的遗留代码(这不会发生).只要旧方法可用,这就是人们将继续使用的方式,因为这是他们熟悉的方式. (9认同)
  • @muntoo:相信我,有时我希望我能.但是我仍然喜欢使用以0开头的字符串而不是Pascal字符串. (8认同)
  • @John烧掉所有遗留代码.(将其打印出来然后刻录.);) (7认同)
  • @muntoo嗯...兼容性吗? (2认同)
  • 谈论遗留...... C++ 字符串现在被强制要求以 NUL 结尾。 (2认同)

Dan*_*ral 31

Calavera对的,但由于人们似乎没有明白他的观点,我将提供一些代码示例.

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

如果C有string类型,int或者类型,char那么它将是一种不适合寄存器或堆栈的类型,并且需要以任何方式处理内存分配(及其所有支持基础结构).所有这些都违背了C的基本原则.

所以,C中的字符串是:

char s*;
Run Code Online (Sandbox Code Playgroud)

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

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;
}
Run Code Online (Sandbox Code Playgroud)

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

struct {
  int len; /* cannot be left implementation-defined */
  char* buf;
}
Run Code Online (Sandbox Code Playgroud)

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

有趣的是......这样的结构用C存在!它们不会用于日常向用户处理显示消息.

所以,这是Calavera的观点:C中没有字符串类型.要对它做任何事情,你必须拿一个指针并将其解码为指向两种不同类型的指针,然后它变得非常相关什么是字符串的大小,并且不能只是留下"实现定义".

现在,C 无论如何都可以处理内存,mem库中的函数(in <string.h>,甚至!)提供了处理内存所需的所有工具,作为一对指针和大小. C语言中的所谓"字符串"仅用于一个目的:在写入用于文本终端的操作系统的上下文中显示消息.而且,为此,空终止就足够了.

  • @Billy图书馆的事情是真的,除了C的设计是为了最少或没有库使用.例如,原型的使用在早期并不常见.说前缀是"短"有效地限制了字符串的大小,这似乎是他们不热衷的一件事.我自己,使用8位BASIC和Pascal字符串,固定大小的COBOL字符串和类似的东西,迅速成为无限大小C字符串的巨大粉丝.如今,32位大小将处理任何实际的字符串,但在早期添加这些字节是有问题的. (5认同)
  • 1. +1.2.显然,如果语言的默认行为是使用长度前缀进行的,那么还有其他事情可以使这更容易.例如,那里的所有演员阵容都会被调用"strlen"和朋友隐藏起来.至于"将其留给实现"的问题,你可以说前缀是目标盒上的"short".然后你所有的铸造仍然可以工作.我可以整天想出一些人为的场景,让一个或另一个系统看起来很糟糕. (2认同)
  • @DanielC.Sobral:另外,你提到的结构不需要两次分配.要么在堆栈上使用它(所以只有`buf`需要分配),或者使用`struct string {int len; char buf []};`并将一个分配作为灵活的数组成员分配整个事物,并将其作为`字符串*`传递.(或者可以说,`struct string {int capacity; int len; char buf []};`出于明显的性能原因) (2认同)

R..*_*R.. 18

显然,对于性能和安全性,您需要在使用它时保持字符串的长度,而不是重复执行strlen或等效.但是,将长度存储在字符串内容之前的固定位置是一个非常糟糕的设计.正如Jörgen在对Sanjit的回答的评论中所指出的那样,它排除了将字符串的尾部视为字符串,例如,在没有分配新内存(并且导致失败和错误处理的可能性)的情况下,这会使许多常见操作变得相似path_to_filenamefilename_to_extension不可能. .当然还有一个问题是没有人能够同意字符串长度字段应该占用多少字节(大量不好的"Pascal字符串")

C让程序员选择是否/何处/如何存储长度的设计更加灵活和强大.但当然程序员必须聪明.C惩罚愚蠢的程序崩溃,停止,或给你的敌人根.

  • 相对于字符串数据没有可能的标准位置,但是你当然可以使用一个单独的局部变量(重新计算它而不是在后者不方便且前者不太浪费时传递它)或带有指针的结构到字符串(更好的是,一个标志,指示结构是否"拥有"指针用于分配目的,或者它是否是对其他地方拥有的字符串的引用.当然,您可以在结构中包含一个灵活的数组成员,以便灵活分配适合你的结构字符串. (2认同)

dvh*_*vhh 12

懒惰,注册节俭和可移植性考虑到任何语言的汇编,特别是C比汇编高出一步(因此继承了许多汇编遗留代码).您会同意,因为null char在那些ASCII天中是无用的,它(可能和EOF控件字符一样好).

让我们看看伪代码

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

共有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
Run Code Online (Sandbox Code Playgroud)

共有2个寄存器

那个时候看起来可能是短视的,但考虑到代码和注册的节俭(当时是PREMIUM,你知道的时候,他们使用穿孔卡).因此速度更快(当处理器速度可以以kHz为单位计算时),这个"Hack"非常好,可以轻松地移植到无寄存器处理器.

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

stringLength(string)
     pointer=addressOf(string)
     while(string[pointer]!=CONTROL_CHAR) do
         increment pointer
     return pointer-addressOf(string)
Run Code Online (Sandbox Code Playgroud)

复杂度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
Run Code Online (Sandbox Code Playgroud)

复杂度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也有它的问题,但是受到这里提出的问题的影响很小.

  • @deemoowoor:Concat:`O(m + n)`和nullterm字符串,`O(n)`在其他任何地方都是典型的.长度为"O(n)",带有空字符串,"O(1)"到处都是.加入:带有nullterm字符串的`O(n ^ 2)`,其他地方都是`O(n)`.在某些情况下,空终止字符串更有效(即只添加一个指针的情况),但concat和length是迄今为止最常见的操作(格式,文件输出,控制台显示等等至少需要长度) .如果你缓存长度以分摊"O(n)",你只是指出长度应该与字符串一起存储. (2认同)

Jon*_*ood 9

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

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

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

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

  • 要么限制字符串的长度要么限制内容(没有空字符),要么接受4到8字节计数的额外开销.没有免费的午餐.在开始时,空终止字符串非常有意义.在汇编中,我有时会使用字符的顶部位来标记字符串的结尾,甚至可以节省一个字节! (10认同)

小智 8

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

  • @Billy:我想你刚刚重申了Cristian的观点.C根本不处理这些问题来处理这些问题.你仍然在考虑C实际上包含一个字符串的概念.它只是一个指针,所以你可以将它分配给你想要的任何东西. (3认同)
  • 呃..不,没有.C方法不允许将7 char长字符串分配给3 char长字符串. (2认同)
  • 这就像**矩阵:"没有字符串". (2认同)

Pyr*_*ola 7

不知怎的,我理解这个问题暗示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;
}
Run Code Online (Sandbox Code Playgroud)

但是,这不会带来任何问题,因为您需要特别小心何时专门释放该字符串指针以及何时静态分配(文字char数组).

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

当然,似乎使用零终止字符串是推荐的做法,因为标准库通常不会将字符串长度作为参数,并且因为提取长度char * s = "abc"不像我的示例所示那样简单.

  • 即使存储长度,也不应允许包含嵌入空值的字符串.这是基本的常识.如果您的数据中可能包含空值,则不应将其与期望字符串的函数一起使用. (2认同)

sup*_*cat 5

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

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


Bra*_*don 5

"即使在32位机器上,如果你允许字符串是可用内存的大小,长度前缀字符串只比空终止字符串宽三个字节."

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

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

  • 我引用了你的部分问题,因为在我看来,它低估了效率问题.内存要求增加一倍或四倍(分别为16位和32位)可能会带来很大的性能损失.长串可能很慢,但至少它们是受支持的并且仍然有效.我的另一点,关于对齐,你根本就没有提到. (2认同)