"&s[0]"指向std::string中连续的字符吗?

40

我正在进行一些维护工作,遇到了类似以下的内容:

std::string s;
s.resize( strLength );  
// strLength is a size_t with the length of a C string in it. 

memcpy( &s[0], str, strLength );

我知道如果是std::vector,使用&s[0]会安全,但是在std::string中这样使用是否安全?


3
使用&s[0]是可以的,memcpy()也有争议。为什么不直接赋值,或者使用字符串的assign()成员函数呢? - anon
1
@Neil Butterworth,看着这段代码,我正在问自己... ;) - oz10
随着在C++编程中的经验增加,您将越来越少地使用memsetmemcpy,并学会其中的原因。这是一个值得您积累经验的方面。 - Thomas Matthews
6个回答

50

在 C++98/03 标准下,std::string 的分配不保证是连续的,但 C++11 强制要求它是连续的。实际上,我和Herb Sutter都不知道有哪个实现不使用连续存储。

请注意,&s[0] 总是被 C++11 标准保证可以工作,即使是 0 长度字符串的情况也是如此。如果你使用 str.begin() 或者 &*str.begin() 是不能保证的,但对于 &s[0],标准定义了 operator[] 如下:

返回值:如果 pos < size(),则返回 *(begin() + pos),否则返回类型为 T 的对象引用,其值为 charT();所引用的值不得被修改

接着,data() 的定义如下:

返回值:一个指针 p,满足对于每个 i[0,size()] 中,都有 p + i == &operator[](i)

(注意范围两端的方括号)


注意:C++0x标准化之前并不保证&s[0]能够处理长度为零的字符串(实际上,它明确是未定义的行为),此问题在后来的标准草案中得到了解决,因此本答案已进行了更新。


3
Sutter在评论中表示:"目前的ISO C++确实要求&str[0]提供一个指向连续字符串数据的指针(但不一定以null结尾!)",这实际上证明了OP的用法是正确的。然而,在标准中我找不到任何关于这个的说明(至少在21.3.4 lib.string.access章节中没有)。 - James McNellis
托德,你是不是想说你和萨特都不知道有没有实现?如果你确实知道有实现,请告诉我们它的名称。 - Rob Kennedy
5
James说,几乎是因为对于s[s.length()]的空指针不必连续。 对于所有满足0 <= n < s.length() - 1的n,必须满足&s[n] + 1 == &s[n + 1]。该要求隐藏在21.3.4/1中,即s[n]必须返回与s.data()[n](对于n < length())相同的对象,而data()必须是连续的。 - Roger Pate
有关零长度字符串的信息是不正确的;实际上,最新的C++11草案中写道:“如果pos < size(),则返回*(begin() + pos),否则返回一个类型为T且值为charT()的对象的引用;所引用的值不得被修改。”因此,使用&str[0]始终是安全的。 - Matteo Italia
由于OP似乎不再在SO上活跃,我自己更新了答案,让一个常见问题的高票和被接受的问题传播过时信息是不好的。 - Matteo Italia
显示剩余4条评论

9
这是安全的。我认为大多数答案在标准更改之前都是正确的。引用C++11标准中的《basic_string general requirements [string.require]》第21.4.1.5节,它说:

一个basic_string对象中的字符对象必须被连续存储。也就是说,对于任何basic_string对象s,当0<= n < s.size()时,&*(s.begin() + n) == &*s.begin() + n应该成立。

在此之前,它说所有迭代器都是随机访问迭代器。这两个部分都支持你所问的用法。(另外,Stroustrup显然在他的最新书中使用了它;))

很可能这个变化是在C++11中完成的。我似乎记得同样的保证也是在那个版本中添加给向量的,它还获得了非常有用的data()指针。

希望对你有所帮助。


2
这个问题是针对 C++11 之前的(已经打了标签)。你是正确的,C++11 正式确保了这样做的安全性。 - oz10

7

从技术上讲,不是的,因为并不要求 std::string 在内存中连续存储其内容。

然而,在几乎所有实现中(我所知道的每个实现),内容都是连续存储的,这样做是可行的。


你能否确定一些不能正常工作的实现方式? - Rob Kennedy
2
不行。但是如果你想的话,你可以实现这样的功能。 - James McNellis
啊,抱歉,我的大脑有点混乱了 - 我想到的是向量,而不是字符串。对不起,给大家带来麻烦了。 - anon
@JamesMcNellis:我不明白为什么 string 不需要在内存中连续存储其内容(即在 C++11 之前必须要求的)。如果字符串是不连续的,那么 datac_str 是否无法在常数时间内实现呢? - user541686
@Mehrdad:在C++03中,没有要求datac_str具有常数时间复杂度。此外,C++03 §21.3/5指出,datac_str都可能使序列中的迭代器、引用和指针失效。 - James McNellis
显示剩余2条评论

3
请注意,本问题是在2009年提出的,当时C++03标准是当前版本。本回答基于该标准,其中std :: string不保证使用连续的存储空间。由于这个问题并没有在特定平台(如gcc)的背景下提出,我不会对OP的平台做任何假设,特别是对于string是否使用连续的存储空间。

合法吗?也许,也许不是。安全吗?可能是,但也可能不是。好的代码?好吧,我们不去那里......

为什么不直接这样做:

std::string s = str;

...or:

std::string s(str);

...或:

std::string s;
std::copy( &str[0], &str[strLen], std::back_inserter(s));

...或者:

std::string s;
s.assign( str, strLen );

?


1
std::string s (str, strLen); - Roger Pate
@Downvoter:请注意,这个问题是在2009年提出的,与C++03标准有关。如果您因为质疑我的答案的技术准确性或其他原因而进行了投票,请给予反馈。 - John Dibling

2
这通常是不安全的,无论内部字符串序列是否在内存中连续存储。除了连续性之外,与std::string对象存储受控序列的方式相关的实现细节可能还有很多。
一个真正的实际问题可能是以下情况。std::string的受控序列不需要存储为零终止字符串。然而,在实践中,许多(大多数?)实现选择通过1来超额配置内部缓冲区,并将序列存储为零终止字符串,因为这简化了c_str()方法的实现:只需返回指向内部缓冲区的指针即可。
你在问题中引用的代码没有任何努力将数据零终止复制到内部缓冲区中。很可能它根本不知道是否需要为这个std::string实现进行零终止。很可能它依赖于调用resize后内部缓冲区被填充为零,因此实现分配给零终止符的额外字符方便地预先设置为零。所有这些都是实现细节,这意味着这种技术取决于一些相当脆弱的假设。
换句话说,在某些实现中,你可能必须使用strcpy而不是memcpy来强制将数据复制到受控序列中。而在一些其他实现中,你可能必须使用memcpy而不是strcpy。

1
在调用 resize 后,您可以相当确定内部字符串是否符合实现要求的空终止。最终,在调用 resize 后,您必须拥有一个有效的 n 字符串(根据需要填充零字符)。- 然而,这表明对 std::string 类的理解不足:memcpy 被使用时要么是出于无知,要么是出于错误的性能优化尝试(由于 resize 调用,代码最终将两次向缓冲区分配值)。 - UncleBens
@UncleBens:我不理解你的第一句话。无论如何,是的,语言标准保证大小增加的resize调用用零填充字符串。但是,标准仅保证填充到请求的大小(在这种情况下为strLength),但对于额外字符,如果实现分配了一个,则标准没有保证。 - AnT stands with Russia
从C++11开始,当字符串不为空时,内部缓冲区必须以空字符结尾,因为data()c_str()必须返回相同的缓冲区,并且c_str()必须始终返回指向以空字符结尾的缓冲区的指针(当为空时,data()允许返回nullptr)。在C++11之前,内部缓冲区不必须以空字符结尾(甚至不连续),但大多数实现都是这样做的,因为它简化了c_str()的实现。 - Remy Lebeau

0
代码可能能够工作,但更多的是侥幸而非判断,它对实现做出了一些不被保证的假设。我建议在这个毫无意义的过于复杂的情况下,确定代码的有效性是无关紧要的,因为它很容易简化为:
std::string s( str ) ;

或者如果要分配给现有的std::string对象,只需:

s = str ;

然后让std::string本身确定如何实现结果。如果你要诉诸这种无稽之谈,那么你可能就不应该使用std::string,而应该坚持使用C字符串,因为你重新引入了与C字符串相关的所有危险。


我其实不能确定被赋值的字符串是否以空字符结尾。所以我能做的最好的可能就是 s.assign(ptr, ptrLength);,但我认为这仍然是一种改进。 - oz10
使用构造函数形式:std::string s (str, strLen); - GManNickG

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