为什么在C++中,ostream::write()需要使用'const char_type*'而不是'const void*'?

21

C语言中的fwrite()函数使用const void *restrict buffer作为第一个参数,因此您可以直接将指向struct的指针作为第一个参数传递。
http://en.cppreference.com/w/c/io/fwrite
例如:fwrite(&someStruct, sizeof(someStruct), 1, file);

但是在C++中,ostream::write()需要const char_type*,这就迫使您使用reinterpret_cast。(在Visual Studio 2013中,它是const char*。)
http://en.cppreference.com/w/cpp/io/basic_ostream/write
例如:file.write(reinterpret_cast<char*>(&someStruct), sizeof(someStruct));

在几乎所有情况下,要写入文件的二进制数据不是char数组,那么为什么标准更喜欢看起来更复杂的风格呢?

P.S.
1. 实际上我使用了ofstream中的write()方法,并采用ios::binary模式,但是根据参考资料,它继承自ofstream。所以我在上面使用了ostream::write()
2. 如果您想打印一串字符流,可以使用operator<<()
3. 如果write()不是写入二进制数据的方法,那么标准中有什么方法来实现呢?(尽管由于不同平台上的各种内存对齐策略,这可能会影响代码的可移植性)


2
因为这使得转换显式化,并要求程序员花一点时间思考将他的(二进制)结构体转换为字符序列可能会影响可移植性?(这是我的猜测,因此是一个注释而不是答案。) - DevSolar
12
你想知道为什么字符流表现得像字符流吗?这是因为它本来就是一个字符流,无论你是否想通过将其他类型的原始字节写入其中来(滥用)它。 - Mike Seymour
2
你可以写入文件的任何内容都是一个 char 数组。 - n. m.
问题在于,如果您有一个 unsigned char 数组 - 一种用于保存二进制数据的类型,您不能将其写入 fstream 而不使用 reinterpret_cast。而 clang-tidy 和其他 linters 也会因此而报错。 - Calmarius
4个回答

13
将其描述为C vs C ++的事情是误导性的。 C ++提供了std :: fwrite(const void *,...),就像C一样。 C ++选择更具防御性的地方是特定的std :: iostream 版本。

“几乎在所有情况下,要写入文件的二进制数据不是char数组”

这是有争议的。 在C ++中,将I / O间接化一层并不罕见,因此对象被流式传输或序列化为方便的表示形式 - 可能是可移植的(例如,端口标准化,没有或具有标准化的结构填充) - 然后在重新读取时进行反序列化/解析。 逻辑通常局限于涉及的各个对象,因此顶级对象不需要知道其成员的内存布局细节。 序列化和流处理倾向于以字节级别考虑/缓冲等 - 更符合字符缓冲区,并且read()write()返回当前可以传输的字符数 - 再次是字符而不是对象级别 - 因此假装否则你会有一个混乱的部分成功的I / O操作。

原始二进制写入/读取通常存在风险,因为它们不能处理这些问题,所以使得使用这些函数略微困难可能是一件好事,reinterpret_cast<> 有点像代码异味/警告。

尽管如此,C++ 使用 char* 的一个不幸之处在于,它可能会鼓励一些程序员首先读取字符数组,然后使用不适当的强制转换来实现数据的“重新解释” - 比如将 int* 用于字符缓冲区,但对齐方式可能不正确。

如果要打印一串字符流,可以使用 operator<<()。而 write() 方法是否设计用于写入原始数据?

使用operator<<()打印一系列字符是有问题的,因为唯一相关的重载采用const char*并期望一个'\0'/NUL结尾缓冲区。这使得它在输出中想要打印一个或多个NUL时变得无用。此外,当从较长的字符缓冲区开始时,operator<<通常会很笨拙、冗长且容易出错,需要在流中交换NUL,有时可能会导致性能和/或内存使用问题,例如在写入一些长字符串字面量的时候,其中你不能交换NUL,或者当字符缓冲区可能正在被其他线程读取时,这些线程不应该看到NUL。提供的std::ostream::write(p, n)函数避免了这些问题,让您精确地指定要打印的内容。

谢谢~也许我应该使用“原始数据”来替换“二进制数据”,因为计算机中的所有数据都是二进制的...写入原始数据确实是跨平台应用程序的危险行为。但对于嵌入式系统上的应用程序,更注重效率而不是可移植性,这可能是一个有用的方法 :) - Mr. Ree
1
@Mr.Ree: 当然-函数是可以被调用的-只是对于class/struct类型,需要进行强制转换才能实现。关于风险-如果有虚函数、指针数据成员等,即使在嵌入式系统上也可能会出问题,但C++提供了traits,您可以轻松assert以确保您的struct数据作为二进制块应该是安全的read/write - Tony Delroy
@Mr.Ree:在我的答案中,附加了对你另一个问题的回复。干杯。 - Tony Delroy
我同意你所写的内容。但是,Java有3个不同层次的类用于从文件中读取文本(流、读取器和缓冲读取器)。我并不是在为他们的设计决策辩护,但这也表明了当前的std::iostream将不同的职责实现为一个巨大的结构。使用std::对象通常是被Stroustrup本人鼓励的,因为它们具有RAII行为。因此,对于字节流,标准中应该有更好的替代当前流类的选择。 - zahir
@zahir:从某种角度来看,我同意你的观点,它“勾选了一个框”,但从另一方面来看,对于新开发人员来说,只是因为有一个额外的流类集合,其read和/或write函数采用void*而不是char*似乎过于冗长和令人困惑——标准已经足够长了。想要使用它的用户可以轻松地自己编写。缓冲区变化已经得到支持——请参见.rdbuf() - Tony Delroy

10

char_type不完全等同于char *,它是表示流的字符类型的流的模板参数:

template<typename _CharT, typename _Traits>
class basic_ostream : virtual public basic_ios<_CharT, _Traits>
{
public:
    // Types (inherited from basic_ios):
    typedef _CharT                  char_type;
    <...>

std::ostream 仅是 char 实例化:

typedef basic_ostream<char> ostream;

4
这个回答是否能解决问题? - Marc van Leeuwen
1
@MarcvanLeeuwen 我认为这样做比基于观点的答案更好,即使结论留给读者作为练习。 - Drax
1
但为什么要使用模板参数?为什么不使用void *或某种RawDataPointer类型?我对这个设计还有一些疑问... - Mr. Ree
这可能有点偏题,但是unsigned char*不是更好地表示原始数据块吗? - Samuel Li

1
在C/C++中,char是表示一个字节的数据类型,因此char[]是二进制数据的自然数据类型。
我认为你的问题更好地针对于C/C++没有为“字节”和“字符”设计不同的数据类型,而不是针对流库的设计。

1
谈论虚构的语言“C/C++”通常是没有帮助的,特别是当我们在研究C和C++之间的区别时。 - Pete Becker
2
我不同意 char[] 是二进制数据的自然类型。C和C++都有关于处理负值的混乱属性;使用 unsigned char 处理二进制数据要容易得多。 - M.M
2
@Matt: 如果我认为一个字节作为数字类型或位集合而不是不透明的东西,我通常也更喜欢使用“unsigned char”,但我认为在这个语境中提到它并不值得。顺便说一下,如果你想将一个字节视为带符号数字类型,“char”仍然不适用,因为实现定义了“char”是有符号还是无符号类型! - user1084944

-3

您好,

根据cplusplus.com网站上的信息,ostream::write的签名如下:

ostream& write (const char* s, streamsize n);

我刚在VS2013上检查过,你可以很容易地编写:

std::ofstream outfile("new.txt", std::ofstream::binary);
char buffer[] = "This is a string";
outfile.write(buffer, strlen(buffer));

3
问题并不是关于编写“char”字符串。 - Jonathan Wakely

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