如果写一个swap函数改变传入变量的值,我们知道可以在传入参数时使用int &a, int &b
void swap(int &a, int &b)
但是如果参数是一个类似结构体的怎么办呢,具体遇到问题的代码如下:
#include <iostream>
using namespace std;
template <class T>
class Tree
{
public:
int data;
Tree<T> *left;
Tree<T> *right;
};
template <class T>
void change(Tree<T> *temp)
{
Tree<int> *cur = new Tree<int>;
cur->data = 5;
temp = cur;
cout<<temp->data<<endl;
}
int main()
{
Tree<int> *a;
change(a);
cout<<a->data<<endl;
}
这个时候遇到的问题就是程序直接崩溃,因为a
没有分配内存空间,其实也就是a
并没有被change
函数里面已经分配内存空间的temp
赋值,那么除了下面这种修改方法,还有别的修改方法吗
#include <iostream>
using namespace std;
template <class T>
class Tree
{
public:
int data;
Tree<T> *left;
Tree<T> *right;
};
template <class T>
void change(T &x)
{
Tree<int> *cur = new Tree<int>;
cur->data = 5;
x = cur->data;
}
int main()
{
Tree<int> *a = new Tree<int>;
change(a->data);
cout<<a->data<<endl;
}
二级指针, 详见我以前的这篇博文, 看完了你应该就有答案了http://czxyl.me/2017/05/14/In...
以防被墙, 在这里贴出原文.
title: Introduction to double pointers---part1
date: 2017-05-14 19:48:03
tags:
thumbnail: https://d3nmt5vlzunoa1.cloudf...
categories: [c/c++, tricky]
bgimage: https://d3nmt5vlzunoa1.cloudf...
[TOC]
warm up
case0: preface
Q0.1: 这里的x和x_address代表什么?
answer: x 代表
1
这个rvalue的地址, 也就是说它的值是就是一个整数的地址 或者说x这个地址上存储着一个int 1
, 如果我们对x进行dereference
, 得到的值就是1了x_address
代表着x这个地址的地址. 如果我们对x_address
进行dereference
, 那么得到的值就是x, 也就是一个整数的地址了. 如果再进一步dereference
得到的就是1了. 所以本文就是围绕着这么个最简单的知识展开的.case1: swap
swap: version A
swap: version B
swap: version C
swap: version D
Q1.1: 哪个version是对/错的?
answer: A是对的, B, C, D是错的.
Q1.2: A和B的区别是什么?
answer: 很明显, A交换了a和b指向的address之上的value. 而B交换的是两个address本身,
然而其上的value依然没有变.
Q1.3: version C错在哪里(以后会省略前缀version)?
answer: 懂了B的错误后, 很轻松的就能发现C里面交换的只是地址的地址. 所以最顶层的value依然是不变的. 这里提这个形式的例子主要是为了引出主题(double pointer)
case2: allocate
allocate: version A
allocate: version B
Q2.1: A与B的对错?
answer: A错B对
Q2.2: A会造成哪些问题?
answer: 1. 无法准确打印出
*p
的值. 2. 会有内存泄漏Q2.3: swap::version D 与 allocate::version A有什么相同的错误之处?
answer: 都妄图通过传
value
来改变value
, 注意, 此处我定义的value
是广义上的, 就像allocate::version A传入的是一个指针int *p
, 同时接受的也是int *
类型, 所以我在这里统一看做传value
. 与之相对的就是allocate::version B的传参就是传address
了.Q2.4: swap::version A 与 allocate::version B有什么相同的正确之处?
answer: 使用
function
来改变值时都传入的是其地址.Q2.5: 为什么无法准确打印出
*p
的值?answer: 结合Q2.3, 可以看出其实p是作为一个值传入的. 自然alloc1()里面的p再怎么分配malloc也和main()中的p没有关系了. 同时, 由于alloc里面分配的内存的所有权并没有返回转交到main()中, 所以会造成
p = (int*)malloc(sizeof(int));
出来的内存就被抛弃了, 造成内存泄漏Q2.6: 如何确定真的出现了内存泄漏了?
answer: 当然, 这里比较直观, 所以能肉眼看出来, 但是程序一旦复杂, 想检测这种内存泄漏的情况可就不一定能一眼看出来了. 所以我们需要一些工具, 比如vs下的
_CrtDumpMemoryLeaks
或者linux下的valgrind
.Q2.7: 详细阐述下version B的正确性?
answer: 首先, 传入的是一个
int*
的地址, 也可以说成pointer to pointer
. 然后在这个地址的值(本质也是一个地址)上开辟了空间*p = (int*)malloc(sizeof(int));
. 因此相当于在swap时需要往下探入一层(从value到address), 这里也是在地址上的基础上再往下探入到地址的地址, 这样地址能够被在main()中的p保存. 因此allocate2()中对**p的修改也能在main()中反映.Derive a conclusion from case1 and case2
我们想要通过函数修改任何一个值必须传入它地址. 而不能仅仅传入值, 即使它本身是一个地址, 而你想要修改的就是这个地址, 那么你需要做的就是传入这个地址的地址. 不过写到这里, 读者可能还是不知道什么时候需要用到这种
double pointers
. 所以, 接下来我们通过各种例子来展示下此技巧的威力Application for double pointers
Linkedlist
case 1: insert(tail)
insert: version A(trival && singal pointer)
insert: version B(non-trival && double pointers)
insert: version C(trival && singal pointer)
Q1.1: version A省略掉
if
部分的判断会怎么样?answer: 插入第一个结点(头结点)时会访问到
NULL->next
, 非法内存访问.Q1.2: version B为什么不需要这个version A的
if
条件?answer: 想要回答这个问题, 只需要考虑version B在插入头结点时的是如何处理的就行了. 与version A不同, version B是用
*current_node
做循环条件的, 而不是(*current_node)->next
. 这样做的好处是避免了NULL->next
这样的错误步骤出现.Q1.3: version A可以写成
while (current_node)
来避免使用if条件, 也就是version C这样的吗?answer: 我们来看下完整的代码, 希望读者能上机运行下
首先我们必须要搞清楚我们传参的目的是什么, 以上面这份代码为例, 我们的第一个insert其实是需要修改head的value, 但是, 我们我们传入的其实也是head的value, 回想下文章开始的case1, case2, 毫无疑问, 这样做的话, main()中的head依然是NULL, 得不到更新. 并且一直到
return 0
, head保持着NULL不变. 自然而然我们会想到把node *head = NULL
替换为node head(you_choose_integer)
. 这样做的话其实是和其它version逻辑稍有区别的, 即这里的头结点是在main()里直接给出, 而不是insert中插入一个头结点. 当然, 后面的逻辑依然是一致的. 下面给出完整代码后我们继续分析问题(依然没有析构仅供这里参考下)好, 逻辑上终于修改完善了, 并且正确性大家上机测下很容易 我们以上面这份代码来看Q1.3本身. 首先我们将代码改为问题描述的样子
首先看参数传递, 的确传的是要修改的值的地址, 然后, 因为main()中初始时有一个非
NULL
的头结点, 所以while (current_node->next)
不会出错. 下面的问题我会进一步讲解这种写法可能出错的地方. 但是这个其实是特例, 毕竟很少有这种把head一开始就定义的情况...不过下面的一些例子还是会让大家看到有些情况double pointer
能够写出比singal pointer
更简练的代码的.Q1.4: 将Q1.3的answer里面
while (current_node->next)
改为while (current_node)
会怎么样?answer: 这样最后跳出时我们得到
current_node
是NULL, 看上去是合理的, 给那个NULL赋值
inserted_node
就行了. 但是又有一个新问题, 给这个NULL赋值后我们能确保prev->next = inserted_node
吗? 这里其实是很反直觉的, 答案是不能, 首先我们要知道了next实际上都指向实际的内存地址, 而当current_node = NULL
(循环终止条件)时, 我们再给它进行复制, prev结点的next依然是NULL, 因为在这里current_node
只是一个独立的指针, 与链表结构已经无关了. 如果我们一定要这么做, 必须维护着一个实际存在的prev结点.Q1.5: 将Q1.3里面的最后一份代码的第一步node head(0)改为node *head = new node(0); 会怎么样?
answer:
其实这个问题的依旧隐藏着一个前提条件, 就是这样修改依旧会实现在main()中定义好了一个头结点, 这样的话即使形参实参都是node*类型, 也就是说没有传地址的情况, 依旧是正确的. 因为它不需要改变head或者inserted_node, 只需要改变next就行了. 但是一旦写成
node *head = NULL
, 就会犯NULL->next这样的read access violation
错误了.Q1.6:有没有办法将version A改为可以设置头结点在insert建立?
answer: 没有, 我们讨论两者情况,
法1的问题是想修改head(题目要求)但是传入的是
node *head
, 自然不能成功法2的问题是这样必须在main()中建立了一个value是0的头结点了, 所以已经与题意不符了.
Q1.7: 验证version B的正确性?
answer: 我们首先再回顾下(别怪我啰嗦, 我认为这是最重要的), 传入的是要修改的
head
的地址(别被形参和实参名一样所迷惑, 他们代表的是两个不同的东西, 形参是实参的地址). 我们先看对头结点的处理: 将NULL的地址传入, 并且修改为了inserted_node
. 然后看非head结点的建立, 此时就需要用到while了. 我们分析下这个while的含义, 循环条件是对current_node进行dereference
, 判断是不是NULL. 循环执行语句是每次讲current_node这个地址的地址下移(next). 逻辑还是很清楚的.Q1.8: 本例中double pointers的优势在哪里?
answer: 可以任性的在main()中设置head, 比如
node *head = NULL
, 也可以node head(0)
, 也可以node *head = new node(0)
. 其他version就有着这样那样的限制.case2: remove
remove: version A(right && c style(not completely) && double pointers)
remove: version B(right && c++ style && double pointers)
remove: version C(right && c style && single pointer)
remove: version D(right && c++ style && single pointer)
remove: version E(wrong && c++ style && single pointer)
Q2.1: version E有哪些错误?
answer:
Q2.2: version E该如何改正?
answer: refer to version C/D: 如果prev是NULL, 那么就该维护head了(head = next). 使用
prev->next = next
,prev = curr
来维护羁绊. next还有一个作用就是在delete curr前保存着下一个结点, 如果贪心省略掉这个next 你还是需要一个temp来保存的, 反而变丑了. 这也是写代码时容易忽略的一点.Q2.2: 分析这里两个right version的逻辑差别?
answer: single pointer version都需要维护prev和next结点, 而double pointers仅需要维护一个entry结点(除curr, 并且本质上entry起着prev的作用). single pointer需要多一个判断head的操作
Q2.3: double pointers version不需要判断head(prev == NULL)?
answer: 这个问题依然要回到之前反复强调的value-address了, 传入的head在这里是可以被修改的, 所以能在if里面直接维护.
Q2.4: double pointers version是如何处理非head结点的?
answer: 这个问题也需要回到value-address来回答. 其实无论是head还是non-head, 处理方式都是一样的----改变这个结点的地址为->next. 这样是不会破坏与prev的羁绊的. 即使是NULL都无妨(ctrl/command f
任性
, 参考这里).case3: Insertion Sort for Singly Linked List
sort: version A(single pointer)
Q3.1: 将上面那份代码改写为double pointers?
answer:
In Summary
可能很多人认为指针是一个糟糕的特性, 也有很多人认为掌握指针的各种技巧在C++11 里各种封装好的
smart pointer
前只是班门弄斧. 但是我想说, 世界上不存在神这种玩意, 所以永远不要把一个人当做神, 即使他/她再强, 说的话也不应该全盘被信任, 所以你必须做出自己的判断, 这判断不是为了判断而判断, 而是必须尽快的切换到学习知识而不是犯傻逼脑残的所谓选择困难症的状态, 否则只会在一票大神面前永远迷失方向, 今天某大v说学这个是没有意义的, 明天另一个大v说学那个是有意义的. 如果你信任所有人, 就会陷入什么都想学, 却什么都没心思学的困境, 解决问题的唯一途径就是凭着自己的兴趣走下去, 何必管边上的人所说的话?等这些天手边的事忙完后我在再写一些 double pointers的其它应用, 比如链表的其他操作, 树的各种......