为什么C++中的字符串通常以'\0'结尾?

20

在许多代码示例中,人们通常在创建新的 char 数组之后使用 '\0',例如:

string s = "JustAString";
char* array = new char[s.size() + 1];
strncpy(array, s.c_str(), s.size());
array[s.size()] = '\0';

我们为什么要在这里使用'\0'


12
在C++代码中,通常不建议使用cstrings。 - anthony sottile
C字符串本质上是一个字符数组,必须以NUL结尾。否则,string.h中的函数将无法按预期工作。 - nhahtdh
3
在 C 语言中,你会经常看到这个。在 C++ 中,可能有更好的方法来完成相同的事情。 - jedwards
1
为了让编译器知道字符串已经结束。 - cppcoder
6
这不是为编译器准备的,而是为库和可能的代码准备的。C语言不很好地支持数组,你可以定义局部数组,但是无法传递它们。如果尝试传递,只会传递起始地址(第一个元素的地址)。因此,你可以将最后一个元素设为特殊符号例如 '\0' ,或者总是传递大小,注意不要弄错。我使用一组宏来传递起始地址和长度二元组。结构体是另一种方法,类是最好的方法,但是C语言没有类。 - ctrl-alt-delor
显示剩余2条评论
5个回答

47

你的问题标题提到了C字符串。C++中的std::string对象和标准C风格的字符串处理方式不同。在使用C字符串时,\0非常重要,而当我在答案中使用术语“字符串”时,我是指“标准C字符串”。

\0作为字符串终止符在C语言中起作用。它被称为“空字符”或“NUL”,标准C字符串是以它为结尾的。这个终止符通知处理字符串的代码 - 标准库以及你自己的代码 - 字符串的结束位置在哪里。一个很好的例子是strlen,它返回字符串的长度:strlen的工作原理是基于字符串以\0结尾的假设。

当你声明一个常量字符串时:

const char *str = "JustAString";

如果你处理的是常量字符串,那么\0会自动添加。但如果像你的数组示例一样处理非常量字符串,有时需要自己处理它。在你的示例中使用的 strncpy 的文档是一个很好的例子:strncpy 复制了空终止字符除外,当指定长度达到之前就复制了整个字符串。因此,通常会看到将 strncpy 与可能多余的空终止符赋值结合在一起。为了解决由于忽略处理此情况而引起的潜在问题,设计了 strlcpystrcpy_s

在你的特定示例中,array[s.size()] = '\0';是一种冗余:由于array 的大小是 s.size() + 1,而 strncpy 正在复制 s.size() 个字符,函数将附加 \0

标准 C 字符串实用程序的文档将指示何时需要小心包括这样的空终止符。但要仔细阅读文档:与 strncpy 一样,细节很容易被忽略,导致潜在的缓冲区溢出。


那么,在C++中字符串是如何终止的呢?我发现它们不是以NULL结尾的,因为在任意索引处添加'\0'并不会像在C语言中那样截断字符串,而只会将该索引处的字符替换为空字符。 - CaptainDaVinci
1
@CaptainDaVinci,它们不一定被终止,因为长度是在内部存储的。如果您调用c_str(),那么您将获得一个正确终止的缓冲区,但这只是因为您礼貌地请求了。 - tadman
@tadman 除了始终在字符串对象的内部字节数组末尾保留 NUL 结束符字节之外,std::string 是否有其他有效的实现方式来实现 c_str() 方法? - Jeremy Friesner
@JeremyFriesner,你还有什么其他方法可以完成这件事吗?实际上,std::string 可能会分配比请求的数量稍微多一点,这是实现定义的,因此可能已经存在零填充。 - tadman
@tadman同意 - 我能想象的另一种方法就是在调用c_str()时动态分配一个单独的缓冲区,其中包含NUL终止符,并返回该缓冲区...但是对于长字符串来说,这当然会非常低效,并可能导致内存泄漏。因此,我的假设是每个生产就绪的std::string实现实际上都只是在内部存储了一个以NUL结尾的字符串,以便c_str()可以简单地返回指向它的指针。 - Jeremy Friesner
@JeremyFriesner 实现c_str()时进行分配并不是非常理想,因此可能不会这样做。如果您处于无法假设的位置,请进行测试,并探索在长度从1到1GB变化时它的行为如何,因为某些大小可能是“有问题的”。 - tadman

16

为什么C++中的字符串通常以'\0'结尾?

需要注意的是,C++字符串和C字符串不同。
在C++中,字符串指的是模板类std::string,提供了许多直观的函数来处理字符串。
需要注意的是,C++的std::string不是以\0结尾的,但该类提供了函数以获取底层的\0结尾的C风格字符串。

在C语言中,字符串是一组字符。这个集合通常以\0结束。
除非使用像\0这样的特殊字符,否则就无法知道字符串何时结束。
它也准确地称为字符串结束符。

当然,可能有其他方式来跟踪字符串的长度,但使用特殊字符具有两个明显的优点:

  • 更加直观
  • 没有额外的开销

需要注意的是,\0是必需的,因为大多数标准C库函数都假定它们操作的字符串是以\0结尾的。
例如:
如果你有一个不以\0结尾的字符串,而你使用printf()函数,则printf()会持续向stdout写入字符,直到遇到\0为止。

为什么我们要在这里使用'\0'

有两种情况不需要使用\0终止字符串:

  • 如果您在任何用途中都显式地跟踪字符串的长度,或者
  • 如果您使用某些标准库API将自动将\0添加到字符串中。

在您的情况下,第二种情况已经适用于您。

array[s.size()] = '\0';

在您的示例中,上述代码语句是多余的。

对于您的示例,使用strncpy()使其变得无用。 strncpy()s.size()个字符复制到您的array中,并在复制字符串后追加空终止符(null termination)。由于array的大小为s.size() + 1,因此\0会自动添加。


1
不一定。您也可以通过在某个地方保留长度(就像Java的工作方式一样..我假设)来存储任意长度的数组。 - Brendan Long
@BrendanLong:希望这回答了您的问题。 - Alok Save
@BrendanLong 我假设在那条评论之后进行了编辑,但是正如指出的那样,它会消除额外的开销。按照你建议的方式执行,你需要创建一个带有 int 和数组的结构体,这将导致更差的性能和更多的内存消耗。 - evanmcdonnal
2
@evanmcdonnal 更多的开销,是的,但是认为空指针没有“开销”这个想法是不正确的——它是一个额外的字符(1-4字节)。如果您使用UTF32(出于某种原因),那么它们的大小将完全相同。在任何需要查找长度的情况下,存储长度也要快得多(因为使用空终止符,您需要遍历整个字符串才能确定其长度)。我只是想指出这并不是“一种方式显然更好”。值得注意的是,C++为字符串和向量存储长度。 - Brendan Long
1
我也不同意“更直观”的观点,因为像使用哨兵值一样,在存储数据长度方面似乎对我来说同样直观。 - Brendan Long
@BrendanLong 这是C语言,所以最可能是ASCII码,因此只需要1个字节,节省了3个字节。此外,它还可以节省您在字符串增长时必须执行的所有递增操作。长度点是一个不错的点,也许甚至可以平衡它(取决于您有多频繁地关心长度)。另外,我同意你提到的直觉性。我更喜欢for循环而不是while循环,这将允许使用所有for循环。 - evanmcdonnal

6

'\0'是空字符终止符。如果您的字符数组没有它,而您尝试进行strcpy,则会发生缓冲区溢出。许多函数依赖于它来知道何时需要停止读取或写入内存。


4
strncpy(array, s.c_str(), s.size());
array[s.size()] = '\0';

为什么我们要在这里使用 '\0'?
实际上不需要,第二行是浪费空间的。如果你知道如何使用strncpy,它已经自动添加了空终止符。代码可以重写为:
strncpy(array, s.c_str(), s.size()+1);

strncpy 是一个有点奇怪的函数,它假设第一个参数是大小为第三个参数的数组。所以只有在复制字符串后还有剩余空间时才会复制空结束符。

在这种情况下,你也可以使用 memcpy() ,尽管这可能会使代码更难以理解,但它会略微提高效率。


另一方面,strncpy 的奇怪行为可能使代码比直接使用 memcpy 不够直观。但是当我看到上面展示的代码时,我的第一反应通常是检查是否可以通过直接使用 c_str() 内容来完全避免将数据复制到数组中,因为最终的零经常添加到之后不会被修改的字符串(输出字符串)中。 - kriss
如果你想复制到第一个 \0,可以使用 strcpy(array, &s[0]);。这个长度是 std::strlen(&s[0])+1 个字符。如果你想复制到第一个 \0 并用 \0 填充剩余部分,可以使用 strncpy(array, &s[0], s.size()+1);。如果你想从 &s[0] 复制给定大小,可以使用 memcpy(array, &s[0], s.size()+1);。(因此嵌入的 \0 不会清除字符串的其余部分) - Puddle

2
在C语言中,我们用一个字符数组(或w_char)来表示字符串,并使用特殊字符来标志字符串的结尾。与Pascal不同的是,Pascal在数组的索引0中存储了字符串的长度(因此字符串有字符数的硬性限制),而在C语言中,理论上没有字符串(表示为字符数组)可以包含的字符数限制。
在C语言默认库和其他库中,特殊字符被期望为NUL,并且所有使用字符串精确长度的库函数将以NUL作为字符串的结束符。如果要使用这样的库函数,就必须以NUL终止字符串。你完全可以定义自己的结束字符,但你必须明白,涉及字符串(表示为字符数组)的库函数可能会出现意想不到的错误。
在代码片段中,需要显式设置终止字符为NUL,因为你不知道分配的数组中是否有垃圾数据。这也是一种好的编程习惯,因为在大型代码中,你可能看不到字符数组的初始化。

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