【问题标题】:C++ variable length arrays in struct结构中的 C++ 可变长度数组
【发布时间】:2014-11-14 07:00:42
【问题描述】:

我正在编写一个用于创建、发送、接收和解释 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 个数组的大小留空即可编译,但我不知道它实际上是如何工作的。从逻辑上讲它不能工作,因为如果第一个数组的大小未知,编译器将不知道第二个数组从哪里开始。

【问题讨论】:

  • 如果最大地址大小为 6,你不能制作大小为 [6] 的数组,然后相应地解释它们吗?如果您想避免大量代码,这是最简单的解决方案。另一种选择是为所有地址使用一个固定长度的大数组,并编写一个函数来根据地址的长度准备一个字节数组
  • 零长度数组或灵活数组成员不是有效的 C++。
  • 为什么不使用 std::string 或 std::vector 作为结构成员?如果你使用结构,为什么不给它们功能呢?将代码划分为数据和程序与面向对象编程完全相反。您的问题本身听起来像是设计失败!
  • 嗯,operationCode 之后的内存块的解释实际上取决于 protocolType (IPv4/IPv6),对。我建议在此处放置一个不透明的占位符指针,并将其余部分解释为 mac 和 IP 地址的两个附加结构。与netinet/in.h 结构定义中的处理类似。
  • 重载operator char*ArpHeader(char* data) 不适合您的需要吗?你已经试过了吗? AFAIK 然后真正的底层结构变得无关紧要。

标签: c++ arrays struct arp


【解决方案1】:

您想要一个相当低级别的东西,一个 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_refconst uint8_t * .

【讨论】:

    【解决方案2】:

    如果您想保持对数据的简单访问和数据本身public,有一种方法可以在不改变访问数据的方式的情况下实现您想要的。首先,您可以使用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 的答案在较低级别上更优雅,它实际上是我一直在寻找的,因为它只需要小代码。
    【解决方案3】:

    简答:你不能在 C++ 中拥有可变大小的类型。

    C++ 中的每种类型在编译期间都必须具有已知(且稳定)的大小。 IE运营商sizeof()必须给出一致的答案。请注意,您可以使用堆来拥有保存可变数据量的类型(例如:std::vector&lt;int&gt;),但实际对象的大小始终是恒定的。

    因此,您永远无法生成一个类型声明,您可以强制转换并神奇地调整字段。这深入到了基本的对象布局——每个成员(又名字​​段)都必须有一个已知的(并且稳定的)偏移量。

    通常,通过编写(或生成)解析输入数据并初始化对象数据的成员函数来解决问题。这基本上是一个古老的数据序列化问题,在过去 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_;
    };
    

    注意事项:

    • 上面的代码显示了可以让您执行以下操作的非常基本的结构:

      packet p;
      if (!p.parse_in(input, sz))
          return false;
      
    • 通过 RAII 做同样事情的现代方式如下所示:

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

    【讨论】:

    • 我了解面向对象的编程(包括类等)是如何工作的。这篇文章的目的是找出是否有一种更快的方法,用更少的代码来编写一个 ARP 头对象,就像我编写一个 UDP 头结构一样,它本质上可以转换为一个字节数组,以避免长转换函数。但无论如何,谢谢你澄清这一点。
    猜你喜欢
    • 1970-01-01
    • 2023-03-04
    • 2014-03-14
    • 1970-01-01
    • 2013-11-26
    • 2015-08-09
    相关资源
    最近更新 更多