C++中字符串初始化的性能表现

8
我可以翻译中文。以下是关于C++中字符串的问题:
1>>哪种选项更好(从性能考虑),为什么?
1.
string a;
a = "hello!";

2.

string *a;
a = new string("hello!");
...
delete(a);

2>>

string a;
a = "less"; 
a = "moreeeeeee"; 

当一个更大的字符串复制到一个较小的字符串中时,C++如何处理内存管理?C ++字符串是否可变?

你使用特定的字符串类,还是只使用标准的以 null 结尾的 C 字符串? - tsellon
我认为这个问题是关于std::string的。 - rmeador
9个回答

15

几乎从来不需要或者说是不可取的去这样做

string * s = new string("hello");

毕竟,你(几乎)从不会说:

int * i = new int(42);

你应该使用以下说法

string s( "hello" );

或者

string s = "hello";

是的,C++字符串是可变的。


9
以下是一个天真的编译器所做的事情。当然,只要不改变程序的行为,编译器可以自由进行任何优化。
string a;
a = "hello!";

首先,您需要初始化一个包含空字符串的a变量(将长度设置为0,并执行一两个其他操作)。然后,您会分配一个新值,覆盖之前已经设置的长度值。它可能还需要检查当前缓冲区的大小,以及是否需要分配更多内存。

string *a;
a = new string("hello!");
...
delete(a);

调用new需要操作系统和内存分配器找到一个空闲的内存块,这是很慢的。然后您立即对其进行初始化,因此您不会重复分配任何内容或要求缓冲区重新调整大小,就像在第一个版本中一样。 然后发生了一些不好的事情,您忘记调用delete,就会有内存泄漏,此外,字符串分配非常缓慢。所以这很糟糕。

string a;
a = "less"; 
a = "moreeeeeee";

和第一种情况一样,您首先将a初始化为空字符串。然后您分配一个新字符串,接着又分配了另一个新字符串。每个字符串的分配 可能 需要调用new来分配更多内存。每行代码还需要分配长度和可能需要分配其他内部变量。

通常,您可以像这样分配它:

string a = "hello";

一行代码,执行一次初始化,而不是先进行默认初始化,然后再分配所需的值。

这也可以最小化错误,因为你的程序中没有无意义的空字符串。如果字符串存在,则包含你想要的值。

关于内存管理,请查阅 RAII。简单来说,字符串在内部调用 new/delete 来调整其缓冲区大小。这意味着你 永远不需要 使用 new 来分配字符串。字符串对象具有固定大小,并设计为在堆栈上分配,因此当它超出范围时,析构函数会 自动 调用。析构函数确保释放任何已分配的内存。这样,你就不必在用户代码中使用 new/delete,这意味着你不会泄漏内存。


字符串a =“hello”;也可以使用显式构造函数编写 string a(“hello”);或在C++0x中: string a = {"hello"}; - Dean Michael
没错,如果没有至少三种方式做同一件事情存在歧义,那这就不是C++了,不是吗? ;) 但它们都有相同的效果。字符串是在构造函数中创建和初始化的,而不是先调用构造函数,然后再进行赋值。 - jalf

4
有没有特定的原因你一直使用赋值而不是初始化?也就是说,为什么你不写成
string a = "Hello";

等等?这避免了默认构造,从语义上讲更有意义。仅仅为了在堆上分配一个字符串而创建指向字符串的指针是没有意义的,也就是说,你的情况2没有意义,并且效率略低。

至于你最后的问题,是的,在C++中字符串是可变的,除非声明为const


2
string a;
a = "hello!";

2个操作:调用默认构造函数std:string(),然后调用operator::=运算符。

string *a; a = new string("hello!"); ... delete(a);

只有一个操作:调用构造函数std:string(const char*),但您不应忘记释放指针。

那么,对于以下情况呢? string a("hello");


你确定这些都没问题吗?你看过编译器生成的代码了吗?空构造函数真的被调用了吗? - Tim
当然,如果编译器足够聪明地确定它没有副作用,那么它可以进行优化并将声明和初始化合并在一起。但这并不是一个确定的事情。 - jalf
指针版本真的只有一个操作吗?间接寻址不会造成额外开销吗? - yungchin
我猜这取决于你所谓的“操作”,但就堆分配而言,指针版本是很糟糕的。
  • 为字符串对象分配一个块
  • 调用字符串的构造函数,该函数为内容分配一个块。 然后,“delete a”执行相反的操作(即释放这两个块)。
- Éric Malenfant
同意Éric的观点,最好使用:std::string a("hello");,它等同于std::string a = "hello"; 两者都调用const char*构造函数。 - David Rodríguez - dribeas

0
在情况1.1中,您的字符串成员(包括指向数据的指针)存储在堆栈中,并且当a超出范围时,类实例占用的内存将被释放。
在情况1.2中,成员的内存也是从堆中动态分配的。
当您将char*常量赋给一个字符串时,将realloc内存以适应新数据。
您可以通过调用string :: capacity()来查看分配了多少内存。
当您调用string a("hello")时,内存会在构造函数中分配。
构造函数和赋值操作符都会在内部调用相同的方法来分配内存并复制新数据。

0

如果您查看STL字符串类的文档(我相信SGI文档符合规范),许多方法列出了复杂度保证。我认为许多复杂度保证故意保持模糊,以允许不同的实现。我认为一些实现实际上使用了复制修改的方法,这样将一个字符串赋值给另一个字符串是一个常数时间操作,但当您尝试修改其中一个实例时,可能会产生意外的代价。不确定在现代STL中是否仍然如此。

您还应该检查capacity()函数,它将告诉您可以在给定的字符串实例中放置的最大长度字符串,然后它将被强制重新分配内存。如果您知道将来要在变量中存储大字符串,则还可以使用reserve()来导致重新分配到特定数量。

正如其他人所说,就您的示例而言,您应该真正青睐初始化而不是其他方法,以避免创建临时对象。


复制-写入技术通常不再使用,因为在多线程环境中效率变得低下。 - jalf

0

在堆内直接创建字符串通常不是一个好主意,就像创建基本类型一样。这并不值得,因为对象可以轻松地留在堆栈上,并且它具有所有所需的复制构造函数和赋值运算符,以便进行高效的复制。

std:string 本身在堆中有一个缓冲区,可能由多个字符串共享,具体取决于实现。

例如,在 Microsoft 的 STL 实现中,您可以这样做:

string a = "Hello!";
string b = a;

并且直到您更改它,两个字符串将共享相同的缓冲区:

a = "Something else!";

这就是为什么将 c_str() 存储以供以后使用非常糟糕;c_str() 仅保证在对该字符串对象进行另一次调用之前有效。

这导致非常恶劣的并发错误,如果您在多线程应用程序中使用它们,则需要通过定义关闭此共享功能。


0

很可能

   string a("hello!");

是最快的。


0
你是从Java来的吧?在C++中,对象(在大多数情况下)与基本值类型相同。对象可以存在于堆栈或静态存储中,并且可以按值传递。当您在函数中声明一个字符串时,它会在堆栈上分配字符串对象所需的字节数。字符串对象本身确实使用动态内存来存储实际字符,但这对您来说是透明的。另一件需要记住的事情是,当函数退出并且您声明的字符串不再在作用域内时,它所使用的所有内存都将被释放。无需垃圾回收(RAII是您最好的朋友)。
在您的示例中:
string a;
a = "less"; 
a = "moreeeeeee";

这会在堆栈上放置一个内存块并将其命名为a,然后调用构造函数并将a初始化为空字符串。编译器将“less”和“moreeeeeee”的字节存储在exe的.rdata部分中(我认为是这样)。字符串a将具有一些字段,例如长度字段和char *(我大幅简化了)。当您将“less”分配给a时,将调用operator =()方法。它动态分配内存以存储输入值,然后将其复制。当您稍后将“moreeeeeee”分配给a时,再次调用operator =()方法,并在必要时重新分配足够的内存来容纳新值,然后将其复制到内部缓冲区中。

当字符串a的作用域退出时,将调用字符串析构函数,并释放动态分配用于保存实际字符的内存。然后递减堆栈指针,并且保存a的内存不再处于堆栈上。


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