为啥 ostream::write() 在 C++ 中需要“const char_type*”而不是“const void*”?

Posted

技术标签:

【中文标题】为啥 ostream::write() 在 C++ 中需要“const char_type*”而不是“const void*”?【英文标题】:Why does ostream::write() require ‘const char_type*’ instead of ‘const void*’ in C++?为什么 ostream::write() 在 C++ 中需要“const char_type*”而不是“const void*”? 【发布时间】:2015-04-17 23:02:18 【问题描述】:

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 数组,那么为什么标准更喜欢看起来更复杂的样式呢?

附言 1、其实我在ofstreamios::binary模式下使用了write()方法,但是根据参考,它继承了ofstream。所以我在上面使用ostream::write()。 2. 如果要打印字符流,可以使用operator<<()write() 方法不是专为写入原始数据设计的吗? 3.如果write()不是写入二进制数据的方式,那么标准内的方式是什么? (尽管由于不同平台上的各种内存对齐策略,这可能会影响代码的可移植性)

【问题讨论】:

因为这使得强制转换显式,并要求程序员考虑一下将他的(二进制)结构转换为字符序列会如何影响可移植性? (这是我的猜测,因此是评论而不是答案。) 你在问为什么一个字符流表现得像一个字符流?因为它是一个字符流,所以无论您是否想(ab)通过将其他类型的原始字节写入其中来使用它。 您可以写入文件的任何内容 char 数组。 @DevSolar 问题是如果你有一个unsigned char 数组——一种用于保存二进制数据的类型,你不能在没有reinterpret_cast 的情况下将它写入fstream。而 clang-tidy 和其他 linters 会为此咬牙切齿。 【参考方案1】:

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;

【讨论】:

这能回答问题吗? @MarcvanLeeuwen 我认为它比基于意见的答案要好,即使结论留给读者作为练习 但是为什么要取模板参数呢?为什么不用void* 或一些RawDataPointer 类型?我对这个设计还是有些怀疑的…… 这可能有点离题,但unsigned char* 不是更好地代表一个原始数据块吗?【参考方案2】:

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

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

这是有争议的。在 C++ 中,在 I/O 中添加间接级别并不少见,因此对象被流式传输或序列化为方便的 - 并且可能是可移植的(例如,字节序标准化,没有或带有标准化结构填充) - 表示,然后反序列化/重读时解析。该逻辑通常与所涉及的各个对象一起本地化,这样***对象不需要知道其成员的内存布局的详细信息。序列化和流式传输往往在字节级别被考虑/缓冲等 - 更适合字符缓冲区,read()write() 返回当前可以传输的许多字符 - 再次在字符而不是对象级别 - 所以假装不是很有效率,否则恢复部分成功的 I/O 操作会一团糟。

天真地完成原始二进制写入/读取有点危险,因为它们不处理这些问题,所以这些函数的使用稍微困难一点可能是一件好事,reinterpret_cast&lt;&gt; 有点代码味道/警告。

也就是说,C++ 使用 char* 的一个不幸方面是它可能会鼓励一些程序员首先读取字符数组,然后使用不适当的强制转换来动态“重新解释”数据 - 就像 @987654330 @ 以可能未适当对齐的方式瞄准字符缓冲区。

如果要打印字符流,可以使用operator&lt;&lt;()write() 方法不是专为写原始数据设计的吗?

使用operator&lt;&lt;() 打印字符流是有问题的,因为唯一相关的重载需要const char* 并期望'\0'/NUL 终止的缓冲区。如果您想在输出中打印一个或多个 NUL,这将变得毫无用处。此外,当从较长的字符缓冲区开始时,operator&lt;&lt; 通常会显得笨拙、冗长且容易出错,需要在流中交换NUL,并且有时会是一个重要的性能和/或内存使用问题,例如当写一些 - 但不是结尾 - 长字符串文字时,您不能将 NUL 交换到其中,或者当字符缓冲区可能正在从不应该看到 NUL 的其他线程读取时。

提供的std::ostream::write(p, n) 函数避免了这些问题,让您可以准确指定要打印的数量。

【讨论】:

谢谢~也许我应该用“原始数据”来代替“二进制数据”,因为计算机中的所有数据都是二进制的……写原始数据确实是跨平台应用程序的危险行为.但是对于更注重效率而不是可移植性的嵌入式系统上的应用程序,这可能是一种有用的方法:) @Mr.Ree:当然 - 函数是要被调用的 - 对于 class/struct 类型来说,这样做稍微困难一些,需要那个演员表。关于危险 - 如果存在虚函数、指针数据成员等,即使在嵌入式系统上也会遇到麻烦,但 C++ 提供了traits,您可以轻松地通过assert 确保您的struct 数据对@ 来说是安全的987654343@/write作为二进制块.... @Mr.Ree:在我的回答中附加了对您的另一个问题的回答。干杯。 我同意你写的。但是,Java 有 3 层不同的类用于从文件中读取文本(流、读取器和缓冲读取器)。我不是为他们的设计决定辩护,但这也表明当前的std::iostream 在一个巨大的结构中实现了不同的职责。 Stroustrup 本人通常鼓励使用 std:: 对象,因为它们的 RAII 行为。因此,对于字节流标准中的当前流类,应该有一个更好的替代方案,而不是fwrite @zahir:从一个角度来看,我同意你的观点,它“打勾”,从另一个角度来看,对于新开发人员来说,拥有一组额外的流类似乎过度、冗长和令人困惑,而这些流类的不同之处在于拥有read 和/或 write 函数采用 void* 而不是 char* - 标准已经足够长了。想要它的用户可以轻而易举地自己动手。已经支持缓冲变体 - 请参阅 .rdbuf()。【参考方案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));

【讨论】:

问题不在于写 char 字符串。【参考方案4】:

在 C/C++ 中,char 是表示字节的数据类型,因此char[] 二进制数据的自然数据类型。

我认为,您的问题更好地针对 C/C++ 并非旨在为“字节”和“字符”提供不同数据类型这一事实,而不是针对流库的设计。

【讨论】:

谈论想象中的语言“C/C++”通常没有帮助,尤其是在检查 C 和 C++ 之间的差异时。 我不同意char[] 是二进制数据的自然类型。 C 和 C++ 在处理负值方面都有混乱的特性。使用unsigned char 处理二进制数据要容易得多。 @Matt:如果我将字节视为数字类型或位集合而不是不透明的东西,我通常也更喜欢unsigned char,但我认为不值得带来在这种情况下。顺便说一句,如果您想将字节视为有符号数字类型,char 仍然不合适,因为它的实现定义了 char 是有符号还是无符号类型!

以上是关于为啥 ostream::write() 在 C++ 中需要“const char_type*”而不是“const void*”?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 C++ 语法如此复杂? [关闭]

为啥我无法在 C++ 中初始化静态字段 [重复]

为啥在 c++ 11 和 c++ 17 中给出代码存在输出差异?

为啥模板参数推导在 C++ 中不起作用?

为啥 C++ 成员函数在参数中使用 &? [复制]

为啥dll不能在c++中使用?