向量 vs 字符串

25

在C++中,std::vector和std::basic_string之间有什么本质区别(如果有的话)?


1
查看文档,它们有不同的接口。如果您指定了实际解决的问题,答案也可以更具体。 - Gene Bushuyev
1
@Gene:它们确实具有不同的接口,但两者都实现了成为STL序列容器所需的一切必要条件。 - Billy ONeal
1
@Gene:我并没有解决任何特定的问题,我只是好奇为什么在不同的情况下应该选择其中之一:我并不认为存在一些额外的字符串方法是基本的。我也不认为性能是基本的。然而,迭代器的有效性肯定是重要的。而且我有一个模糊的怀疑,字符串的数据类型必须具有“类似于零”的值,以放置在data()方法的末尾(从traits thingy中获得)。 - Yttrill
4
FYI:string最初并不是STL容器。尽管Pete Becker反对,但整个委员会决定将其作为一个容器。这使它像vector一样,并消除了许多优化的可能性。回想起来,我认为Pete Becker实际上是正确的。 - Yttrill
实际上,考虑到C++11的限制,我预计主要区别在于输出到适当流时字符串不会被转换,即元素代表它们自己并且连续输出。而向量可能会打印“vector(char(63), char(64), char(65))”而不是“ABC”。 - Yttrill
显示剩余3条评论
7个回答

22
  • basic_string 不调用其元素的构造和析构函数,但 vector 调用。

  • 交换 basic_string 会使迭代器无效(启用小字符串优化),而交换 vector 不会。

  • C++03 中,basic_string 内存可能不是连续分配的,vector 总是连续的。这种差异在 C++0x [string.require] 中被移除:

    一个 basic_string 对象中的类 char 对象必须是连续存储的。

  • basic_string 具有字符串操作接口,而 vector 没有。

  • 在 C++11 之前,basic_string 可能使用写时复制策略,而 vector 不能。

相关名言:

[basic.string]:

类模板 basic_string 符合序列容器(23.2.3)、可逆容器(23.2)和支持分配器的容器的要求(表99),但 basic_string 不使用 allocator_traits::construct 和 allocator_traits::destroy 构造或销毁其元素,并且 basic_string 的 swap() 使迭代器无效。basic_string 支持的迭代器是随机访问迭代器(24.2.7)。


据我所知,那个引用不是来自“标准”,而是来自C++0x草案。这很好并值得一提,但您需要在前面加上“在C++0x中...”。 - GManNickG
@GMan: 嗯,看来你是对的。但是只有表述方式不同,C++98仍然表示它只分配和释放元素。这个变化只是一个澄清。 - Yakov Galka
那么对此是否有共识?单独的 ctor/dtor/mov/assign 操作可能比 bitblits 更加昂贵,因此有充分的理由希望字符串元素是 POD,但这是必需的吗?不能根据特征将 POD 和非 POD 的情况进行分割吗?或者至少对于常见情况如 char?这是否也适用于 vector? - Yttrill
顺便提一下,basic_string 提供的默认元素分配仅有一个命令,即 resize,它会“将新字符初始化为 CharT()”。对于那些不信的人,这里有一个例子:http://ideone.com/UjQzrM - Jonathan Mee
1
@Johnathan Mee:您使用的 basic_string 不符合规范,因此会导致未定义的行为;在这种情况下不调用复制构造函数。basic_string 仅适用于具有 char_traits<T> 的类型,并且例如 char_traits<T>::copy 不需要调用构造函数。(所有必须提供的 char_traits 特化都可以将 char_traits<T>::copy 实现为对 memcpy 的调用,因为对于 char/wchar_t/char16_t/char32_t 这是有效的) - Billy ONeal

13

basic_string给编译器和标准库的实现提供了比vector更多的自由:

  1. "小字符串优化"在字符串上是有效的,它允许实现在字符串对象中存储实际字符串而不是指向字符串的指针,当字符串很短时。大致如下:

  2. class string
    {
        size_t length;
        union
        {
            char * usedWhenStringIsLong;
            char usedWhenStringIsShort[sizeof(char*)];
        };
    };
    
  3. 在C++03中,基础数组不一定是连续的。按照 "绳索" 等方式实现 basic_string 是符合当前标准的。 (虽然没有人这样做,因为那样会使成员函数 std::basic_string::c_str()std::basic_string::data() 实现过于昂贵。)
    不过,C++11 现在禁止了这种行为。

  4. 在 C++03 中,basic_string 允许编译器/库供应商对数据使用写时复制 (可节省复制操作),而对于 std::vector 则不允许。实际上,这曾经更为普遍,但现在由于它对多线程的影响较大,所以越来越少见。不管怎样,你的代码不能依赖于是否使用了 COW 来实现 std::basic_string
    C++11 再次禁止了这种行为。

basic_string 还附带了一些辅助方法,但大多数都很简单,当然也可以轻松地在 vector 上实现。


我不喜欢把(1)作为选择或使用std::string的原因,这是标准措辞在新标准中被紧缩的意外副作用。(2)是使用std::string的一个很好的理由,因为它使得从方法中返回字符串非常高效(几乎没有成本),不幸的是,由于并行性的要求,这一点正在被移除(尽管从推荐这一点的论文中看来,rope最终将接替COW的位置(我们将等待并观察这是否奏效)。 - Martin York
@Martin:没错。另一方面,移动语义可以消除很多COW实现加速的情况 :) - Billy ONeal

6

关键区别在于std::vector应该在连续的内存中保留其数据,而std::basic_string则不必如此。因此:

std::vector<char> v( 'a', 3 );
char* x = &v[0]; // valid

std::basic_string<char> s( "aaa" );
char* x2 = &s[0];     // doesn't point to continuous buffer
//For example, the behavior of 
std::cout << *(x2+1);
//is undefined.
const char* x3 = s.c_str(); // valid

在实践中,这种差别并不那么重要。


我也不知道有这样的编译器,但你不应该指望 x2 指向连续的缓冲区,因为 C++ 标准没有给出任何保证。 - Kirill V. Lyadvinsky
2
实际上,它们都是无效的。两个对象都没有大小,因此第一个元素实际上是超出末尾的,您不能引用该元素。只有 c_str() 是定义良好的。 - Edward Strange
@Kirill:我的观点是你的例子没有展示连续缓冲区方面。如果有人将x2解释为指向单个字符的指针(而不是指向以空字符结尾的C字符串的指针),那么代码就是完全有效的。例如,有人可以执行std::cout << *x2,一切都会很好,但是std::cout << *(x2 + 1)将是无效的。@Noah:哈哈--好观点。 - Billy ONeal
1
@Noah,我更新了这个例子。重点不在于这些容器的初始化,所以我跳过了它。 - Kirill V. Lyadvinsky
@Steve:相关链接:https://dev59.com/C3E95IYBdhLWcg3wkeuq - Billy ONeal
显示剩余5条评论

3

简述: string 只优化了包含字符原语的情况,而 vector 可以包含原语或对象。

vectorstring 的主要区别在于,vector 可以正确地包含对象,而 string 仅适用于原语。因此,vector 提供了以下对于只处理原语的 string 来说是无用的方法:

  1. vector::emplace
  2. vector::emplace_back
  3. vector::~vector

即使扩展 string,也无法正确处理对象,因为它缺少析构函数。这不应被视为缺点,因为它允许在优化方面比 vector 更显著,因为 string 可以:

  1. 使用短字符串优化,可以避免堆分配,并且在少量数据没有增加存储开销的情况下进行操作。
  2. 使用char_traits,是string的模板参数之一,用于定义如何对包含的基元进行操作(其中仅实现了charwchar_tchar16_tchar32_thttp://en.cppreference.com/w/cpp/string/char_traits)。
特别相关的是 char_traits::copy, char_traits::movechar_traits::assign,这明显意味着直接赋值将被使用,而不是构造或销毁,这对于原始类型来说是更可取的。所有这些专门化对 string 的额外缺点是:
  1. 只使用charwchar_tchar16_tchar32_t原始类型。显然,大小不超过32位的原始类型可以使用相应大小的char_typehttps://dev59.com/dVsW5IYBdhLWcg3wCTaB#35555016,但对于像long long这样的原始类型,需要编写一个新的char_traits专业化,而且专业化char_traits::eofchar_traits::not_eof的想法,而不仅仅是使用vector<long long>,似乎不是最好的时间利用。
  2. 由于短字符串优化,所有会使vector迭代器无效的操作都会使迭代器无效,但string迭代器还会被string::swapstring::operator=无效。

vectorstring接口的其他差异:

  1. 没有可变的 string::data: 为什么 std::string.data() 不提供可变的 char*?
  2. string 提供了与 vector 不可用的单词处理功能:string::c_str, string::length, string::append, string::operator+=, string::compare, string::replace, string::substr, string::copy, string::find, string::rfind, string::find_first_of, string::find_first_not_of, string::flind_last_of, string::find_last_not_of, string::operator+, string::operator>>, string::operator<<, string::stoi, string::stol, string::stoll, string::stoul, string::stoull, string::stof, string::stod, string::stold, stirng::to_string, string::to_wstring
  3. 最后,无论何处 vector 接受另一个 vector 的参数,string 都接受一个 string 或一个 char*
请注意,本答案是针对C++11编写的,因此需要对string进行连续分配。

0

std::stringstd::vector之间的一个区别是,程序可以从以空字符结尾的字符串构造一个字符串,而对于向量则不行。

std::string a = "hello";          // okay
std::vector<char> b = "goodbye";  // compiler error

这通常使得字符串更易于处理。


0

向量是一种模拟数组的数据结构。在其深层内部,它实际上是一个(动态)数组。

basic_string类表示字符序列。它包含所有序列的常规操作,并且还包含标准字符串操作,例如搜索和连接。

您可以使用向量来保留任何数据类型,例如std::vector<int><float>甚至是std::vector< std::vector<T> >,但是basic_string只能用于表示“文本”。


我相信我可以找到一种方法,使一个充满双精度浮点数或其他同样邪恶的东西的basic_string实际上“工作”。 - Edward Strange
大多数实现都是基于类似于“vector”的东西来实现basic_string。只要定义了一个char_traits类,任何类型都可以与std::basic_string一起使用,即使你编写了一个char_traits<double>(如@Noah的评论中所述)。 - Billy ONeal
@Billy - 不是真的。在两者中可能会使用一些相同的技术,但我从来没有看到过以vector为基础实现的basic_string。事实上,一个非常常见的实现,即MSVC++附带的实现,非常不同,因为它们使用小字符串优化(任何足够小以适合指针的东西都只是被粘贴到指向缓冲区的指针中,而不是分配一个)。 - Edward Strange
@Noah:如果你再读一遍我的评论,它说的是“类似于向量”,而不是“向量”——这两个词很重要。(我的意思是通常是某种形式的动态数组) - Billy ONeal
不想自己回答这个问题...但是UTF-8字符串类型呢?它们不像数组那样,它们会更少吗?它们如何与basic_string配合? - Yttrill
... 我不知道有没有关于将字符以UTF-8(或任何其他可变宽度)编码存储在内存中的字符串类型的计划。从性能角度来看,这是一个不好的想法;你会失去对字符的随机访问能力。 - Karl Knechtel

0

basic_string提供了许多特定于字符串的比较选项。你说得没错,底层内存管理接口非常相似,但是string包含许多其他成员,如c_str(),对于向量来说毫无意义。


我认为没有使用小字符串优化的向量这种东西。虽然我没有去找过,但我相当确定它不存在。它不会那么有用。事实上,这两个东西是完全不同的。它们有不同的目的,因此通常实现方式非常不同,尽管一个朴素的方法在两者中可能是相似的。 - Edward Strange
@Noah:这就是我为什么说“接口”的原因。 - Puppy
哪些比较是字符串特定的?鉴于元素数据类型在两种情况下都是可变的,像词典排序比较对于向量和字符串来说都有意义。当然,不区分大小写的比较将是字符串特定的,但这样字符串就不会是多态的了。 - Yttrill

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