审查基本术语
通常足够好 – 除非你是编程组装 – 设想一个指针包含一个数字内存地址,1指的是进程的内存中的第二个字节,第二个字节,第三个,第三个,第四个等等。
> 0和第一个字节发生了什么?好吧,我们会得到后来 – 见下面的空指针。
>要更精确地定义指针存储的方式,以及内存和地址的关系,请参阅“关于内存地址的更多信息,以及您可能不需要知道的原因”。
当您想要访问指针指向的内存中的数据/值时,使用该数字索引的地址内容,然后解引用指针。
不同的计算机语言有不同的符号来告诉编译器或解释器,你现在对指向的价值感兴趣 – 我在下面关注C和C。
指针场景
考虑在C,给一个指针,如下面的p …
const char* p = "abc";
…具有用于编码字母’a’,’b’,’c’和用于表示文本数据的结束的0字节的数字值的四个字节被存储在存储器的某处,数据存储在p中。
例如,如果字符串文字恰好在地址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)
assert(++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 = 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...
C智能指针
在C中,最佳实践是使用smart pointer对象来存储和管理指针,在智能指针的析构函数运行时自动释放它们。自C 11以来,标准库提供两个,unique_ptr,当一个分配的对象有单个所有者时…
{
std::unique_ptr 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
{
std::shared_ptr p(new T(3.14, "pi"));
number_storage.may_add(p); // might copy p into its container
} // p's destructor will only delete the T if number_storage didn't copy
空指针
在C中,NULL和0以及另外在C nullptr中可以用于指示指针当前不保持变量的存储器地址,并且不应当在指针算术中取消引用或使用。例如:
const char* p_filename = NULL; // or "= 0", or "= nullptr" in C++
char c;
while ((c = getopt(argc, argv, "f:")) != EOF)
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。当它们是静态变量或(仅C)静态对象的直接或间接成员变量或其基础时,所有这些都设置为0 / false / NULL,或者经历零初始化(例如新的T() y,z);对T的成员执行零初始化,包括指针,而新的T;不)。
此外,当将0,NULL和nullptr指定给指针时,指针中的位不必全部复位:指针在硬件级别可能不包含“0”,或者指向虚拟地址空间中的地址0。编译器允许存储别的东西,如果有理由,但无论它做什么 – 如果你来比较指针为0,NULL,nullptr或另一个指针,分配任何这些,比较必须按预期工作。因此,在编译器级别的源代码下,“NULL”在C和C语言中可能有点“神奇”….
更多关于内存地址,以及为什么你可能不需要知道
更严格地说,初始化的指针存储识别NULL或(通常为virtual)存储器地址的位模式。
简单的情况是这是一个数字偏移到进程的整个虚拟地址空间;在更复杂的情况下,指针可以相对于一些特定的存储器区域,CPU可以基于CPU“段”寄存器或以位模式编码的某种方式的段id来选择,和/或根据使用地址的机器码指令。
例如,正确初始化为指向int变量的int *可能 – 在转换为float *之后 – 访问“GPU”内存中与“int”变量完全不同的值,那么一旦强制转换为函数指针可能引用不同的内存保持机器操作码的功能。
3GL编程语言(如C和C)倾向于隐藏这种复杂性,从而:
>如果编译器给你一个变量或函数的指针,你可以自由地解除引用(只要变量没有被破坏/释放的同时),这是编译器的问题,无论是否。需要预先恢复特定的CPU寄存器,或者使用不同的机器代码指令
>如果你得到一个指向数组中的元素的指针,你可以使用指针算术移动数组中的任何其他地方,或者甚至形成数组的一个地址,这是合法的,以便与其他指针进行比较元素(或者通过指针运算被类似地移动到相同的一个过去的结束值);再次在C和C,它是由编译器,以确保这个“只是工作”
>特定OS功能。共享内存映射可能会给你指针,他们将“只工作”在对他们有意义的地址范围内
>尝试将合法指针移动到这些边界之外,或者将任意数字转换为指针,或者使用指向不相关类型的指针,通常具有undefined behaviour,因此在更高级别的库和应用程序中应该避免,但是对于操作系统,设备驱动程序等。可能需要依赖于未定义的C或C的行为,这是由它们的特定硬件来定义的永远不会。