C字符串与C++字符串的效率比较

37

书籍《C++ Primer》说:

对于大多数应用程序而言,在使用字符串时,使用库字符串比使用C风格的字符串更安全,也更高效。

安全性已经被理解。那么,为什么C++字符串库更加高效呢?毕竟,在底层,字符串仍然表示为字符数组吧?

为了澄清,作者是在谈论程序员效率(已被理解)还是处理效率?


8
简单的代码 = 更高的生产力 = 更多时间优化慢速代码 = 更高的效率。 - scientiaesthete
6
我们讨论的是程序员效率还是处理效率? - James Leonard
@scientiaesthete 是的,因此它们有助于计算机程序的总能量最小化,使世界更加干净和和平 ;) - Christian Rau
2
我们也可以问它是在谈论内存效率还是处理器效率。 - hippietrail
1
此外,不管字符串使用什么编码方式,这些内容是否都适用?例如,东亚语言中使用的多字节编码或Unicode的UTF-8编码方式是否也适用? - hippietrail
8个回答

29

C-字符串通常更快,因为它们不调用 malloc/new。但是有些情况下,std::string 更快。函数 strlen() 的时间复杂度为 O(N),而 std::string::size() 的时间复杂度为 O(1)。

此外,当搜索子串时,在 C 字符串中需要在每个循环中检查 '\0',而在 std::string 中则不需要。在朴素的子串搜索算法中,这并不重要,因为您只需检查 i<s.size() 而不是检查 '\0'。但是现代的高性能子串搜索算法会以多字节步长遍历字符串。在每个字节中需要进行'\0'检查会拖慢速度。这就是 GLIBC memmem 快于 strstr 的原因。我进行了大量的算法基准测试

这不仅适用于子串搜索算法。许多其他字符串处理算法对零终止字符串的处理速度较慢。


21
C字符串何种情况下不需要调用malloc/new?当你需要一个动态大小的字符串时,你需要动态分配内存,这对于C字符串和std::string都适用。除此之外,我认为在搜索子串时,C字符串和std::string之间不应该有区别。最后,选择了50%错误答案之一,但好吧,接受哪个回答是James的决定。 - Christian Rau
3
@ChristianRau - 你需要重新阅读我的帖子。另外,你有机会对比基准测试C字符串和std::string的子字符串函数吗?我做了。 - Leonid Volnitsky
2
不,我没有进行剖析,只是在考虑搜索子字符串时,并不一定每次都需要计算长度,尽管我可能在这方面是错误的。当然,我知道O(1)长度是主要优点(其他正确答案也写了这个),但即使重新阅读,我也看不出C字符串“不调用malloc/new”,这只是胡说八道,而且这是我对你的回答的主要批评观点。 - Christian Rau
4
你可以将C字符串(或几乎任何其他类型)放入动态分配的内存中,但不是必须的。如果你动态分配C字符串,那么这将由你完成,而不是由C字符串的成员函数完成。C字符串(像int、double或任何POD类型一样)不会调用自己的malloc/new函数。 - Leonid Volnitsky
13
当然,指针本身不会分配内存。但是C字符串通常并不是“没有调用malloc/new”,因为使用简单的编译时数组不是处理字符串的通常方式,特别是在考虑到任何字符串处理算法的情况下。当然,你总可以扔个char[1024]过去就算了,但是嗯。但你是对的,在某些情况下,小的字符数组确实是合适的,并且不需要任何动态分配,但我认为这些情况远非通常情况,特别是在进行复杂的字符串处理时。 - Christian Rau
显示剩余5条评论

24
为什么C++的字符串库更高效呢?毕竟,在底层,字符串不都是以字符数组的形式表示吗?
因为使用char *或char []的代码如果没有仔细编写,则更容易导致低效率。例如,您是否见过像这样的循环:
char *get_data();

char const *s = get_data(); 

for(size_t i = 0 ; i < strlen(s) ; ++i) //Is it efficent loop? No.
{
   //do something
}

这是高效的吗?不是。函数 strlen() 的时间复杂度为 O(N),而且在上面的代码中每次迭代都要计算一次。

现在你可能会说“如果我只调用一次 strlen() 就可以使它变得高效。” 当然,你可以这样做。但你必须自己进行所有这些优化并且要有意识地去做。如果你漏掉了某些东西,就会浪费 CPU 周期。但是使用 std::string,大多数这样的优化已经由类本身完成了。所以你可以这样写:

std::string get_data();

std::string const & s = get_data(); //avoid copy if you don't need  it

for(size_t i = 0 ; i < s.size() ; ++i) //Is it efficent loop? Yes.
{
   //do something
}

这是否高效?是的。 size() 的时间复杂度为O(1),无需手动优化代码,这样往往会使代码看起来丑陋且难以阅读。与char*相比,使用std::string得到的代码几乎总是整洁清晰的。

此外,请注意,std::string不仅可以使代码在CPU周期方面更加高效,而且还可以提高程序员的效率!


5
在C语言中,通常用以下方式遍历字符串:for (int i = 0; s[i]; ++i),这种方法非常高效。但是关于字符串类存储字符串长度并能在O(1)时间内获得长度的问题,仍需讨论。 - Daniel Fleischman
3
你误读了我的回答。我在先问“你见过这种循环吗?”之后才举了那个例子,并解释了其他指针。因此,我认为这次的负评是不合理的。请注意,这里的O(N)和O(1)都是时间复杂度的表示。 - Nawaz
4
在gcc实现的C标准库中,strlen函数被打上一个属性标记,指示编译器函数调用的结果仅取决于参数(除了返回值以外没有副作用)。这使得编译器可以从循环中分离出strlen函数。尽管如此,这是实现质量,并且该优化在其他编译器/库实现中不可用。 - David Rodríguez - dribeas
3
这取决于编译器的质量,不应该依赖它来进行这样的优化。 - ratchet freak
1
for(size_t i = 0 ; i < strlen(s) ; ++i) //这是一个高效的循环吗?不是。 -- 因为这样做是完全错误的,所以被下投票了。 - Jim Balter
显示剩余9条评论

9

std::string知道它的长度,这使得许多操作更快。

例如,给定:

const char* c1 = "Hello, world!";
const char* c2 = "Hello, world plus dog!";
std::string s1 = c1;
std::string s2 = c2;
s1.length()strlen(c1)更快。在比较方面,strcmp(c1, c2)需要比较多个字符才能确定字符串不相等,但是s1 == s2可以立即判断长度不同并返回false。
其他操作也受益于预先知道长度,例如strcat(buf, c1)需要在buf中查找空终止符以确定数据追加的位置,但s1 += s2已经知道了s1的长度,可以立即将新字符追加到正确的位置。
在内存管理方面,std::string每次增长时都会分配额外的空间,这意味着未来的追加操作不需要重新分配空间。

7

有些情况下,std::string 可能比 char[] 更好。例如,C风格字符串通常没有传递显式长度,而是通过 NUL 结束符来隐式定义长度。

这意味着一个循环不断地对一个 char[] 执行 strcat 操作实际上是执行 O(n²) 的工作,因为每次 strcat 都需要处理整个字符串以确定插入点。相反,std::string 只需要执行将新字符复制到末尾的操作(可能还需要重新分配存储空间,但为了公平比较,您必须事先知道最大大小并且使用 reserve())。


我相信大多数C开发人员都足够聪明,可以自己存储大小。这样做速度大致相同,但也更容易出错并且更难理解,因为代码更多 - 更多的变量,更多的函数参数等等。 - user904963
是的。strlen() 函数曾经作为一个例子,用来说明 Shlemiel 画家算法(它有很多形式,非常容易无意中“应用”)。 - Peter Mortensen

3

字符串是包含字符数组及其大小和其他功能的对象。最好使用字符串库中的字符串,因为它们可以避免你分配和释放内存、查找内存泄漏和其他指针错误。但由于字符串是对象,所以它们会占用额外的内存空间。

C 字符串仅是字符数组。当您在实时工作且不完全知道手头有多少内存空间时,应使用它们。如果您使用 C 字符串,则需要注意内存分配,然后通过 strcpy 或逐个字符复制数据,然后在使用后取消分配等等。

因此,如果您想避免许多麻烦,请使用字符串库中的字符串。

字符串增加了程序效率,但减少了处理效率(虽然不一定)。与之相反的是 C 字符串。


3
“std::string”并不一定会降低处理效率。 - Jonathan Wakely
1
这在某种程度上与问题中的引语所说的相矛盾,而不是解释为什么引语会这样说。这不是关于头痛的问题,而是关于处理效率的问题。 - Christian Rau

3
很明显,一个简单而明显的提高运行效率的方法是将字符串长度与数据一起存储(或者至少其size方法必须为O(1),这实际上意味着相同的事情)。因此,每当您需要在C字符串中查找NUL字符(因此需要遍历整个字符串)时,您可以在常数时间内获取大小。这种情况经常发生,例如在复制或连接字符串时,并且预先分配一个新字符串,您需要知道其大小。但我不知道作者是否指的是这个,或者它在实践中是否有很大的区别,但它仍然是一个有效的观点。

1

这里是一个简短的观点。

首先,C++字符串是对象,因此在面向对象的语言中使用它们更加一致。

其次,标准库提供了许多有用的字符串函数、迭代器等。所有这些都是你不必再编写的东西,所以你节省了时间,并且可以确保这些代码(几乎)没有错误。

最后,C字符串是指针,在你刚开始学习时很难理解,而且会带来复杂性。由于在C++中引用优于指针,因此使用std::string而不是C字符串更有意义。


3
C++ 不是一种面向对象的语言。 - Jonathan Wakely
8
它不仅是一种面向对象语言。然而,在其中执行面向对象编程是完全可能的,这在我看来也是“面向对象语言”唯一合理的定义。 - John Calsbeek
2
面向对象编程是C和C++之间的基本区别。最初,C++被命名为“带类的C”。 - Coding Mash
3
@CodingMash,还有许多其他差异,比如模板,与面向对象无关。C++是一种多范式语言,而不是一种面向对象语言。你可以在C中编写面向对象的代码(付出足够的努力),但这并不意味着它是一种“面向对象语言”。 - Jonathan Wakely
2
我刚才只是说了基本的区别。你说得对,还有很多其他的东西,如模板、命名空间等等。我只是评论了“C++不是面向对象语言”的说法。它可能不是纯粹的面向对象语言,也有过程式编程范式(没有冒犯之意)。 - Coding Mash

1
C风格字符串的困难在于,除非了解包含它们的数据结构,否则无法对它们进行太多操作。例如,在使用“strcpy”时,必须知道目标缓冲区是可写的,并且有足够的空间容纳源中到第一个零字节为止的所有内容(当然,在许多情况下,人们并不确切知道这一点...)。很少有库例程提供任何支持以便根据需要分配空间,而我认为所有这些方法都是无条件地分配它(因此,如果有一个具有1000个字节空间的缓冲区,并且想要复制一个900字节的字符串,则使用这些方法的代码将不得不放弃1000字节的缓冲区,然后创建一个新的900字节缓冲区,即使更好的做法可能是简单地重用1000字节的缓冲区)。

使用面向对象的字符串类型在许多情况下可能不如使用标准C字符串高效,但需要找出最佳的分配和重用方式。另一方面,编写为最优化分配和重用字符串的代码可能非常脆弱,对需求的轻微更改可能需要对代码进行大量棘手的微调——未能完美地调整代码可能会导致明显且严重的错误或更加隐蔽但更加严重的错误。避免使用标准C字符串的代码脆性的最实用方法是非常保守地设计它。记录最大输入数据大小,截断任何过大的内容,并为所有内容使用大缓冲区。这种方法可行,但效率不高。

相比之下,如果使用面向对象的字符串类型,它们使用的分配模式可能不是最优的,但很可能比“分配所有大型内容”的方法更好。因此,它们结合了手动优化代码方法的大部分运行时效率和比“分配所有大型内容”的方法更好的安全性。


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