如何构建一个包含嵌入空值的std::string?

108

如果我想用这样的一行构造一个 std::string:

std::string my_string("a\0b");

我想在最终的字符串中包含三个字符(a,null,b),但我只得到了一个。正确的语法是什么?

我希望最终的字符串包含三个字符(a,null,b),但我只得到了一个。请问应该使用什么正确的语法?


4
请注意,如果您将'b'替换为任何数字字符,将会默默地创建错误的字符串。参见:https://dev59.com/5Wkv5IYBdhLWcg3w91hF - David Stone
11个回答

152

自C++14起

我们可以创建literal std::string

#include <iostream>
#include <string>

int main()
{
    using namespace std::string_literals;

    std::string s = "pl-\0-op"s;    // <- Notice the "s" at the end
                                    // This is a std::string literal not
                                    // a C-String literal.
    std::cout << s << "\n";
}

在C++14之前

问题出在std::string构造函数中接受const char*参数的假定输入是一个C字符串。C字符串以\0结尾,因此当遇到\0字符时解析停止。

为了弥补这个问题,你需要使用从char数组(而不是C字符串)构建字符串的构造函数。它需要两个参数 - 一个指向数组的指针和一个长度:

std::string   x("pq\0rs");   // Two characters because input assumed to be C-String
std::string   x("pq\0rs",5); // 5 Characters as the input is now a char array with 5 characters.

注意:C++的std::string不是以null结尾的(与其他帖子中建议的不同)。但是,您可以使用c_str()方法提取包含C-String的内部缓冲区的指针。
还请查看下面关于使用vector的Doug T's answer
还请查看RiaD提供的C++14解决方案。

9
更新:自C++11起,字符串已经是以空字符结尾的。话虽如此,Loki的帖子仍然有效。 - matthewaveryusa
17
从存储的角度来看,它们是以空字符结尾的,但并不是在具有含义的空字符结尾的意义上(即具有定义字符串长度的语义),这通常是该术语的意思。 - Lightness Races in Orbit
讲解得很清楚。谢谢。 - Joma

23

如果您正在执行与C风格字符串(字符数组)类似的操作,请考虑使用

std::vector<char>

您可以更像对待C字符串一样自由地将其视为数组。您可以使用copy()函数将其复制到一个字符串中:

std::vector<char> vec(100)
strncpy(&vec[0], "blah blah blah", 100);
std::string vecAsStr( vec.begin(), vec.end());

你可以在许多与c字符串相同的地方使用它

printf("%s" &vec[0])
vec[10] = '\0';
vec[11] = 'b';

然而,与 C 字符串一样,您自然也会遇到同样的问题。可能会忘记添加空终止符或超出分配的空间。


如果您正在尝试将字节编码为字符串(grpc字节存储为字符串),请使用答案中指定的向量方法,而不是通常的方式(请参见下文),后者将无法构造整个字符串。 std::memcpy(bytes, image.data, dataSize * sizeof(byte)); std::string test(reinterpret_cast(bytes)); std::cout << "Encoded String length " << test.length() << std::endl;``` - Alex Punnen

13

我不知道为什么你想做这样的事情,但可以尝试以下操作:

std::string my_string("a\0b", 3);

3
如果您将二进制数据存储为字符串,那么您做错了什么。这就是vector<unsigned char>unsigned char*所发明的。 - Mahmoud Al-Qudsi
2
我在尝试了解字符串安全性时遇到了这个问题。我想测试我的代码,以确保即使在从文件/网络读取期望为文本数据的内容时读取了空字符,它仍然可以正常工作。我使用std::string来表示数据应被视为纯文本,但我正在进行一些哈希处理,并且我想确保所有内容都可以处理包含空字符的情况。这似乎是使用嵌入空字符的字符串字面量的有效用法。 - David Stone
3
不,那不是真的。在UTF-8字符串中,\0字节只能是NUL。一个多字节编码的字符永远不会包含\0——也不包含任何其他ASCII字符。 - John Kugelman
1
在测试用例中,我偶然发现了这个问题,从而试图挑战算法。虽然理由很少,但确实存在有效的原因。 - namezero
1
@Ezra,无论如何,在C++11中,写时复制都是无效的。 - graywolf
显示剩余6条评论

13

什么新功能由C++的用户定义字面常量添加?展示了一个优雅的答案:定义

std::string operator "" _s(const char* str, size_t n) 
{ 
    return std::string(str, n); 
}

那么,你可以用以下方式创建你的字符串:

std::string my_string("a\0b"_s);

或者甚至如此:

auto my_string = "a\0b"_s;

有一个“旧风格”的方法:

#define S(s) s, sizeof s - 1 // trailing NUL does not belong to the string

那么您可以定义

std::string my_string(S("a\0b"));

8
以下内容可行...
std::string s;
s.push_back('a');
s.push_back('\0');
s.push_back('b');

你必须使用括号而不是方括号。 - jk.

6
在C++14中,现在可以使用字面值。
using namespace std::literals::string_literals;
std::string s = "a\0b"s;
std::cout << s.size(); // 3

1
第二行可以换成更加优美的写法,即 auto s{"a\0b"s}; - underscore_d
很好的回答,谢谢。 - Joma

6

您需要小心处理这个问题。如果您将 'b' 替换为任何数字字符,则大多数方法会悄悄地创建错误的字符串。请参阅:C ++字符串文字转义字符规则

例如,我将这个看似无害的片段放在程序的中间

// Create '\0' followed by '0' 40 times ;)
std::string str("\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00", 80);
std::cerr << "Entering loop.\n";
for (char & c : str) {
    std::cerr << c;
    // 'Q' is way cooler than '\0' or '0'
    c = 'Q';
}
std::cerr << "\n";
for (char & c : str) {
    std::cerr << c;
}
std::cerr << "\n";

这是程序对我的输出结果:
Entering loop.
Entering loop.

vector::_M_emplace_ba
QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ

那是我的第一个打印语句,两次出现了几个不可打印的字符,然后是一个换行符,接着是内存中的某些内容,我刚刚覆盖了它(然后打印出来,表明已被覆盖)。最糟糕的是,即使使用彻底而冗长的gcc警告编译此代码也没有指示有什么错误,并且通过valgrind运行程序也没有抱怨任何不当的内存访问模式。换句话说,现代工具完全无法检测到它。
你可以在更简单的std::string("0", 100);中遇到同样的问题,但上面的例子更加棘手,因此更难看出问题所在。
幸运的是,C++11为我们提供了一个很好的解决方案,使用初始化列表语法。这样可以避免指定字符数(如上所示,可能会出错),并避免组合转义数字。与需要一个char数组和一个大小的版本不同,std::string str({'a', '\0', 'b'})对于任何字符串内容都是安全的。

2
作为我准备这篇文章的一部分,我向gcc提交了一个错误报告,希望他们能添加一个警告来使其更加安全:http://gcc.gnu.org/bugzilla/show_bug.cgi?id=54924 - David Stone

1
最好使用std :: vector ,如果这个问题不仅仅是为了教育目的。

1

anonym的回答很好,但在C++98中也有非宏解决方案:

template <size_t N>
std::string RawString(const char (&ch)[N])
{
  return std::string(ch, N-1);  // Again, exclude trailing `null`
}

通过这个函数,RawString(/*文本*/)将会产生与S(/*文本*/)相同的字符串:
std::string my_string_t(RawString("a\0b"));
std::string my_string_m(S("a\0b"));
std::cout << "Using template: " << my_string_t << std::endl;
std::cout << "Using macro: " << my_string_m << std::endl;

此外,宏存在一个问题:表达式实际上不是按照写作方式的std::string,因此不能用于简单的赋值初始化。
std::string s = S("a\0b"); // ERROR!

"...因此最好使用以下内容:"
#define std::string(s, sizeof s - 1)

显然,在您的项目中只应使用其中一种解决方案,并根据您认为合适的方式进行调用。

-5

我知道这个问题已经被问了很长时间。但是对于任何遇到类似问题的人,可能会对以下代码感兴趣。

CComBSTR(20,"mystring1\0mystring2\0")

这个答案过于针对微软平台,没有回答原问题(原问题询问的是std::string)。 - June Rhodes

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