重新定义NULL
我正在写地址为0x0000的系统的C代码,并且包含端口I / O。 因此,访问一个NULL指针的任何可能的错误都不会被检测到,同时也会导致危险的行为。
为此,我希望重新定义NULL为另一个地址,例如一个无效的地址。 如果我不小心访问这样的地址,我会得到一个硬件中断,我可以处理这个错误。 我恰好有权访问stddef.h这个编译器,所以我实际上可以改变标准头并重新定义NULL。
我的问题是:这会与C标准冲突吗? 据我在7.17中可以看出的,这个macros是实现定义的。 标准中是否有其他地方说NULL 必须是0?
另一个问题是大量编译器通过将所有内容设置为零来执行静态初始化,而不pipe数据types如何。 即使标准说编译器应该将整数设置为零,指针指向NULL。 如果我将为我的编译器重新定义NULL,那么我知道这样的静态初始化将失败。 我可以认为这是不正确的编译器行为,即使我大胆地手动改变编译器头文件? 因为我确实知道这个特定的编译器在进行静态初始化时不访问NULLmacros。
C标准不要求空指针在机器地址零。 然而,将一个0
常量转换为一个指针值必须导致一个NULL
指针(第6.3.2.3 / 3节),并且将空指针评估为布尔值必须是false。 如果你真的想要一个零地址,而NULL
不是零地址,这可能有点尴尬。
尽pipe如此,对编译器和标准库进行(重)修改的时候,用替代的位模式表示NULL
并不是不可能的,而仍然严格遵守标准库。 然而,仅仅改变NULL
本身的定义是不够的,因为NULL
会被评估为真。
具体来说,你需要:
- 在赋值到指针(或强制转换为指针)时将文字零置换为其他一些魔术值,例如
-1
。 - 安排指针之间的等式testing和一个常量整数
0
来检查魔法值(§6.5.9/ 6) - 安排一个指针types被评估为布尔值的所有上下文,以检查是否与魔法值相等,而不是检查零。 这来自于相等性testing语义,但是编译器可以在内部以不同的方式实现它。 参见第6.5.13 / 3,第6.5.14 / 3,第6.5.15 / 4,第6.5.3.3 / 5,第6.8.4.1 / 2,第6.8.5 / 4
- 正如caf指出的那样,更新静态对象(§6.7.8/ 10)和部分复合初始值设定项(§6.7.8/ 21)初始化的语义以反映新的空指针表示。
- 创build一个替代方法来访问真正的地址零。
有些事情你不必处理。 例如:
int x = 0; void *p = (void*)x;
在此之后, p
不能保证是空指针。 只需要处理常量赋值(这是访问真实地址零的一个好办法)。 同样:
int x = 0; assert(x == (void*)0); // CAN BE FALSE
也:
void *p = NULL; int x = (int)p;
x
不能保证为0
。
总而言之,C语言委员会明显考虑了这种情况,并为那些selectNULL替代表示的人们考虑。 现在你所要做的就是对你的编译器进行重大改动,嘿,你已经完成了:)
作为一个方面说明,在编译器正确之前,可以通过源代码转换阶段来实现这些更改。 也就是说,不是预处理程序 – >编译程序 – >汇编程序 – >连接程序的正常stream程,而是添加预处理程序 – > NULL转换 – >编译程序 – >汇编程序 – >连接程序。 那么你可以做如下的转换:
p = 0; if (p) { ... } /* becomes */ p = (void*)-1; if ((void*)(p) != (void*)(-1)) { ... }
这将需要一个完整的C语法分析器,以及typesparsing器和types定义和variables声明的分析来确定哪些标识符对应于指针。 但是,通过这样做,您可以避免对编译器本身的代码生成部分进行更改。 铿锵可能是有用的实现这一点 – 我明白它的devise与这种想法的转变。 当然你也可能需要修改标准库。
该标准规定,值为0的整数常量expression式或者转换为void *
types的expression式是一个空指针常量。 这意味着(void *)0
总是一个空指针,但是给出int i = 0;
, (void *)i
不需要。
C实现由编译器和它的头文件组成。 如果您修改标题以重新定义NULL
,但不要修改编译器来修复静态初始化,那么您已经创build了一个不合规的实现。 整个实施过程都是不正确的行为,如果破坏了,就没有人责怪;)
当然,由于上面的规则, if (p)
等于if (p != NULL)
,你必须修复更多的静态初始化。
如果使用C std库,则会遇到可能返回NULL的函数的问题。 例如, malloc文档指出:
如果函数未能分配请求的内存块,则返回一个空指针。
由于malloc和相关函数已经被编译成具有特定NULL值的二进制文件,所以如果重新定义NULL,除非可以重build整个工具链,包括C标准库,否则将无法直接使用C std库。
另外,由于std库使用NULL,如果在包含std头之前重新定义NULL,则可能会覆盖头中列出的NULL定义。 任何内联内容将与编译对象不一致。
我会自己定义自己的NULL,“MYPRODUCT_NULL”,以避免或转换到C标准库。
将NULL保留为NULL,并将IO作为特殊情况处理为端口0x0000,可能使用汇编程序编写的例程,从而不受标准C语义的限制。 IOW,不要重新定义NULL,重新定义端口0x00000。
请注意,如果您正在编写或修改C编译器,则不pipeNULL如何定义,避免取消引用NULL所需的工作(假定您的情况CPU不起作用)是相同的,因此更容易保留NULL作为零,并确保零不能从C中解除引用。
空指针的位模式可能与整数0的位模式不同。但是,NULLmacros的扩展必须是一个空指针常量,也就是可以被赋值为(void *)。
为了在保持一致的同时获得所需的结果,您必须修改(或者可能configuration)您的工具链,但这是可以实现的。
考虑到在其他人重新定义NULL时遇到极大的困难,也许更容易重新定义知名硬件地址的解引用 。 在创build地址时,将1添加到每个众所周知的地址,以便您熟知的IO端口是:
#define CREATE_HW_ADDR(x)(x+1) #define DEREFERENCE_HW_ADDR(x)(*(x-1)) int* wellKnownIoPort = CREATE_HW_ADDR(0x00000000); printf("IoPortIs" DEREFERENCE_HW_ADDR(wellKnownIoPort));
如果您所关心的地址被分组在一起,并且您可以安心地将地址加1不会与任何事情(在大多数情况下不应该)冲突,那么您可以安全地做到这一点。 然后你不需要担心重build你的工具链/标准库和expression式的forms:
if (pointer) { ... }
还在工作
疯狂的我知道,但只是想我会抛出这个想法:)。
你在问问题 将NULL
重新定义为非空值将会破坏下面的代码:
如果(myPointer) { // myPointer不为null ... }