11

这里的用法与直接在 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);
        }
    }
}

该程序按预期打印向量容量,并写入与输入相同大小的输出文件,但在 offset 之前仅与输入相同的字节,initSize之后全为零......

使用&buf[bufLen]inread()绝对是一种未定义的行为,但是&buf[0] + bufLen因为保证了连续分配,所以得到了正确的写入位置,不是吗?(提供initSize != 0。请注意std::vector<char> buf(initSize);大小bufinitSize。是的,如果initSize == 0,在我的环境中发生了一个正常的致命错误。)我错过了什么吗?这也是UB吗?标准是否说明了 std::vector 的这种用法?

是的,我知道我们可以先计算文件大小并分配完全相同的缓冲区大小,但是在我的项目中,可以预期输入文件几乎总是小于某个SIZE,所以我可以设置initSizeSIZE期望没有开销(像文件大小计算),并仅将重新分配用于“异常处理”。是的,我知道我可以reserve()resize()capacity()替换size(),然后用很少的开销来工作(每次调整大小时将缓冲区归零),但我仍然想摆脱任何冗余操作,只是一种偏执狂......

更新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

而这里的 200 可以替换为每个buf.size() <= i < buf.capacity()类似的扣除成立。

更新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。)由于我的答案仍然存在争议(但我相信不是伪造的),我不接受它,而是对批评性评论和其他答案持开放态度。

4

2 回答 2

5

reserve实际上并没有将空间添加到向量中,它只是确保在调整它的大小时不需要重新分配。而不是使用reserve你应该使用,然后在你知道你实际读入多少字节resize后做一个最终的。resize

保证reserve要做的就是防止迭代器和指针在您将向量的大小增加到capacity(). 除非它们size().

例如,使用 Debug 标志构建的代码通常会包含额外的功能,以便更容易找到错误。也许新分配的内存将充满定义明确的模式。也许该类会定期扫描该内存以查看它是否已更改,并在假设只有错误可能导致该更改的情况下抛出异常。这样的实现仍然是符合标准的。

的例子std::string更好,因为有一个案例几乎肯定会失败。 string::c_str()将返回一个指向末尾带有空终止符的字符串的指针。现在,一个符合要求的实现可以为终止 null 分配第二个缓冲区,并在复制字符串后返回该指针,但这将非常浪费。更有可能的是,字符串类只会确保其保留的缓冲区有空间容纳额外的空字符,并在必要时在此处写入空值。但是标准并没有规定何时写入 null,它可能在调用中,c_str也可能在可能修改字符串的任何位置。所以你无法知道你的一个字节何时会被覆盖。

如果您真的想要一个未初始化字节的缓冲区,std::vector<char>那么无论如何可能是错误的工具。您应该查看智能指针,例如std::unique_ptr<char>

于 2013-10-02T04:13:13.633 回答
-1

答案中的粗体字是我的主要主张。我通过引用/参考标准付出了应有的努力和谨慎,但我对我的阅读/理解可能存在差距/错误持开放态度。

我阅读C++03 标准是因为它更短更容易,并且我相信相关部分在最新标准中本质上是相同的。简而言之,问题的最后两个代码块中没有UB,因为reserve()ed内存是行为良好的对象,vector操作对对象的影响是由标准定义的。

在问题的更新 1中显示,连续内存由 分配reserve(),无需重新分配,我们可以将正确的地址放入其中。(如果需要,我可以提供相应的标准文本。)更可疑的部分是是否可以像问题中那样访问分配的内存(基本上,我们是否可以安全地读/写内存)。让我们进入这个。

首先,内存不在一些“临时空间”中。reserve()使用vector'sallocator分配内存。以及allocator使用运算符new(标准 20.4.1.1),它又调用分配函数(18.4.1.1)。因此存储持续时间是直到delete在内存(3.7.3)上调用解除分配(例如, )。会有一个关于生命周期的问题,但这对我们来说实际上没有问题(见下文)。

其次,是否真的像马克所说的“还没有对它们做任何事情——那里还没有构建任何对象”?首先,什么是对象?(1.8) “对象是一个存储区域”,它“具有影响其生命周期 (3.8) 的存储持续时间 (3.7)”以及类型 (3.9)。对我们来说重要的是,“一个对象是由 [...] 一个新表达式创建的”。因此,与其说“什么都没做”,不如说一个对象(这里是类型 char)是使用allocator! (当然,对象没有初始化,但这对我们来说没有问题。)对我们也很重要,因为charPOD(3.8 1)。对于任何 POD 对象,我们可以memcpy从它返回到它,并且存储在那里的值保持不变,即使该值对于该类型无效(例如,未初始化的垃圾)!(3.9 2)。因此,我们有权读取/写入内存(作为char对象)。此外,我们可以使用该类型的其他已定义操作(比如“=”),因为对象处于生命周期中。

一般来说,我们可以使用问题最后一部分中建议的像缓冲区这样的 POD 向量。特别是,访问reserve()POD 向量的 ed 内存 out ofsize()是明确定义的。准确地说,我们可以访问由, where and指向的内存&vec[m] + nm < size()m+n < capacity()(但是&vec[m+n]是UB!)。

请记住,我们仍然拥有 size()的,我们甚至可以推理vector方法的定义行为。例如,由size()触发的重新分配后,内存 out of 将不会被复制reserve()。因为reserve()只分配(或重新分配)(未初始化的)内存,所以容器只需要将对象复制size()到重新分配的内存中,而size()内存之外应该保持未初始化。

PS:最后一个例子来自 TC++PL 4ed,应该只适用于C++11及更高版本。在 C++11 及更高版本中,内存string是连续的,但对于较低版本则不是(&s[0]” 是否指向 std::string 中的连续字符?)。

编辑:马克在评论中提出了一个很好的观点:即使我们可以访问reserve()ed 内存,它会被vector我们无法控制的写入吗?我相信不会。容器上的每个操作(方法algorithm)都具有标准定义的效果,由专门的“效果”段落或整体要求(23.1)。所以,如果一个操作对reserve()ed 内存有影响,标准应该指定它

例如,效果erase(p1,p2)是“擦除范围 [q1, q2) 中的元素”(23.1.1) 和“使擦除点处或之后的迭代器和引用无效”(23.2.4.4)。因此,erase()reserve()ed 内存没有影响。

另一方面,我们知道insert()reserve()ed 记忆有影响,但这是可以推理的,从这个意义上说,我们处于控制之中。标准中没有任何地方说任何容器操作都具有“可以定期清除 [ size()] 之外的任何内容”的效果,所以它不应该这样做!

于 2021-09-11T08:18:49.177 回答