3

我对 C++ 比较陌生,但我仍在掌握 C++ 标准库。为了帮助从 C 转换,我想std::string使用 printf 样式格式化程序格式化 a。我意识到stringstream这是一种更类型安全的方法,但我发现自己发现 printf 样式更容易阅读和处理(至少目前是这样)。这是我的功能:


using namespace std;

string formatStdString(const string &format, ...)
{
    va_list va;
    string output;
    size_t needed;
    size_t used;

    va_start(va, format);
    needed = vsnprintf(&output[0], 0, format.c_str(), va);
    output.resize(needed + 1); // for null terminator??
    va_end(va);    

    va_start(va, format);
    used = vsnprintf(&output[0], output.capacity(), format.c_str(), va);
    // assert(used == needed);
    va_end(va);

    return output;
}

这行得通,有点。我不确定的几件事是:

  1. 我是否需要为空终止符腾出空间,还是没有必要?
  2. capacity()在这里调用正确的函数吗?我一直在想length()会返回 0,因为字符串中的第一个字符是 a '\0'

偶尔在将此字符串的内容写入套接字时(使用它的c_str()and length()),我在接收端弹出空字节,这引起了一些悲伤,但它们似乎不一致。如果我根本不使用此功能,则不会出现空字节。

4

7 回答 7

13

使用当前标准(此处即将发布的标准不同)无法保证由 管理的内部内存缓冲区std::string将是连续的,或者该.c_str()方法返回指向内部数据表示的指针(允许实现生成连续的 read-只为该操作阻塞并返回一个指针。指向实际内部数据的指针可以使用.data()成员方法检索,但请注意,它还返回一个常量指针:即,它不适合您修改内容。由它返回的缓冲区.data()不一定是空终止的,实现只需要保证在c_str()调用时空终止,所以即使在实现中.data().c_str()被调用时,实现可以在\0调用后者时添加到缓冲区的末尾。

该标准旨在允许绳索实现,因此原则上做您正在尝试的事情是不安全的,并且从标准的角度来看,您应该使用中间std::vector(保证连续性,并且有保证&myvector[0]是指向实际缓冲区的第一个分配块)。

在我知道的所有实现中,由处理的内部内存std::string实际上是一个连续的缓冲区,并且使用.data()是未定义的行为(写入常量变量),但即使不正确它也可能有效(我会避免它)。您应该使用为此目的而设计的其他库,例如boost::format.

关于空终止。如果您最终决定遵循未定义的路径...您将需要为空终止符分配额外的空间,因为库会将其写入缓冲区。现在,问题在于,与 C 风格的字符串不同,std::strings 可以在内部保存空指针,因此您必须缩小字符串的大小以适应从一开始就包含 no 的最大连续内存块\0。这可能是您在使用虚假空字符时发现的问题。这意味着vsnprintf必须遵循使用(或系列)的坏方法str.resize( strlen( str.c_str() ) )来丢弃第一个之后的字符串的所有内容\0

总的来说,我建议不要使用这种方法,并坚持要么习惯 C++ 格式化方式,要么使用第三方库(boost 是第三方库,但它也是最标准的非标准库),要么使用向量或管理内存就像在 C 中一样......但最后一个选项应该像瘟疫一样避免。

// A safe way in C++ of using vsnprintf:
std::vector<char> tmp( 1000 ); // expected maximum size
vsnprintf( &tmp[0], tmp.size(), "Hi %s", name.c_str() ); // assuming name to be a string
std::string salute( &tmp[0] );
于 2010-05-21T07:59:58.353 回答
5

boost::format如果您更喜欢printf()流,请使用。

编辑:为了清楚起见,实际上我完全同意 Alan,他说你应该使用流。

于 2010-05-21T07:47:32.880 回答
2

我认为无法保证 &output[0] 引用的字符串布局是连续的,并且您可以写入它。

改用 std::vector 作为缓冲区,从 C++03 开始​​保证具有连续存储。

using namespace std;

string formatStdString(const string &format, ...)
{
    va_list va;
    vector<string::value_type> output(1); // ensure some storage is allocated
    size_t needed;
    size_t used;

    va_start(va, format);
    needed = vsnprintf(&output[0], 0, format.c_str(), va);
    output.resize(needed); // don't need null terminator
    va_end(va);    

    // Here we should ensure that needed != 0
    va_start(va, format);
    used = vsnprintf(&output[0], output.size(), format.c_str(), va); // use size()
    // assert(used == needed);
    va_end(va);

    return string(output.begin(), output.end());
}

注意:您必须为向量设置初始大小,因为语句 &output[0] 可能会尝试引用不存在的项目(因为内部缓冲区可能尚未分配)。

于 2010-05-21T08:01:50.753 回答
0

std::string 类为您处理空终止符。

但是,正如所指出的那样,由于您将 vnsprintf 用于原始的底层字符串缓冲区(C 时代错误很难...),您必须确保空终止符有空间。

于 2010-05-21T07:31:11.717 回答
0

1)您不需要为空终止符腾出空间。
2) capacity() 告诉您字符串在内部保留了多少空间。length() 告诉你字符串的长度。你可能不想要容量()

于 2010-05-21T07:32:55.270 回答
0

我对函数的变量参数列表的实现是这样的:

std::string format(const char *fmt, ...)
{
  using std::string;
  using std::vector;

  string retStr("");

  if (NULL != fmt)
  {
     va_list marker = NULL;

     // initialize variable arguments
     va_start(marker, fmt);

     // Get formatted string length adding one for NULL
     size_t len = _vscprintf(fmt, marker) + 1;

     // Create a char vector to hold the formatted string.
     vector<char> buffer(len, '\0');
     int nWritten = _vsnprintf_s(&buffer[0], buffer.size(), len, fmt,
marker);

     if (nWritten > 0)
     {
        retStr = &buffer[0];
     }

     // Reset variable arguments
     va_end(marker);
  }

  return retStr;
}
于 2010-05-21T08:17:31.677 回答
0

为了帮助从 C 转换,我想使用 printf 样式格式化程序格式化 std::string。

只是不要:(

如果这样做,您实际上并不是在学习 C++,而是在使用 C++ 编译器编写 C。这是一种不好的心态,不好的做法,它传播了std::o*stream创建类来避免的问题。

我意识到 stringstream 是一种更安全的方法,但我发现自己发现 printf 样式更容易阅读和处理(至少目前是这样)。

这不是一种类型安全的方法。这是一种类型安全的方法。更重要的是,它最大限度地减少了依赖关系,减少了您必须跟踪的问题数量(例如显式缓冲区分配和跟踪空字符终止符),并且更容易维护您的代码。

除此之外,它是完全可扩展/可定制的:

  • 您可以扩展语言环境格式

  • 您可以为自定义数据类型定义 i/o 操作

  • 您可以添加新类型的输出格式

  • 您可以添加新的缓冲区 i/o 类型(例如使 std::clog 写入窗口)

  • 您可以插入不同的错误处理策略。

std::o*stream类家族非常强大,一旦你学会正确使用它,毫无疑问你不会回去。

除非您有非常具体的要求,否则您的时间可能会花在学习 o*stream 类上,而不是用 C++ 编写 printf。

于 2010-05-21T08:32:16.490 回答