T*与char*指针运算

T* versus char* pointer arithmetic

本文关键字:运算 指针 char      更新时间:2023-10-16

假设有一个包含N个t类型元素的数组

T a[N];

根据c++ 14标准,在哪些条件下我们可以保证

(char*)(void*)&a[0] + n*sizeof(T) == (char*)(void*)&a[n],  (0<=n<N) ?

虽然这对于许多类型和实现都是正确的,但标准在脚注中以一种模棱两可的方式提到了它:

§5.7.6,footnote 85)另一种实现指针运算的方法…

几乎没有迹象表明这另一种方式被认为等同于标准的方式。它可能是对实现者的一个提示,暗示了许多符合标准的实现之一。


编辑:

人们低估了这个问题的难度。

这个问题不是关于你能在课本上读到什么,而是关于你能通过运用逻辑和推理从c++ 14标准中推断出什么。

如果你使用"连续的"或"连续的",请同时说明什么是连续的。

虽然T[]和T*密切相关,但它们是抽象,并且T* x N的加法可以由实现以任何一致的方式定义。

使用指针加法重新排列方程。如果p指向一个char,则p+1总是使用(§5.7(4))或一元加法来定义,因此我们不会遇到UB。原始版本包含一个指针减法,这可能在早期导致了UB。(char指针只能比较,不能解引用)。

In [dcl.array]:

数组类型的对象包含连续分配的非空对象T类型的N子对象集

连续表示T类型的任何连续子对象之间的偏移量为sizeof(T),这表示n子对象的偏移量为n*sizeof(T)

n < N的上界来自[expr.add]:

当对指针进行整型表达式的加减运算时,结果具有指针操作数的类型。如果表达式P指向包含n个元素的数组对象x的元素x[i],如果0 <= i + j < n,则表达式P + JJ + P(其中J的值为j)指向(可能是假设的)元素x[i + j];否则,行为未定义。

这总是正确的,但是你必须依赖sizeof运算符(5.3.3[expr.sizeof])给出的语义,而不是查看指针算术规则:

当应用于引用或引用类型时,结果是被引用类型的大小。当应用于类时,结果是该类对象中的字节数,包括在数组中放置该类型对象所需的任何填充。最派生类的大小应大于零。对基类子对象应用sizeof的结果是基类类型的大小。当应用于数组时,结果是数组中的总字节数。这意味着包含n个元素的数组的大小是n元素大小的倍。

应该清楚的是,只有一种包装将n非重叠元素放在n * sizeof(element)的空间中,即它们间隔sizeof (element)字节。关系操作符章节(5.9[expr.rel])中的指针比较规则只允许一种排序:

指针与对象的比较定义如下:

  • 如果两个指针指向同一数组的不同元素或其子对象,则指向下标高的元素的指针比下标高的元素的指针大。

第一行的声明也是一个定义。(§3.1 (2))它创建了数组对象。(§1.8 (1))

一个对象可以通过多个左值访问由于混叠规则。(§3.10(10))特别是,上面的物体右侧可以通过char指针合法访问(别名)。

让我们看一下数组定义中的一个句子,然后消除'连续'的歧义。

"数组类型的对象包含连续分配的非空集t型的N个子主体(dcl。数组)§8.3.4 .


消歧

我们从char对象的二进制对称关系"连续"开始,这应该是显而易见的。("iff"是"当且仅当"的缩写,集合和序列是数学上的,而不是c++容器)如果可以的话链接到一个更好或更公认的定义,注释。

A序列x_1…char对象的x_N是连续的对于所有i=1…N-1, x_i和x_{i+1}在内存中是连续的

一个集合M的char对象是连续的M可以编号,x_1…比如说,x_N,使得序列(x_i)_i是连续的。也就是说,iff M是一个连续的内射序列的像。

两个集合M_1, M_2的char对象在这里是连续的存在于M_1中的x_1和M_2中的x_2,使得x_1和x_2是连续的

A序列M_1…char对象集合的M_N是连续的对于所有i=1…N-1, M_i和M_{i+1}是连续的。

一组char对象的集合是连续的,如果它是的图像一个连续的、内射的char对象集合序列。

现在应用哪个版本的"连续"?语言重载解析:

1)"连续的"可能指的是"分配"。分配函数调用提供了一个可用char对象的子集,这将调用字符集变量。也就是说,在N个子对象中出现的所有char对象的集合将意味着是连续的。

2)"连续的"可能指的是"集合"。这将调用set-of- of-chars变体,并将每个子对象视为一组char对象。


这意味着什么?首先,虽然作者将数组子对象编号为a[0]…a[N-1],他们选择不说任何关于内存中子对象的顺序:他们使用'set'而不是'sequence'。他们说分配是连续的,但他们没有这么说A [j]和A [j+1]在内存中是连续的。而且,他们选择不写下简单的公式涉及(char*)指针和sizeof()。虽然看起来他们刻意将相邻性与排序问题分开,§5.9(3)要求所有类型的数组子对象都有一个相同的顺序。

如果指针指向同一数组的两个不同元素,或其子对象,则该指针

现在,组成数组子对象的字节是否符合上面引用的意义上的子对象?阅读§1.8(2)和完整对象或子对象?答案是:不,至少对于元素不包含子对象且不是字符数组的数组(例如int型数组)来说不是。因此,我们可以找到没有对数组元素施加特定顺序的例子。

但是现在让我们假设我们的数组子对象只使用字符填充。考虑到"连续"的两种可能解释,这意味着什么?

1)我们有一个连续的字节集,它与有序的子对象集一致。那么OP中的断言无条件为真。

2)我们有一个连续的子对象序列,每个子对象单独可以是不连续的。这可能以两种方式发生:要么子对象可能有间隙,即它们包含两个距离大于sizeof(subbobject)-1的char对象。或者是子对象可以分布在不同的连续字节序列中。

在情况2中,不能保证OP中的声明是真实的。

因此,明确"连续"的含义是很重要的。


最后,这里有一个实现的例子,在§5.9中没有明显的顺序强加给数组子对象,因为数组子对象本身没有子对象。读者担心这会与其他地方的标准相矛盾,但目前还没有明确的矛盾。

假设T是int,并且我们有一个特定的符合标准的实现,它的行为与预期的一样天真,只有一个例外:

它以反向内存顺序分配int数组,将数组的第一个元素放在对象的高内存地址端:

a[N-1], a[N-2], ... a[0]  

不是

a[0], a[1],   ... a[N-1]  

这个实现满足任何合理的连续性需求,所以我们不必就单一的解释达成一致'连续'继续参数

则如果p指向a,将p映射到&a[0](调用[conv.array])将使指针在a的高内存端附近跳转。由于数组算术必须与指针算术兼容,我们还需要

int * p= &intVariable;
(char*)(p+1) + sizeof(int) == (char*)p

int a[N];
(char*)(void*)&a[n] + n*sizeof(int)==(char*)(void*)&a[0],  (0<=n<N)

那么,对于T=int,不能保证原帖子中的说法是正确的。


编辑历史:删除并以修改后的形式重新引入一个可能错误的快捷方式,这是由于没有应用指针的相关部分<关系规范。目前还没有确定这是否合理,但无论如何,关于邻近的主要论点已经出现了。>