为什么指针减法是C ++中未定义的行为?

时间:2017-12-19 16:51:00

标签: c++ pointers

对于下面的示例,可能导致undefined behavior的原因是什么?和为什么?

#include <cstddef> 
#include <iostream> 

template <typename Ty> 
bool in_range(const Ty *test, const Ty *r, size_t n) 
{ 
    return 0 < (test - r) && (test - r) < (std::ptrdiff_t)n; 
}

void f() { 
     double foo[10]; 
     double *x = &foo[0]; 
     double bar; 
     std::cout << std::boolalpha << in_range(&bar, x, 10);
}

我在When is pointer subtraction undefined in C?

中找不到答案

3 个答案:

答案 0 :(得分:9)

指针算法,包括两个指针的减法,仅在指针指向同一数组中的元素或超过该数组末尾的元素时才定义。在此上下文中,标量计为大小为1的数组。

在任何其他实例中允许指针运算是没有意义的。要做到这一点会不必要地限制C的内存模型,并可能削弱其灵活性和移植到异国情调架构的能力。

答案 1 :(得分:4)

对于代码,正如您所编写的那样,C ++的答案基本上与C的答案相同:当且仅当涉及的两个指针引用同一数组的部分时,才会得到定义的行为,或者在结束之前(如@bathsheba已经指出的那样,非数组对象被视为与一个项目的数组相同)。

然而,

C ++确实增加了一条可能对此有用的皱纹:即使在进行有意义的结果时,也不需要减法或有序比较(例如,<)应用于指向单独对象的指针,std::less<T>和朋友,<functional> 需要这样做。所以,给出两个单独的对象:

Object a;
Object b;

...将两个对象的地址与比较对象进行比较必须&#34;产生在这些特化之间一致的严格总顺序,并且与内置运算符强加的部分顺序一致&lt; ,&gt;,&lt; =,&gt; =。&#34; (N4659,[比较] / 2)。

因此,您可以编写如下函数:

template <typename Ty> 
bool in_range(const Ty *test, const Ty *begin, const Ty *end) 
{ 
    return std::less_equal<Ty>()(begin, test) && std::less<Ty>()(test, end);
}

如果你真的想要维护原始的功能签名,你也可以这样做:

template <typename Ty> 
bool in_range(const Ty *test, const Ty *r, size_t n) 
{ 
    auto end = r + n;
    return std::less_equal<Ty>()(r, test) && std::less<Ty>()(test, end);
}

[请注意,我已使用std::less_equal为第一次比较编写了它,第二次使用std:less来匹配C ++的典型预期语义,其中范围定义为{{ 1}}。 ]

这确实带有一个附带条件:您需要确保[begin, end)指向至少r 1 的数组的开头,否则{ {1}}将产生未定义的行为。

至少对于我期望的这种函数的典型用例,你可以通过传递数组本身来简化使用,而不是指针和显式长度:

n

在这种情况下,您只需传递数组的名称和要测试的指针:

auto end = r + n;

支持对实际数组进行测试。如果您尝试传递一个非数组项作为第一个参数,它只是不会编译:

template <class Ty, size_t N>
bool in_range(Ty (&array)[N], Ty *test) {
    return  std::less_equal<Ty *>()(&array[0], test) && 
            std::less<Ty *>()(test, &array[0] + N);
}

前者可能会阻止一些事故。后者可能不是很理想。如果我们想要支持两者,我们可以通过重载来实现(对于所有三种情况,如果我们选择):

int foo[10];
int *bar = &foo[4];

std::cout << std::boolalpha << in_range(foo, bar) << "\n"; // returns true

1.如果你想获得真正的技术,它不必指向数组的开头 - 它必须指向一个数组的一部分,后面跟着至少{{1}数组中的项目。 功能

答案 2 :(得分:1)

在这种情况下,未定义的行为通常不会导致崩溃,但是会导致无意义或不一致的结果。

在大多数现代体系结构中,减去2个不相关的指针只是计算地址差除以指针类型的大小,大约是:

    A *p1, *p2;
    ...
    ptrdiff_t diff = ((intptr_t)p2 - (intptr_t)p1) / (intptr_t)sizeof(*p1);

行为异常的架构示例是英特尔的16位分段大中型机型:

  • 这些模型在386及其32位模型问世之前曾经在PC上盛行。
  • 远指针分为两部分存储:一个16位段(或处于保护模式的选择器)和一个16位偏移量。
  • 比较2个指针是否相等需要2条单独的比较指令以及该段和偏移量的条件跳转。
  • 比较指向NULL的指针通常被优化为将段部分与0进行一次比较
  • 仅对偏移量部分减去2个指针并进行相对位置比较,无声地假设两个指针都指向同一数组,因此具有相同的段。
  • 在您的示例中,两个对象都具有自动存储功能,因此它们都位于同一段中,指向SS,但是对于从堆分配的2个对象,您可以拥有p <= q && q <= p和{{ 1}},或者p != qp - q == 0一起被未定义的行为覆盖。