使用 std:vector 作为低级缓冲区
Using std:vector as low level buffer
这里的用法与直接在 C++ std:vector 中使用 read(( 相同,但需要重新分配。
输入文件的大小未知,因此当文件大小超过缓冲区大小时,缓冲区将通过加倍大小来重新分配。这是我的代码:
#include <vector>
#include <fstream>
#include <iostream>
int main()
{
const size_t initSize = 1;
std::vector<char> buf(initSize); // sizes buf to initSize, so &buf[0] below is valid
std::ifstream ifile("D:\Pictures\input.jpg", std::ios_base::in|std::ios_base::binary);
if (ifile)
{
size_t bufLen = 0;
for (buf.reserve(1024); !ifile.eof(); buf.reserve(buf.capacity() << 1))
{
std::cout << buf.capacity() << std::endl;
ifile.read(&buf[0] + bufLen, buf.capacity() - bufLen);
bufLen += ifile.gcount();
}
std::ofstream ofile("rebuild.jpg", std::ios_base::out|std::ios_base::binary);
if (ofile)
{
ofile.write(&buf[0], bufLen);
}
}
}
程序按预期打印矢量容量,并写入与输入大小相同的输出文件,BUT,在偏移量initSize
之前只有与输入相同的字节,之后的所有零...
在read()
中使用&buf[bufLen]
肯定是一种未定义的行为,但&buf[0] + bufLen
得到了正确的写入位置,因为连续分配是有保证的,不是吗?(initSize != 0
提供。请注意,std::vector<char> buf(initSize);
大小buf
到 initSize
。是的,如果initSize == 0
,我的环境中会出现一个rumtime致命错误。我错过了什么吗?这也是一个UB吗?标准有没有说明std::vector的这种用法?
我知道我们可以先计算文件大小并分配完全相同的缓冲区大小,但是在我的项目中,可以预期输入文件几乎总是小于某个SIZE
,因此我可以将initSize
设置为 SIZE
并期望没有开销(如文件大小计算(,并且仅将重新分配用于"异常处理"。是的,我知道我可以用 resize()
替换reserve()
,用 size()
替换capacity()
,然后以很少的开销让事情工作(每次调整大小时将缓冲区归零(,但我仍然想摆脱任何多余的操作,只是一种偏执狂......
更新 1:
事实上,我们可以从标准中逻辑地推断出&buf[0] + bufLen
得到正确的位置,考虑一下:
std::vector<char> buf(128);
buf.reserve(512);
char* bufPtr0 = &buf[0], *bufPtrOutofRange = &buf[0] + 200;
buf.resize(256); std::cout << "standard guarantees no reallocation" << std::endl;
char* bufPtr1 = &buf[0], *bufInRange = &buf[200];
if (bufPtr0 == bufPtr1)
std::cout << "so bufPtr0 == bufPtr1" << std::endl;
std::cout << "and 200 < buf.size(), standard guarantees bufInRange == bufPtr1 + 200" << std::endl;
if (bufInRange == bufPtrOutofRange)
std::cout << "finally we have: bufInRange == bufPtrOutofRange" << std::endl;
输出:
standard guarantees no reallocation
so bufPtr0 == bufPtr1
and 200 < buf.size(), standard guarantees bufInRange == bufPtr1 + 200
finally we have: bufInRange == bufPtrOutofRange
在这里,每buf.size() <= i < buf.capacity()
都可以替换 200,并且类似的扣除额成立。
更新 2:
是的,我确实错过了一些东西...但问题不在于连续性(见更新1(,甚至不在于写入内存失败(见我的回答(。今天我有一些时间来研究这个问题,程序得到了正确的地址,将正确的数据写入保留内存,但在下一个reserve()
中,buf
被重新分配,只有范围内的元素[0, buf.size())
复制到新内存。所以这就是整个谜语的答案...
最后说明:如果在缓冲区填充了一些数据后不需要重新分配,您绝对可以使用reserve()/capatity()
而不是resize()/size()
,但如果需要,请使用后者。此外,在此处提供的所有实现(VC++、g++、ICC(下,该示例按预期工作:
const size_t initSize = 1;
std::vector<char> buf(initSize);
buf.reserve(1024*100); // assume the reserved space is enough for file reading
std::ifstream ifile("D:\Pictures\input.jpg", std::ios_base::in|std::ios_base::binary);
if (ifile)
{
ifile.read(&buf[0], buf.capacity()); // ok. the whole file is read into buf
std::ofstream ofile("rebuld.jpg", std::ios_base::out|std::ios_base::binary);
if (ofile)
{
ofile.write(&buf[0], ifile.gcount()); // rebuld.jpg just identical to input.jpg
}
}
buf.reserve(1024*200); // horror! probably always lose all data in buf after offset initSize
这是另一个例子,引用自'TC++PL, 4e' pp 1041,注意函数中的第一行使用reserve()
而不是resize()
:
void fill(istream& in, string& s, int max)
// use s as target for low-level input (simplified)
{
s.reserve(max); // make sure there is enough allocated space
in.read(&s[0],max);
const int n = in.gcount(); // number of characters read
s.resize(n);
s.shrink_to_fit(); // discard excess capacity
}
更新3(8年后(:这些年发生了很多事情,我近6年没有用C++作为我的工作语言,现在我是一名博士生!此外,尽管许多人认为有UB,但他们给出的原因却大不相同(有些已经被证明不是UB(,这表明这是一个复杂的案例。因此,在投票和写答案之前,强烈建议阅读并参与评论。
另一件事是,通过博士培训,我现在可以相对轻松地进入C++标准,这在几年前我不敢。我相信我在自己的答案中表明,基于标准,上述两个代码块应该可以工作。(string
示例需要 C++11。由于我的回答仍然有争议(但我相信不是伪造的(,我不接受它,而是对批评性评论和其他答案持开放态度。
reserve
实际上并没有向向量添加空间,它只是确保在调整其大小时不需要重新分配。 与其使用reserve
不如使用resize
,然后在知道实际读取多少字节后进行最后的resize
。
reserve
保证所做的只是防止迭代器和指针失效,因为您将向量的大小增加到 capacity()
.不保证维护这些保留字节的内容,除非它们是size()
的一部分。
例如,使用 Debug 标志生成的代码通常会包含额外的功能,以便更轻松地查找 bug。 也许新分配的内存将被一个定义良好的模式填充。 也许该类会定期扫描该内存以查看它是否已更改,如果假设只有错误可能导致该更改,则会引发异常。 这样的实现仍将符合标准。
std::string
的例子甚至更好,因为有一个案例几乎肯定会失败。 string::c_str()
将返回指向字符串的指针,末尾带有 null 终止符。 现在,一个符合标准的实现可以为终止 null 分配第二个缓冲区,并在复制字符串后返回该指针,但这是非常浪费的。 更有可能的是,字符串类只会确保其保留的缓冲区有空间容纳额外的空字符,并根据需要在那里写入一个空值。 但是该标准并没有规定何时写入该 null,它可能在对 c_str
的调用中,也可能在字符串可能被修改的任何时候。 因此,您无法知道何时会覆盖其中一个字节。
如果你真的想要一个未初始化字节的缓冲区,std::vector<char>
可能是错误的工具。 您应该改为查看智能指针,例如std::unique_ptr<char>
。
答案中的粗体文本是我的主要主张。我通过引用/参考标准付出了应有的努力和谨慎,但我愿意接受我的阅读/理解可能存在差距/错误的可能性。
我读C++03标准是因为它更短,更容易,我相信相关部分在最新标准中本质上是相同的。简而言之,问题的最后两个代码块中没有UB,因为reserve()
ed内存是行为良好的对象,并且vector
操作对对象的影响由标准定义。
在问题的更新 1 中表明,连续内存由 reserve()
分配,无需重新分配,我们可以在其中获取正确的地址。(如果需要,我可以提供相应的标准文本。更可疑的部分是是否可以像问题中那样访问分配的内存(基本上,我们是否可以安全地读取/写入内存(。让我们进入这个。
首先,内存不在某个"暂存空间"中。 reserve()
使用vector
的allocator
来分配内存。allocator
使用运算符new
(标准 20.4.1.1(,后者又调用分配函数 (18.4.1.1(。因此,存储持续时间直到在内存(3.7.3(上调用释放(例如,delete
(。会有关于寿命的担忧,但这实际上对我们来说没有问题(见下文(。
其次,它真的像马克所说的"还没有对它们做任何事情——那里没有建造任何物体"吗?首先,什么是对象?(1.8("对象是一个存储区域","具有影响其生存期(3.8(的存储持续时间(3.7("以及类型(3.9(。对我们来说重要的是,"一个对象是由[...]一个新的表达创造的"。因此,我们应该说一个对象(这里类型为 char
(是使用 allocator
创建的,而不是"什么都不做"!(当然,对象没有初始化,但这对我们来说没有问题。对我们来说也很重要,因为char
是 POD,所以分配对象的生存期在获得存储后立即开始 (3.8 1(。对于任何 POD 对象,我们可以从中memcpy
和返回,并且存储在那里的值保持不变,即使该值对于该类型无效(例如,未初始化的垃圾(!(3.9 2).因此,我们有权读/写内存(作为char
对象(。此外,我们可以使用该类型的其他已定义操作(例如"="(,因为对象在生命周期中。
通常,我们可以按照问题的最后一部分的建议使用像缓冲区这样的 POD 向量。特别是,从size()
访问 POD 向量的reserve()
ed 内存是明确定义的。准确地说,我们可以访问 &vec[m] + n
指向的内存 ,其中m < size()
和m+n < capacity()
(但&vec[m+n]
是 UB!
请记住,我们仍然有 旧size()
,我们甚至可以推理vector
方法的定义行为。例如,reserve()
触发重新分配后,不会复制size()
内存。由于reserve()
仅分配(或重新分配((未初始化(内存,因此容器只需要将size()
中的对象复制到重新分配的内存中,并且在size()
内存之外应保持未初始化状态。
PS:最后一个示例来自TC++PL 4ed,应该仅适用于C++11及更高版本。在 C++11 及更高版本中,string
的内存是连续的,但对于较低版本则不是("&s[0]"是否指向 std::string 中的连续字符?(。
编辑:马克在评论中提出了一个很好的观点:即使我们可以访问reserve()
ed内存,它是否会被我们无法控制的vector
写入?我相信不会。容器上的每个操作(方法,algorithm
(都有一个标准定义的效果,通过专门的"效果"段落或总体要求(23.1(。因此,如果操作对reserve()
内存有影响,则标准应指定它。
例如,erase(p1,p2)
的效果是"擦除范围 [q1, q2( 中的元素"(23.1.1(和"在擦除点或之后使迭代器和引用无效"(23.2.4.4(。因此,erase()
对reserve()
记忆没有影响。
另一方面,我们知道insert()
对reserve()
记忆有影响,但这是可以推理的,从这个意义上说,我们可以控制。标准中没有任何地方说任何容器操作都具有"可以定期清除 [ size()
] 以外的任何东西"的效果,所以它不应该这样做!
- std::带有自定义缓冲区的 iostream 不允许我写入
- istream std::cin如何修改自定义istream缓冲区
- 在共享缓冲区内存中创建 ::std::string 对象
- 是否可以将 std::basic_ifstream 和 std::basic_ofstream 与自定义缓冲区一起使用?
- std::vector::assign/std::vector::operator=(const&) 是否保证在"this"中重用缓冲区?
- 如何将文件的一部分读取到std::list缓冲区?
- 在 std::string::end() 和 std::string::capacity() 之间使用缓冲区
- std::ifstream.read() 不会向我的缓冲区返回任何内容
- 输出操纵器 std::ends 是否向输出缓冲区添加空字符?
- 调整我的std :: string时,角色缓冲区会发生什么
- std::字符串与字节缓冲区(C++的差异)
- STD ::向量如何调整其内部缓冲区大小
- 在C 中,是否有可能在不兼容类型的std ::向量对象之间传输不同类型的缓冲区
- 我可以做一个循环,直到“std::cin”在输入缓冲区中看到“”字符
- 缓冲区刷新究竟是如何工作的(std::endl 和 之间的区别)?
- C++如何检查std::cin缓冲区是否为空
- 如何使用 std::istringstream 获取正确的缓冲区大小以进行数据解析
- 初始化的std ::阵列从指针优雅地变成缓冲区
- 别名,用于使用 std::aligned_union 和 std::aligned_union 进行小型缓冲区优化
- 如何使用 std::make_shared 创建动态大小缓冲区