尽管我很喜欢C和c++,但我还是忍不住对空结尾字符串的选择抓耳挠脑:
Length prefixed (i.e. Pascal) strings existed before C
Length prefixed strings make several algorithms faster by allowing constant time length lookup.
Length prefixed strings make it more difficult to cause buffer overrun errors.
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. On 16 bit machines this is a single byte. On 64 bit machines, 4GB is a reasonable string length limit, but even if you want to expand it to the size of the machine word, 64 bit machines usually have ample memory making the extra seven bytes sort of a null argument. I know the original C standard was written for insanely poor machines (in terms of memory), but the efficiency argument doesn't sell me here.
Pretty much every other language (i.e. Perl, Pascal, Python, Java, C#, etc) use length prefixed strings. These languages usually beat C in string manipulation benchmarks because they are more efficient with strings.
C++ rectified this a bit with the std::basic_string template, but plain character arrays expecting null terminated strings are still pervasive. This is also imperfect because it requires heap allocation.
Null terminated strings have to reserve a character (namely, null), which cannot exist in the string, while length prefixed strings can contain embedded nulls.
其中一些东西比C语言出现得更晚,所以C语言不知道它们是有道理的。然而,在C语言出现之前,有些语言就已经很简单了。为什么会选择空终止字符串,而不是明显更好的长度前缀?
编辑:因为有些人问了关于我上面提到的效率点的事实(他们不喜欢我已经提供的事实),他们源于以下几点:
使用空结尾字符串的Concat需要O(n + m)时间复杂度。长度前缀通常只需要O(m)。
使用空结尾字符串的长度需要O(n)时间复杂度。长度前缀为O(1)。
Length和concat是迄今为止最常见的字符串操作。在一些情况下,以空结尾的字符串可能更有效,但这种情况发生的频率要低得多。
从下面的答案,这些是一些情况下,空终止字符串更有效:
When you need to cut off the start of a string and need to pass it to some method. You can't really do this in constant time with length prefixing even if you are allowed to destroy the original string, because the length prefix probably needs to follow alignment rules.
In some cases where you're just looping through the string character by character you might be able to save a CPU register. Note that this works only in the case that you haven't dynamically allocated the string (Because then you'd have to free it, necessitating using that CPU register you saved to hold the pointer you originally got from malloc and friends).
上面这些词都没有length和concat常见。
下面的答案中还有一个断言:
你需要把绳子的一端剪掉
但这个是不正确的——对于以null结尾的字符串和有长度前缀的字符串,它的时间是相同的。(以Null结尾的字符串只是在你想要的新结尾的地方插入一个Null,长度前缀只是从前缀中减去。)
围绕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);
}
后者会稍微便宜一点(因此是首选),因为它只需要传递一个参数而不是两个。如果被调用的方法不需要知道数组的基址,也不需要知道其中的索引,那么将这两个值组合在一起传递一个指针比分别传递值要便宜。
While there are many reasonable ways in which C could have encoded string lengths, the approaches that had been invented up to that time would have all required functions that should be able to work with part of a string to accept the base address of the string and the desired index as two separate parameters. Using zero-byte termination made it possible to avoid that requirement. Although other approaches would be better with today's machines (modern compilers often pass parameters in registers, and memcpy can be optimized in ways strcpy()-equivalents cannot) enough production code uses zero-byte terminated strings that it's hard to change to anything else.
PS——为了在某些操作上稍微降低速度,以及在较长的字符串上稍微增加一点额外开销,可以让处理字符串的方法直接接受指向字符串的指针、经过边界检查的字符串缓冲区或标识另一个字符串的子字符串的数据结构。像“strcat”这样的函数看起来像[现代语法]
void strcat(unsigned char *dest, unsigned char *src)
{
struct STRING_INFO d,s;
str_size_t copy_length;
get_string_info(&d, dest);
get_string_info(&s, src);
if (d.si_buff_size > d.si_length) // Destination is resizable buffer
{
copy_length = d.si_buff_size - d.si_length;
if (s.src_length < copy_length)
copy_length = s.src_length;
memcpy(d.buff + d.si_length, s.buff, copy_length);
d.si_length += copy_length;
update_string_length(&d);
}
}
比K&R strcat方法大一点,但它支持边界检查,而K&R方法不支持。此外,与当前的方法不同,它可以轻松地连接任意子字符串,例如。
/* Concatenate 10th through 24th characters from src to dest */
void catpart(unsigned char *dest, unsigned char *src)
{
struct SUBSTRING_INFO *inf;
src = temp_substring(&inf, src, 10, 24);
strcat(dest, src);
}
注意,由temp_substring返回的字符串的生命周期将受到s和src的生命周期的限制,后者更短(这就是为什么该方法需要传入inf——如果它是本地的,它将在方法返回时死亡)。
In terms of memory cost, strings and buffers up to 64 bytes would have one byte of overhead (same as zero-terminated strings); longer strings would have slightly more (whether one allowed amounts of overhead between two bytes and the maximum required would be a time/space tradeoff). A special value of the length/mode byte would be used to indicate that a string function was given a structure containing a flag byte, a pointer, and a buffer length (which could then index arbitrarily into any other string).
当然,K&R并没有实现任何这样的东西,但这很可能是因为他们不想在字符串处理上花费太多精力——即使在今天,许多语言在这方面似乎都相当薄弱。
Calavera是对的,但由于人们似乎没有理解他的观点,我将提供一些代码示例。
首先,让我们考虑一下C是什么:一种简单的语言,其中所有代码都可以直接转换为机器语言。所有类型都适合寄存器和堆栈,并且它不需要一个操作系统或一个大的运行时库来运行,因为它是用来编写这些东西的(考虑到今天甚至没有一个可能的竞争对手,这个任务非常适合)。
如果C语言有一个字符串类型,比如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来定义字符串:
struct {
int len; /* cannot be left implementation-defined */
char* buf;
}
此时,所有的字符串操作都需要进行两次分配,这实际上意味着您将通过一个库来进行任何处理。
有趣的是……这样的结构体在C中确实存在!它们只是不用于日常显示消息给用户处理。
所以,这就是Calavera的观点:在c语言中没有字符串类型,要对它做任何事情,你必须获取一个指针,并将其解码为指向两个不同类型的指针,然后字符串的大小就变得非常相关,而不能仅仅是“实现定义”。
现在,C可以以任何方式处理内存,并且库中的mem函数(甚至在<string.h>中!)提供了将内存作为一对指针和大小来处理所需的所有工具。C语言中所谓的“字符串”的创建只有一个目的:在为文本终端编写操作系统的上下文中显示消息。因此,空终止就足够了。
不知怎的,我把这个问题理解为C中没有编译器支持以长度为前缀的字符串。下面的例子显示,至少你可以开始你自己的C字符串库,其中字符串长度在编译时计算,使用这样的构造:
#define PREFIX_STR(s) ((prefix_str_t){ sizeof(s)-1, (s) })
typedef struct { int n; char * p; } prefix_str_t;
int main() {
prefix_str_t string1, string2;
string1 = PREFIX_STR("Hello!");
string2 = PREFIX_STR("Allows \0 chars (even if printf directly doesn't)");
printf("%d %s\n", string1.n, string1.p); /* prints: "6 Hello!" */
printf("%d %s\n", string2.n, string2.p); /* prints: "48 Allows " */
return 0;
}
然而,这不会带来任何问题,因为你需要小心什么时候特别释放字符串指针,什么时候它是静态分配的(字面字符数组)。
编辑:作为对这个问题更直接的回答,我的观点是,这是C既可以支持可用的字符串长度(作为编译时间常数)的方式,如果你需要它,但如果你只想使用指针和零终止,仍然没有内存开销。
当然,使用以零结尾的字符串似乎是推荐的做法,因为标准库一般不接受字符串长度作为参数,而且提取长度的代码不像char * s = "abc"那样简单,正如我的示例所示。
懒惰、寄存器节俭和可移植性考虑到任何语言的汇编核心,尤其是C语言,它比汇编高出一步(因此继承了大量汇编遗留代码)。
你会同意null字符在那些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计),这个“黑客”是相当不错的,可轻松移植到无寄存器处理器。
为了便于讨论,我将实现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来考虑在长度和bit-endianness注册,帕斯卡字符串的众所周知的限制255字符(0 xff)因为中存储的长度是1个字节(8位),而且你想要更长的字符串(16位- >任何)你必须考虑在一层的架构代码,这意味着在大多数情况下不相容的字符串API如果你想要更长的字符串。
例子:
One file was written with your prepended string api on an 8 bit computer and then would have to be read on say a 32 bit computer, what would the lazy program do considers that your 4bytes are the length of the string then allocate that lot of memory then attempt to read that many bytes.
Another case would be PPC 32 byte string read(little endian) onto a x86 (big endian), of course if you don't know that one is written by the other there would be trouble.
1 byte length (0x00000001) would become 16777216 (0x0100000) that is 16 MB for reading a 1 byte string.
Of course you would say that people should agree on one standard but even 16bit unicode got little and big endianness.
当然,C也有它的问题,但它不会受到这里提出的问题的影响。