“de-referencing a NULL pointer”是什么意思?

91

我对C语言完全不熟悉,在大学期间,我在代码注释中经常看到引用NULL指针的内容。虽然我有C#的背景,但我认为这可能类似于在.NET中遇到的"NullReferenceException",但现在我开始严重怀疑了。

请有人以通俗易懂的方式解释一下这是什么,为什么会出现这种情况,并且为什么这是不好的?


1
请记住这样做会导致未定义的行为。在C或C++中,您不会得到任何异常或其他东西。 - GManNickG
1
你可能需要放一些示例代码。看起来人们(包括我)不理解你想要询问什么。 - user168237
3
不需要代码(因为没有)- 我现在面临的是一个概念性问题,我正在努力理解“取消引用”的术语以及为什么我应该关心它。 - Ash
7
抱歉,我无法打开这个链接。请提供您需要翻译的具体内容。 - Veer Singh
8个回答

111

NULL指针指向不存在的内存。 这可以是地址0x00000000或任何其他实现定义的值(只要它永远不可能是真正的地址)。 对其进行解引用意味着尝试访问指针所指向的任何内容。 *运算符是解引用运算符:

int a, b, c; // some integers
int *pi;     // a pointer to an integer

a = 5;
pi = &a; // pi points to a
b = *pi; // b is now 5
pi = NULL;
c = *pi; // this is a NULL pointer dereference

这与C#中的NullReferenceException完全相同,唯一的区别是在C语言中指针可以指向任何数据对象,甚至是数组内的元素。


18
指针包含一个内存地址,它“引用”某个东西。要访问由该内存地址“引用”的东西,您必须对内存地址进行“解引用”。 - In silico
1
@Ash,In silico说过,但是当你解除引用时,你会得到存储在内存地址中的值。试一试。做int p; printf(“%p \ n”,&p);它应该打印出一个地址。当你不创建指针(*var)时,要获取地址,你使用&var。 - Matt
1
@Greg 当你执行 char *foo = NULL 然后使用 &foo 时怎么样? - Bionix1441
1
@Bionix1441:在你的例子中,&foo 指的是名为 foo 的变量的地址,这是可以的,因为它已经被声明。 - Greg Hewgill
1
我认为重要的是也要声明这是未定义行为,假设 Adam Rosenfield 的答案是正确的。 - axel22
1
地址0x0存在:“在罕见的情况下,当NULL等同于0x0内存地址且特权代码可以访问它时,则可能进行读写内存,这可能导致代码执行。”,来自https://cwe.mitre.org/data/definitions/476.html - baz

50

Dereferencing 的意思是访问给定地址的内存值。因此,当您有一个指向某个东西的指针时,解引用该指针 意味着读取或写入指针所指向的数据。

在 C 中,一元运算符 * 是解引用运算符。如果 x 是一个指针,则 *xx 所指向的内容。一元运算符 &取地址 运算符。如果 x 是任何数据,则 &xx 在内存中存储的地址。 *& 运算符是彼此的反函数:如果 x 是任何数据,y 是任何指针,则以下等式始终成立:

*(&x) == x
&(*y) == y

空指针是一个不指向任何有效数据的指针(但并不是唯一这样的指针)。C标准规定对空指针解引用为未定义行为。这意味着绝对会发生任何事情:程序可能崩溃,也可能继续默默地工作,或者它可能擦除您的硬盘(虽然这相当不可能)。

在大多数实现中,如果您尝试这样做,您将获得“segmentation fault”或“access violation”,这几乎总会导致操作系统终止您的程序。以下是一种空指针可能被解引用的方式:

int *x = NULL;  // x is a null pointer
int y = *x;     // CRASH: dereference x, trying to read it
*x = 0;         // CRASH: dereference x, trying to write it

没错,解引用空指针就像在C#中的NullReferenceException或Java中的NullPointerException一样,只是语言标准在这里更加有帮助。在C#中,解引用空引用具有明确定义的行为:它总是抛出NullReferenceException。与C不同的是,程序不可能继续静默工作或擦除硬盘(除非语言运行时存在错误,但这也极不可能)。


3
实际上,“NULL”指针有时会指向有效数据。许多微控制器在地址0处实现闪存/外设。 - Sapphire_Brick

3
这意味着...
myclass *p = NULL;
*p = ...;  // illegal: dereferencing NULL pointer
... = *p;  // illegal: dereferencing NULL pointer
p->meth(); // illegal: equivalent to (*p).meth(), which is dereferencing NULL pointer

myclass *p = /* some legal, non-NULL pointer */;
*p = ...;  // Ok
... = *p;  // Ok
p->meth(); // Ok, if myclass::meth() exists

基本上,涉及到(*p)或者隐含涉及到(*p)的几乎所有事情,比如p->...,它是(*p). ...的简写;除了指针声明。

1
他将他的问题标记为C而不是C++。 - GWW
3
在C和C++中,"@GWW: and it has exactly the same semantic in C and in C++, except perhaps C does not have costum class."的语义完全相同,除非C不支持自定义类。 - Lie Ryan
2
“p->meth()”不就是“(*p).meth()”的简写吗?后者在C和C++中都可用。 - Arun
1
@Arun 这两种写法在 C 和 C++ 中都是合法的。 - Sapphire_Brick

1

这里有很多混淆和混淆的答案。首先,严格来说,没有所谓的“NULL指针”。有空指针、空指针常量和NULL宏。

从Codidact上我的回答开始学习:null指针和NULL之间有什么区别? 在这里引用一些部分:

有三个相关的概念很容易混淆: - 空指针(null pointers) - 空指针常量(null pointer constants) - NULL宏
正式定义: C17 6.3.2.3/3 正式定义了这两个术语: 整数常量表达式的值为0,或将这样的表达式强制转换为void*类型,称为“空指针常量”67)。如果将空指针常量转换为指针类型,则所得到的指针称为“空指针”,保证与任何对象或函数的指针比较不相等。
换句话说,空指针是指向明确定义的“无处”的任何类型的指针。当指针被分配空指针常量时,任何指针都可以变成空指针。
标准提到了0和(void*)0作为两个有效的空指针常量,但请注意,它说“具有值0的整数常量表达式”。这意味着像0u、0x00和其他变体也是空指针常量。这些是特殊情况,可以分配给任何指针类型,而不考虑通常适用的各种类型兼容性规则。
值得注意的是,对象指针和函数指针都可以是空指针。这意味着我们必须能够将空指针常量分配给它们,而不管实际的指针类型是什么。
NULL: 上面的注释67)补充说(非正式): 在stddef.h(和其他头文件)中定义了宏NULL作为一个空指针常量;请参见7.19。
其中7.19简单地将NULL定义为(正式): NULL扩展为一个实现定义的空指针常量;
理论上,这可能是除0和(void*)0之外的其他内容,但实现定义的部分更可能是说NULL可以是#define NULL 0或#define NULL (void*)0或一些其他整数常量表达式,其值为零,具体取决于使用的C库。但我们需要知道并关心的是,NULL是一个空指针常量。
在C代码中,NULL也是首选的空指针常量,因为它是自说明和明确的(不像0)。它应该只与指针一起使用,而不用于任何其他目的。
此外,不要将其与“字符串的空终止”混淆,这是一个完全不同的话题。字符串的空终止只是一个值为零的值,通常被称为nul(一个L)或'\0'(八进制转义序列),只是为了将其与空指针和NULL区分开来。
解引用
在澄清这一点之后,我们无法访问空指针指向的内容,因为正如前面所述,它是一个明确定义的“不存在”的位置。访问指针指向的内容的过程称为解引用,在C(和C++)中通过一元*间接运算符完成。规定此运算符如何工作的C标准仅说明(C17 6.5.3.3):

如果向指针分配了无效值,则一元*运算符的行为未定义

其中,一条信息提示补充道:

通过一元*运算符对指针进行解引用的无效值包括空指针、不适当对齐于所指对象类型的地址以及对象生命周期结束后的地址。

这就是可能会抛出“段错误”或“空指针/引用异常”的地方。这样做的原因几乎总是应用程序中的错误,例如以下示例:
int* a = NULL; // create a null pointer by initializing with a null pointer constant
*a = 1;        // null pointer is dereferenced, undefined behavior

int* b = 0;    // create a null pointer by initializing with a null pointer constant
               // not to be confused with similar looking dereferencing and assignment:
*b = 0;        // null pointer is dereferenced, undefined behavior

在某些情况下,不太清楚一个表达式是否“取消引用”指针。例如,像(uintptr_t)&(structPtr->member)这样的表达式可以在不执行任何访问所涉及的指针的情况下进行评估,即使在大多数情况下使用有用的陷阱空指针的实现中,它也可以识别上述表达式最终形成整数而不是指针。 - supercat

1

来自维基百科

空指针具有保留值,通常但不一定是零值,表示它不引用任何对象。
..

由于空指针不引用有意义的对象,尝试对空指针进行解引用通常会导致运行时错误。

int val =1;
int *p = NULL;
*p = val; // Whooosh!!!! 

1
谢谢您的回答,我知道什么是空指针,只是不确定“解引用”如何适应整个方案。 - Ash
7
我不知道程序崩溃会发出“哗啦!”的声音。 - Sapphire_Brick

1

引用自wikipedia

指针是一个内存中的位置,获取指针所指位置的值被称为解引用指针。

通过在指针上应用一元*运算符来执行解引用操作。

int x = 5;
int * p;      // pointer declaration
p = &x;       // pointer assignment
*p = 7;       // pointer dereferencing, example 1
int y = *p;   // pointer dereferencing, example 2

“解引用空指针”是指在指针 pNULL 时执行 *p

1

一个空指针指向不存在的内存,会导致分段错误。有一种更简单的方法来解除引用指针,请看下面。

int main(int argc, char const *argv[])
{
    *(int *)0 = 0; // Segmentation fault (core dumped)
    return 0;
}

由于0不是一个有效的指针值,因此会发生错误。

SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=NULL}

(int*)0 严格来说不是空指针常量,所以这段代码可能会写入地址零,而不是空指针。您的示例中没有任何东西可以正式保证空指针访问或段错误。为了创建空指针,您需要将空指针常量赋值给指针。空指针常量可以是 0(void*)0NULL 宏可以是它们中的任何一个。 - Lundin
@Lundin - 如果我没记错,(void *)0 是一个有效的空指针常量,所以它与(int *)0没有区别。该代码将零值分配给整数指针,然后对其进行反引用。 - Roi
1
(void *)0 是一个有效的空指针常量,因为标准明确规定了这一点。它对 (int*)0 没有任何说明。 - Lundin

-1

让我们来看一下解除 NULL 指针引用的例子,并谈论一下它。

这里有一个解除 NULL 指针引用的例子,来自于这个重复的问题:uint32_t *ptr = NULL;

int main (void) 
{
    uint32_t *ptr = NULL;
    
    // `*ptr` dereferences the NULL ptr
    *ptr = 0;
    
    return 0;
}

内存没有为 uint32_t 分配,因此调用“取消引用”指针ptr,或者换句话说:访问未分配(NULL - 通常为0,但有些情况由具体实现决定)地址的内存是非法的。这是"未定义行为" - 即:一个错误。

因此,你应该静态地(可以的话)或动态地分配空间给一个uint32_t,然后只取消引用指向有效内存的指针,如下所示。

以下是如何静态分配内存并使用指针的方法。请注意,即使在我的例子中,指针本身的内存也是静态分配的!

// allocate enough memory for a 4-byte (32-bit) variable
uint32_t variable;

// allocate enough memory for a pointer, which is **usually** 2 bytes on an
// 8-bit microcontroller such as Arduino, or usually 4 bytes on a 32-bit
// architecture, or usually 8 bytes on a 64-bit Linux computer, for example 
uint32_t* ptr;

// assign the address of `variable` to the pointer; you can now say that
// `ptr` "points to" the variable named `variable`; in literal terms, `ptr` now
// contains the numerical value of the address of the first byte of the
// variable `variable`
ptr = &variable;

// Store a number into the 4-byte variable named `variable`, via a pointer to it
*ptr = 1234;
// OR, same exact thing as just above: store a number into that 4-byte
// variable, but this time via the variable name, `variable`, directly
variable = 1234;

请注意,使用动态分配也可以,但是静态内存分配更安全、确定性更强、速度更快,对于内存受限的嵌入式系统来说更好,等等。关键是您不能合法地引用任何指针(意思是:在其前面放置星号“取消引用运算符”,例如*ptr),它没有指向已分配的内存块。我通常通过声明变量静态分配内存。

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