在C/C++中,“解引用”指的是什么?

687
请在解释中包含一个例子。

26
http://cslibrary.stanford.edu/106/ - Erik
32
int *p; 的意思是定义一个指向整数的指针,而 *p 则是对该指针进行解引用操作,也就是实际获取 p 所指向的数据。 - Peyman
5
Binky的指针乐趣(http://cslibrary.stanford.edu/104/)是一部关于指针的极好视频,可能有助于澄清问题。@Erik-你很棒,放出斯坦福CS图书馆的链接。那里有很多好东西... - templatetypedef
7
哈利的回答完全没有帮助。 - Jim Balter
1
@Peyman *p 不会检索 p 所指向的数据。相反,它指定了内存位置。该表达式可以用于存储新数据、检索数据或什么也不做。 - M.M
显示剩余2条评论
6个回答

888

基本术语回顾

通常情况下(除非你在编写汇编程序),可以将指针想象成包含一个数字内存地址的变量,其中1表示进程内存中的第二个字节,2表示第三个字节,3表示第四个字节,以此类推......

  • 0和第一个字节怎么了?稍后我们会讨论这个问题 - 请参阅下面的空指针
  • 有关指针存储内容以及内存和地址之间关系的更准确定义,请参见本答案末尾的"更多关于内存地址以及为什么您可能不需要知道"

当您想要访问指针指向的内存中的数据/值 - 即具有该数字索引的地址的内容 - 然后您解引用指针。

不同的计算机语言具有不同的符号来告诉编译器或解释器,您现在对指向对象的(当前)值感兴趣 - 下面我重点介绍C和C ++。

指针场景

在C中考虑给定指针p的情况...

const char* p = "abc";

...四个字节用于编码字母'a'、'b'、'c',以及一个0字节表示文本数据的结尾,这些内容存储在内存中的某个地方,并且该数据的数值地址存储在p中。C语言在内存中编码文本的方式被称为ASCIIZ

例如,如果字符串文字恰好位于地址0x1000处,而p是一个32位指针,位于0x2000处,则内存内容将如下:

Memory Address (hex)    Variable name    Contents
1000                                     'a' == 97 (ASCII)
1001                                     'b' == 98
1002                                     'c' == 99
1003                                     0
...
2000-2003               p                1000 hex

注意,地址0x1000没有变量名/标识符,但我们可以间接地使用指向存储其地址的指针来引用字符串字面量:p

解引用指针

为了引用p指向的字符,我们使用以下其中一种符号(同样是针对C语言)来解引用p
assert(*p == 'a');  // The first character at address p will be 'a'
assert(p[1] == 'b'); // p[1] actually dereferences a pointer created by adding
                     // p and 1 times the size of the things to which p points:
                     // In this case they're char which are 1 byte in C...
assert(*(p + 1) == 'b');  // Another notation for p[1]

你还可以通过移动指针来访问指向的数据,并在移动过程中对其进行解引用:
++p;  // Increment p so it's now 0x1001
assert(*p == 'b');  // p == 0x1001 which is where the 'b' is...

如果你有一些可以写入的数据,那么你可以像这样做:

int x = 2;
int* p_x = &x;  // Put the address of the x variable into the pointer p_x
*p_x = 4;       // Change the memory at the address in p_x to be 4
assert(x == 4); // Check x is now 4

在编译时,您必须知道需要一个名为x的变量,并且代码要求编译器安排它应该存储的位置,确保地址可以通过&x访问。

解除引用和访问结构体成员

在C中,如果您有一个指向具有数据成员的结构体的变量,您可以使用->解除引用运算符来访问这些成员:
typedef struct X { int i_; double d_; } X;
X x;
X* p = &x;
p->d_ = 3.14159;  // Dereference and access data member x.d_
(*p).d_ *= -1;    // Another equivalent notation for accessing x.d_

多字节数据类型

为了使用指针,计算机程序还需要一些关于指向的数据类型的了解 - 如果该数据类型需要超过一个字节来表示,则指针通常指向数据中最低编号的字节。

因此,看一个稍微复杂一点的例子:

double sizes[] = { 10.3, 13.4, 11.2, 19.4 };
double* p = sizes;
assert(p[0] == 10.3);  // Knows to look at all the bytes in the first double value
assert(p[1] == 13.4);  // Actually looks at bytes from address p + 1 * sizeof(double)
                       // (sizeof(double) is almost always eight bytes)
++p;                   // Advance p by sizeof(double)
assert(*p == 13.4);    // The double at memory beginning at address p has value 13.4
*(p + 2) = 29.8;       // Change sizes[3] from 19.4 to 29.8
                       // Note earlier ++p and + 2 here => sizes[3]

指向动态分配内存的指针

有时候在程序运行时才能确定需要多少内存,这时可以使用malloc来动态分配内存。通常习惯将地址存储在一个指针中...

int* p = (int*)malloc(sizeof(int)); // Get some memory somewhere...
*p = 10;            // Dereference the pointer to the memory, then write a value in
fn(*p);             // Call a function, passing it the value at address p
(*p) += 3;          // Change the value, adding 3 to it
free(p);            // Release the memory back to the heap allocation library

在C++中,内存分配通常使用new操作符进行,而释放内存则使用delete操作符:

int* p = new int(10); // Memory for one int with initial value 10
delete p;

p = new int[10];      // Memory for ten ints with unspecified initial value
delete[] p;

p = new int[10]();    // Memory for ten ints that are value initialised (to 0)
delete[] p;

下面还有关于C++智能指针的内容。

地址的丢失和泄漏

通常情况下,指针可能是某些数据或缓存在内存中存在的唯一指示。如果需要持续使用该数据/缓存,或者需要调用free()delete以避免内存泄漏,则程序员必须对指针的副本进行操作...

const char* p = asprintf("name: %s", name);  // Common but non-Standard printf-on-heap

// Replace non-printable characters with underscores....
for (const char* q = p; *q; ++q)
    if (!isprint(*q))
        *q = '_';

printf("%s\n", p); // Only q was modified
free(p);

...或者仔细策划任何更改的逆转...

const size_t n = ...;
p += n;
...
p -= n;  // Restore earlier value...
free(p);

C++智能指针

在C++中,最佳实践是使用智能指针对象来存储和管理指针,在智能指针的析构函数运行时自动释放它们。自C++11以来,标准库提供了两种智能指针类型,unique_ptr用于分配给单个所有者的对象...

{
    std::unique_ptr<T> p{new T(42, "meaning")};
    call_a_function(p);
    // The function above might throw, so delete here is unreliable, but...
} // p's destructor's guaranteed to run "here", calling delete

...并使用引用计数实现共享所有权的shared_ptr...

{
    auto p = std::make_shared<T>(3.14, "pi");
    number_storage1.may_add(p); // Might copy p into its container
    number_storage2.may_add(p); // Might copy p into its container    } // p's destructor will only delete the T if neither may_add copied it

空指针

在C语言中,NULL0可以用来表示一个指针当前没有持有任何变量的内存地址,因此不能被解引用或用于指针运算。在C++中还可以使用nullptr。例如:

const char* p_filename = NULL; // Or "= 0", or "= nullptr" in C++
int c;
while ((c = getopt(argc, argv, "f:")) != -1)
    switch (c) {
      case f: p_filename = optarg; break;
    }
if (p_filename)  // Only NULL converts to false
    ...   // Only get here if -f flag specified

在C和C ++中,与内置数字类型不一样的是,bools默认不是false,指针也不总是设置为NULL。当它们是static变量或(仅限C ++)静态对象或其基类的直接或间接成员变量时,所有这些都设置为0 / false / NULL,或者经历零初始化(例如,new T();new T(x,y,z);对T的成员进行零初始化,包括指针,而new T;则不会)。
此外,当您将0NULLnullptr分配给指针时,指针中的位不一定全部重置:指针在硬件级别上可能不包含“0”,或者在您的虚拟地址空间中引用地址0。如果编译器有理由这样做,它可以在那里存储其他内容,但无论它做什么-如果您沿着并将指针与0NULLnullptr 或分配了其中任何一个的另一个指针进行比较,则比较必须按预期工作。因此,在编译器级别下方,"NULL"在C和C ++语言中可能有点“神奇”......
更多关于内存地址的信息,以及为什么您可能不需要知道
更严格地说,初始化的指针存储一个位模式,该模式标识NULL或(通常是virtual)内存地址。
简单情况下,这是进程整个虚拟地址空间中的数字偏移量;在更复杂的情况下,指针可能相对于某些特定的内存区域,CPU 可能基于 CPU "段" 寄存器或位模式中编码的某种段 ID 选择该内存区域,并且/或者根据使用地址的机器代码指令在不同位置查找。
例如,一个正确初始化为指向 int 变量的 int* 可能 - 在转换为 float* 后 - 访问与 int 变量所在的内存完全不同的 "GPU" 内存,然后一旦转换为并用作函数指针,它可能指向进一步不同的内存,其中包含程序的机器操作码(int* 的数值实际上是这些其他内存区域中的随机、无效指针)。
C 和 C++ 等 3GL 编程语言往往隐藏了这种复杂性,使其看起来像:
  • 如果编译器给你一个变量或函数的指针,只要该变量在此期间未被销毁/释放,你可以自由地对其进行解引用,而编译器会负责处理例如特定CPU段寄存器是否需要在此之前恢复,或使用不同的机器码指令等问题

  • 如果你得到一个数组元素的指针,你可以使用指针算术运算在数组中移动到任何其他位置,甚至形成一个合法的地址,该地址是与数组中其他元素的指针(或通过指针算术运算移动到相同的一过结束值的指针)进行比较的;同样在C和C++中,编译器将确保这种情况“正常工作”

  • 特定的操作系统函数(例如共享内存映射)可能会给你指针,在适当的地址范围内它们将“正常工作”

  • 试图将合法指针移动到这些边界之外,或将任意数字转换为指针,或使用转换为不相关类型的指针通常具有未定义的行为,因此应在更高级别的库和应用程序中避免,但是针对操作系统、设备驱动程序等的代码可能需要依赖于C或C++标准未定义的行为,但是这些行为在特定实现或硬件中是明确定义的。


3
在N1570草案C标准(我在网上找到的第一个)的6.5.2.1/2中,“下标运算符[]的定义是E1[E2]等同于(*((E1)+(E2)))。” - 我无法想象为什么编译器不会在编译的早期立即将它们转换为相同的表示形式,并在此之后应用相同的优化,但我不知道如何证明代码一定是相同的,除非调查每个编写过的编译器。 - Tony Delroy
3
@Honey:十六进制数值1000太大了,无法用一个字节(8位)的内存编码:一个字节只能存储从0到255的无符号数字。因此,在“只有”地址2000上无法存储1000 hex。相反,32位系统将使用32位(即四个字节),地址从2000到2003。64位系统将使用64位(即8个字节),地址从2000到2007。无论哪种方式,指针p的基地址仍然是2000:如果你有另一个指向p的指针,它必须在其四或八个字节中存储2000。希望这能帮到你!谢谢。 - Tony Delroy
1
@TonyDelroy:如果一个联合体u包含一个数组arr,那么gcc和clang都会认识到lvalue u.arr[i]可能访问与其他联合成员相同的存储空间,但不会认识到lvalue *(u.arr+i)可能这样做。我不确定这些编译器的作者是否认为后者会引发UB,或者前者会引发UB,但它们应该有用地处理它,但他们显然将这两个表达式视为不同的。 - supercat
7
我很少看到有人如此简明扼要地解释C/C++中指针及其用法。 - kayleeFrye_onDeck
1
@TonyDelroy:为了安全和优化,不需要“位转换”运算符,而是需要一种“受限指针”类型,在其生命周期内,要求使用受限指针访问的对象的所有部分都必须通过它进行独占式访问,并且其构造函数可以接受任何类型的指针,并将通过受限指针进行的访问视为对原始类型的访问。大多数需要使用类型转换的代码都可以适用于这样的结构,并且它将允许许多有用的优化,超越TBAA。 - supercat
显示剩余15条评论

134
解引用指的是获取指针所指内存位置中存储的值。使用运算符*来执行此操作,该运算符被称为解引用运算符。
int a = 10;
int* ptr = &a;

printf("%d", *ptr); // With *ptr I'm dereferencing the pointer. 
                    // Which means, I am asking the value pointed at by the pointer.
                    // ptr is pointing to the location in memory of the variable a.
                    // In a's location, we have 10. So, dereferencing gives this value.

// Since we have indirect control over a's location, we can modify its content using the pointer. This is an indirect way to access a.

 *ptr = 20;         // Now a's content is no longer 10, and has been modified to 20.

16
指针并不指向一个,它指向一个对象 - Keith Thompson
71
指针并不指向一个对象,它指向一个内存地址,在那里可以找到一个对象(可能是一个基本类型变量)。 - mg30rg
5
我不确定您所要表达的区别。指针值本质上就是一个地址。而对象根据定义是“在执行环境中存储数据的区域,其内容可以表示值”。至于您所说的“原始数据类型”,C标准并没有使用这个术语。 - Keith Thompson
11
我只是想指出,您并没有为答案增加价值,而只是吹毛求疵术语(而且还做错了)。指针值确实是一个地址,这就是它如何“指向”内存地址。在我们面向对象的世界中,“对象”一词可能会引起误解,因为它可以被解释为“类实例”(是的,我没有意识到问题标记为[C]而不是[C++]),我使用“原始”的词来表示与“复杂”相反(像结构体或类这样的数据结构)。 - mg30rg
4
让我补充一下这个答案,数组下标运算符 [] 也会对指针进行解引用(a[b]的定义是 *(a + b))。 - cmaster - reinstate monica
显示剩余6条评论

23

指针是对一个值的"引用",就像图书馆的索书号是对一本书的引用一样。"取消引用"这个索书号就是实际上去查找并检索那本书。

int a=4 ;
int *pA = &a ;
printf( "The REFERENCE/call number for the variable `a` is %p\n", pA ) ;

// The * causes pA to DEREFERENCE...  `a` via "callnumber" `pA`.
printf( "%d\n", *pA ) ; // prints 4.. 
如果那本书不在那里,图书管理员会开始大声喊叫,关闭图书馆,并派几个人去调查一个人去找一本不在那里的书的原因。

23

简单地说,解引用指的是访问指针所指向的某个内存位置上的值。


8

来自指针基础知识的代码和解释:

解引用操作从指针开始,并沿着指针箭头访问其指向的点。其目的可能是查看指向点的状态或更改指向点的状态。如果一个指针没有指向任何点,那么指针上的解引用操作将无法工作——必须分配指向点并设置指针指向它。指针代码中最常见的错误是忘记设置指向点。因为这个错误,在代码中最常见的运行时崩溃是解引用失败操作。在Java中,不正确的解引用会被运行时系统礼貌地标记出来。在编译语言如C、C++和Pascal中,不正确的解引用有时会崩溃,而有时会以某种微妙随机的方式破坏内存。正因为这个原因,编译语言中指针错误可能很难追踪。

void main() {   
    int*    x;  // Allocate the pointer x
    x = malloc(sizeof(int));    // Allocate an int pointee,
                            // and set x to point to it
    *x = 42;    // Dereference x to store 42 in its pointee   
}

实际上,您需要为x指向的位置分配内存。 您的示例具有未定义的行为。 - Peyman

3
我认为之前的所有答案都是错误的,因为它们声称解除引用意味着访问实际值。相反,维基百科给出了正确的定义:https://en.wikipedia.org/wiki/Dereference_operator 它作用于指针变量,并返回与指针地址处的值等效的l-value。这被称为“解除引用”指针。
也就是说,我们可以解除引用指针,而不必访问它所指向的值。例如:
char *p = NULL;
*p;

我们解除了空指针的引用,但没有访问它的值。或者我们可以这样做:

p1 = &(*p);
sz = sizeof(*p);

再次强调,解引用指的是不访问值的情况。这样的代码不会崩溃:

当你通过无效指针实际访问数据时,会发生崩溃。然而,不幸的是,根据标准,即使你不尝试访问实际数据,解引用一个无效指针也是未定义的行为(有一些例外情况)。

简而言之,解引用指针意味着对其应用解引用运算符。该运算符只返回一个左值供您将来使用。


1
@stsp 因为代码现在不崩溃并不意味着它将来或在其他系统上不会崩溃。 - user146043
问题是关于解引用指针的含义。无论在不触及实际内存位置的情况下,它是否理论上会崩溃,都不在问题的范围之内。在任何常见的架构和编译器上,这都不会崩溃。 - stsp
1
*p; 会导致未定义的行为。虽然您是正确的,解引用并不直接访问值,但代码 *p; 确实访问了该值。 - M.M
请证明你的说法。解引用与访问值无关,正如我在主要答案中已经解释过的那样。operator =是访问值的方式,在*p;中没有operator =,只有一个解引用操作符。它不会访问该值。一些编译器提供了“volatile dummy read”功能,如果p被标记为volatile,则插入获取操作。 - stsp
我并没有质疑声明中的“未定义行为”部分。他说“*p;可以访问值”,这是错误的。 - stsp
显示剩余6条评论

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