如何干净地使用:const char* 和 std::string?

Posted

技术标签:

【中文标题】如何干净地使用:const char* 和 std::string?【英文标题】:How to cleanly use: const char* and std::string? 【发布时间】:2015-07-08 14:30:33 【问题描述】:

tl:dr

如何将const char*std::string 巧妙地连接起来 优雅地,没有多个函数调用。理想情况下在一个功能中 调用并让输出为const char*。这不可能吗,什么 是最优解吗?

最初的问题

到目前为止,我在使用 C++ 时遇到的最大障碍是它如何处理字符串。在我看来,在所有广泛使用的语言中,它对字符串的处理最差。我见过与此类似的其他问题,这些问题的答案要么是“使用std::string”,要么只是指出其中一个选项最适合您的情况。

但是,当尝试像在其他语言中使用字符串一样动态使用字符串时,这是无用的建议。我不能保证总是能够使用std::string,而在我不得不使用const char* 的时候,我遇到了“它是恒定的,你不能连接它”的明显墙。

我在 C++ 中看到的任何字符串操作问题的每个解决方案都需要重复的多行代码,这些代码仅适用于该格式的字符串。 我希望能够将任何字符集与 + 符号连接起来,或者像在 C# 或 Python 中一样使用简单的 format() 函数。为什么没有简单的选择?

现状

标准输出

我正在编写一个 DLL,到目前为止,我已经通过 << 运算符将文本输出到 cout。到目前为止,使用以下形式的简单 char 数组一切都很好:

cout << "Hello world!"

运行时字符串

现在我想在运行时构造一个字符串并将其存储在一个类中,这个类将保存一个报告一些错误的字符串,以便它们可以被其他类拾取并可能发送到cout 稍后,字符串将由函数SetReport(const char* report) 设置。所以我真的不想为此使用超过一行,所以我继续写如下内容:

SetReport("Failure in " + __FUNCTION__ + ": foobar was " + foobar + "\n"); // __FUNCTION__ gets the name of the current function, foobar is some variable

我当然马上就明白了:

expression must have integral or unscoped enum type 和... '+': cannot add two pointers

丑陋的字符串

没错。所以我试图将两个或更多const char*s 添加在一起,这不是一个选择。所以我发现这里的主要建议是使用std::string,有点奇怪,输入"Hello world!" 不仅首先给你一个,而且让我们试一试吧:

SetReport(std::string("Failure in ") + std::string(__FUNCTION__) + std::string(": foobar was ") + std::to_string(foobar) + std::string("\n"));

太棒了!有用!但是看看那有多丑!!这是我见过的最丑陋的代码。我们可以简化为:

SetReport(std::string("Failure in ") + __FUNCTION__ + ": foobar was " + std::to_string(foobar) + "\n");

仍然可能是我每次遇到简单的单行字符串连接时最糟糕的方式,但现在一切都应该没问题吧?

转回常数

好吧,不,如果您正在处理 DLL,我经常会做一些事情,因为我喜欢单元测试,所以我需要由单元测试库导入我的 C++ 代码,您会发现当您尝试将该报告字符串设置为类的成员变量为std::string,编译器会抛出警告:

warning C4251: class 'std::basic_string<_Elem,_Traits,_Alloc>' needs to have dll-interface to be used by clients of class'

除了“忽略警告”(不好的做法!)之外,我发现的唯一真正解决此问题的方法是使用 const char* 成员变量而不是 std::string 但这并不是真正的解决方案,因为现在您必须将丑陋的串联(但动态)字符串转换回您需要的 const char 数组。但是你不能只在最后标记.c_str()(即使你为什么要这样做,因为这种连接在第二个变得更加荒谬?)你必须确保std::string 不会清理你新构建的字符串,给你留下垃圾。所以你必须在接收字符串的函数中这样做:

const std::string constString = (input);
m_constChar = constString.c_str();

这太疯狂了。因为现在我遍历了几种不同类型的字符串,使我的代码变得丑陋,添加了比应该需要的更多的行,所有这些只是为了将一些字符粘在一起。为什么这么难?

解决方案?

那么解决办法是什么?我觉得我应该能够制作一个将const char*s 连接在一起的函数,但也可以处理其他对象类型,例如std::stringintdouble,我强烈觉得这应该能够在一行中,但我找不到任何实现它的例子。我应该使用 char* 而不是常量变体,即使我已经读到您永远不应该更改 char* 的值,那么这有什么帮助?

是否有经验丰富的 C++ 程序员已经解决了这个问题并且现在对 C++ 字符串感到满意,您的解决方案是什么?没有解决办法吗?不可能吗?

【问题讨论】:

这一行能做到吗? 你可以在一行中写出这样的内容,但不能用一条语句。 我认为你应该能够做到这一点:SetReport("Failure in " __FUNCTION__ ": foobar was " + foobar + "\n"); ,这不是 + 连接 __FUNCTION__ 定义std::string operator+(const char*, const char*)可行吗? @tivn Gah,所以基本上我问错了问题?我应该关闭这个。首先,为什么这会起作用? 【参考方案1】:

构建字符串、将非字符串类型格式化为字符串的标准方法是string stream

#include <sstream>

std::ostringstream ss;
ss << "Failure in " << __FUNCTION__ << ": foobar was " << foobar << "\n";
SetReport(ss.str());

如果你经常这样做,你可以编写一个可变参数模板来做到这一点:

template <typename... Ts> std::string str(Ts&&...);
SetReport(str("Failure in ", __FUNCTION__, ": foobar was ", foobar, '\n'));

实现留给读者作为练习。

在这种特殊情况下,字符串文字(包括__FUNCTION__)可以通过简单地一个接一个地写入来连接;并且,假设foobarstd::string,可以使用+ 与字符串文字连接:

SetReport("Failure in " __FUNCTION__ ": foobar was " + foobar + "\n");

如果foobar是数字类型,可以使用std::to_string(foobar)进行转换。

【讨论】:

这似乎是解决我的问题的绝妙方法,但我收到了:'.str' 的左侧必须有类/结构/枚举并且 ostringstream 没有构造函数... @sydan:你包括&lt;sstream&gt;吗? 我尝试了 sstream 和 ostream 但使用 sstream 我得到 str 不是 ostream 的成员。抱歉......我觉得我真的错过了一些重要的东西...... @sydan:对不起,单行不能用,我忘了那个细节。 &lt;&lt; 的结果是ostream&amp; 而不是ostringstream&amp;,所以你不能在上面调用str()。您可以添加演员表,但这会变得相当难看。 啊好吧...所以我觉得我们最终回到了同一个地方。我正在尝试实现一个可变参数模板,但我似乎也无法让它工作,但这是一个单独的问题。【参考方案2】:

纯字符串文字(例如"abc"__FUNCTION__)和char const* 不支持串联。这些只是普通的 C 风格 char const[]char const*

解决方案是使用一些字符串格式化工具或库,例如:

std::string 和使用+ 的连接。可能涉及太多不必要的分配,除非operator+ 使用表达式模板。 std::snprintf。这个不会为你分配缓冲区,也不是类型安全的,所以人们最终会为它创建包装器。 std::stringstream。无处不在且标准,但其语法充其量是笨拙的。 boost::format。输入安全,但据说速度很慢。 cppformat。据报道,现代且快速。

【讨论】:

供个人使用 cppformat 看起来是一个很好的解决方案。可悲的是我不能为这个项目使用第三方库,所以我坚持使用标准的东西,这会让我剩下 printf 和 stringstream 吗?【参考方案3】:

最简单的解决方案之一是使用C++ 空字符串。在这里,我声明了名为_ 的空字符串变量,并在字符串连接之前使用了它。确保始终将其放在前面。

#include <cstdio>
#include <string>

using namespace std;
string _ = "";

int main() 
        char s[] = "chararray";
        string result =
                _ + "function name = [" + __FUNCTION__ + "] "
                "and s is [" + s + "]\n";
        printf( "%s", result.c_str() );
        return 0;

输出:

function name = [main] and s is [chararray]

关于__FUNCTION__,我发现在Visual C++ 中它是一个宏,而在GCC 中它是一个变量,所以SetReport("Failure in " __FUNCTION__ "; foobar was " + foobar + "\n"); 只适用于Visual C++。请参阅:https://msdn.microsoft.com/en-us/library/b0084kay.aspx 和 https://gcc.gnu.org/onlinedocs/gcc/Function-Names.html

上面使用空字符串变量的解决方案应该适用于 Visual C++ 和 GCC。

【讨论】:

这很好,除非字符串被清理并且你失去了它的记忆。只要所有内容都在范围内,就可以很好地工作,非常整洁的解决方案还不够:/ 嗯,你可以把它设为const,然后把它作为全局变量放在头文件中。 我对此进行了更深入的调查。除非您将字符串连接的结果分配给 const char* 成员变量,否则整个方法都可以正常工作,这是为什么呢? 您可能需要检查这个其他 SO 问题:***.com/questions/7352099/stdstring-to-char 之所以选择,是因为我的 Visual Studio 版本无法使用可变参数模板。【参考方案4】:

我的解决方案

我一直在尝试不同的东西,我有一个解决方案,它结合了 tivn 的答案,包括制作一个空字符串来帮助将长 std::string 和字符数组连接在一起,以及我自己的一个允许单行的函数将 std::string 复制到 const char*,当字符串对象离开范围时可以安全使用。

我会使用 Mike Seymour 的可变参数模板,但我正在运行的 Visual Studio 2012 似乎不支持它们,我需要这个解决方案非常通用,所以我不能依赖它们。

这是我的解决方案:

Strings.h

#ifndef _STRINGS_H_
#define _STRINGS_H_

#include <string>

// tivn's empty string in the header file
extern const std::string _;

// My own version of .c_str() which produces a copy of the contents of the string input
const char* ToCString(std::string input);

#endif

Strings.cpp

#include "Strings.h"

const std::string str = "";

const char* ToCString(std::string input)

    char* result = new char[input.length()+1];
    strcpy_s(result, input.length()+1, input.c_str());
    return result;

用法

m_someMemberConstChar = ToCString(_ + "Hello, world! " + someDynamicValue);

我认为这非常简洁,并且在大多数情况下都有效。谢谢大家帮我解决这个问题。

【讨论】:

【参考方案5】:

从 C++20 开始,fmtlib 已进入 ISO 标准,但即使在较旧的迭代中,您仍然可以下载和使用它。

它提供了与 Python 的 str.format()(a) 类似的功能,然后您的“丑陋字符串”示例就变得相对简单了:

#include <fmt/format.h>

// Later on, where code is allowed (inside a function for example) ...

SetReport(fmt::format("Failure in : foobar was \n", __FUNCTION__, foobar));

它很像 printf() 系列,但内置了可扩展性和类型安全性。


(a) 但是,不幸的是,不是它的字符串插值功能(使用 f 字符串),它具有将表达式放在字符串中的输出位置的额外优势,类似:

set_report(f"Failure in __FUNCTION__: foobar was foobar\n");

如果fmtlib 拥有这种能力,我可能会兴奋得尿裤子:-)

【讨论】:

以上是关于如何干净地使用:const char* 和 std::string?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 UTF-8 字符序列在 C++ 中初始化 const char* 和/或 const std::string?

C++标准库 如何连接两个const char *类型字符串,并返回const char * 类型结果?

如何将 std::string 转换为 const char*? [复制]

const char*、const char(&)[N] 和 std::string 的函数重载

如何在 C++ 中将 std::string 转换为 const char [重复]

传递 const char 而不是 std::string 作为函数参数