【问题标题】:Pointer arithmetic: out of bound without dereferencing指针算术:越界而没有解除引用
【发布时间】:2014-10-29 09:26:21
【问题描述】:

我想知道以下代码是否不被 C++ 标准接受。

int n{ 10 };
double* p = new double[0];
double* q = p + n;
std::cout << "n = " << static_cast<int>(q - p) << std::endl;

我希望该程序显示 n 的值。

由于这个问题可能看起来很奇怪,这里是对这个问题的起源的解释。我想在 2D 中设计一个动态数组类(想想 std::vector 类型的容器,但在 2D 中而不是 1D 中)。一个简单的方法是:

template <typename T>
class Array2D<T> {
private:
    T* data_;
    int nb_rows_;
    int nb_columns_;
public:
    ...
};

不幸的是,这种设计不像循环这样的 SIMD 友好

Array2D<int> A(5, 6);
for (int i = 0; i < A.nb_rows(); ++i) {
    for (int j = 0; j < A.nb_columns(); ++j) {
        A(i, j) += 1;
    }
}

将无法矢量化,因为编译器无法确定 nb_columns_ 在循环期间是否因指针别名而未更改。因此,我使用与大多数 std::vector 实现相同的设计,其中向量的大小“隐藏”在指针中。

template <typename T>
class Array2D<T> {
private:
    T* data_;
    T* nb_rows_;
    T* nb_columns_;
public:
    Array2D(int n, int p) {
        data_ = new T[n * p];
        nb_rows_ = data_ + n;
        nb_columns_ = data_ + p;
    }
    ...
    int nb_columns() const {
        return static_cast<int>(nb_columns_ - data_);
    }
    ...
};

只要 n >= 1 和 p >= 1,这个设计就可以很好地工作。但是如果 n = 0 和 p = 5,你最终会遇到上面解释的那种“问题”。由于我的类中有以下方法,构造一个 0 行的 Array2D 可能很有用

void push_back(const Array1D<T>& B);

接受大小为 p 的 Array1D(使用断言检查)并向我的 Array2D 对象添加一行。你可以这样做:

Array2D<double> A(0, 10);
Array1D<double> B(10);

// work with B
A.push_back(B);

代码在 clang、g++ 和 icpc 上运行良好,但我仍然想知道它是否有效。 C++11 标准的第 5.7 节是关于这个问题的,但谈论的是“数组对象”。我想知道我的 p 是否指向他们所谓的“数组对象”,或者数组对象是否是诸如“double p[5]”之类的东西。

【问题讨论】:

  • @Joachim Pileborg:我想确定这一点。你能在这一点上引用标准吗?也可能有一些指针溢出,因为 data_ + n 可能会溢出。
  • 我不知道如何处理零大小的数组?
  • 正如 Ali Kazmi 所说,零大小的数组有什么好处?难道你不能只检查 n*p==0 然后像这样返回:Array2D(int n, int p) { if (n*p==0) return; data_ = new T[n * p]; nb_rows_ = data_ + n; nb_columns_ = data_ + p; }
  • @Ali Kazmi: new double[0] 被 C++ 标准接受。它可能有用的原因是我的类被设计成可以添加一行(如 std::vector 中的 push_back)。
  • @JoachimPileborg 指针算法仅针对同一数组内的指针定义,或指向该数组末尾的指针。在他的示例中,将 10 添加到 p 会导致指针不再满足这些条件,因此是未定义的行为。

标签: c++ arrays language-lawyer


【解决方案1】:

这是未定义的行为。在实践中,它可能适用于大多数现代系统,但过去曾有一些系统会导致程序崩溃。指针只是一种特殊类型的整数;它可以有各种结构,只是将指向未映射内存的指针加载到寄存器中可能会导致陷阱。

来自标准(强调添加),§5.7/5:

当一个具有整数类型的表达式被添加到或 从指针中减去,结果的类型为 指针操作数。如果指针操作数指向一个元素 一个数组对象,并且数组足够大,结果 指向从原始元素偏移的元素,使得 结果和原始的下标的差异 数组元素等于整数表达式。换一种说法, 如果表达式 P 指向数组的第 i 个元素 对象,表达式 (P)+N(等价于 N+(P))和 (P)-N (其中 N 的值为 n)分别指向第 i + n 个 和 i - 数组对象的第 n 个元素,前提是它们存在。 此外,如果表达式 P 指向一个 数组对象,表达式 (P)+1 指向最后一个 数组对象的元素,如果表达式 Q 指向 1 经过数组对象的最后一个元素,表达式 (Q)-1 指向数组对象的最后一个元素。 如果两者 指针操作数和结果指向相同的元素 数组对象,或数组对象的最后一个元素, 评估不应产生溢出;否则, 行为未定义。

最后一句话很重要:“否则, 行为未定义”。

【讨论】:

  • 那是我所指的标准的一部分。不幸的是,该标准似乎没有定义“数组对象”是什么。我想知道它是否只是诸如 double p[5] 之类的东西,还是诸如 double* p = new double[5] 之类的东西。您是否在标准中找到了定义什么是“数组对象”的内容?
  • @InsideLoop 如果您分配为数组(例如new [...]),则生成的指针是指向数组对象的指针。此外,如果您声明一个数组,它也是一个数组对象。通过阅读 C++11 标准的 $5.3.4/1 可以稍微澄清一下。另见例如18.6.1.2/1 美元。
  • @InsideLoop :检查上面的段落:“7 对于这些运算符,指向不是数组元素的对象的指针与指向第一个元素的指针的行为相同一个长度为 1 的数组,其元素类型为对象的类型。”,因此在这种情况下,一切都是数组。
  • @Joachim:感谢您引用标准。
  • @JoachimPileborg 如果使用数组 new,则创建一个数组对象,但生成的指针是指向数组对象第一个元素的指针(如果是,则指向数组对象末尾的指针) new [0])。它是一个进入数组对象的指针,而不是指向数组对象的指针。
【解决方案2】:
double* q = p + n;

这一行会引发未定义的行为。 [expr.add]/5 指定

当一个具有整数类型的表达式被添加到指针或从指针中减去时,结果具有类型 的指针操作数。 […] 如果指针操作数和结果都指向 相同的数组对象,或数组对象的最后一个元素, 评估不应产生溢出;否则,行为 未定义。

结果不指向p 的元素,因此适用最后一句。它肯定会在您测试它的任何系统上工作*,但它并未以任何方式包含在标准中。


不幸的是,标准似乎没有定义什么是“数组 对象”是。

数组object就是一个数组类型的对象,简单来说就是:数组。

int a[5];

声明一个包含五个元素和大小sizeof int * 5 的数组(对象)。还可以考虑 [expr.add]/4:

对于这些运算符,指向非数组对象的指针 行为与指向数组的第一个元素的指针相同 长度为一,对象的类型作为其元素类型。

* 你使用的系统大概会将指针中的地址表示为简单的整数,加法和减法只会调用简单的算术。可能还有一些系统检查加载到寄存器中的指针值。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-07-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多