请包括一个例子的解释。


当前回答

简而言之,解引用意味着从指针所指向的某个内存位置访问值。

其他回答

复习基本术语

这通常已经足够好了——除非您正在编程程序集——设想一个包含数值内存地址的指针,其中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所指向的字符,我们使用以下符号之一对p进行解引用(同样,对于C):

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语言中,NULL和0——以及在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++中,就像内置的数字类型不一定默认为0,bool也不一定默认为false一样,指针也不一定总是设置为NULL。当它们是静态变量或(仅限c++)静态对象或其基的直接或间接成员变量时,所有这些都被设置为0/false/NULL,或者进行零初始化(例如new T();和新T(x, y, z);对T的成员(包括指针)执行零初始化,而new T;不)。

Further, when you assign 0, NULL and nullptr to a pointer the bits in the pointer are not necessarily all reset: the pointer may not contain "0" at the hardware level, or refer to address 0 in your virtual address space. The compiler is allowed to store something else there if it has reason to, but whatever it does - if you come along and compare the pointer to 0, NULL, nullptr or another pointer that was assigned any of those, the comparison must work as expected. So, below the source code at the compiler level, "NULL" is potentially a bit "magical" in the C and C++ languages...

更多关于内存地址的内容,以及为什么您可能不需要知道

更严格地说,初始化的指针存储一个标识NULL或(通常是虚拟的)内存地址的位模式。

简单的情况是,这是一个数值偏移到进程的整个虚拟地址空间;在更复杂的情况下,指针可能相对于某些特定的内存区域,CPU可能根据CPU“段”寄存器或比特模式中编码的段id的某种方式来选择,并且/或根据使用地址的机器代码指令查找不同的位置。

例如,一个正确初始化的int*指向一个int变量,在转换为float*后,可能会在“GPU”内存中访问与int变量所在内存截然不同的内存,然后一旦转换为并用作函数指针,它可能会指向进一步不同的内存保存机操作码(int*的数值实际上是这些其他内存区域中的随机无效指针)。

像C和c++这样的3GL编程语言倾向于隐藏这种复杂性,例如:

If the compiler gives you a pointer to a variable or function, you can dereference it freely (as long as the variable's not destructed/deallocated meanwhile) and it's the compiler's problem whether e.g. a particular CPU segment register needs to be restored beforehand, or a distinct machine code instruction used If you get a pointer to an element in an array, you can use pointer arithmetic to move anywhere else in the array, or even to form an address one-past-the-end of the array that's legal to compare with other pointers to elements in the array (or that have similarly been moved by pointer arithmetic to the same one-past-the-end value); again in C and C++, it's up to the compiler to ensure this "just works" Specific OS functions, e.g. shared memory mapping, may give you pointers, and they'll "just work" within the range of addresses that makes sense for them Attempts to move legal pointers beyond these boundaries, or to cast arbitrary numbers to pointers, or use pointers cast to unrelated types, typically have undefined behaviour, so should be avoided in higher level libraries and applications, but code for OSes, device drivers, etc. may need to rely on behaviour left undefined by the C or C++ Standard, that is nevertheless well defined by their specific implementation or hardware.

简而言之,解引用意味着从指针所指向的某个内存位置访问值。

来自Pointer Basics的代码和解释:

The dereference operation starts at the pointer and follows its arrow over to access its pointee. The goal may be to look at the pointee state or to change the pointee state. The dereference operation on a pointer only works if the pointer has a pointee -- the pointee must be allocated and the pointer must be set to point to it. The most common error in pointer code is forgetting to set up the pointee. The most common runtime crash because of that error in the code is a failed dereference operation. In Java the incorrect dereference will be flagged politely by the runtime system. In compiled languages such as C, C++, and Pascal, the incorrect dereference will sometimes crash, and other times corrupt memory in some subtle, random way. Pointer bugs in compiled languages can be difficult to track down for this reason.

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   
}

解除对指针的引用意味着获取指针所指向的内存位置中存储的值。操作符*用于执行此操作,称为解引用操作符。

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.

指针是一个值的“引用”。就像图书馆的电话号码是一本书的参考资料一样。“取消引用”电话号码是在物理上检查和检索这本书。

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.. 

如果书不在那里,图书管理员就开始大喊大叫,关闭图书馆,然后几个人开始调查一个人去找不在那里的书的原因。