我正在为地址0x0000有效且包含端口I / O的系统编写C代码。因此,访问NULL指针的任何可能的错误都将保持不被发现,同时会导致危险的行为。
出于这个原因,我希望将NULL重新定义为另一个地址,例如一个无效的地址。如果我不小心访问了这样的地址,我将得到一个硬件中断,我可以处理错误。我碰巧有权访问此编译器的stddef.h,因此我实际上可以更改标准头并重新定义NULL。
我的问题是:这会与C标准发生冲突吗?据我所知,从标准中的7.17开始,宏是实现定义的。标准中是否还有其他内容表明NULL 必须为0?
另一个问题是,无论数据类型如何,大量编译器都会通过将所有内容设置为零来执行静态初始化。尽管标准规定编译器应将整数设置为零并将指针设置为NULL。如果我将为我的编译器重新定义NULL,那么我知道这样的静态初始化将失败。我是否可以将其视为不正确的编译器行为,即使我手动大胆地更改了编译器头?因为我确实知道这个特定的编译器在进行静态初始化时不会访问NULL宏。
答案 0 :(得分:82)
C标准不要求空指针位于机器的地址零处。但是,将0
常量强制转换为指针值必须生成NULL
指针(§6.3.2.3/ 3),并将空指针作为布尔值计算必须为false。如果你真的做想要一个零地址,并且NULL
不是零地址,这可能有点尴尬。
然而,对编译器和标准库进行(重)修改后,用NULL
表示替换位模式并不是不可能,同时仍然严格符合标准库。 不足以简单地更改NULL
本身的定义,因为NULL
会评估为真。
具体来说,您需要:
-1
。0
之间的相等测试以检查魔术值(§6.5.9/ 6)你做的一些事情不必须处理。例如:
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语言委员会显然考虑了这种情况,并考虑了那些为NULL选择替代表示的人。你现在所要做的就是对你的编译器进行重大修改,嘿,你已经完成了:)
作为旁注,可以在编译器正确之前使用源代码转换阶段实现这些更改。也就是说,而不是预处理器的正常流程 - >编译器 - >汇编程序 - >链接器,你要添加一个预处理器 - > NULL转换 - >编译器 - >汇编程序 - >连接。然后你可以进行如下转换:
p = 0;
if (p) { ... }
/* becomes */
p = (void*)-1;
if ((void*)(p) != (void*)(-1)) { ... }
这将需要一个完整的C解析器,以及一个类型解析器和typedef和变量声明的分析,以确定哪些标识符对应于指针。但是,通过这样做,您可以避免必须更改编译器的代码生成部分。 clang可能对实现这一点很有用 - 我知道它的设计考虑了这样的转换。当然,您仍然可能需要对标准库进行更改。
答案 1 :(得分:19)
该标准规定,值为0的整型常量表达式或转换为void *
类型的表达式是空指针常量。这意味着(void *)0
始终是空指针,但是int i = 0;
,(void *)i
不需要。{/ p>
C实现由编译器及其头文件组成。如果您修改标头以重新定义NULL
,但不修改编译器以修复静态初始化,那么您已创建了一个不合规的实现。这是整个实现一起采取的行为不正确,如果你打破它,你真的没有别人责备;)
您必须修复的不仅仅是静态初始化 - 当然,由于上述规则,给定指针p
,if (p)
等同于if (p != NULL)
。
答案 2 :(得分:7)
如果使用C std库,则会遇到可返回NULL的函数问题。例如malloc documentation个州:
如果功能未能分配 请求内存块,null 返回指针。
因为malloc和相关函数已经被编译成具有特定NULL值的二进制文件,所以如果重新定义NULL,除非可以重建整个工具链,包括C std,否则将无法直接使用C std库。库。
另外由于std库使用NULL,如果在包含std头之前重新定义NULL,则可能会覆盖头中列出的NULL定义。内联的任何内容都与编译对象不一致。
我会为你自己的用途定义你自己的NULL,“MYPRODUCT_NULL”,并避免或转换为/到C std库。
答案 3 :(得分:6)
单独保留NULL并将IO作为特殊情况处理为端口0x0000,可能使用汇编程序编写的例程,因此不受标准C语义的限制。 IOW,不要重新定义NULL,重新定义端口0x00000。
请注意,如果您正在编写或修改C编译器,则无论如何定义NULL,避免解除引用NULL所需的工作(假设在您的情况下CPU没有帮助)都是相同的,因此更容易将NULL定义为零,并确保不能从C中取消引用零。
答案 4 :(得分:3)
考虑到其他人提到的重新定义NULL的极端困难,可能更容易重新定义解除引用用于众所周知的硬件地址。创建地址时,将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不会与任何内容发生冲突(在大多数情况下不应该这样做),您可以安全地执行此操作。然后您不必担心以下列形式重建工具链/ std lib和表达式:
if (pointer)
{
...
}
仍在工作
疯了我知道,但我以为我会把想法抛弃在那里:)。
答案 5 :(得分:2)
空指针的位模式可能与整数0的位模式不同。但是,NULL宏的扩展必须是空指针常量,即0的常量整数,可以进行转换to(void *)。
为了在保持一致的同时实现您想要的结果,您必须修改(或者可能配置)您的工具链,但这是可以实现的。
答案 6 :(得分:1)
你在找麻烦。将NULL
重新定义为非空值将破坏此代码:
if (myPointer) { // myPointer is not null ... }