当前位置: 首页 > news >正文

C++蓝桥杯之指针10.20(竞赛中很少使用)

uu们,这是C++最后一篇啦,属于拓展噢,接下来会写C++语言的数据结构哦

 一.内存和地址

在讲内存和地址之前,我们想有个生活中的案例: 假设有一栋宿舍楼,把你放在楼里,楼上有100个房间,但是房间没有编号,你的一个朋友来找你玩, 如果想找到你,就得挨个房子去找,这样效率很低,但是我们如果根据楼层和楼层的房间的情况,给每个房间编上号,如:

一楼:101,102,103...
二楼:201,202,203..

有了房间号,如果你的朋友得到房间号,就可以快速的找房间,找到你

如果把上面的例子对照到计算机中,又是怎么样呢? 我们知道计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,那我们买电脑的时候,电脑上内存是8GB/16GB/32GB 等, 那这些内存空间如何高效的管理呢?

其实也是把内存划分为一个个的内存单元,每个内存单元的大小为1个字节。

每个内存单元也都有一个编号(这个编号就相当于宿舍房间的门牌号),有了这个内存单元的编 号,CPU就可以快速找到编号对应的内存单元。

生活中我们的门牌号也叫地址,在计算机中我们 把内存单元的编号也称为地址。C语言中给地址起了新的名字叫:指针。 所以我们可以理解为:

内存单元的编号==地址==指针

计算机中常见的单位(补充):

一个比特位可以存储一个2进制的位1或者0

 bit - 比特位Byte - 字节KBMBGBTBPB
 1Byte = 8bit1KB = 1024Byte1MB = 1024KB1GB = 1024MB1TB = 1024GB1PB = 1024TB

1.指针变量

1.1.取地址操作符(&)

理解了内存和地址的关系,我们再回到C++,在C++中创建变量的本质是向内存申请空间,比如:


#include <iostream>using namespace std; int main()
{int a = 10;return 0;
}

比如,上述的代码就是创建了整型变量a,在内存中申请4个字节,用于存放整数10,申请到的每个字节都有地址,上图中4个字节的地址分别是:

 0x0117FAD80x0117FAD90x0117FADA0x0117FADB

那我们如何能得到a的地址呢?

这里就得学习一个操作符(&)-取地址操作符

#include <iostream>using namespace std;int main()
{int a = 10;&a;//取出a的地址cout  << &a << endl;//0x6ffe4creturn 0;
}

&a取出的是a所占4个字节中地址较小的字节的地址。

虽然整型变量占用4个字节,我们只要知道了第一个字节地址,顺藤摸瓜访问到4个字节的数据也是可行的。

那我们通过取地址操作符(&)拿到的地址是一个数值,比如: 0x0117FAD8 ,这个数值有时候也是需要存储起来,方便后期再使用的,那我们把这样的地址值存放在哪里呢?

答案是:指针变量中。

比如:

#include <iostream>using namespace std;int main()
{int a = 10;int * pa = &a;//取出a的地址并存储到指针变量pa中return 0;}

指针变量也是一种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址。 pa 就是存放 a 的地址,指针变量中存放谁的地址,我们就说这个指针变量指向了谁;上面代码中 们就是 pa 指向了 a 变量。 但是有时候一个指针变量创建的时候,还不知道存储谁的地址,那怎么办呢?在C++中这时候,我们会给指针变量赋值为 NULL , NULL 的值其实是 0 ,表示空指针,意思是没有指向任何有效的变量。 当然 0 也是作为地址编号的,这个地址是无法使用的,读写该地址会报错。

int *p = NULL; 

当然在C++11后,使用 nullptr 来代替了NULL,我们在代码中也可以直接使用 nullptr 。

1.2.如何拆解指针类型

我们看到 pa 的类型是 int* ,我们该如何理解指针的类型呢?

int a = 10;
int * pa = &a;

这里pa左边写的是 int* , * 是在说明pa是指针变量,而前面的 int 是在说明pa指向的是整型(int) 类型的对象。

那如果有一个 char 类型的变量 ch , ch 的地址,要放在什么类型的指针变量中呢?

char ch = 'w';
pc = &ch;//pc 的类型怎么写呢?

1.3.解引用操作符

我们将地址保存起来,未来是要使用的,那怎么使哦那个呢? 在现实生0活中,我们用地址要找到一个房间,在房间里可以拿去或者存放物品。 C++语言中其实也是一样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这里必须学习一个操作符叫解引用操作符( * )。

#include <iostream>using namespace std;int main()
{int a = 100;int* pa = &a;*pa = 0;cout << a << endl;//0return 0;
}

上面代码中第7行就使用了解引用操作符, *pa 其实就是 a 变量了;所以 *pa 的意思就是通过pa中存放的地址,找到指向的空间, *pa = 0 ,这个操作符是把 a 改成了 0 .

有同学肯定在想,这里如果目的就是把 呢? 其实这里是把a的修改交给了pa来操作,这样对a的修改,就多了一种的途径,写代码就会更加灵活, 后期慢慢就能理解了。 注意:如果一个指针变量的值是 NULL 时,表示这个指针变量没有指向有效的空间,所以一个指针变量的值是 NULL 的候,是不能解引用操作的。

2.指针类型

2.1.指针类型的意义

就两个方面

2.11指针的解引用 

对比,下面2段代码,主要在调试时观察内存的变化。

//代码1
#include <iostream>using namespace std;int main()
{int n = 0x11223344;int *pi = &n; *pi = 0;   return 0;
}
//代码2
#include <iostream>using namespace std;int main()
{int n = 0x11223344;char *pc = (char *)&n;*pc = 0;return 0;
}

调试我们可以看到,代码1会将 n 的 4 个字节全部改为 0 ,但是代码2只是将 n 的第1个字节改为 0 。

结论:指针的类型决定了,对指针解引⽤的时候有多大的权限(一次能操作几个字节)。

比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。

2.12指针+-整数

先看一段代码,调试观察地址的变化。这里为了方便观察,将环境改成x86(32位环境),在X86的环 境下地址是32个比特位的长度,在X64环境下地址是64个比特位的长度。我们这里仅仅是为了说明问题。

#include <cstdio>
#include <iostream> using namespace std;int main()
{int n = 10;char* pc = (char*)&n;int * pi = &n;   printf("&n   = %p\n", &n);printf("pc   = %p\n", pc); //字符的地址使用cout打印会以为是字符串,//所以这里使用printf来打印printf("pc+1 = %p\n", pc + 1);printf("pi   = %p\n", pi);printf("pi+1 = %p\n", pi + 1);//在vs2022中//&n   = 012FF904//pc   = 012FF904//pc+1 = 012FF905//pi   = 012FF904//pi+1 = 012FF908return 0;
}

我们可以看出, char* 类型的指针变量 +1 跳过 1 个字节, int* 类型的指针变量+1 跳过了 4 个字节。这就是指针变量的类型差异带来的变化。指针+1 ,其实跳过 1 个指针指向的元素。指针可以 +1 ,那也可以-1 。

结论:指针的类型决定了指针向前或者向后走一步有多大(距离)。

2.2.void* 指针

在指针类型中有一种特殊的类型是 void * 类型的,可以理解为无具体类型的指针(或者叫泛型指 针),这种类型的指针可以用来接受任意类型地址。但是也有局限性,void* 类型的指针不能直接进行指针的+-整数和解引用的运算。

#include <cstdio>
#include <iostream> using namespace std;int main()
{int a = 10;int* pa = &a;char* pc = &a;return 0;
}

在上面的代码中,将一个int类型的变量的地址赋值给一个 char* 类型的指针变量。 编译器给出一个报错(无法转换类型),是因为类型不兼容。而使用 void* 类型就不会有这样的问题。

3. 指针运算

使用 void* 类型的指针接收地址:

#include <cstdio>
#include <iostream> using namespace std;int main()
{int a = 10;void* pa = &a;void* pc = &a;*pa = 10;*pc = 0;return 0;
}
//运行报错

这里我们可以看到, void* 类型的指针可以接收不同类型的地址,但是无法直接进行指针运算。

那么 void* 类型的指针到底有什么用呢?

一般 void* 类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以 实现泛型编程的效果。指针在竞赛中使用的不是很多,但是在工程中非常多,所以大家下来需要深入学习指针哦。

4.指针访问数组

有了前面的知识铺垫,我们就可以使用指针来访问内存了,这就会涉及到指针运算。 常见的指针运算就是:

练习:有一个整型数组,10个元素,默认初始化为0,现在要将数组的内容设置为1~10,然后打印数组的内容。 我们可以使用数组的形式,来完成任务,也可以使用指针的形式来完成。 我们知道数组在内存中是连续存放的,那是只要给定一个起始位置,顺藤摸瓜就能到后边的其他元素 了。下面我们来使用指针,给一个整型数组做一个初始化,再将数组的内容打印出来。


#include <iostream>using namespace std;int main()
{int arr[10] = {0};int *p = &arr[0];int i = 0;for(i = 0; i < 10; i++){*(p + i) = i + 1;}for(i = 0; i < 10; i++){cout << *p << " ";p++;}//1 2 3 4 5 6 7 8 9 10return 0;
}

上述代码中要注意几个点:

1. 这里操作的是整型数组,我们写代码时期望以一 个整型为单位的访问,解引用时访问一个整型, +1 跳过一个整型,所以我们使用了 int* 类型的指针,如果是其他类型的数据要选择最恰当的指针类型。

2. 代码中第一个 for 循环,我们选择使用个元素,第而个 p+i 的方式, p 不变, i 在不断地变化,找到数组的每 for 循环,我们选择 p++ 的效果,让 p 不断地往后寻找元素,要体会这两种的差异。

3. 我们在代码中使用 for 循环,通过元素个数控制循环的次数。其实指针就是地址,是一串编号, 这个编号是有大小的,那就可以比较大小,这就是指针的关系运算。使用指针关系运算,也能完成 上面代码。

请看下方代码:

#include <iostream>using namespace std;int main()
{int arr[10] = {0};int *p = &arr[0];int i = 0;for(i = 0; i < 10; i++){*(p + i) = i + 1;}while(p < &arr[10]){cout << *p << " ";p++; }//1 2 3 4 5 6 7 8 9 10return 0;
}

二. 动态内存管理

其实我们之前已经学习了变量、数组等知识,我们知道变量的创建会为变量申请一块内存空间,数组的创建其实也向内存申请一块连续的内存空间。

int n = 10; 
//向内存申请4个字节的空间
char arr1[5];
//向内存申请5个字节的空间
int arr2[5]; 
//向内存申请20个字节的空间

这两种方式,如果创建是全局的变量和数组,是在内存的静态区(数据段)申请的,如果是局部的变量和数组,是在内存的栈区申请的。不管是全局变量还是局部变量,申请和回收都是系统自动完成的,不需要程序员自己处理。

栈区:局部的变量,函数的形参

堆区:动态内存管理new和delete

静态区(数据段):全局变量,静态变量

其实C++还提供了另外一种方式,就是:动态内存管理,允许程序员在适当的时候,自己申请空间,自己释放空间,⾃主维护这块空间的生命周期。动态内存管理所开辟到的空间是在内存的堆区。

1.new和delete

C++中通过 new 和 delete 操作符进行动态内存管理。

new 负责申请内存, new 操作符返回的是申请到的内存空间的起始地址,需要指针存放。

new申请一个变量的空间, new[] 申请一个数组的空间

de lete 负责释放(回收)内存

de lete 负责释放⼀个变量的空间,delete[] 释放一个数组的空间

new 和 delete 配对, new[] 和 delete[] 释放一个数组的空间 delete[] 配对使用

 //动态申请一个int类型的空间 
int* ptr1 = new int; // 动态申请一个int类型的空间并初始化为10  
int* ptr2 = new int(10);// 动态申请10个int类型的空间 
int* ptr3 = new int[10];//释放内存空间
delete ptr1;
delete ptr2;
delete[] ptr3;

new 不是只能给内置类型开辟空间,也可以给自定义类型开辟空间。

举个例子~

#include <iostream>using namespace std;int main()
{int*p = new int;*p = 20;cout << *p << endl;delete p;int *ptr = new int[10];for(int i = 0; i < 10; i++){*(ptr + i) = i;}for(int i = 0; i < 10; i++){cout << *(ptr + i) << " ";}delete[]  ptr;return 0;
}

其实数组是连续的空间,new[] 申请到的空间也是连续的,那上述代码中ptr 指向的空间能不能使用数组的形式访问呢?答案是可以的,上面代码中第18行代码可以换成:

 cout << ptr[i] << " ";

三.使用指针实现链表

什么是数据结构

数据结构是计算机的内存中存储和组织数据的方式。

为什么需要数据结构?

通过数据结构,能够有效将数据组织和管理在一起。 按照我们的方式任意对数据进行增、删、改、查等操作。

常见的数据结构分类

数据结构一般根据组织形式,分为:

线性数据结构和非线性数据结构。

线性的数据结构有:数组(顺序表)、链表、栈和队列等。

非线性的数据结构有:树、散列表、堆、图等。

链表的概念

链表是一种线性数据结构,由一系列节点组成,每个节点包含数据和指向下一个节点的指针。 链表中的元素在内存中不必顺序排列,而是通过指针相互连接。

链表的结构跟火车车厢相似,淡季时车次的车厢会相应减少,旺季时车次的车厢会额外增加几节。只 需要将火车里的某节车厢 去掉 / 加上,不会影响其他车厢,每节车厢都是独立存在的。

车厢是独立存在的,且每节车厢都有车门,想象一下这样的场景,假设每节车厢的车门都是锁上的状态,需要不同的钥匙才能解锁,每次只能携带一把钥匙的情况下如何从车走到车尾? 最简单的做法:每节车厢里都放一把下一节车厢的钥匙。 火车的每节车厢就相当于链表的一个节点。 在链表里,每个节点(车厢)是什么样的呢?

链表的结构

链表的基本结构由节点组成,每个节点包含数据和指向下一个节点的指针。

 //链表节点类型的声明struct Node 
{int data;Node* next;
};

数据域:存放每个节点携带的数据

指针域:存放下一个节点的地址

链表的分类

链表可以分为单向链表、双向链表和循环链表

单向链表:每个节点只有一个指针指向下一个节点。

双向链表:每个节点有两个指针,分别指向前一个节点和后一个节点

循环链表:尾节点指向头节点。

链表的结构非常多样,以下情况组合起来就有8种(2x2x2)链表结构:

动态申请链表节点和链表构建

动态申请节点并初始化

Node* createNode(int data) 
{Node* newNode = new Node;newNode->data = data;newNode->next = nullptr;return newNode;}

单链表元素的打印

打印链表的所有节点数据。

void printList(Node* cur) 
{while (cur != nullptr) {cout << cur->data << "-->";cur = cur->next;}cout << "nullptr" << endl;cout << endl;}

单链表的尾部插入元素

在链表的尾部插入节点。

void PushBack(Node*& phead, int x)
{Node* newnode = createNode(x);// 1、空链表// 2、非空链表if (phead == nullptr){phead = newnode;}else{Node* tail = phead;while (tail->next != nullptr){tail = tail->next;}tail->next = newnode;}
}

单链表头部删除元素

void PopFront(Node*& phead)
{//空if (phead == nullptr){cout << "链表为空,没有元素可以删除了" << endl;return;}//非空Node* tmp = phead;phead = phead->next;delete tmp;tmp = nullptr;
}

单链表尾部删除元素

void PopBack(Node*& phead)
{//空链表if (phead == nullptr) {cout << "链表为空,没有元素可以删除了" << endl;return;}// 1、一个节点// 2、一个以上节点if (phead->next == nullptr){free(phead);phead = nullptr;}else{// 找尾Node* tail = phead;while (tail->next->next != nullptr){tail = tail->next;}free(tail->next);tail->next = nullptr;}
}

释放链表的所有节点

当链表不再需要的时候,申请的节点资源最好能释放掉,否则可能带来内存泄漏的风险。所有我们再提供一个函数,释放链表的所有节点。其实这个就很简单了,我们调用一个删除元素的函数,直到把链表删除为空链表就释放了所有的节点

void PopFront(Node* & ph)
{//  空if (ph == nullptr){cout << "链表为空,没有元素可以删除了" << endl;return;}//非空 Node* tmp = ph;ph = ph->next;delete tmp;tmp = nullptr;}void DestropList(Node* & ph)
{while(ph){PopFront(ph);}
}

面向对象的方式实现链表

#include <iostream>using namespace std;//节点类型声明
struct Node 
{int data;Node* next;}; struct List{//指向头节点的指针Node* phead;//成员函数//构造函数List(){cout << "构造函数调用" << endl;phead = nullptr;}//析构函数 - 在结束时释放所有剩余节点~List(){cout << "析构函数" << endl;while (phead){PopFront();}}//创建节点Node* createNode(int data) {Node* newNode = new Node;newNode->data = data;newNode->next = nullptr;return newNode;}//打印链表void printList() {Node* cur = phead;while (cur != nullptr) {cout << cur->data << "-->";cur = cur->next;}cout << "nullptr" << endl;cout << endl;}//头部添加元素void PushFront(int data){Node* newNode = createNode(data);newNode->next = phead;phead = newNode;}//头部删除元素void PopFront(){// 空if (phead == nullptr){cout << "链表为空,没有元素可以删除了" << endl;return;}//非空Node* tmp = phead;phead = phead->next;delete tmp;tmp = nullptr;}//尾部添加元素void PushBack(int x){Node* newnode = createNode(x);// 1、空链表// 2、非空链表if (phead == nullptr){phead = newnode;}else{Node* tail = phead;while (tail->next != nullptr){tail = tail->next;}tail->next = newnode;}}//尾部删除元素void PopBack(){//空链表if (phead == nullptr) {cout << "链表为空,没有元素可以删除了" << endl;return;}// 1、一个节点// 2、一个以上节点if ((phead)->next == nullptr){delete phead;phead = nullptr;}else{// 找尾Node* tail = phead;while (tail->next->next != nullptr){tail = tail->next;}delete tail->next;tail->next = nullptr;}}void DestropList(){while(phead){PopFront();}}};int main()
{List list;list.PushBack(1);list.PushBack(2);list.PushBack(3);list.PushBack(4);list.printList();//头部删除并打印list.PopBack();list.printList();list.PopBack();list.printList();list.PopBack();list.printList();list.PopBack();list.printList();list.PopBack();list.printList();//头部添加元素list.PushFront(1);list.PushFront(2);list.PushFront(3);list.PushFront(4);list.printList();//头部删除list.PopFront();list.printList();list.PopFront();list.printList();return 0;
}

运行结果

OK~uu们,到此C++结束咯,本系列只适用于竞赛,想熟练使用只了解这些是不够的,还需系统学习,我以后也会出深入系统篇哒,再见哦

http://www.dtcms.com/a/512766.html

相关文章:

  • 平安大模型面试题:Self-Attention 原理与多头注意力设计
  • 手机网站是怎么做的邢台最近发生的新闻
  • 三周期共振强度系数的量化模型
  • swarm集群部署
  • 建设网站的工作wordpress更改导航栏样式
  • 企业网站建设时间表廊坊哪里有做阿里巴巴网站的
  • 使用 mina-sshd 库通过 SCP 上传文件并解决无法上传大文件的问题
  • apifox mock 假数据
  • 脑机接口:LFP相关知识
  • 外网访问象过河软件
  • dedecms小说网站模板下载wordpress收录提交插件
  • HarmonyOS 5 鸿蒙多设备适配与分布式开发指南
  • 使用C#代码删除 Excel 中的公式但保留数值
  • 科技设计网站广告设计制作发布
  • 网站域名管理怎么登陆网站响应式首页模板
  • 企业公司做网站企业网站访问对象有哪些
  • Eureka 多层缓存机制详解
  • HarmonyOS 5 鸿蒙Context上下文机制与资源管理详解
  • wordpress播放器插件杭州百度seo
  • 网站维护费用2021国内军事新闻大事件
  • vue 中 directive 作用,使用场景和使用示例
  • Orleans 与 Kubernetes 完整集成指南
  • 珠海网站建设网有心学做网站
  • 网站建设 教学大纲wordpress 文章查询
  • 推广方案设计台州seo优化公司
  • 新浪微博 搭建网站建立网站的方案
  • 用易语言做抢购网站软件下载云搜索神器
  • C#上位机软件:2.5 体验CLR实现多语言混合编程
  • 网页站点江苏网站集约化建设
  • 怎么把做的网站传怎样设置自己的网站