由链表问题引发对堆、栈的讨论
刚听到链表其实还是有些惧怕的,我记得第一次见链表是在高中的时候,那时见到一个简单的Demo还读不懂,思路更是模糊。直到在大学课本上才真正对链表有了深入的了解,但由于大学老师一笔带过的授课,我并没有刻意关注链表这东西。
前几天,Sheldon Zhong 就一些关于链表的问题与我讨论,几经引用和修改最终整出个马马虎虎的Demo。
#include <iostream>
using namespace std;
class Node{
private:
int value;
Node * next;
public:
Node(){ }
Node * getNext() { return this->next; }
void setNext(Node * p) { this->next = p; }
int getValue() { return this->value; }
void setValue(int value) { this->value = value; }
};
class LinkedList{
private:
Node * head;
public:
LinkedList() {
head = NULL;
Node one;
one.setNext(NULL);
one.setValue(0);
head = &one;
cout << "head address in construction: " << head << endl;
cout << "head next address in construction: " << head->getNext() << endl;
}
void add(int value);
};
void LinkedList::add(int value)
{
Node * p;
cout << "head address in add (previous): " << head << endl;
cout << "head next address in add (previous): " << head->getNext() << endl;
p = head;
cout << "head address in add (current): " << head << endl;
cout << "head next address in add (current): " << head->getNext() << endl;
cout << "p address in add (current): " << p << endl;
cout << "p next address in add (current): " << p->getNext() << endl;
// do something...
}
int main()
{
LinkedList a;
a.add(50);
return 0;
}
但运行的结果却有些诡异(以至于某人认为编译器坏了)。
观察LinkedList类内的add函数,可以看到我们只是将head指针给了p指针,没有改变head和head->next的指向,而结果却显示next指向已改变。
void LinkedList::add(int value)
{
Node * p;
cout << "head address in add (previous): " << head << endl;
cout << "head next address in add (previous): " << head->getNext() << endl;
p = head;
cout << "head address in add (current): " << head << endl;
cout << "head next address in add (current): " << head->getNext() << endl;
cout << "p address in add (current): " << p << endl;
cout << "p next address in add (current): " << p->getNext() << endl;
// do something...
}
我们尝试给Node类添加构造函数,并将LinkedList类构造函数中的(Node one;)定义为全局,结果却意外的正常了。
回过头看看构造函数,我们定义了一个Node对象,但作用域是在构造函数中,当构造函数结束后会被回收释放,我们初步认为是作用域问题,但后来我们尝试将LinkedList类构造函数中新建head节点(Node)的代码移动到add函数中,错误依旧。我们渐渐把矛头指向Node对象是否被系统回收释放上,最终的结果印证了我们的猜想。
我们将定义Node对象的语句从
Node one;
one.setValue(0);
one.setNext(NULL);
head = &one;
更改为
Node * one = new Node();
one->setValue(0);
one->setNext(NULL);
head = one;
无论放在构造函数中还是放在add函数中,结果均正确。我们了解到定义对象主要有两种方式
- ClassName object(param); //在栈(Stack)中分配空间,由系统自动释放。
- ClassName *object = new ClassName(); //在堆(Heap)中分配空间,手动释放。
关于五大内存分区
- 栈(Stack),由编译器在需要的时候分配,在不需要的时候自动清除的变量存储区。里面的变量通常是局部变量、函数参数等。
- 堆(Heap),由new分配的内存块,它们的释放编译器不会处理,由我们的应用程序控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。(new char; delete char;new char2[2]; delete char2;)
- 自由存储区,由malloc等分配的内存块,它和堆是十分相似的,不过它是用free来结束自己的生命的。
- 全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在C语言中,全局变量又分为初始化的和未初始化的,在C++里面不再做这个区分,它们共同占用同一块内存区。
- 常量存储区,这是一块比较特殊的存储区,里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多,在《const的思考》一文中,有6种方法。
因此,垃圾回收是针对堆的,而栈因为本身是FILO(first in, last out.),即先进后出,能够自动释放。所以new创建的对象,都放到堆中,而之前使用第一种方法定义对象则在构造函数结束后被系统回收。
实际上,有关于堆和栈的认识我曾经在CnBlogs转载过一片文章,由于时间过去比较久,记忆不深刻,以至于没能意识到这微妙的区别。以后学习的路还很长,永远“Stay hungry, Stay foolish.”的心态。