请在解释中包含一个示例。
它 通常 足够好 - 除非你正在编程汇编 - 设想一个包含数字内存地址的 指针 ,其中 1 指代进程内存中的第二个字节, 2 第三个, 3 第四个等等......
当您想要访问指针指向的内存中的数据/值时 - 具有该数字索引的地址的内容 - 然后您 取消引用 指针。
不同的计算机语言有不同的符号来告诉编译器或解释器您现在对指向对象的(当前)值感兴趣 - 我在下面重点关注 C 和 C++。
考虑在 C 中,给定p如下指针…
p
const char* p = "abc";
…四个字节,其数值用于对字母“a”、“b”、“c”进行编码,一个 0 字节表示文本数据的结尾,它们存储在内存中的某个位置,其数字地址数据存储在p. C 在内存中编码文本的这种方式称为 ASCIIZ 。
例如,如果字符串字面量恰好位于地址 0x1000,而p32 位指针位于 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.
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. 通常将地址存储在指针中…
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:
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避免内存泄漏的能力,则程序员必须对指针的副本进行操作......
free()
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 中,最好使用智能指针对象来存储和管理指针,并在智能指针的析构函数运行时自动释放它们。由于 C11 标准库提供了两个,unique_ptr当分配的对象只有一个所有者时......
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对于股份所有权(使用引用计数)…
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- 可用于指示指针当前不保存变量的内存地址,并且不应取消引用或在指针算术中使用。例如:
NULL
0
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,也不一定默认bools为false,指针并不总是设置为NULL。当它们是static变量或(仅限 C)静态对象或其基的直接或间接成员变量,或经历零初始化(例如new T();,new T(x, y, z);对 T 的成员(包括指针)执行零初始化)时,所有这些都设置为 0/false/NULL,而new T;才不是)。
bools
false
static
new T();
new T(x, y, z);
new T;
此外,当您将0,NULL和分配nullptr给指针时,指针中的位不一定全部重置:指针在硬件级别可能不包含“0”,或者引用虚拟地址空间中的地址 0。如果编译器有理由这样做,则允许编译器在其中存储其他内容,但无论它做什么 - 如果您来比较指向0,NULL的指针nullptr或分配了其中任何一个的另一个指针,则比较必须按预期工作。因此,在编译器级别的源代码之下,“NULL”在 C 和 C++ 语言中可能有点“神奇”......
更严格地说,初始化的指针存储一个位模式,标识一个NULL或一个(通常是虚拟的)内存地址。
最简单的情况是,这是进程整个虚拟地址空间的数字偏移量;在更复杂的情况下,指针可能与某个特定的内存区域相关,CPU 可以根据 CPU“段”寄存器或以位模式编码的某种段 id 方式来选择,和/或根据不同的位置查看不同的位置使用地址的机器代码指令。
例如,一个int*正确初始化为指向一个变量的int变量可能 - 在转换为一个float*- 访问“GPU”内存中的内存后,与int变量所在的内存完全不同,然后一旦转换为并用作函数指针,它可能会指向更远的地方程序的不同内存保持机器操作码(int*在这些其他内存区域内具有有效随机、无效指针的数值)。
int*
int
float*
像 C 和 C++ 这样的 3GL 编程语言倾向于隐藏这种复杂性,例如:
如果编译器给你一个指向变量或函数的指针,你可以自由地取消引用它(只要变量没有同时被破坏/释放),这是编译器的问题,例如一个特定的 CPU 段寄存器是否需要预先恢复,或者使用了不同的机器代码指令
如果你得到一个指向数组中元素的指针,你可以使用指针算法来移动数组中的任何其他位置,甚至可以在数组的末尾形成一个地址,与其他指向元素的指针进行比较是合法的在数组中(或者已经通过指针算法类似地移动到相同的过去的值);再次在 C 和 C++ 中,由编译器来确保它“正常工作”
特定的操作系统功能,例如共享内存映射,可能会为您提供指针,并且它们会在对它们有意义的地址范围内“正常工作”
尝试将合法指针移动到这些边界之外,或将任意数字转换为指针,或使用转换为不相关类型的指针,通常具有 未定义的行为 ,因此应避免在更高级别的库和应用程序中,但为操作系统、设备驱动程序等编写代码. 可能需要依赖 C 或 C++ 标准未定义的行为,但它们的特定实现或硬件已经很好地定义了这些行为。