我必须用sprintf格式化std::string,并将其发送到文件流。我该怎么做呢?


当前回答

现代c++使得这非常简单。

C + + 20

c++ 20引入了std::format,它允许你这样做。它使用的替换字段类似于python中的替换字段:

#include <iostream>
#include <format>
 
int main() {
    std::cout << std::format("Hello {}!\n", "world");
}

代码来自cppreference.com, CC BY-SA和GFDL

查看编译器支持页面,看看它是否在您的标准库实现中可用。截至2021年11月28日,Visual Studio 2019 16.10(于2021年05月25日发布)和Clang 14(可在此处跟踪)提供了部分支持。在所有其他情况下,您可以求助于下面的c++ 11解决方案,或使用{fmt}库,它具有与std::format相同的语义。


C++11

使用c++ 11的std::snprintf,这已经成为一个非常简单和安全的任务。

#include <memory>
#include <string>
#include <stdexcept>

template<typename ... Args>
std::string string_format( const std::string& format, Args ... args )
{
    int size_s = std::snprintf( nullptr, 0, format.c_str(), args ... ) + 1; // Extra space for '\0'
    if( size_s <= 0 ){ throw std::runtime_error( "Error during formatting." ); }
    auto size = static_cast<size_t>( size_s );
    std::unique_ptr<char[]> buf( new char[ size ] );
    std::snprintf( buf.get(), size, format.c_str(), args ... );
    return std::string( buf.get(), buf.get() + size - 1 ); // We don't want the '\0' inside
}

上面的代码片段是根据CC0 1.0许可的。

逐行解释:

目的:写入一个char*使用std::snprintf,然后将其转换为std::string。

首先,使用snprintf中的一个特殊条件确定所需的char数组长度。从cppreference.com:

返回值 […如果结果字符串由于buf_size限制而被截断, 函数返回字符总数(不包括 终止空字节),如果限制为 没有实施。

这意味着所需的大小是字符数加1,因此空结束符将位于所有其他字符之后,并且可以再次被字符串构造函数截断。@alexk7在评论中解释了这个问题。

int size_s = std::snprintf( nullptr, 0, format.c_str(), args ... ) + 1;

如果发生错误,Snprintf将返回负数,因此我们随后检查格式是否按预期工作。不这样做可能会导致无声错误或分配一个巨大的缓冲区,正如@ead在评论中指出的那样。

if( size_s <= 0 ){ throw std::runtime_error( "Error during formatting." ); }

因为我们知道size_s不能为负,所以我们使用静态强制转换将size_t从有符号整型转换为无符号size_t。这样,即使是最迂腐的编译器也不会抱怨下一行可能发生的转换。

size_t size = static_cast<size_t>( size_s );

接下来,我们分配一个新的字符数组,并将其分配给std::unique_ptr。通常建议这样做,因为您不必再次手动删除它。

注意,这不是一种使用用户定义类型分配unique_ptr的安全方法,因为如果构造函数抛出异常,您就不能释放内存!

std::unique_ptr<char[]> buf( new char[ size ] );

在c++ 14中,您可以使用make_unique,这对于用户定义的类型是安全的。

auto buf = std::make_unique<char[]>( size );

在此之后,我们当然可以将snprintf用于其预期用途,并将格式化的字符串写入char[]。

std::snprintf( buf.get(), size, format.c_str(), args ... );

最后,我们创建并返回一个新的std::string,确保省略结尾的空结束符。

return std::string( buf.get(), buf.get() + size - 1 );

您可以在这里看到一个实际的例子。


如果你也想在参数列表中使用std::string,看看这个要点。


针对Visual Studio用户的其他信息:

正如回答中所解释的,微软将std::snprintf重命名为_snprintf(是的,没有std::)。MS进一步将其设置为弃用,并建议使用_snprintf_s代替,但是_snprintf_s不会接受缓冲区为零或小于格式化输出,如果发生这种情况,将不会计算输出长度。 因此,为了消除编译过程中的弃用警告,您可以在文件顶部插入以下一行,其中包含了_snprintf的使用:

#pragma warning(disable : 4996)

最终的想法

这个问题的很多答案都是在c++ 11之前编写的,并且使用固定的缓冲区长度或vargs。除非你一直使用旧版本的c++,否则我不建议你使用这些解决方案。理想情况下,走c++ 20的路。

因为这个答案中的c++ 11解决方案使用模板,如果它被大量使用,它可以生成相当多的代码。但是,除非您正在为一个二进制文件空间非常有限的环境进行开发,否则这不会成为问题,并且在清晰度和安全性方面仍然比其他解决方案有很大的改进。

如果空间效率非常重要,这两个带有vargs和vsnprintf的解决方案可能会很有用。 不要使用任何具有固定缓冲长度的解决方案,那只是在自找麻烦。

其他回答

这个问题已经解决了。但是,我认为这是c++中格式化字符串的另一种方式

class string_format {
private:
    std::string _result;
public:
    string_format( ) { }
    ~string_format( ) { std::string( ).swap( _result ); }
    const std::string& get_data( ) const { return _result; }
    template<typename T, typename... Targs>
    void format( const char* fmt, T value, Targs... Fargs ) {
        for ( ; *fmt != '\0'; fmt++ ) {
            if ( *fmt == '%' ) {
                _result += value;
                this->format( fmt + 1, Fargs..., 0 ); // recursive call
                return;
            }
            _result += *fmt;
        }
    }
    friend std::ostream& operator<<( std::ostream& ostream, const string_format& inst );
};
inline std::string& operator+=( std::string& str, int val ) {
    str.append( std::to_string( val ) );
    return str;
}
inline std::string& operator+=( std::string& str, double val ) {
    str.append( std::to_string( val ) );
    return str;
}
inline std::string& operator+=( std::string& str, bool val ) {
    str.append( val ? "true" : "false" );
    return str;
}
inline std::ostream& operator<<( std::ostream& ostream, const string_format& inst ) {
    ostream << inst.get_data( );
    return ostream;
}

并测试这个类:

string_format fmt;
fmt.format( "Hello % and is working ? Ans: %", "world", true );
std::cout << fmt;

你可以在这里查一下

如果你只想要一个类似printf的语法(不需要自己调用printf),可以看看Boost Format。

Poco Foundation库有一个非常方便的格式函数,它在格式字符串和值中都支持std::string:

道格:http://pocoproject.org/docs/Poco.html # 7308 来源:https://github.com/pocoproject/poco/blob/develop/Foundation/src/Format.cpp

以下是我的(简单的解决方案):

std::string Format(const char* lpszFormat, ...)
{
    // Warning : "vsnprintf" crashes with an access violation
    // exception if lpszFormat is not a "const char*" (for example, const string&)

    size_t  nSize     = 1024;
    char    *lpBuffer = (char*)malloc(nSize);

    va_list lpParams;

    while (true)
    {
        va_start(lpParams, lpszFormat);

        int nResult = vsnprintf(
            lpBuffer,
            nSize,
            lpszFormat,
            lpParams
        );

        va_end(lpParams);

        if ((nResult >= 0) && (nResult < (int)nSize) )
        {
            // Success

            lpBuffer[nResult] = '\0';
            std::string sResult(lpBuffer);

            free (lpBuffer);

            return sResult;
        }
        else
        {
            // Increase buffer

            nSize =
                  (nResult < 0)
                ? nSize *= 2
                : (nResult + 1)
            ;

            lpBuffer = (char *)realloc(lpBuffer, nSize);
        }
    }
}

根据Erik Aronesty提供的答案:

std::string string_format(const std::string &fmt, ...) {
    std::vector<char> str(100,'\0');
    va_list ap;
    while (1) {
        va_start(ap, fmt);
        auto n = vsnprintf(str.data(), str.size(), fmt.c_str(), ap);
        va_end(ap);
        if ((n > -1) && (size_t(n) < str.size())) {
            return str.data();
        }
        if (n > -1)
            str.resize( n + 1 );
        else
            str.resize( str.size() * 2);
    }
    return str.data();
}

这避免了需要从原始答案中的.c_str()结果中取消const。