浅谈C/C++指针、地址、内存空间、链表&新手常见误区

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;
}

具体实现见后续。

  • 8
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值