C++中的指针解密

C++中的指针解密

指针基础

  1. 指针是C,C++里面一个很特殊也很强大的结构。首先,指针的值表示所指向的内存空间,也就是所指向值的地址。明确p; &p表示俩个完全不同的意义。例如node *p = new node(2) p就是2所在的地址,&p则是p这个变量在内存中的地址。

    如下图所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    struct node{
    int val;
    node *next;
    node(int x):val(x),next(nullptr){}
    }
    int main() {
    node *p = new node(1);
    cout << p << " " << &p << endl;
    return 0;
    }
    //结果
    0x7f0c3c025a0 0x7fff5e71fb40

    可以明显看到俩者结果不同。

  2. 明确了这点之后,再看一个常见的操作,指针赋0. 经常使用p=NULL或者p=nullptr来表示一个指针为空,或者说这个指针没有指向有用的空间。因此在使用的时候可以通过判定指针是否为空来检查其合法性。这里顺便提一下NULL和nullptr的区别,NULL是从C中继承过来的,NULL既可以表示0,也可以表示空指针,例如int p= NULL, int* p = NULL这些都是可以的。但是nullptr只表示空指针,int p=nullptr是无法通过编译的,而int*p = nullptr则是正确的。因此现在最好使用nullptr更加安全,意思更加明确。

指针进阶

指针的指针

已经知道指针的目的就是方便操作所指向的对象,那么指针的指针呢? 指针的指针其实就是指针的地址。指针的指针有一个作用就是可以不通过函数返回值获得改变后的指针值(指针所指向的地方)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
using namespace std;
struct node{
int val;
node *next;
node(int x) : val(x), next(nullptr) {}
};
void test1(node *p){
p = new node(2);
cout << p->val << " " << p << " " << &p << endl;
}
void test2(node **p){
*p = new node(3);
}
int main() {
node *p = new node(1);
test1(p);
cout << p->val << " " << p << " " << &p << endl;
test2(&p);
cout << p->val << " " << p << " " << &p << endl;
return 0;
}
// 运行的结果是
2 0x7fddf8500010 0x7fff57e33ad8
1 0x7fddf8500000 0x7fff57e33b30
3 0x7fddf8500020 0x7fff57e33b30

这里的问题跟浅拷贝是类似的。可以看到test1中的p已经不是原始的p,&p的值已经发生了改变。所以如果想要获得更新之后的指向,test1必须有返回值返回给p才行。这也是指针的指针存在的一个意义。

从快慢指针中学习到的

1
2
3
4
5
6
7
8
9
10
11
12
slow = head;
fast = head->next; //注意这里的初始化操作,fast必须要初始化为head的next
while(fast){
fast = fast->next;
if(fast){
slow = slow->next;
fast = fast->next;
}
}
front = head;
back = slow->next; // 必须是slow->next,而不能是slow,主要为了之后nullptr考虑
slow->next = nullptr; // 只要slow->next=nullptr才能中断原始链表

初始化的问题主要是为了跟最后back选择slow->next配套,这样的初始化才能处理比如个数只有俩个的链表。

这里主要想讲的是为什么要选择slow->next而不能选择slow. 假设跟slow指向同一个点的指针叫a->next. 这里slow==a->next这是成立的,他俩指向同一个值。但是&p != &(a->next)这个意思是slow和a->next是俩个不同的指针。如何上述代码写成了back = slow; slow=nullptr; 那么back依然可以正确获得一半链表,但是slow=nullptr,并没有将原始链表从slow处断开,因为slow=nullptr只是说slow指向0了,是无效指针了,并没有跟原始链表产生任何联系。

但是slow->next 和a->next->next,他们存在slow->next == a->next->next表示这俩个指针指向相同的内容,而且&(slow->next) == &(a->next->next)也就是说他俩实际上是一个指针,只是不同的名字而已。(这里有一些地方我还不是非常理解,例如head是一个链表的头,p是另一个链表的头,当p=head之后,那么p->next也跟head->next是一个指针了。应该是有实现上的更加底层的东西,留待以后慢慢发现。)。这里就像已经有一个链表head, 和一个指针p, p = head之后,p和head是俩个指针,但是指向相同的地方,而p->next和head->next就变成了相同的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main() {
node *a = new node(1);
node *b = new node(2);
node *c = new node(3);
a->next = b;
b->next = c;
// cout << a << " " << a->val << " " << &a << endl;
// cout << b << " " << b->val << " " << &b << endl;
// cout << c << " " << c->val << " " << &c << endl;
cout << a->next << " " << a->next->val << " " << &(a->next) << endl;
cout << b << " " << b->val << " " << &(b) << endl;
cout << endl;
cout << a->next->next << " " << a->next->next->val <<" " << &(a->next->next) << endl;
cout << b->next << " " << b->next->val << " " << &(b->next) << endl;
cout << c << " " << c->val << " " << &c << endl;
}
0x7f9abf4025b0 2 0x7f9abf4025a8 // 不同
0x7f9abf4025b0 2 0x7fff51148ae0 // 不同
0x7f9abf4025c0 3 0x7f9abf4025b8 // 相同
0x7f9abf4025c0 3 0x7f9abf4025b8 // 相同
0x7f9abf4025c0 3 0x7fff51148ad8 // 不同

从结果可以看到,编译器为所有的next维护了且仅维护了一套指针。所以只有通过->next才能同时改变当前和原始(或者说只有->next才是同一个指针)。