2021.8.28
浅谈指针、地址、内存空间、链表&新手常见误区
-1 几个问题
- c/c++中一个指针的大小是多少?
int*
、double*
、Node*
大小分别是多少? - Node型变量大小为什么是16字节,不是12字节?
- 一个节点Node到底长什么样?
- head指针和头节点一样吗?
- 每句代码在内存中究竟做了什么操作?
0 写在前面
数据结构重要。
本文适合完整的学过c++,但是对「指针、地址、内存空间、链表的理解与实现」仍存在模糊的初学者。
如果未完整的学过c++,学过c的指针,可以看到「指针、地址、内存空间」。
作为一个不合格的计算机专业的学生,我第一次听说数据结构是大二,那时真实的心路历程很坎坷,老师课上说的稀碎(弱者的借口),严蔚敏书本的晦涩,对c语言的不熟悉,一切都让我绝望,得到一个结论,数据结构没用。也真的好奇,链表真的能写在业务里吗,不谈业务,因为至今也没写过真正的业务,就说算法题里,我问过很多年级里的大牛,数据结构真的能写进算法里吗?甚至问过,你们真的会用到节点,会手写一个单链表吗?
我在得到数据结构没用的结论后,当然就没再学过数据结构。可是,要考试,为了考试我看着小甲鱼的课程加上练习选择题,我顺利通过,算是学了一遍,在学完的时候,我觉得我第一次学懂数据结构了,得到了第二个结论,数据结构是一种思想,不是代码,考试必备,但是没用。
如此混过大三,再一次接触数据结构的时候,是准备报考南大计算机系研究生。我认为最重要的是数学,我文科不好,所以英语政治也要多花一点时间,等我意识到我还没开始复习专业课的时候已经来不及了,那时我连单链表都不知道是什么、双向循环链表、带头节点、不带头节点、栈、队列,更不用说树、图。
可我也算是比大二的时候多明白了一些,我知道一个节点是这样写,
//# 1号写法
struct Node{
int data;
struct Node* next;
};
但是书上写的却是这样(c语言为例,稍后c++):
//# 2号写法
typedef int datatype;
typedef struct Node{
datatype data;
struct Node* next;
}Node,*linkList;
我看不懂,我不知道是什么意思。严蔚敏C++版更是写成了这样:
//# 3号写法
//定义方式分为 复合、嵌套、继承、结构
//这里以以继承方式为例
class ListNode{
protected:
int data;
ListNode* link;
};
class List:public class ListNode{
//链表类 直接使用链表节点类的数据和操作
private:
ListNode* first;//表头指针
};
在我对c和c++区别还停留在只有头文件不同、printf可以换成cout外,我以为他们就是一种语言的时候,我当然震惊,我所认识的只剩下int。class、private、protected、:符号,我一无所知。然而,还有甚者,严蔚敏c++书上给出的最终版本是这样:
//# 4号写法
template<class T,class E>//定义在“LinkList.h" //那个时候我甚至不知道什么是分文件编写
struct LinkNode{
E data;
LinkNode<T,E>* link;
ListNode(){link=NULL;}//构造函数
ListNode(E item,LinkNode<T,E>* ptr=NULL){data=item;link=ptr;}//构造函数
}
template<class T,class E>
class List{
protected:
LinkNode<T,E>* first;
public:
List(){first=new LinkNode<T,E>;}//构造函数
List(E x){first=new LinkNode<T,E>(x);}//构造函数
List(List<T,E>& L);//复制构造函数
~List(){}//析构函数
}
这是一段让我放弃数据结构与算法,放弃专业课的代码。考研成绩,数学、英语、政治都不错,数学甚至优异,但是专业课77分,无缘南大。在数据结构上,经历懵懂,一路摸爬滚打,单链表敲的磕磕盼盼,以为线性表就是单链表,以为栈和链表是两回事(确实两回事,栈是一种数据结构,链表是一种存储/实现方式,栈应该分为链栈、顺序栈,而说一回事是因为他们本质都是线性表,只不过栈是受限制的线性表),c的malloc都不会用,更不用说c++的new,看不懂指针,更不用说c++的模版,&符。但是今天,总算瞥见一丝痕迹,对数据结构、对数据结构中最简单的单链表有了更深刻的理解。
在最开始接触数据结构的时候,我对语言的熟悉仅限于int、for、if、while。甚至不会struct。那么说一下这四种写法分别需要掌握的基础知识,也是我一点一点慢慢才掌握的,默认会使用最基础的c语言,int、for、if、while:
- #1 c语言 struct结构体的使用,指针是什么
- #2 c语言 typedef的用法
- #3 c++ 面向对象class是什么,私有,保护,继承
- #4 c++ 模版类,构造函数,析构函数,重载,数据的引用
在本文中,会从指针、地址、内存空间开始,最后实现一个语言为c++,体现面向对象思想的完整的单链表。
阅读本文适合完整的学过c++,但是对指针、地址、内存空间、链表的理解与实现仍存在模糊的初学者。
另外这是本人自己摸索所得,是自己的理解,可能不完全严谨,但会给出例证。如有谬误,敬请指正。
1 数据类型的大小
首先看一段代码:
//test.cpp
#include <iostream>
using namespace std;
struct Node{
int data;
Node* next;
};
class linkList{
Node* head;
};
void test01(){
cout<<"sizeof(int)"<<sizeof(int)<<" ";
cout<<"sizeof(int*)"<<sizeof(int*)<<endl;
cout<<"sizeof(double)"<<sizeof(double)<<" ";
cout<<"sizeof(double*)"<<sizeof(double*)<<endl;
cout<<"sizeof(Node)"<<sizeof(Node)<<" ";
cout<<"sizeof(Node*)"<<sizeof(Node*)<<endl;
cout<<"sizeof(linkList)"<<sizeof(linkList)<<" ";
cout<<"sizeof(linkList*)"<<sizeof(linkList*)<<endl;
}
int main(){
test01();
getchar();
return 0;
}
输出如下:
sizeof(int) 4 sizeof(int*) 8
sizeof(double) 8 sizeof(double*) 8
sizeof(Node) 16 sizeof(Node*) 8
sizeof(linkList)8 sizeof(linkList*)8
结论1
由此得到第一个结论:所有的「指针型变量」,无论是什么指针类型,因为存放的是地址,所有都是8字节。
而所谓的「指针」即,地址。不同类型的指针存放的是不同数据类型的地址,而关于什么是「指向」稍后再说。
猜想:明明存放的都是地址,也都是八个字节,为何要区分不同的数据类型?我想可能是int型变量存储在一段内存,Node型变量存储在一段内存,由此区分地址的不同类型,方便管理。
一个问题
在结构体
struct Node{
int data;
Node* next;
}
中,int大小是4字节,Node*大小是8字节,为何Node是16字节不是12字节?
2 Node型变量为何16字节?
为了弄清Node型变量为何不是12字节我又写了几个结构体,一一测试大小:
struct DNode{
int data;
DNode* next;
DNode* pre;
};
struct threeInt{
int a,b,c;
}
struct intAndDouble{
int a;
double b;
}
void test02(){
cout<<"sizeof(DNode)"<<sizeof(DNode)<<endl;
cout<<"sizeof(threeInt)"<<sizeof(threeInt)<<endl;
cout<<"sizeof(intAndDouble)"<<sizeof(intAndDouble)<<endl;
}
输出:
sizeof(DNode)24
sizeof(threeInt)12
sizeof(intAndDouble)16
结论2
我一开始想到了内存对齐,但是忘记了需要不同类型,所以写出了threeInt的结构体,后来查阅资料才找到。由此得到结论2:
64位环境下,不同类型的变量如果在一个结构体中,要「内存对齐」。
int 4字节,指针 8字节,一共12字节,内存对齐后为16字节。
又一个问题
那么一个节点Node在内存中到底长啥样?
3 一个节点Node到底长什么样?指向是什么意思?
我在学习链表的时候,在学习节点的时候,看书,看博客,看到几乎所有介绍的文章都会加上配图。
像这样:
所以一个Node是不是长这样?
而第一张图中的那些奇奇怪怪的箭头,为什么就是指向?
我一直以为一个Node节点长成图二那样,而所谓的指针就是一个箭头。当然不是,如前所说,首先一个指针是地址,而一个指针变量在内存中当然也有其存储位置。再看前文说的一个Node节点是16个字节,int4字节,指针8字节,对齐4字节,一个Node节点在内存中该是长这样!
我写了如下代码进行验证:
void test03(){
Node* q=new Node;//new会在堆区开辟一块内存,返回一个Node型的指针地址,Node指针型变量存储在栈区。
cout<<"q address is:"<<&q<<" "<<"q is:"<<q<<endl<<endl;
cout<<"q->data address is:"<<&q->data<<endl;
cout<<"q->next address is:"<<&q->next<<endl<<endl;
cout<<"sizeof(q->data)"<<sizeof(q->data)<<endl;
}
输出:
q address is:0x16fdff3d8 q is:0x1060a9d70
q->data address is:0x1060a9d70
q->next address is:0x1060a9d78
sizeof(q->data)4
0x表示16进制数,后面则是16进制表示的内存地址,内存中该是怎样的呢?暂不考虑大端小端存储,如下:
其实橙色矩阵中变量名的说法不太准确,我们使用的是匿名对象。而q->data与q->next只是一种指代。
Note: 内存地址空间:0x1060a9d70实际上省略了7个0,应该是0x00 00 00 01 06 0a 9d 70
计算机是64位的。一个字符是4位0000-1111(0-f)两个字符8位 是一字节。0x代表16进制。
结论3
由此可见,得到结论3:Node指针类型的变量q,其存储的即是在堆区new出来的对象的内存的首地址。
结论4
上述所提出的节点模型(橙色节点图)正确。存在指针对齐,16字节。
不使用匿名对象,修改一下代码:
void test04(){
Node p;
Node* q=&p;
cout<<"q address is :"<<&q<<" "<<"q="<<q<<endl;;
cout<<"p address is :"<<&p<<" "<<endl;
cout<<"p.data/q->val address is:"<<&q->data<<endl;
cout<<"p.next/q->next address is:"<<&p.next<<endl;
}
输出:
q address is :0x16fdff3c8 q=0x16fdff3d0
p address is :0x16fdff3d0
p.data/q->val address is:0x16fdff3d0
p.next/q->next address is:0x16fdff3d8
结论5
节点指针q存储的是节点p的地址,因此q=&p,可以说q指向p
那么我们所说的「指向」一个东西,即是,存储 了这个东西的内存地址。
4 指针head就是头节点吗?
我一直以来的重大混淆,是把头节点当作头指针。指针head当然不是头节点!
证据:头节点首先属于节点,一个节点在内存空间中占16个字节;而head指针是指针,在内存空间中只占8个字节。
所以头节点和头指针是两回事。头指针指向头节点;头指针存储的是头节点的地址。
5 每句代码在内存中做了什么操作?
下面以带头节点的单链表,在头部插入为例,剖析其中每句代码在内存中所做的操作。首先代码如下:
//linklist.h
#ifndef linklist_h
#define linklist_h
#include <iostream>
using namespace std;
//构建一个节点类
class Node {
public:
datatype data;
Node* next;
};
//构建一个单链表类
class linkList{
public:
linkList();
~linkList();
void insertAtHead(int value);
void showlinkList();
private:
Node* head;
};
#endif
//linklist.cpp
#include "linklist.h"
linkList::linkList(){
head=new Node;
cout<<"this->head address is "<<&head<<endl;
cout<<"this->head = "<<this->head<<endl;
cout<<"head->data address is "<<&head->data<<endl;
cout<<"before"<<endl;
cout<<"head->data"<<head->data<<endl;
cout<<"head->next"<<head->next<<endl;
head->data=-1;
head->next=nullptr;
cout<<"after"<<endl;
cout<<"head->data="<<head->data<<endl;
cout<<"head->next="<<head->next<<endl;
}
linkList::~linkList(){
//暂时不实现
}
void linkList::insertAtHead(int value){
Node* cur=nullptr;//指向nullptr
cout<<"cur address is "<<&cur<<endl;
cout<<"cur = "<<cur<<endl;
cur=head->next;
//cur指向头节点,即存储头节点,头指针的->next指向头节点
cout<<"cur address is "<<&cur<<endl;
cout<<"cur = "<<cur<<endl;
Node* p=new Node;
cout<<"p address is "<<&p<<endl;
cout<<"p= "<<p<<endl;
p->data=value;
p->next=cur;
head->next=p;
cout<<"head->next="<<head->next<<endl;
p=nullptr;//不写也没事。
//delete p;栈空间的变量函数结束自动销毁
cur=nullptr;
cout<<"p="<<p<<endl;
cout<<"p->data address is"<<&p->data<<endl;
//cout<<"p->next= "<<p->next<<endl;错误
//此时p指向0地址,已经没有已经没有next指针。
//所有的变量都是地址空间的代号
}
void linkList::showlinkList(){
if(head==nullptr||head->next==nullptr){
cout<<"the linkList is empty"<<endl;
return;
}
Node* cur;
cur=this->head;
cout<<"the linkList is: ";
while(cur->next){
cur=cur->next;
cout<<cur->data<<" ";
}
cout<<endl;
}
//main.cpp
#include "linklist.h"
int main() {
linkList L;
cout<<"L address is "<<&L<<endl;
L.insertAtHead(3);
L.showlinkList();
L.insertAtHead(4);
L.showlinkList();
getchar();
getchar();
return 0;
}
输出:
this->head address is 0x16fdff420
this->head = 0x100642c70
head->data address is 0x100642c70
before
head->data0
head->next0x8000000010064135
after
head->data=-1
head->next=0x0
L address is 0x16fdff420
cur address is 0x16fdff3d8
cur = 0x0
cur address is 0x16fdff3d8
cur = 0x0
p address is 0x16fdff3d0
p= 0x10065a7f0
head->next=0x100641350
the linkList is: 3
cur address is 0x16fdff3d8
cur = 0x0
cur address is 0x16fdff3d8
cur = 0x100641350
p address is 0x16fdff3d0
head->next=0x100642e00
the linkList is: 4 3
//这个输出和上面代码可能有点不对应,因为代码在写的时候增加了一些log输出,图是按照这个log画的
linkList L;
首先在栈区,创建了linkList类型的变量L,其有Node*型的成员变量head。
调用linkList的构造函数,new出head,在before前其示意图如下:
head->next还没有初始化,是野指针。
head->data=-1;
head->next=nullptr;
head->data默认初始化为0。用红笔标记修改,其接着执行代码,示意图如下:
0x0即nullptr。
L.insertAtHead(3);
Node* cur=nullptr;
栈区声明了一个Node型指针cur,初始为nullptr,示意图如下:
cur=head->next;
cur指向头指针指向的next,
头指针指向头节点,而头指针指向的next即为第一个节点或为空。
所以这句话的意思是,让cur指向头节点的下一个节点(即为第一个节点)或为空。
由于链表为空,所以此时curl里存储的依然是nullptr,如图:
Node* p=new Node;
在堆区new出一块新的16字节的内存空间。
p->data=value;
p->next=cur;
赋值,p->next指向头节点,而cur指向头节点,即cur存储头节点的地址。
head->next=p;
头节点指向p。
p=nullptr;
cur=nullptr;
insertAtHead(4)
cur=head->next;
head是头指针,头指针指向头节点,存储头节点的地址。
cur指向头节点->next,存储第一个节点的地址
Node* p=new Node;
p->data=value;
p->next=cur;
head->next=p;
head->next指针存储新插进来的节点地址,使其成为头节点的下一个,也就是第一个节点。
Note:刚刚去洗澡,突然想到的。所有的变量都只是地址空间的代号。我们所做的就是给地址空间起名字。
p=nullptr;
cur=nullptr;
栈区的变量在函数执行结束自动销毁。
总结
linkList L指向头指针,头指针指向头节点,头指针的next指向第一个节点,第一个节点的next指向第二个节点,第二个节点的next指向nullptr。即像这样。
6 实现
去掉log打印输出,最简单的单链表形式如下:
//linklist.h
#ifndef linklist_h
#define linklist_h
#include <iostream>
using namespace std;
//构建一个节点类
class Node {
public:
datatype data;
Node* next;
};
//构建一个单链表类
class linkList{
public:
linkList();
~linkList();
void insertAtHead(int value);
void showlinkList();
private:
Node* head;
};
#endif
//linklist.cpp
#include "linklist.h"
linkList::linkList(){
head=new Node;
head->data=-1;
head->next=nullptr;
}
linkList::~linkList(){
//暂时不实现
}
void linkList::insertAtHead(int value){
Node* cur=nullptr;
cur=head->next;
Node* p=new Node;
p->data=value;
p->next=cur;
head->next=p;
}
void linkList::showlinkList(){
if(head==nullptr||head->next==nullptr){
cout<<"the linkList is empty"<<endl;
return;
}
Node* cur;
cur=this->head;
cout<<"the linkList is: ";
while(cur->next){
cur=cur->next;
cout<<cur->data<<" ";
}
cout<<endl;
}
//main.cpp
#include "linklist.h"
int main() {
linkList L;
L.insertAtHead(3);
L.showlinkList();
L.insertAtHead(4);
L.showlinkList();
getchar();
getchar();
return 0;
}
具体实现见后续。