指针(地址)是否可能为负数?

43

我有一个函数,希望它能返回特殊的值来表示失败未初始化(成功时返回指针)。

目前它在失败时返回NULL,在未初始化时返回-1,这似乎起作用...但我可能在欺骗系统。如果我没记错,地址始终是正数,不是吗?(尽管编译器允许我将地址设置为-1,这似乎很奇怪)。

[更新]

我另一个想法(如果-1是有风险的)是在全局范围内使用malloc分配一个字符@,并将该地址用作哨兵。


你想用这个做什么?接口被用来做什么?在返回值上结合这么多不同种类的输出似乎不是正确的做法。 - Ken Bloom
@kirk.burleson:在G++上,int* foo(){ return -1;}会产生警告“warning: return makes pointer from integer without a cast”。我不确定这证明或反驳了你的观点,但当你说“C编译器不在乎你给它什么,它们会尝试编译任何东西”时,我仍然感到恼火。(顺便说一下,在g++中,这是一个错误error: invalid conversion from ‘int’ to ‘int*’。) - Ken Bloom
@kirk.burleson:在我上一个评论中,警告是关于GCC的,错误则是关于G++的。 - Ken Bloom
为什么不返回一个简单的两个值的结构体,其中一个值是指针,另一个值是状态码?这样做不会增加太多麻烦,而且提供了更大的灵活性,也是线程安全的。 - Richard Chambers
13个回答

83
不,地址并不总是正数 - 在 x86_64 上,指针被符号扩展,并且地址空间在 0 周围对称聚集(虽然通常“负”地址是内核地址)。
但是这一点大多无关紧要,因为 C 只定义了指向同一对象的指针或数组结束后的指针之间的 < 和 > 比较的含义。完全不同的对象的指针不能有实际意义上的比较,除了精确相等外,在标准的 C 中至少是如此 - if (p < NULL) 没有明确定义的语义。
你应该创建一个具有静态存储期的虚拟对象,并使用其地址作为您的未初始化值:
extern char uninit_sentinel;
#define UNINITIALISED ((void *)&uninit_sentinel)

它保证在整个程序中只有一个唯一的地址。


@caf:你能指出一个资源来验证amd64中的指针是否是符号扩展的吗?我从未读过任何暗示或陈述这一点的内容。也许你是在提到规范地址必须具有虚拟地址的第48到63位是位47的副本的要求?如果是这个意思,它并不意味着“负”指针。RIP相对寻址也不是。 - Michael Foukarakis
5
个人认为,与维基百科相比,体系结构ABI文档更具权威性。最终,正如你所知道的那样,这归根结底是一种解释或者说关于地址空间概念化的方式。 - caf
1
@mfukar:也许您可以告诉我们,“地址空间的负半部分”是什么意思,如果不是显而易见的话。(至少我没有修改我的评论!) - caf
2
“地址空间的负半部分”是指x64变体的System V OS规范对位模式的有符号值解释,因此仅适用于选择进行该解释的环境。它本可以很容易地写成“最高有效位设置的地址”。Windows采用无符号解释,并将这样的地址称为高地址空间。x64指令集中没有暗示值应该以一种方式或另一种方式进行解释,尽管我见过的大多数操作系统使用无符号- +1来查找有符号指针环境。 - Pete Kirkham
@caf OP问道:“指针(地址)是否可能为负数?”你回答说:“不可能。”这本来没问题,但是你接着解释说地址并不总是正数,这与之前的回答相矛盾。 - WonderWorker
显示剩余6条评论

22

指针的有效值完全取决于具体实现,因此指针地址可能是负数。

更重要的是,考虑一个可能的实现选择示例,例如,您在32位平台上使用32位指针大小。任何可以由该32位值表示的值都可能是一个有效的指针。除了空指针以外,任何指针值都可能是对象的有效指针。

对于您的特定用例,应该考虑返回状态码,并且可能将指针作为函数参数传递。


51
请注意,如果您的指针过于负数,可能会导致访问当前机器旁边的机器。 - Noon Silk

18

试图将特殊值复用为返回值通常是一种不良设计...你尝试在单个值上做太多的事情。更加清晰的方法是通过参数返回您的“成功指针”,而不是返回值。这样就可以在返回值中留下大量的非冲突空间来描述您想要描述的所有条件:

int SomeFunction(SomeType **p)
{
    *p = NULL;
    if (/* check for uninitialized ... */)
        return UNINITIALIZED;
    if (/* check for failure ... */)
        return FAILURE;

    *p = yourValue;
    return SUCCESS;
}

你还应该进行典型的参数检查(确保“p”不为NULL)。


6
这绝对是设计此函数的正确方式,任何其他方法都会给代码的使用者带来维护灾难和漏洞吸引器,并且应该强烈不推荐。 - Ken Bloom
3
可能。我记得那个“发明”空指针的人说它是个错误。另一个特殊情况的值可能会有问题。即使如此,有时候使用两个不同的值却只需要使用一个会导致代码过于复杂。简化一些常见算法的常用方法是分配特殊情况的越过末尾对象,而不是使用空值 - 这避免了特殊情况的空检查。仍然需要在结尾处进行有效性检查,但是以不同的形式进行。指向特殊对象的有效指针特殊情况指针,并且通常可以节省很多复杂性。 - user180247
2
“那个人”是C.A.R. Hoare。另一方面,他通过发明快速排序算法更是弥补了“十亿美元的错误”。 - James McNellis
2
@James - 所有那些人,他们只是人而已,你知道的吧?我可能应该记得 Hoare,但“谁”只是历史罢了。思想才是更重要的。而且,我发现含糊其辞会有帮助——当别人不知道我在引用谁时,很难与我争辩;-) - user180247
@Steve314:是的,在非常特定的情况下(在我的示例中,您可以控制SomeType),拥有一些常见的“特殊情况”对象/指针可能会起作用,并且更加流畅...但在一般情况下,将状态和返回的对象分开更易于维护。 - JaredReisinger
显示剩余2条评论

6
C语言没有为指针定义“负数”的概念。 “负数”属性主要是算术性质,不适用于指针类型的值。
如果您有一个返回指针的函数,则无法有意义地从该函数返回值-1。在C语言中,整数值(除零外)不能隐式转换为指针类型。尝试从返回指针的函数返回-1是一种立即约束违规,这将导致诊断消息。简而言之,这是一个错误。如果编译器允许它,那么它只意味着它并未严格执行该约束(大多数时候,它们为了与预标准代码兼容而这样做)。
如果您通过显式转换将-1的值强制转换为指针类型,则转换结果将是实现定义的。语言本身对此不作任何保证。它可能很容易被证明与其他有效的指针值相同。
如果您想创建一个保留的指针值,则不需要malloc任何内容。您可以简单地声明所需类型的全局变量,并使用其地址作为保留值。这是保证唯一的。

4
指针可以像无符号整数一样是负数。也就是说,在二进制补码表示中,你可以将数值解释为负数,因为最高有效位为1。

你是说当强制转换为有符号类型,如int时,它们可以为负数吗?我相信你已经知道了,但其他读者可能不知道,无符号数字不会存储负值。詹姆斯所提到的符号位仅存在于有符号类型中,并且正是这使得数据类型成为有符号类型。 - WonderWorker
1
WonderWorker,你提到的位在有符号和无符号类型中都存在,唯一的区别是如何解释该位。在有符号类型中,它表示符号。在无符号类型中,它是该类型范围的后半部分。 - onlycparra

1

失败和未初始化之间有什么区别。如果未初始化不是另一种失败类型,那么您可能需要重新设计接口以将这两种情况分开。

最好的方法可能是通过参数返回结果,以便返回值仅指示错误。例如,您原本会写成:

void* func();

void* result=func();
if (result==0)
  /* handle error */
else if (result==-1)
  /* unitialized */
else
  /* initialized */

将此更改为

// sets the *a to the returned object
// *a will be null if the object has not been initialized
// returns true on success, false otherwise
int func(void** a);

void* result;
if (func(&result)){
  /* handle error */
  return;
}

/*do real stuff now*/
if (!result){
  /* initialize */
}
/* continue using the result now that it's been initialized */

我并没有特别返回未初始化的值。我正在使用一个作为参数传递的链表,但它可能已经初始化或者没有初始化。之前我将其设置为NULL,但这与我的返回“NULL”表示失败相冲突。感谢您的建议。 - Jared Forsyth

0
实际上,在x86上,NULL指针异常不仅通过取消引用NULL指针生成,而且通过更大范围的地址(例如,前65kb)生成。这有助于捕获此类错误。
int* x = NULL;
x[10] = 1;

因此,有更多的地址在解引用时保证会生成空指针异常。 现在考虑这段代码(为AndreyT编译):

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#define ERR_NOT_ENOUGH_MEM (int)NULL
#define ERR_NEGATIVE       (int)NULL + 1
#define ERR_NOT_DIGIT      (int)NULL + 2

char* fn(int i){
    if (i < 0)
        return (char*)ERR_NEGATIVE;
    if (i >= 10)
        return (char*)ERR_NOT_DIGIT;
    char* rez = (char*)malloc(strlen("Hello World ")+sizeof(char)*2);
    if (rez)
        sprintf(rez, "Hello World %d", i);
    return rez;
};

int main(){
    char* rez = fn(3);
    switch((int)rez){
        case ERR_NOT_ENOUGH_MEM:    printf("Not enough memory!\n"); break;
        case ERR_NEGATIVE:          printf("The parameter was negative\n"); break;
        case ERR_NOT_DIGIT:         printf("The parameter is not a digit\n"); break;
        default:                    printf("we received %s\n", rez);
    };
    return 0;
};

这在某些情况下可能很有用。 它不能在一些哈佛架构上工作,但可以在冯·诺伊曼架构上工作。


2
我不确定这在x86上是否正确,更多的是现代操作系统。芯片提供了将进程地址空间映射到物理地址空间等功能,但通常是操作系统决定哪些进程地址空间部分是有效的。 - user180247
这甚至无法编译。一些具有相当宽松错误检查的 C 编译器将允许您将整数值分配给指针(尽管在 C 中是非法的),但我所知道的没有一个编译器会允许您使用指针作为“switch”语句的控制值。 - AnT stands with Russia

0
不要使用malloc来实现这个目的。如果在调用malloc并分配哨兵到高地址时已经使用了大量内存,它可能会占用不必要的内存,并且会混淆内存调试器/泄漏检测器。相反,只需返回指向本地static const char对象的指针。该指针永远不会与程序以其他方式获取的任何指针相等,并且它只浪费了一个字节的bss。

你确定 const char 对象永远不会像字符串字面量那样合并吗?(如果你确定,也许你想回答一下我在这个问题上的提问,我认为它没有真正的共识。) - icktoofay
是的,非常确定。请参见6.5.9第6段。 - R.. GitHub STOP HELPING ICE

0

@James 是正确的,但我想补充一点,指针并不总是代表绝对的内存地址,理论上这些地址总是正数。指针也可以表示相对于某个内存点的地址,通常是堆栈或帧指针,这些地址既可以是正数也可以是负数。

因此,最好让您的函数接受一个指向指针的参数,并在成功时将该指针填充为有效的指针值,同时从实际函数返回结果代码。


确定吗?根据我的经验,相对偏移通常是一个整数。 - user180247

0

James的回答可能是正确的,但当然描述了一种实现选择,而不是你可以做出的选择。

个人认为,地址在直觉上是无符号的。找到一个与空指针比较小的指针似乎是错误的。但是,对于相同的整数类型,~0-1给出相同的值。如果它在直觉上是无符号的,~0可能会产生更直观的特殊值 - 我经常将其用于错误情况下的无符号整数。它并不是真正不同(默认情况下零是int,因此~0-1,直到你将其转换),但它看起来不同。

顺便说一句,在32位系统上,指针可以使用所有32位,尽管在实践中,-1~0是极不可能发生的真实分配指针。还有平台特定的规则 - 例如,在32位Windows上,进程只能拥有2GB的地址空间,并且有很多代码将某种标志编码到指针的最高位中(例如,用于平衡二叉树中的平衡标志)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接