C++中的动态缓冲区类型是什么?

31

我并不是C++的新手,但过去很少涉及它,所以我对其设施的了解比较粗略。

我正在用C++编写一个快速的概念证明程序,并且我需要一个可动态调整大小的二进制数据缓冲区。也就是说,我将从网络套接字接收数据,而我不知道会有多少数据(尽管不超过几MB)。我可以自己编写这样的缓冲区,但如果标准库已经有了类似的东西,为什么要麻烦呢?我正在使用VS2008,因此一些微软特定的扩展完全可以。我只需要四个操作:

  • 创建缓冲区
  • 将数据写入缓冲区(二进制垃圾,不是零终止符)
  • 获取已写入数据作为char数组(连同其长度)
  • 释放缓冲区

我需要的类/函数集合/任何东西的名称是什么?

补充:有几票投给std::vector。都很好,但我不想逐字节地推送几MB的数据。套接字将以几KB的大块向我提供数据,因此我想一次将它们全部写入。此外,最后我需要将数据作为简单的char*获取,因为我需要将整个blob不经修改地传递给一些Win32 API函数。


3
你不需要逐字逐句地推进。你可以将一个数据块插入到向量的末尾。 - Joe
10个回答

47

您需要一个 std::vector

std::vector<char> myData;

vector会自动分配和释放其内存。使用push_back添加新数据(如果需要,vector会自动调整大小),使用索引运算符[]检索数据。

如果您可以猜测所需的内存量,请调用reserve,以便后续的push_back不必重新分配太多内存。

如果您想读取一块内存并将其附加到缓冲区,则最简单的方法可能是:

std::vector<char> myData;
for (;;) {
    const int BufferSize = 1024;
    char rawBuffer[BufferSize];

    const unsigned bytesRead = get_network_data(rawBuffer, sizeof(rawBuffer));
    if (bytesRead <= 0) {
        break;
    }

    myData.insert(myData.end(), rawBuffer, rawBuffer + bytesRead);
}

myData现在拥有了所有读取的数据,我们以块的形式进行读取。然而,我们复制了两次。

相反地,我们尝试像这样:

std::vector<char> myData;
for (;;) {
    const int BufferSize = 1024;

    const size_t oldSize = myData.size();
    myData.resize(myData.size() + BufferSize);        

    const unsigned bytesRead = get_network_data(&myData[oldSize], BufferSize);
    myData.resize(oldSize + bytesRead);

    if (bytesRead == 0) {
        break;
    }
}

这种方法可以直接将数据读入缓冲区,但代价是会偶尔过度分配。

可以通过使向量大小在每次重新调整大小时增加两倍(就像第一个解决方案隐式地执行的那样),从而更加智能地实现此操作。当然,如果您具有对最终缓冲区可能的大小有先验知识,则可以事先使用 reserve() 来准备一个更大的缓冲区以尽量减少重新调整大小。

这两个方法都留给读者自己练习:)

最后,如果您需要将数据视为原始数组:

some_c_function(myData.data(), myData.size());

std::vector保证是连续的。


1
Vilx -- 使用myData.insert(myData.end(), bytes_ptr, bytes_ptr + bytes_count)。 - atzz
假设您有一个已知大小的缓冲区,vec.insert(vec.end, buf, buf+length) - KeithB
3
向量必须是连续的,这样才能够取得元素的地址并使用memcopy()函数将一个数据块复制到其中。你可能会因此感到恐惧,但请放心。 - RobH
4
为什么要使用中间缓冲区?为什么不直接将网络数据读入向量中?将向量调整为其旧大小+ N,将最大N个字节接收到&vector [old_vector_size]。 为什么要使用中间缓冲区?因为这可以减少内存分配和复制的次数。如果直接将网络数据读入向量中,则需要经常调整向量大小以适应新数据,这可能会导致额外的内存分配和复制操作。通过使用中间缓冲区,可以将所有数据读入缓冲区,然后一次性将其复制到向量中,从而减少了内存分配和复制的次数。为什么不直接将网络数据读入向量中?因为它可能需要多次调整向量大小,这可能会导致额外的内存分配和复制操作,从而影响性能。将向量调整为其旧大小+ N,接收最大N个字节到&vector[old_vector_size]。这样做是为了确保接收到新数据时,向量具有足够的空间来存储它。将向量大小设置为旧大小加上N,可以保证向量有足够的空间来存储N个新字节。然后,将最大N个字节接收到&vector[old_vector_size],从而将新数据添加到向量的末尾。 - sbk
默认情况下,resize将对所有元素进行零初始化,因此直接读入向量的第二个答案是用零初始化替换数据复制成本。为了获得更好的性能,请参见https://dev59.com/qWEi5IYBdhLWcg3wseGA#21028912以避免零初始化。 - Martin Sherburn
显示剩余4条评论

10

使用 std::string 可以实现这个功能:

  • 它支持嵌入的 null 字符。
  • 您可以通过调用带有指针和长度参数的 append() 方法,将多字节数据块附加到它上面。
  • 通过调用 data() 方法,您可以将其内容作为 char 数组获取,并通过调用 size()length() 方法获取其当前长度。
  • 析构函数自动处理缓冲区的释放,但您也可以通过调用 clear() 方法来擦除其内容而不销毁它。

但是我们可以在缓冲区内部拥有几个 \0 吗? - Offirmo
3
是的,当我说它支持嵌入式空值时,我的意思就是这个。 - Wyzard
1
有趣的是,std::string比我想象的更强大。参见https://dev59.com/lFXTa4cB1Zd3GeqPz0F2#5319584 +1 - Offirmo

9
std::vector<unsigned char> buffer;

每次 push_back 都会在末尾添加新的字符(如果需要,将重新分配内存)。如果您大致知道期望的数据量,可以调用 reserve 来最小化分配次数。

buffer.reserve(1000000);

如果你有以下这样的内容:
unsigned char buffer[1000];
std::vector<unsigned char> vec(buffer, buffer + 1000);

7

再次推荐使用std::vector。这个方法的代码量最小,避免了GMan的代码中额外的复制操作。

std::vector<char> buffer;
static const size_t MaxBytesPerRecv = 1024;
size_t bytesRead;
do
{
    const size_t oldSize = buffer.size();

    buffer.resize(oldSize + MaxBytesPerRecv);
    bytesRead = receive(&buffer[oldSize], MaxBytesPerRecv); // pseudo, as is the case with winsock recv() functions, they get a buffer and maximum bytes to write to the buffer

    myData.resize(oldSize + bytesRead); // shrink the vector, this is practically no-op - it only modifies the internal size, no data is moved/freed
} while (bytesRead > 0);

关于调用WinAPI函数 - 使用 &buffer[0](是的,这有点笨拙,但这就是它的方式)传递给 char* 参数,使用 buffer.size() 作为长度。
最后注意一点,您可以使用 std::string 替代 std::vector,除了您可以在缓冲区是字符串时使用 buffer.data() 而不是 &buffer[0],其他没有任何区别。

1
+1:如果你选择使用向量,这就是实现的方式。 我仍然认为在这里,向量只是一个{size,capacity,pointer}集合,你完全可以自己调用“realloc”函数。 - Useless
我认为C++实际上只是一些汇编指令,你应该使用它们。 :P - GManNickG
很好;D 我只是认为向量在这里并没有增加太多的抽象或表现力 - 尽管这可能取决于用户/读者对C内存分配的熟悉程度。 - Useless
1
@Useless:那么如何实现无麻烦的异常安全内存管理呢? - Sandeep Datta
好的,说得对:我习惯于使用惯用的C代码进行低级套接字编程(而POSIX套接字API不会抛出异常),但这既不是一般的良好风格,也不是惯用的C ++。 - Useless
@sbk 我认为这只是一个小错误,应该将 myData.resize(oldSize + bytesRead); 更改为 buffer.resize(oldSize + bytesRead); - enthusiasticgeek

4
我建议您查看Boost basic_streambuf,它是为此类目的而设计的。如果您不能(或不想)使用Boost,则可以考虑std::basic_streambuf,它非常相似,但需要更多的工作才能使用。无论哪种方式,您基本上都要从该基类派生,并重载underflow()以将数据从套接字读入缓冲区中。通常,您会将std::istream附加到缓冲区,因此其他代码从中读取数据的方式与它们从键盘(或其他设备)读取用户输入的方式几乎相同。

2

1
使用std::vector,这是一个保证存储连续的可增长数组(你的第三点)。

0
关于您的评论“我看不到一个append()”,在末尾插入是一样的。

vec.insert(vec.end,


0

如果您使用std::vector,那么您只是在使用它来管理原始内存。 您可以只malloc您认为需要的最大缓冲区,并跟踪写入偏移量/到目前为止读取的总字节数(它们是相同的)。 如果到达结尾...要么realloc,要么选择一种失败的方式。

我知道,这不是很C++风格,但这是一个简单的问题,其他提议似乎是引入不必要的复制的重量级方法。


1
好的,基本上那就是我想要做的。我只是想知道是否已经有一些内置的方法来实现这个了。 - Vilx-

0
这里的关键是,你想使用缓冲区来做什么。 如果你想保留带有指针的结构体,缓冲区必须首先分配内存地址并保持固定。 为了绕过这个问题,你必须使用相对指针和修正列表来在最终分配后更新指针。这将值得开设一门课程。(没有找到这样的东西)。

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