将一个 vector<int> 转换为字符串

110

我有一个整数向量 vector<int>(例如 {1,2,3,4}),我想将其转换为形式如下的字符串:

"1,2,3,4"

在C++中最简洁的方法是什么? 在Python中,我会这样做:

>>> array = [1,2,3,4]
>>> ",".join(map(str,array))
'1,2,3,4'

1
紧密相关:http://stackoverflow.com/questions/4850473/pretty-print-c-stl-containers - Ciro Santilli OurBigBook.com
在Haskell中,我会说"show array"。哈哈哈。 - undefined
26个回答

110

在C++中没有什么能像Python那样优雅,但绝对不像Python那样优雅。

您可以使用stringstream ...

#include <sstream>
//...

std::stringstream ss;
for(size_t i = 0; i < v.size(); ++i)
{
  if(i != 0)
    ss << ",";
  ss << v[i];
}
std::string s = ss.str();

你也可以使用 std::for_each


我想你是指的array.size()而不是v.size(),对吗? - Mark Elliot
1
无论这个向量被称为什么。 - Brian R. Bondy
3
应该这样写:std::string s = ss.str()。如果你想要一个 const char*,使用 s.c_str()。请注意,虽然 ss.str().c_str() 语法上是正确的,但它会给你一个指向临时对象的 const char*,在完整表达式结束后就会消失,这很危险。 - sbi
1
为什么不直接使用string.append呢? - Baiyan Huang
13
没有包含 #include <sstream>,答案是不完整的。 - renadeen
显示剩余2条评论

46

使用 std::for_each 和 lambda,你可以做一些有趣的事情。

#include <iostream>
#include <sstream>

int main()
{
     int  array[] = {1,2,3,4};
     std::for_each(std::begin(array), std::end(array),
                   [&std::cout, sep=' '](int x) mutable {
                       out << sep << x; sep=',';
                   });
}

请查看这个问题,其中包含我编写的一个小类。这将不会打印尾随逗号。此外,如果我们假设C++14将继续为我们提供像这样的算法的基于范围的等效物:

namespace std {
   // I am assuming something like this in the C++14 standard
   // I have no idea if this is correct but it should be trivial to write if it  does not appear.
   template<typename C, typename I>
   void copy(C const& container, I outputIter) {copy(begin(container), end(container), outputIter);}
}
using POI = PrefexOutputIterator;   
int main()
{
     int  array[] = {1,2,3,4};
     std::copy(array, POI(std::cout, ","));
  // ",".join(map(str,array))               // closer
}

12
我认为这与Python的join方法并不完全等同 - 它会在末尾额外插入一个“,”。 - 1800 INFORMATION
2
不是等价的,但同样优雅(事实上我认为更优雅,但这只是个人观点)。 - Martin York
22
显然,优雅是主观的。因此,如果你和其他两个人更喜欢更冗长、重复且无法正常运行的代码,那么它就更加优美 ;-p - Steve Jessop
1
您可以使用string::substr成员函数忽略最后一个逗号。将从0到n-1的子字符串分配给结果变量。 - Dan
@SteveJessop:更好了吗? - Martin York
@LokiAstari:这是一种改进。个人而言,我可能会编写一个算法join而不是加入OutputIterator,但它基本上是相同的。 - Steve Jessop

28

你可以使用std::accumulate。考虑以下示例

if (v.empty() 
    return std::string();
std::string s = std::accumulate(v.begin()+1, v.end(), std::to_string(v[0]),
                     [](const std::string& a, int b){
                           return a + ',' + std::to_string(b);
                     });

',' should be "," - Matt
3
@Matt,“string”类有一个重载的“+”运算符,可以接受字符。所以“','”也是可以的。 - Pavan Manjunath

23

另一种选择是使用std::copyostream_iterator类:

#include <iterator>  // ostream_iterator
#include <sstream>   // ostringstream
#include <algorithm> // copy

std::ostringstream stream;
std::copy(array.begin(), array.end(), std::ostream_iterator<>(stream));
std::string s=stream.str();
s.erase(s.length()-1);

这个功能不如Python好用。为此,我创建了一个 join 函数:

template <class T, class A>
T join(const A &begin, const A &end, const T &t)
{
  T result;
  for (A it=begin;
       it!=end;
       it++)
  {
    if (!result.empty())
      result.append(t);
    result.append(*it);
  }
  return result;
}

然后像这样使用它:
std::string s=join(array.begin(), array.end(), std::string(","));

也许你会问为什么我传入了迭代器。其实,我想反转数组,所以我这样使用它:

std::string s=join(array.rbegin(), array.rend(), std::string(","));

理想情况下,我希望将模板化到可以推断字符类型,并使用字符串流,但我还没有弄清楚。

由于这太长了,不适合作为评论,我已经发布了一个答案(https://dev59.com/anM_5IYBdhLWcg3wThV3),试图解决你最后一句话中提出的谜题。 - sbi
你的 join 函数也可以用于向量吗?能否给个例子,我是 C++ 新手。 - Noitidart
1
你能否将迭代器更改为前缀递增运算符? - Millie Smith

16

使用Boost和C++11,可以这样实现:

auto array = {1,2,3,4};
join(array | transformed(tostr), ",");

好的,几乎就是这样了。这里是完整的例子:

#include <array>
#include <iostream>

#include <boost/algorithm/string/join.hpp>
#include <boost/range/adaptor/transformed.hpp>

int main() {
    using boost::algorithm::join;
    using boost::adaptors::transformed;
    auto tostr = static_cast<std::string(*)(int)>(std::to_string);

    auto array = {1,2,3,4};
    std::cout << join(array | transformed(tostr), ",") << std::endl;

    return 0;
}

感谢Praetorian提供的帮助。

您可以像这样处理任何值类型:

template<class Container>
std::string join(Container const & container, std::string delimiter) {
  using boost::algorithm::join;
  using boost::adaptors::transformed;
  using value_type = typename Container::value_type;

  auto tostr = static_cast<std::string(*)(value_type)>(std::to_string);
  return join(container | transformed(tostr), delimiter);
};

11

这只是尝试解决1800 INFORMATION的评论对他的第二个解决方案缺乏通用性的谜语,而不是回答问题的尝试:

template <class Str, class It>
Str join(It begin, const It end, const Str &sep)
{
  typedef typename Str::value_type     char_type;
  typedef typename Str::traits_type    traits_type;
  typedef typename Str::allocator_type allocator_type;
  typedef std::basic_ostringstream<char_type,traits_type,allocator_type>
                                       ostringstream_type;
  ostringstream_type result;

  if(begin!=end)
    result << *begin++;
  while(begin!=end) {
    result << sep;
    result << *begin++;
  }
  return result.str();
}

在我的机器上可以工作(TM)。


Visual Studio 2013 对 typedef 的处理非常混乱。当然,你在2009年也不可能知道这一点。 - Grault
@Jes:我已经盯着这个问题5分钟了,但是没能找出VS可能会遇到的问题。你能具体说明一下吗? - sbi
很抱歉,我认为我尝试连接对象时没有使用<<重载,现在意识到这对您的代码不合适。我不能使用字符串向量使您的代码无法编译。顺便说一句,VS 2013社区版是免费且功能齐全的,不像“Express”版本。 - Grault
@Jes:这应该适用于任何可以流式传输的类型(即,已重载operator<<的类型)。当然,一个没有operator<<的类型可能会导致非常令人困惑的错误消息。 - sbi
很遗憾,这个无法编译:join(v.begin(), v.end(), ",")。如果sep参数是字符串字面量,模板参数推导就不能产生正确的结果。我尝试解决这个问题。同时提供了一个更现代化的基于范围的重载。 - zett42

7

有很多模板/想法。我的不是那么通用或高效,但我也遇到了同样的问题,想把这个简短而甜美的东西加入其中。它以最少的行数获胜... :)

std::stringstream joinedValues;
for (auto value: array)
{
    joinedValues << value << ",";
}
//Strip off the trailing comma
std::string result = joinedValues.str().substr(0,joinedValues.str().size()-1);

1
感谢提供简洁的代码。不过建议改为“auto &”,以避免额外的拷贝并获得更好的性能表现。 - Millie Smith
不要使用 substr(...),而是使用 pop_back() 来删除最后一个字符,这样会更加清晰和简洁。 - ifyalciner

7
string s;
for (auto i : v)
    s += (s.empty() ? "" : ",") + to_string(i);

8
欢迎来到Stack Overflow!尽管这段代码可以解决问题,但最好添加详细说明并解释它的运作方式,以便那些可能不理解这段代码的人能够理解。 - Papershine
1
当前的最佳答案并没有更详细的解释,而且这是最小/最干净的可行答案。对于大型数组来说,它不如std::stringstream高效,因为stringstream可以乐观地分配内存,导致性能为O(n.log(n)),而对于大小为n的数组,该答案的性能为O(n²)。此外,stringstream可能不会为to_string(i)构建临时字符串。 - aberaud

4

如果你想执行std::cout << join(myVector, ",") << std::endl;,你可以这样做:

template <typename C, typename T> class MyJoiner
{
    C &c;
    T &s;
    MyJoiner(C &&container, T&& sep) : c(std::forward<C>(container)), s(std::forward<T>(sep)) {}
public:
    template<typename C, typename T> friend std::ostream& operator<<(std::ostream &o, MyJoiner<C, T> const &mj);
    template<typename C, typename T> friend MyJoiner<C, T> join(C &&container, T&& sep);
};

template<typename C, typename T> std::ostream& operator<<(std::ostream &o, MyJoiner<C, T> const &mj)
{
    auto i = mj.c.begin();
    if (i != mj.c.end())
    {
        o << *i++;
        while (i != mj.c.end())
        {
            o << mj.s << *i++;
        }
    }

    return o;
}

template<typename C, typename T> MyJoiner<C, T> join(C &&container, T&& sep)
{
    return MyJoiner<C, T>(std::forward<C>(container), std::forward<T>(sep));
}

注意,此方案直接将联接操作执行到输出流中,而不是创建一个辅助缓冲区,并且适用于任何具有对ostream的operator<<的类型。
当您有一个vector<char*>而不是vector<string>时,这也可以解决boost::algorithm::join()失败的问题。

2

我喜欢1800的回答。然而,我会将第一次迭代移出循环,因为if语句的结果在第一次迭代后只会改变一次。

template <class T, class A>
T join(const A &begin, const A &end, const T &t)
{
  T result;
  A it = begin;
  if (it != end) 
  {
   result.append(*it);
   ++it;
  }

  for( ;
       it!=end;
       ++it)
  {
    result.append(t);
    result.append(*it);
  }
  return result;
}

当然,如果您愿意,这可以减少到更少的语句:
template <class T, class A>
T join(const A &begin, const A &end, const T &t)
{
  T result;
  A it = begin;
  if (it != end) 
   result.append(*it++);

  for( ; it!=end; ++it)
   result.append(t).append(*it);
  return result;
}

不应该对未知的迭代器类型使用后置递增。那可能会很昂贵。(当然,处理字符串时,这可能没有太大的区别。但一旦养成习惯...) - sbi
只要使用返回的临时值,后置递增就可以了。例如,"result.append(*it); ++it;" 几乎和 "result.append(*it++);" 一样昂贵,但后者多了一个迭代器的副本。 - iain
哎呀,我刚刚在 for 循环中发现了后置递增操作符。这是复制粘贴错误。我已经修复了它。 - iain
1
@Ian:当我教授C++时,我向我的学生灌输要使用 ++i ,除非他们真正需要 i++ ,因为这是他们不会在有区别的情况下忘记的唯一方法。(顺便说一句,这也是我的情况。)他们之前学过Java,在那里所有种类的C语言习惯都很流行,他们花了几个月(每周1次讲座+实验课)时间,但最终大多数人学会了使用预增量的习惯。 - sbi
1
@sbi:同意,我总是默认使用前增量,那个流氓的后增量是从复制别人的for循环并进行更改而来。在我的第一次回复中,我以为你担心的是“result.append(* it ++)”,而不是for循环。看到循环中的后增量有点尴尬。有些人似乎遵循不使用后增量的建议,并且即使在适当的情况下也不使用或更改它。然而,现在我知道你不属于这个类别。 - iain

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