指针索引


11

我目前正在阅读一本书,标题为“ C的数字食谱”。在本书中,作者详细介绍了如果我们的索引以1开头(我不完全遵循他的论点,而这不是本文的重点),某些算法在本质上会更好地工作,但是C总是以0开头的数组索引为了解决这个问题,他建议在分配后简单地减小指针,例如:

float *a = malloc(size);
a--;

他说,这将有效地为您提供一个索引,该索引的索引从1开始,然后将被释放:

free(a + 1);

据我所知,这是C标准未定义的行为。在HPC社区中,这显然是一本享有盛誉的书,所以我不想简单地无视他在说什么,但是对我而言,仅将指针减少到所分配的范围之外似乎是很粗略的。这是C中的“允许”行为吗?我已经使用gcc和icc对其进行了测试,这两个结果似乎都表明我没有担心什么,但我想绝对肯定。


3
您指的是什么C标准?我之所以问是因为,根据我的回忆,“ C的数字食谱”已于1990年代在K&R甚至ANSI C的远古时代出版
gna


3
“我已经使用gcc和icc对其进行了测试,这两个结果似乎都表明我担心的只是什么,但我想绝对肯定。” 永远不要假设由于编译器允许使用C语言允许使用它。除非,当然,除非您将来会破坏代码。
Doval 2014年

5
不希望公开,“数值食谱”通常被认为是一本有用,快速而肮脏的书,而不是软件开发或数值分析的范例。查阅Wikipedia上有关“数字接收方”的文章,以总结一些批评。
查尔斯E.格兰特

Answers:


16

您是对的,例如

float a = malloc(size);
a--;

根据ANSI C标准第3.3.6节,产生不确定的行为:

除非指针操作数和结果都指向同一数组对象的一个​​成员,或者指向数组对象的最后一个成员,否则该行为是不确定的

对于这样的代码,书中的C代码质量(在我1990年代后期使用该代码时)并不是很高。

未定义行为的问题在于,无论编译器产生什么结果,该结果在定义上都是正确的(即使它具有很高的破坏性和不可预测性)。
幸运的是,很少有编译器会努力在这种情况下真正引起意外行为,并且malloc用于HPC的计算机上的典型实现正好在返回地址之前具有一些簿记数据,因此减量通常会为您提供指向该簿记数据的指针。在那里写代码不是一个好主意,但是在这些系统上创建指针是无害的。

请注意,当更改运行时环境或将代码移植到其他环境时,代码可能会中断。


4
确实,在多存储区体系结构中,malloc可能会为您提供存储区中的第0个地址,而递减该地址可能会导致CPU陷阱,并且下溢为1。

1
我不同意那是“幸运的”。我认为,如果编译器发出的代码在您调用未定义行为时立即崩溃,那会更好。
戴维·康拉德

4
@DavidConrad:那么C不是适合您的语言。C语言中许多未定义的行为无法轻易检测到,或者仅严重影响性能。
Bart van Ingen Schenau 2014年

我当时想添加“使用编译器开关”。显然,您不希望将其用于优化代码。但是,您是对的,这就是为什么十年前我放弃编写C的原因。
戴维·康拉德

@BartvanIngenSchenau取决于您所说的“严重影响性能”的含义,其中C的象征性执行(例如clang + klee)以及消毒剂(asan,tsan,ubsan,valgrind等)对调试非常有用。
Maciej Piechotka 2014年

10

正式地,即使从未取消引用,在数组之外(除了末尾的指针之外)都有一个指针是未定义的行为。

实际上,如果您的处理器具有平面内存模型(而不是像x86-16这样的怪异模型),并且如果编译器在创建无效指针时没有给出运行时错误或错误的优化信息,则代码将起作用正好。


1
那讲得通。不幸的是,如果按照我的喜好,那是两个太多了。
wolfPack88

3
最后一点是恕我直言,这是最成问题的。由于这些时期的编译器不仅不会让平台在UB情况下自然发生任何事情,而且优化程序正在积极地利用它,因此我不会轻易地对其进行研究。
Matteo Italia 2014年

3

首先,它是不确定的行为。如今,一些优化的编译器对未定义的行为变得非常激进。例如,由于a--在这种情况下是未定义的行为,因此编译器可以决定保存一条指令和处理器周期,而不减少a。这在官方上是正确且合法的。

忽略这一点,您可能会减去1或2或1980。例如,如果我有1980年至2013年的财务数据,则可能会减去1980。现在,如果我们使用float * a = malloc(size); 肯定有一些大常数k,使得-k为空指针。在这种情况下,我们确实希望出问题。

现在采用一个大结构,例如一个兆字节。分配一个指向两个结构的指针p。p-1可能是空指针。p-1可能会回绕(如果一个结构是一个兆字节,并且malloc块从地址空间的开始是900 KB)。因此,p-1> p可能没有编译器的任何恶意。事情可能会变得有趣。


1

...仅将指针递减到分配的范围之外对我来说似乎是很粗略的。这是C中的“允许”行为吗?

允许吗 是。好主意?通常不行。

C是汇编语言的缩写,在汇编语言中没有指针,只有内存地址。C的指针是内存地址,具有在执行算术时会根据其指向的大小递增或递减的副作用。从语法的角度来看,这使以下内容很好:

double *p = (double *)0xdeadbeef;
--p;  // p == 0xdeadbee7, assuming sizeof(double) == 8.
double d = p[0];

在C中,数组并不是真正的东西。它们只是指向类似于数组的连续内存范围的指针。该[]运营商是做指针运算和非关联化,所以简写a[x]实际手段*(a + x)

进行上述操作是有正当理由的,例如某些I / O设备将几个doubles映射到0xdeadbee7和中0xdeadbeef。很少有程序需要这样做。

当您创建某物的地址时,例如通过使用&运算符或调用malloc(),您希望保持原始指针不变,以便您知道它指向的内容实际上是有效的。减少指针意味着一些错误的代码可能会尝试对其进行取消引用,从而获得错误的结果,破坏某些内容,或者根据您的环境而违反分段要求。对于尤其如此malloc(),因为您将负担加在了free()要记住传递原始值的任何人身上,而不是使所有内容崩溃的更改版本。

如果您需要使用C中的基于1的数组,则可以安全地执行此操作,但要分配一个永远不会使用的其他元素:

double *array_create(size_t size) {
    // Wasting one element, so don't allow it to be full-sized
    assert(size < SIZE_MAX);
    return malloc((size+1) * sizeof(double));
}

inline double array_index(double *array, size_t index) {
    assert(array != NULL);
    assert(index >= 1);  // This is a 1-based array
    return array[index];
}

请注意,这并不能防止超出上限,但这很容易处理。


附录:

C99草案的一些章节(很抱歉,我只能链接到这本书):

§6.5.2.1.1说,与下标运算符一起使用的第二个(“其他”)表达式是整数类型。 -1是一个整数,它p[-1]有效,因此也使指针&(p[-1])有效。这并不意味着访问该位置的内存将产生已定义的行为,但是指针仍然是有效的指针。

第6.5.2.2节说,数组下标运算符的求值等同于将元素编号加到指针,因此p[-1]等效于*(p + (-1))。仍然有效,但可能不会产生期望的行为。

§6.5.6.8说(强调我):

将具有整数类型的表达式添加到指针或从指针中减去时,结果将具有指针操作数的类型。

...如果表达式P指向i数组对象的-th元素,则表达式(P)+N(等效为N+(P))和(P)-N (其中N值为n)分别指向数组对象的i+n-th和 i−n-th元素(如果存在)

这意味着指针算术的结果必须指向数组中的元素。并不是说算术必须一次完成。因此:

double a[20];

// This points to element 9 of a; behavior is defined.
double d = a[-1 + 10];

double *p = a - 1;  // This is just a pointer.  No dereferencing.

double e = p[0];   // Does not point at any element of a; behavior is undefined.
double f = p[1];   // Points at element 0 of a; behavior is defined.

我是否建议以这种方式做事?我没有,我的回答解释了原因。


8
-1“允许”的定义(包括C标准声明为产生未定义结果的代码)不是有用的定义。
Pete Kirkham 2014年

其他人指出,这是不确定的行为,因此您不应说它是“允许的”。但是,建议分配一个额外的未使用元素0是好的。
200_success

这确实是不对的,请至少注意,这是C标准所禁止的。

@PeteKirkham:我不同意。请参阅我的答案附录。
Blrfl 2014年

4
在向指针添加整数的情况下,ISO C11标准的@Blrfl 6.5.6声明:“如果指针操作数和结果都指向同一数组对象的元素,或者指向数组对象的最后一个元素之后,则评估不会产生溢出;否则,行为是不确定的。”
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.