我对C语言完全不熟悉,在大学期间,我在代码注释中经常看到引用NULL指针的内容。虽然我有C#的背景,但我认为这可能类似于在.NET中遇到的"NullReferenceException",但现在我开始严重怀疑了。
请有人以通俗易懂的方式解释一下这是什么,为什么会出现这种情况,并且为什么这是不好的?
我对C语言完全不熟悉,在大学期间,我在代码注释中经常看到引用NULL指针的内容。虽然我有C#的背景,但我认为这可能类似于在.NET中遇到的"NullReferenceException",但现在我开始严重怀疑了。
请有人以通俗易懂的方式解释一下这是什么,为什么会出现这种情况,并且为什么这是不好的?
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语言中指针可以指向任何数据对象,甚至是数组内的元素。
char *foo = NULL
然后使用 &foo 时怎么样? - Bionix1441&foo
指的是名为 foo
的变量的地址,这是可以的,因为它已经被声明。 - Greg HewgillDereferencing 的意思是访问给定地址的内存值。因此,当您有一个指向某个东西的指针时,解引用该指针 意味着读取或写入指针所指向的数据。
在 C 中,一元运算符 *
是解引用运算符。如果 x
是一个指针,则 *x
是 x
所指向的内容。一元运算符 &
是 取地址 运算符。如果 x
是任何数据,则 &x
是 x
在内存中存储的地址。 *
和 &
运算符是彼此的反函数:如果 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不同的是,程序不可能继续静默工作或擦除硬盘(除非语言运行时存在错误,但这也极不可能)。
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). ...
的简写;除了指针声明。这里有很多混淆和混淆的答案。首先,严格来说,没有所谓的“NULL指针”。有空指针、空指针常量和NULL
宏。
从Codidact上我的回答开始学习:null指针和NULL之间有什么区别? 在这里引用一些部分:
有三个相关的概念很容易混淆: - 空指针(null pointers) - 空指针常量(null pointer constants) - NULL宏nul
(一个L)或'\0'
(八进制转义序列),只是为了将其与空指针和NULL
区分开来。*
间接运算符完成。规定此运算符如何工作的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来自维基百科
空指针具有保留值,通常但不一定是零值,表示它不引用任何对象。
..由于空指针不引用有意义的对象,尝试对空指针进行解引用通常会导致运行时错误。
int val =1;
int *p = NULL;
*p = val; // Whooosh!!!!
引用自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
p
为 NULL
时执行 *p
。一个空指针指向不存在的内存,会导致分段错误。有一种更简单的方法来解除引用空指针,请看下面。
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*)0
,NULL
宏可以是它们中的任何一个。 - Lundin(void *)0
是一个有效的空指针常量,所以它与(int *)0
没有区别。该代码将零值分配给整数指针,然后对其进行反引用。 - Roi(void *)0
是一个有效的空指针常量,因为标准明确规定了这一点。它对 (int*)0
没有任何说明。 - Lundin让我们来看一下解除 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
),它没有指向已分配的内存块。我通常通过声明变量静态分配内存。