【问题标题】:Portable and safe way to add byte offset to any pointer向任何指针添加字节偏移的便携且安全的方法
【发布时间】:2023-04-09 16:12:01
【问题描述】:

我是 C++ 的新手,还没有掌握该语言的所有复杂性和微妙之处。

在 C++11 中向任何类型的指针添加任意字节偏移量的最可移植、正确和安全的方法是什么?

SomeType* ptr;
int offset = 12345 /* bytes */;
ptr = ptr + offset;             // <--

我在 Stack Overflow 和 Google 上找到了很多答案,但它们都提出了不同的建议。我遇到的一些变种:

  1. Cast to char *:

    ptr = (SomeType*)(((char*)ptr) + offset);
    
  2. 投射到unsigned int:

    ptr = (SomeType*)((unsigned int)ptr) + offset);
    
  3. Cast to size_t:

    ptr = (SomeType*)((size_t)ptr) + offset);
    
  4. “size_t 和 ptrdiff_t 的大小总是与指针的大小一致。因此,正是这些类型应该用作大型数组的索引,用于存储指针和指针运算。” - CodeProject 上的About size_t and ptrdiff_t

    ptr = (SomeType*)((size_t)ptr + (ptrdiff_t)offset);
    
  5. 或者像以前一样,但使用intptr_t instead of size_t,它是有符号的而不是无符号的:

    ptr = (SomeType*)((intptr_t)ptr + (ptrdiff_t)offset);
    
  6. 只转换为intptr_t,因为offset 已经是一个有符号整数并且intptr_t is not size_t:

    ptr = (SomeType*)((intptr_t)ptr) + offset);
    

在所有这些情况下,使用旧的 C 风格转换是否安全,或者为此使用 static_cast 或 reinterpret_cast 是否更安全或更便携?

我应该假设指针值本身是无符号的还是有符号的?

【问题讨论】:

  • 没有。向指针添加任意字节偏移量是未定义的行为。你只能对指向同一个数组的指针进行算术运算(并且超过它的末尾)。
  • @jrok 将任意偏移量添加到指针是完美的定义。未定义的是取消引用不指向有效内存的指针。
  • @sfstewman 它不会导致我知道的实现错误,但是 IIRC 有一个条款禁止超过一个对象超出数组的末尾(即 int a[5]; a + 5; 很好,int a[5]; a + 6不好)。编辑:找到来源:stackoverflow.com/a/988220/395760
  • @sfstewman:C++ 草案 n3092 5.7 5:“如果指针操作数和结果都指向同一个数组对象的元素,或者超过数组对象的最后一个元素,则评估不应产生溢出;否则,行为未定义。”
  • @sfstewman 你错了。该标准明确将其设为 UB(参见上面的评论)。在实践中,是的,它只是有效,至少在你粉碎自己的堆栈或类似的东西之前。

标签: c++ pointers c++11 pointer-arithmetic


【解决方案1】:

我会使用类似的东西:

unsigned char* bytePtr = reinterpret_cast<unsigned char*>(ptr);
bytePtr += offset;

【讨论】:

  • 我会使用更简洁的 (reinterpret_cast&lt;unsigned char *&gt;(ptr) + offset) 可能包含在一个内联(可能是模板)函数中,这取决于我需要它的频率以及返回的类型应该是什么。
  • @Virtlink: unsigned char 首选用于处理字节,因为语言标准要求它是值的简单二进制表示,并且所有位模式都对应于一个值。相比之下,char 和 signed char 可能使用二进制补码、一个补码或有符号幅度,并且可能具有与值不对应的位模式。
  • @Virtlink:对于 C 和 C++,unsigned char 是一个字节。 char 中位数的标准允许变化是针对旧的或深奥的平台,其中内存以 9 位单元组织,而不是 C 或 C++ 实现可以为您提供 16 位char 对象,而寻址使用 8 位单元。
  • @Virtlink 是的,char 不需要是 8 位,但你知道吗,你不在乎。 char 被保证是 C++ 测量大小以及系统寻址粒度的单位。希望您不打算将为 8 位平台编写的代码(并在低级别上工作)与为 9 位平台编写的代码混合使用。
  • @Virtlink 因为该标准不保证转换为 int、干扰 int 和转换回。使用转换为 int 的指针唯一可以做的就是将其转换回去。当然,它很可能适用于任何实用平台(就像任何实用平台都有 8 位字符一样),但之后使用这个指针真的是 UB(如果你不想使用它,那为什么无论如何添加偏移量?)。最后,我什至认为没有人保证指针转换为字节地址(同样,在大多数实际平台上确实可以)。
【解决方案2】:

使用reinterpret_cast(或C-style cast)意味着绕过类型系统并且不可移植且不安全。是否正确,取决于您的架构。 如果您(必须)这样做,则暗示您知道自己在做什么,从那时起您基本上就靠自己了。警告就这么多。

如果将数字n 添加到指针或类型T,则将该指针移动n 类型T 的元素。您正在寻找的是一种类型,其中 1 个元素表示 1 个字节。

来自sizeof 5.3.3.1 节:

sizeof 运算符产生对象中的字节数 其操作数的表示。 [...]sizeof(char)、sizeof(signed char) 和 sizeof(unsigned char) 是 1。大小的结果 适用于任何其他基本类型(3.9.1)的是 实现定义。

注意,没有关于sizeof(int)等的声明。

字节的定义(1.7.1.):

C++ 内存模型中的基本存储单元是字节。一种 字节至少足够大以包含基本的任何成员 执行字符集(2.3)和八位代码单元 Unicode UTF-8 编码形式,由一个连续的序列组成 位数,其数量由实现定义。 [...] 这 C++程序可用的内存由一个或多个序列组成 连续字节。 每个字节都有一个唯一的地址。

因此,如果sizeof 返回字节数且sizeof(char) 为1,则char 的大小为C++ 的一个字节。因此,char逻辑上是 C++ 的一个字节,但不一定是事实上的标准 8 位字节。 将n 添加到char* 将返回一个距离n 字节(就C++ 内存模型而言)的指针。因此,如果您想玩按字节操作对象指针的危险游戏,您应该将其转换为char 变体之一。 如果你的类型也有像const这样的限定符,你也应该把它们转移到你的“字节类型”中。

    template <typename Dst, typename Src>
    struct adopt_const {
        using type = typename std::conditional< std::is_const<Src>::value,
            typename std::add_const<Dst>::type, Dst>::type;
    };

    template <typename Dst, typename Src>
    struct adopt_volatile {
        using type = typename std::conditional< std::is_volatile<Src>::value,
            typename std::add_volatile<Dst>::type, Dst>::type;
    };

    template <typename Dst, typename Src>
    struct adopt_cv {
        using type = typename adopt_const<
            typename adopt_volatile<Dst, Src>::type, Src>::type;
    };

    template <typename T>
    T*  add_offset(T* p, std::ptrdiff_t delta) noexcept {
        using byte_type = typename adopt_cv<unsigned char, T>::type;
        return reinterpret_cast<T*>(reinterpret_cast<byte_type*>(p) + delta);
    }

Example

【讨论】:

    【解决方案3】:

    请注意,NULL 是特殊的。在其上添加偏移量是危险的。
    reinterpret_cast 无法删除 const 或 volatile 限定符。更便携的方式是 C 风格的演员表。
    reinterpret_cast 具有像 @user2218982 的答案这样的特征,似乎更安全。

    template <typename T>
    inline void addOffset( std::ptrdiff_t offset, T *&ptr ) { 
        if ( !ptr )
            return;
        ptr = (T*)( (unsigned char*)ptr + offset );
    } 
    

    【讨论】:

      【解决方案4】:

      我的没有那么优雅,但我希望更具可读性。 字符 helper_ptr; helper_ptr= (char) ptr;

      然后就可以使用 helper_ptr 逐字节遍历了。

      ptr = (SomeType*)(((char*)ptr) + 1) 将 ptr 前进 sizeof(SomeType) 而不是 1 个字节。

      【讨论】:

        【解决方案5】:

        如果你有:

        myType *ptr;
        

        你做到了:

        ptr+=3;
        

        编译器肯定会通过以下方式增加您的变量:

        3*sizeof(myType)
        

        据我所知,这是执行此操作的标准方法。

        如果你想迭代,比如说一个 myType 类型的元素数组,那就是这样做的方法。

        好的,如果你想使用

        进行投射
        myNewType *newPtr=reinterpret_cast < myNewType * > ( ptr )
        

        或者坚持使用普通的旧 C 并执行:

        myNewType *newPtr=(myNewType *) ptr;
        

        然后递增

        【讨论】:

        • 我知道当你不投射时它是如何工作的。我想将 any 字节偏移量(例如,0xABC 字节)添加到任何类型的指针 MyType*,而不管其类型的大小。如果MyType* ptr = (MyType*)0x1000 那么我想以ptr == (MyType*)0x1ABC 结束。
        • 你应该避免在 C++ 中使用 C 风格的类型转换来处理除了数字类型转换之外的任何东西。编译器将应用除 dynamic_cast 之外的第一个 C++ 强制转换。一个问题(除其他外)是 C 风格的强制转换可以删除对象的 const 性,而没有迹象表明它在源代码中或通过编译器诊断发生。
        • 这如何回答这个问题?
        猜你喜欢
        • 2010-12-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-02-17
        • 2020-12-22
        • 1970-01-01
        • 2021-12-23
        • 2011-08-02
        相关资源
        最近更新 更多