C++结构体中的可变长度数组

5
我将为创建、发送、接收和解释ARP数据包编写程序。我有一个表示ARP头的结构,如下所示:
struct ArpHeader
{
    unsigned short hardwareType;
    unsigned short protocolType;
    unsigned char hardwareAddressLength;
    unsigned char protocolAddressLength;
    unsigned short operationCode;
    unsigned char senderHardwareAddress[6];
    unsigned char senderProtocolAddress[4];
    unsigned char targetHardwareAddress[6];
    unsigned char targetProtocolAddress[4];
};

这仅适用于硬件地址长度为6且协议地址长度为4的情况。地址长度也在头部中给出,因此要正确,结构应如下所示:

struct ArpHeader
{
    unsigned short hardwareType;
    unsigned short protocolType;
    unsigned char hardwareAddressLength;
    unsigned char protocolAddressLength;
    unsigned short operationCode;
    unsigned char senderHardwareAddress[hardwareAddressLength];
    unsigned char senderProtocolAddress[protocolAddressLength];
    unsigned char targetHardwareAddress[hardwareAddressLength];
    unsigned char targetProtocolAddress[protocolAddressLength];
};

这显然行不通,因为地址长度在编译时是未知的。模板结构也不是一个选项,因为我想填写结构的值,然后只需将它从(ArpHeader*)转换为(char*),以获取可以发送到网络上的字节数组,或将接收到的字节数组从(char*)转换为(ArpHeader*)以解释它。
其中一种解决方案是创建一个类,拥有所有头字段作为成员变量,一个函数来创建表示ARP头的字节数组,该字节数组可以被发送到网络上,以及一个构造函数,该构造函数仅需要一个字节数组(在网络上接收)并通过读取所有头字段并将它们写入成员变量来解释它。但这不是一个好的解决方案,因为它需要更多的代码。
相反,对于UDP头的类似结构很简单,因为所有头字段都具有已知的恒定大小。
#pragma pack(push, 1)
#pragma pack(pop)

在结构声明周围添加代码,这样我就可以使用简单的C风格转换获取要发送到网络上的字节数组。

这里是否有任何解决方案可以接近结构或至少不需要比结构多很多的代码? 我知道结构中的最后一个字段(如果是数组)不需要特定的编译时大小,我能否在我的问题中使用类似的东西?只需将这4个数组的大小留空即可编译,但我不知道它实际上会如何运作。从逻辑上讲,它不能正常工作,因为如果第一个数组的大小未知,则编译器将无法确定第二个数组从哪里开始。


3
如果最大地址长度为6,您可以创建大小为[6]的数组,然后相应地进行解释,这是避免编写大量代码的最简单解决方案。另一个选择是对所有地址使用一个固定长度的大型数组,并编写一个函数根据地址的长度准备一个字节数组。 - Adam Kosiorek
零长度数组或者是可变数组成员不是有效的C++。 - pmr
1
为什么不要将std::string或std::vector用作结构体成员?如果您使用结构体,为什么不给它们功能?将代码分为数据和程序恰恰是面向对象编程的反面。你的问题本身就像是一个设计失误! - Klaus
3
重载 operator char*ArpHeader(char* data) 是否符合您的需求?您已经尝试过了吗?据我所知,这样实际底层结构变得无关紧要。 - Paweł Stawarz
使用结构的整个目的是能够将其简单地转换为字节数组,以避免长时间的转换函数和获取器/设置器方法用于标头字段,类似于使用UDP标头结构的方式。 - PlanckMax
显示剩余2条评论
3个回答

9

您想要一个相对较低级别的东西,即ARP数据包,并且您正在尝试找到一种正确定义数据结构的方法,以便您可以将blob强制转换为该结构。相反,您可以在blob上使用接口。

struct ArpHeader {
    mutable std::vector<uint8_t> buf_;

    template <typename T>
    struct ref {
        uint8_t * const p_;
        ref (uint8_t *p) : p_(p) {}
        operator T () const { T t; memcpy(&t, p_, sizeof(t)); return t; }
        T operator = (T t) const { memcpy(p_, &t, sizeof(t)); return t; }
    };

    template <typename T>
    ref<T> get (size_t offset) const {
        if (offset + sizeof(T) > buf_.size()) throw SOMETHING;
        return ref<T>(&buf_[0] + offset);
    }

    ref<uint16_t> hwType() const { return get<uint16_t>(0); }
    ref<uint16_t> protType () const { return get<uint16_t>(2); }
    ref<uint8_t> hwAddrLen () const { return get<uint8_t>(4); }
    ref<uint8_t> protAddrLen () const { return get<uint8_t>(5); }
    ref<uint16_t> opCode () const { return get<uint16_t>(6); }

    uint8_t *senderHwAddr () const { return &buf_[0] + 8; }
    uint8_t *senderProtAddr () const { return senderHwAddr() + hwAddrLen(); }
    uint8_t *targetHwAddr () const { return senderProtAddr() + protAddrLen(); }
    uint8_t *targetProtAddr () const { return targetHwAddr() + hwAddrLen(); }
};

如果您需要保持 const 正确性,您需要删除 mutable ,创建一个 const_ref ,并将访问器复制到非 const 版本中,并使 const 版本返回 const_ref const uint8_t *


3
简短回答:在C++中你无法拥有可变大小的类型。
在编译过程中,C++中的每个类型都必须具有已知(并且稳定)的大小。即运算符sizeof() 必须给出一致的答案。请注意,您可以使用堆来存储可变数量的数据(例如:std::vector<int>),但实际对象的大小总是恒定的。
因此,您永远无法生成要进行强制转换并使其字段自动调整的类型声明。这深入到基本对象布局中-每个成员(也称为字段)必须具有已知(并且稳定)的偏移量。
通常,问题通过编写(或生成)成员函数来解决,这些成员函数解析输入数据并初始化对象的数据。这基本上是古老的数据序列化问题,在过去的30年左右已经解决了无数次。
下面是一个基本解决方案的示例:
class packet { 
public:
    // simple things
    uint16_t hardware_type() const;

    // variable-sized things
    size_t sender_address_len() const;
    bool copy_sender_address_out(char *dest, size_t dest_size) const;

    // initialization
    bool parse_in(const char *src, size_t len);

private:    
    uint16_t hardware_type_;    
    std::vector<char> sender_address_;
};

Notes:
注意事项:
  • the code above shows the very basic structure that would let you do the following:

    packet p;
    if (!p.parse_in(input, sz))
        return false;
    
  • the modern way of doing the same thing via RAII would look like this:

    if (!packet::validate(input, sz))
        return false;
    
    packet p = packet::parse_in(input, sz);  // static function 
                                             // returns an instance or throws
    

我理解面向对象编程,包括类等的工作原理。发表此帖的目的是想找出是否有一种更快捷的方式来编写ARP头部对象,就像我编写UDP头部结构体一样,可以将其转换为字节数组,以避免冗长的转换函数。不过还是要谢谢您对此进行澄清。 - PlanckMax

2

如果你想保持对数据的访问简单,同时保持数据本身是公开的,那么有一种方法可以在不改变数据访问方式的情况下实现你想要的目标。首先,你可以使用 std::string 代替 char 数组来存储地址:

#include <string>
using namespace std; // using this to shorten notation. Preferably put 'std::'
                     // everywhere you need it instead.
struct ArpHeader
{
    unsigned char hardwareAddressLength;
    unsigned char protocolAddressLength;

    string senderHardwareAddress;
    string senderProtocolAddress;
    string targetHardwareAddress;
    string targetProtocolAddress;
};

然后,您可以重载转换运算符operator const char*()和构造函数arpHeader(const char*)(当然最好也重载operator=(const char*)),以便保持您当前的发送/接收函数正常工作,如果这是您需要的话。一个简化的转换运算符(省略了一些字段,使其更简单,但您应该可以轻松添加它们回来),看起来像这样:
operator const char*(){
    char* myRepresentation;
    unsigned char mySize
            = 2+ senderHardwareAddress.length()
            + senderProtocolAddress.length()
            + targetHardwareAddress.length()
            + targetProtocolAddress.length();

    // We need to store the size, since it varies
    myRepresentation = new char[mySize+1];
    myRepresentation[0] = mySize;
    myRepresentation[1] = hardwareAddressLength;
    myRepresentation[2] = protocolAddressLength;

    unsigned int offset = 3; // just to shorten notation
    memcpy(myRepresentation+offset, senderHardwareAddress.c_str(), senderHardwareAddress.size());
    offset += senderHardwareAddress.size();
    memcpy(myRepresentation+offset, senderProtocolAddress.c_str(), senderProtocolAddress.size());
    offset += senderProtocolAddress.size();
    memcpy(myRepresentation+offset, targetHardwareAddress.c_str(), targetHardwareAddress.size());
    offset += targetHardwareAddress.size();
    memcpy(myRepresentation+offset, targetProtocolAddress.c_str(), targetProtocolAddress.size());

    return myRepresentation;
}

虽然构造函数可以定义如下:

ArpHeader& operator=(const char* buffer){

    hardwareAddressLength = buffer[1];
    protocolAddressLength = buffer[2];

    unsigned int offset = 3; // just to shorten notation
    senderHardwareAddress = string(buffer+offset, hardwareAddressLength);
    offset += hardwareAddressLength;
    senderProtocolAddress = string(buffer+offset, protocolAddressLength);
    offset += protocolAddressLength;
    targetHardwareAddress = string(buffer+offset, hardwareAddressLength);
    offset += hardwareAddressLength;
    targetProtocolAddress = string(buffer+offset, protocolAddressLength);

    return *this;
}
ArpHeader(const char* buffer){
    *this = buffer; // Re-using the operator=
}

然后使用你的类就很简单了:
ArpHeader h1, h2;
h1.hardwareAddressLength = 3;
h1.protocolAddressLength = 10;
h1.senderHardwareAddress = "foo";
h1.senderProtocolAddress = "something1";
h1.targetHardwareAddress = "bar";
h1.targetProtocolAddress = "something2";

cout << h1.senderHardwareAddress << ", " << h1.senderProtocolAddress
<< " => " << h1.targetHardwareAddress << ", " << h1.targetProtocolAddress << endl;

const char* gottaSendThisSomewhere = h1;
h2 = gottaSendThisSomewhere;

cout << h2.senderHardwareAddress << ", " << h2.senderProtocolAddress
<< " => " << h2.targetHardwareAddress << ", " << h2.targetProtocolAddress << endl;

delete[] gottaSendThisSomewhere;

应该提供所需的实用程序,并在不改变类外任何内容的情况下使您的代码正常工作。

但请注意,如果您愿意稍微更改代码(这里说的是您已经编写的类外部分),jxh的答案应该与此一样快,并且内部更加优雅。


谢谢,这是一个完全有效的解决方案,但正如你已经提到的,jhx的答案在更低的层面上更加优雅,而且实际上也是我正在寻找的,因为它只需要很少的代码。 - PlanckMax

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