尽管我喜欢C和C ++,但我还是忍不住在选择空终止字符串时不知所措:
std::basic_string
模板对此进行了一些纠正,但是期望空终止字符串的普通字符数组仍然很普遍。这也是不完美的,因为它需要堆分配。这些事情中有几个最近比C更明显,所以C对于不了解它们是有意义的。然而,在C出现之前,有几个很平常。为什么选择空终止字符串而不是明显优越的长度前缀?
编辑:由于有些人要求提供事实(并且不喜欢我已提供的事实),我们的效率点来自于以下几点: / p>
从下面的答案中,这些是空终止字符串更有效的一些情况:
以上都不像长度和连续那样常见。
下面的答案还有一个断言:
但是这个不正确 - 它与null终止和长度前缀字符串的时间相同。 (Null终止字符串只是在你希望新结束的地方粘贴一个空值,长度前缀只是从前缀中减去。)
答案 0 :(得分:190)
BCPL,B或C均不支持 人物数据强烈的 语言;每个对待字符串很多 像整数和矢量的向量 一些补充一般规则 约定。在BCPL和B中 string literal表示地址 一个用。初始化的静态区域 字符串的字符,打包成 细胞。在BCPL中,第一个打包字节 包含的字符数 字符串;在B,没有统计 和字符串由a终止 B拼写的特殊字符
*e
。这种变化是部分的 避免长度限制 持有的字符串引起的 计入8位或9位插槽,和 部分是因为保持计数 根据我们的经验,似乎更少 比使用终结者方便。
Dennis M Ritchie, C语言的发展
答案 1 :(得分:148)
C没有字符串作为语言的一部分。 C中的'string'只是指向char的指针。所以也许你问的是错误的问题。
“遗漏字符串类型的理由是什么”可能更相关。为此,我要指出C不是面向对象的语言,只有基本的值类型。字符串是更高级别的概念,必须通过某种方式组合其他类型的值来实现。 C处于较低的抽象层次。
我只想指出,我并不是说这是一个愚蠢或糟糕的问题,或者表示字符串的C方式是最好的选择。我试图澄清,如果考虑到C没有将字符串作为数据类型与字节数组区分开的机制,那么问题会更简洁。鉴于当今计算机的处理能力和内存能力,这是最佳选择吗?可能不是。但后见之明总是20/20,所有这些:)
答案 2 :(得分:100)
答案 3 :(得分:60)
我认为,它有历史原因并找到this in wikipedia:
当时的C(和那些语言 它来自于)开发, 记忆非常有限,所以使用 只有一个字节的开销来存储 一根绳子的长度很有吸引力。该 当时唯一受欢迎的选择, 通常称为“Pascal字符串” (虽然早期版本也使用过 BASIC),使用一个前导字节来存储 字符串的长度。这允许 包含NUL和制作的字符串 找到长度只需要一个 内存访问(O(1)(常数)时间)。 但是一个字节将长度限制为255。 这种长度限制更多 限制比问题 C字符串,所以C字符串一般 赢了。
答案 4 :(得分:31)
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中的所谓“字符串”仅用于一个目的:在写入用于文本终端的操作系统的上下文中显示消息。而且,为此,空终止就足够了。
答案 5 :(得分:19)
显然,为了提高性能和安全性,您需要在使用字符串时保持字符串的长度,而不是重复执行strlen
或等效字符串。但是,将长度存储在字符串内容之前的固定位置是一个非常糟糕的设计。正如Jörgen在对Sanjit的回答的评论中指出的那样,它排除了将字符串的尾部视为字符串,例如,如果不分配新的内存,就会使path_to_filename
或filename_to_extension
等许多常见操作无法实现(并导致失败和错误处理的可能性)。然后当然存在这样的问题:没有人能够同意字符串长度字段应该占用多少字节(大量不好的“Pascal字符串”语言使用16位字段甚至24位字段来排除处理长字符串)。
C让程序员选择是否/何处/如何存储长度的设计更加灵活和强大。但当然程序员必须聪明。 C惩罚愚蠢的程序崩溃,停止,,或让你的敌人扎根。
答案 6 :(得分:13)
考虑到任何语言的汇编内容,懒惰,注册节俭和可移植性,尤其是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
总共使用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位计算机上读取,懒惰程序会认为你的4字节是字符串的长度然后分配然后大量内存尝试读取那么多字节。 另一种情况是PPC 32字节字符串读取(小端)到x86(大端),当然如果你不知道一个是由另一个写,那就会有麻烦。 1字节长度(0x00000001)将变为16777216(0x0100000),读取1字节字符串为16 MB。 当然你会说人们应该就一个标准达成一致,但即使是16位的unicode也只能得到很少的大字节。
当然,C也会遇到问题,但受此处提出的问题的影响很小。
答案 7 :(得分:9)
在许多方面,C是原始的。我很喜欢它。
它比汇编语言高出一步,使用更易于编写和维护的语言为您提供几乎相同的性能。
null终止符很简单,不需要语言的特殊支持。
回过头来看,这似乎并不方便。但是我在80年代使用汇编语言,当时看起来非常方便。我只是认为软件在不断发展,平台和工具不断变得越来越复杂。答案 8 :(得分:8)
假设C实现了Pascal方式的字符串,通过长度为它们加前缀:是一个7字符长的字符串,相同的DATA TYPE是3-char字符串?如果答案是肯定的,那么当我将前者分配给后者时,编译器应该生成什么样的代码?字符串应该被截断,还是自动调整大小?如果调整大小,该操作是否应该通过锁保护以使其线程安全? C方法方面解决了所有这些问题,不管你喜欢与否:)
答案 9 :(得分: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;
}
然而,这不会带来任何问题,因为您需要特别小心何时专门释放该字符串指针以及何时静态分配(文字char
数组)。
编辑:作为问题的更直接的答案,我认为这是C可以支持字符串长度可用的方式(作为编译时常量),如果需要,但是如果你只想使用指针和零终止,仍然没有内存开销。
当然,似乎使用零终止字符串是推荐的做法,因为标准库通常不会将字符串长度作为参数,并且因为提取长度不像{{1}那么简单。 },正如我的例子所示。
答案 10 :(得分:5)
&#34;即使在32位机器上,如果允许字符串为可用内存的大小,则长度前缀字符串只比空终止字符串宽三个字节。&#34;
首先,对于短字符串,额外的3个字节可能是相当大的开销。特别是,零长度字符串现在需要4倍的内存。我们中的一些人正在使用64位计算机,因此我们要么需要8个字节来存储零长度字符串,要么字符串格式无法处理平台支持的最长字符串。
可能还有对齐问题需要处理。假设我有一个包含7个字符串的内存块,例如&#34; solo \ 0second \ 0 \ 0four \ 0five \ 0 \ 0seventh&#34;。第二个字符串从偏移量5开始。硬件可能要求32位整数在4的倍数处对齐,因此您必须添加填充,从而进一步增加开销。相比之下,C表示非常节省内存。 (内存效率很好;例如,它有助于缓存性能。)
答案 11 :(得分:4)
空终止允许基于快速指针的操作。
答案 12 :(得分:3)
还有一点尚未提及:当设计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架构而异。
答案 13 :(得分:2)
围绕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 - 作为对某些操作的轻微速度惩罚的交换,以及对较长字符串的一小部分额外开销,可能有使用字符串的方法接受指针直接指向字符串, bounds -checked 字符串缓冲区,或标识另一个字符串的子字符串的数据结构。像&#34; strcat&#34;会看起来像[现代语法]
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方法则不支持。此外,与当前方法不同,可以容易地连接任意子串,例如
/* 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
的原因in - 如果它是本地的,当方法返回时会死掉。)
就内存开销而言,最多64字节的字符串和缓冲区将有一个字节的开销(与零终止字符串相同);较长的字符串会稍微多一点(两个字节之间允许的开销量是多少,所需的最大值是时间/空间权衡)。长度/模式字节的特殊值将用于指示字符串函数被赋予包含标志字节,指针和缓冲区长度的结构(然后可以任意索引到任何其他字符串中)。
当然,K&amp; R并没有实现任何这样的事情,但这很可能是因为他们不想在字符串处理方面花费太多精力 - 这个领域即使在今天也是如此语言似乎相当贫乏。
答案 14 :(得分:2)
不是必要的理由 ,而是对长度编码的反作用力
带有长度前缀的字符串,可以节省时间,这不是真的。无论是需要提供提供的数据来提供长度,还是在编译时进行计数,还是真正为您提供必须编码为字符串的动态数据。这些大小是在算法中的某个点计算的。可以提供一个单独的变量来存储空终止字符串 的大小。这使得节省时间的比较变得毫无意义。一个只是在末尾有一个额外的NUL ...但是,如果长度编码不包含该NUL,则两者之间实际上没有区别。根本不需要算法更改。只是预传递,您必须手动设计自己,而不需要编译器/运行时为您完成。 C主要是关于手动执行操作。
长度前缀是可选的,这是一个卖点。我并不总是需要算法的额外信息,因此需要对每个字符串进行处理,这会使我的precompute + compute时间永远不会低于O(n)。 (即硬件随机数生成器1-128。我可以从“无限字符串”中提取。假设它生成的字符是如此之快。因此我们的字符串长度一直在变化。但是我对数据的使用可能并不在乎我有很多随机字节,它只想要一个下一个可用的未使用字节,它就可以在请求后尽快得到它。我可能正在设备上等待,但是我也可以预读一个字符缓冲区。不必要的计算浪费。空检查更有效。)
长度前缀可以防止缓冲区溢出吗?合理使用库函数和实现也是如此。如果我输入格式错误的数据怎么办?我的缓冲区长2个字节,但我告诉函数是7个字节! 例如:如果打算将 gets()用于已知数据,则可能需要进行内部缓冲区检查,以测试编译后的缓冲区和 malloc()调用,并且仍然遵循规范。如果将其用作未知STDIN到达未知缓冲区的管道,那么很明显,除了缓冲大小以外,其他人都无法知道,这意味着长度arg是没有意义的,在这里您还需要诸如金丝雀检查之类的东西。因此,您不能为某些流和输入添加长度前缀,而不能。这意味着长度检查必须内置在算法中,而不是打字系统不可思议的部分。 TL; DR 终止于NUL的安全永远都不会不安全,它只是通过滥用而最终结束了。
反计数器点:NUL终止对二进制文件很烦人。您要么需要在此处进行长度前缀,要么以某种方式转换NUL字节:转义码,范围重新映射等……这当然意味着更多的内存使用/减少的信息/每个字节更多的操作。长度前缀在这里主要是赢得战争。转换的唯一好处是不必编写其他函数即可覆盖长度前缀字符串。这意味着在更优化的sub-O(n)例程上,您可以使它们自动充当其O(n)等效项,而无需添加更多代码。当在NUL重弦上使用时,缺点当然是时间/内存/压缩浪费。 取决于最终要复制多少库以对二进制数据进行操作,仅使用长度前缀字符串可能有意义。也就是说,长度前缀字符串也可以做同样的事情... -1长度可以表示以NUL终止,而您可以在长度终止的内部使用NUL终止的字符串。
Concat:“ O(n + m)vs O(m)” 我假设您将m称为连接后字符串的总长度,因为它们都必须具有最少的操作数(您不能只附加到字符串1,如果必须重新分配呢?)。而且我假设n是由于预先计算而不再需要执行的神话操作。如果是这样,那么答案很简单:预先计算。 如果您坚持要始终有足够的内存而不需要重新分配,那是big-O表示法的基础,那么答案就更加简单:对分配的内存进行二进制搜索以结束对于字符串1,显然在字符串1之后有一个大的无限零样本,让我们不必担心重新分配。在那里,很容易将n记录到log(n),而我几乎没有尝试。如果您想起了log(n),那么在实际计算机上,log(n)实际上基本上只有64,这就像说O(64 + m),本质上就是O(m)。 (是的,逻辑已经被用于当今正在使用的 real 数据结构的运行时分析中。这不是胡说八道。)
Concat()/ Len()再次 :记忆结果。简单。如果可能/必要,将所有计算转换为预计算。这是一个算法决策。这不是语言的强制约束。
使用NUL终止更容易/可能通过字符串后缀。根据长度前缀的实现方式,它可能对原始字符串具有破坏性,有时甚至是不可能的。需要副本并传递O(n)而不是O(1)。
与长度前缀相比,NUL终止的参数传递/取消引用要少。显然是因为您传递的信息较少。如果您不需要长度,则可以节省很多空间并可以进行优化。
您可以作弊。它实际上只是一个指针。谁说您必须将其读取为字符串?如果要以单个字符或浮点数形式阅读该怎么办?如果您想做相反的操作并将浮点数读取为字符串怎么办?如果您小心一点,可以使用NUL端接进行操作。您不能使用长度前缀来做到这一点,因为长度数据通常不同于指针。您很可能必须逐字节构建字符串并获取长度。当然,如果您需要类似 entire 浮点数(可能在其中包含NUL)的方式,则无论如何都必须逐字节读取,但是细节由您自己决定。
TL; DR 您正在使用二进制数据吗?如果否,则NUL终止允许更多算法自由度。如果是,那么代码量与速度/内存/压缩的关系是您主要关心的问题。最好将这两种方法或备忘录结合起来。
答案 15 :(得分:1)
Joel Spolsky在this blog post中说,
这是因为发明了UNIX和C编程语言的PDP-7微处理器具有ASCIZ字符串类型。 ASCIZ的意思是“最后用Z(零)的ASCII。”
在看到所有其他答案之后,我确信即使这是真的,这只是C具有空终止“字符串”的部分原因。这篇文章非常有说服力,像字符串这样简单的东西实际上可能非常难。
答案 16 :(得分:1)
我不购买“ C没有字符串”的答案。是的,C不支持内置的高级类型,但是您仍然可以用C表示数据结构,这就是字符串。字符串只是C语言中的指针这一事实并不意味着前N个字节不能作为长度具有特殊含义。
Windows / COM开发人员将非常熟悉BSTR
类型,它完全像这样 -一个长度为前缀的C字符串,其中实际字符数据不是从字节0开始。 / p>
因此,使用空终止的决定似乎只是人们所喜欢的,而不是该语言的必要性。
答案 17 :(得分:-2)
gcc接受以下代码:
char s [4] =&#34; abcd&#34 ;;
如果我们把它当作一个字符数组而不是字符串就可以了。也就是说,我们可以用s [0],s [1],s [2]和s [3]访问它,甚至可以用memcpy(dest,s,4)访问它。但是当我们尝试使用puts(s)时,我们会变得混乱,或者更糟糕的是使用strcpy(dest,s)。