如何制作一个非以null结尾的C字符串?

9

我想知道:如果char *cs = .....;指向一个巨大的内存块,但其中没有'\0',那么strlen()和printf("%s",cs)会发生什么? 我写下了以下代码:

 char s2[3] = {'a','a','a'};
printf("str is %s,length is %d",s2,strlen(s2));

我得到的结果是:"aaa","3",但我认为这个结果是因为空字符(或0字节)恰好驻留在s2 + 3位置造成的。 如何制作一个非空结尾的C字符串?strlen和其他C字符串函数严重依赖于'\0'字节,如果没有'\0',我只想更深入地了解这个规则。 备注:我对在SO上研究以下帖子产生了好奇心。 如何将const char *转换为std :: string 以及该帖子中的这些话: “实际上这比看起来要棘手,因为除非字符串实际上是nul终止的,否则无法调用strlen。”

3
你的代码存在未定义行为。如果你不想要空值,就需要指定一个长度,因此简而言之使用 std::string - chris
你是在问C还是C++?它们是非常不同的语言,有不同的选项来避免或处理这种情况。 - Mike Seymour
1
strlen和and..都是使用以null结尾的C字符串。如果您想要,可以编写自己的字符串库及其函数,例如std::string类,bstr。 - qwr
如果cs指向一个巨大的内存块,但其中没有'\0',那么你无法确定是否存在零,因为你并不真正拥有那个额外的内存。 - Jongware
2
你的问题标题本身就是一个自相矛盾的说法。在C语言中,字符串就是以空字符结尾的char数组。 - Jens Gustedt
显示剩余2条评论
7个回答

27

如果它不是以空字符结尾的,那么它就不是C字符串,你不能使用像 strlen 这样的函数 - 它们会超过数组的末端,导致未定义的行为。您需要通过其他方式来跟踪长度。

只要给出长度,您仍然可以使用printf打印非终止字符数组:

printf("str is %.3s",s2);
printf("str is %.*s",s2_length,s2);

或者,如果您有访问数组本身而不是指针的权限:

printf("str is %.*s", (int)(sizeof s2), s2);

你还给这个问题打上了C++的标签:在这种语言中,通常希望避免所有这些容易出错的麻烦,而是使用std::string


4
使用带有字符串长度参数的 printf 得到了 +1 的赞,我完全忘记了这一点。而“容易出错的胡说八道”让我差点喷出咖啡。 - Floris

10

“C字符串”根据定义是以空字符结尾的。名称源自于C语言约定以空字符结尾表示字符串。如果你想要别的形式,那就不是C字符串。

因此,如果你有一个没有以空字符结尾的字符串,就无法使用C字符串操作函数。你不能使用strlen, strcpy或者strcat,基本上任何接受char*但没有单独长度参数的函数都无法使用。

那么你可以做什么呢?如果你有一个没有以空字符结尾的字符串,你需要单独存储字符串的长度。(如果你没有这个长度信息,那就只能黔驴技穷了。你需要某种方式找到字符串的长度,要么通过终止符,要么通过单独存储长度信息。) 你可以分配一个适当大小的缓冲区,将字符串复制到其中,并附加一个空字符。或者你可以编写自己的一套工作于指针和长度的字符串操作函数。在C++中,你可以使用std::string的构造函数,该函数接受一个char*和一个长度,而不需要终止符。


strncpy?。它仍然限制了其有用性,最好使用memcpy代替,因为它更明确地适用于非空终止的数据缓冲区(这正是OP想要的)。 - gbjbaanb

6
您的猜测是正确的:您的strlen返回了正确的值,但这只是出于纯粹的运气,因为在您未正确终止字符串的情况下,堆栈上恰好有一个零。字符串长度为3个字节,编译器可能会将堆栈上的内容对齐到4字节边界,这可能有所帮助。
您不能依赖这种情况。C语言字符串需要以NUL字符(零)结尾才能正常工作。C字符串处理很混乱,容易出错。虽然有一些库和API可以帮助减少错误,但仍然很容易搞砸。 :)
在这种特殊情况下,您的字符串可以初始化为以下之一:
A: char s2[4] = { 'a','a','a', 0 }; // 如果字符串必须为3个字符长,则为好方法 B: char *s2 = "aaa"; // 如果创建后不需要修改字符串,则为好方法 C: char s2[]="aaa"; // 如果需要在之后修改字符串,则为好方法 此外,请注意声明B和C是“更安全”的,因为如果稍后有人更改字符串声明以更改其长度,则B和C仍然自动正确,而A则取决于程序员记得更改数组大小并保持显式的空终止符。

1
或者 char s[]="aaa";,这样你就可以修改字符串了... 对于“纯粹的运气/对齐”的评论加1。 - Floris
1
@floris 对于 s[]="aaa" 的声明,你提出了一个很好的观点 - 感谢你的补充。 - JVMATL
@jongware 很有趣!你知道哪些编译器可以在调试构建时执行此操作吗?(我宁愿他们用随机垃圾填充堆栈,使有缺陷的程序在开发期间崩溃,而不是在现场出问题。) - JVMATL
@Jongware:“被完全的误解所扭曲”- 我同意。但这也是为什么提出问题的原因...为了增进理解(或减少误解)。 - Floris
@JVMATL:嗯,好问题。在SO上曾经提到过,但没有解释编译器如何知道它是Debug Build...(也许是#ifdef DEBUG)。这可能值得单独提出一个问题。 - Jongware
显示剩余3条评论

4
strlen函数会一直读取内存值,直到最终遇到一个null为止。它会假设这是终止符并返回可能非常大的长度。如果您在期望使用C字符串的环境中使用strlen,则可能会将这个巨大的数据缓冲区复制到另一个不够大的缓冲区中,从而导致缓冲区溢出问题,或者最好的情况下,您只能将大量垃圾数据复制到您的缓冲区中。
将未以null结尾的C字符串复制到std::string中会发生这种情况。如果您随后决定知道此字符串仅长3个字符并且舍弃其余部分,则仍将具有包含前3个有效字符和大量浪费的极长std::string。这是低效的。
道德是,如果您正在使用CRT函数操作C字符串,则它们必须以null结尾。这与任何其他API没有区别,您必须遵循API为正确使用设置的规则。
当然,如果您始终仅使用特定长度版本(例如strncpy),则可以使用CRT函数,但您将不得不仅限于这些函数,并手动跟踪正确的长度。

1
约定规定带有终止符\0的字符数组是以null结尾的字符串。这意味着所有的str*()函数都期望在char数组的末尾找到一个null终止符。但这只是一个约定而已。
按照惯例,字符串也应该包含可打印字符。
如果你像这样创建一个数组 char arr[3] = {'a', 'a', 'a'}; ,那么你就创建了一个字符数组。由于它没有以\0结尾,所以在C中它不被称为字符串,尽管它的内容可以被输出到stdout。

strncpy 不需要一个空终止符。 - Samuel Edwin Ward
从手册中@SamuelEdwinWard:“如果src的长度小于n,(…)”。在C语言中,字符串的长度是通过找到第一个\0来确定的。因此,即使在这里也遵守了惯例。来源:man strncpy - RedX
它会在空字节处停止复制,但如果没有空终止符也可以正常工作,并始终写入 n 个字节。 - Samuel Edwin Ward

0

C标准在第七章库函数之前并没有定义术语“字符串”。C11 7.1.1p1中的定义如下:

  1. 字符串是由以及包括第一个空字符终止的连续字符序列。

(强调是我的)

如果字符串的定义是以空字符终止的字符序列,则未以空字符终止的非空字符序列不是字符串,这就是事实。


嗯,是的,但这已经在六年前说了大约10次。 - user207421
@user207421 对不起,我错了。请展示标准中直接引用的地方和链接。 - Antti Haapala -- Слава Україні

-1
你所做的是“未定义行为”。 你试图写入一个不属于你的内存位置。 将其更改为:
char s2[] = {'a','a','a','\0'};

3
你也没有正确理解原帖的意图。他故意没有给字符串添加结束符,因为他在询问非以空字符结尾的字符串。 - Eregrith
不当的写操作意味着写入不属于你的内存 - 也就是说,你正在覆盖另一个变量(或更糟糕的是一些代码)使用的内存。可以这样想...“不知怎么回事,账户总额变得很大。我只是将'fred'写入成员变量,账户价值就改变了。WTF?!”这就是不当的写操作所导致的问题。 - gbjbaanb
更具体地说,我认为问题代码中没有任何不当的写入。 - Samuel Edwin Ward
@ Eregrith.tks,你理解了我的意图,我只是想制作这个场景。 - basketballnewbie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接