【C++指针】搭建起程序与内存深度交互的桥梁(下)

 

 

    🔥🔥 个人主页 点击🔥🔥


每文一诗  💪🏼

        往者不可谏,来者犹可追——《论语·微子篇》

        译文:过去的事情已经无法挽回,未来的岁月还可以迎头赶上。   


目录

C++内存模型

new与delete动态分配内存

动态分配单个变量(例如; int* ptr = new int(10))

动态分配数组(例如 int* arr = new int[5] )

分配内存失败的情况

一维数组与指针

使用数组名/和数组名加下标访问数组中元素及其地址

使用指针访问数组中元素及其地址

二维数组与指针

行指针

函数指针

指针用作函数参数

用函数指针来传递函数


 

C++内存模型

 

        栈区:由编译器自动管理,用于存储局部变量、函数参数和返回地址。每当调用一个函数时,会在栈上分配一块新的栈帧,函数执行完毕后,栈帧自动释放。

        堆区:也被叫做自由存储区,由程序员手动管理。通过new操作符在堆上分配内存,使用delete操作符释放内存。

        数据存储区:用于存放全局变量和静态变量。在程序启动时分配内存,程序结束时释放内存。

        代码区:用于存放程序的可执行代码。代码区是只读的,防止程序在运行过程中被意外修改。

栈区和堆区区别

  1. 管理方式不同:栈区是系统自动管理的,在离开作用域时,会自动释放
  2. 空间大小不同:栈区大小操作系统预先设定,一般只有8M。如果在栈上分配的内存超过了栈的最大容量,会导致栈溢出(Stack Overflow)错误;堆区空间仅受限与物理内存空间,所以相对较大。
  3. 效率不同:栈区内存的分配和释放速度非常快,因为它只需要移动栈指针;而堆区内存分配和释放需要操作系统复杂的操作。

new与delete动态分配内存

new:动态分配内存

delete:  释放分配的内存

在堆区动态分配内存的步骤:

  1. 声明一个指针
  2. 用new运算符向系统的堆区申请一块内存,并用指针指向这块内存
  3. 通过解引用的方式,来取出这块内存中的值
  4. 当这块内存不用时,需用delete来释放它

动态分配内存的两类:

动态分配单个变量(例如; int* ptr = new int(10))

    int* p = new int(2);std::cout<<"*p的值:"<<*p<<std::endl;//打印内存中的值delete p;

解析:

  • int* p = new int(2);

        首先在堆区new申请了一块内存,这块内存是int型,接着初始化该内存的值为2,最后返回新分配内存的地址,用一个int类型的指针来指向他。

  • delete p;

        释放这块内存

动态分配数组(例如 int* arr = new int[5] )

    int* arry = new int[8];for(int i=0;i<8;i++){*(arry+i) = i;std::cout<<"第"<<i<<"个值"<<*(arry+i)<<std::endl;}delete[] arry;

解析:

  • int* arry = new int[8];

        首先new申请了一块内存,这块内存存储的是一个含有8位int型数据的数组,最后返回新分配内存的地址,用一个int类型的指针来指向他。这里的arry代表数组的首地址。

  •  *(arry+i) = i;

        通过循环和解引用的方式为数组中每个数赋值。

  • delete[] arry;

        释放这块内存

分配内存失败的情况

        如果需要分配含有大量的数据的数组,那么栈空间上分配是远远不够的,需要在堆区分配。但是如果内存分配失败,则会导致程序崩溃,但是我们不希望这样,我们可以在内存分配失败的时候捕捉到这个错误。

使用std::nothrow

int main(int argc, char const *argv[])
{int* arry = new(std::nothrow)int[100000];if(arry == nullptr)std::cout<<"分配内存失败"<<std::endl;else{std::cout<<"分配内存成功"<<std::endl;arry[99999] = 0;delete[] arry;}return 0;
}

 

一维数组与指针

使用数组名/和数组名加下标访问数组中元素及其地址

int arry[3] = {2,4,6};
std::cout<<"数组"<<std::endl;
std::cout<<arry<<std::endl;
std::cout<<&arry[0]<<std::endl;
std::cout<<arry+1<<std::endl;
std::cout<<arry+2<<std::endl;
std::cout<<arry[0]<<std::endl;
std::cout<<arry[1]<<std::endl;
std::cout<<arry[2]<<std::endl;

解析:

  • 数组的名称/数组第一个元素的地址 是同一个地址
  • 数组名+n:数组第n个元素的地址
  • 数组名[n]:数组第n个元素的内容

使用指针访问数组中元素及其地址

int* p = arry;
std::cout<<"指针"<<std::endl;
std::cout<<p<<std::endl;
std::cout<<p+1<<std::endl;
std::cout<<p+2<<std::endl;
std::cout<<*(p)<<std::endl;
std::cout<<*(p+1)<<std::endl;
std::cout<<*(p+2)<<std::endl;

解析:

        如果将数组名称赋给一个指针变量,实际上是将数组的首地址赋给了指针。

  • 指针+n:数组第n个元素的地址
  • *(指针+n):数组第n个元素的内容

对于C++而言

数组名[下标] 解释为 *(数组名首地址+下标)

地址[下标] 解释为 *(地址+下标)

输出

两者是一样的

二维数组与指针

在讲二维数组之前,有必要去介绍对一个一维数组名取地址

void func2()
{int a[3] = {6,7,8};std::cout<<"数组第一个元素的地址:"<<a<<std::endl;std::cout<<"数组第一个元素的地址+1:"<<a+1<<std::endl;std::cout<<"数组的地址:"<<&a<<std::endl;//即为地址的地址,是一个行指针std::cout<<"数组的地址+1:"<<&a+1<<std::endl;int (*p)[3] = &a;//正确// int *p2 = &a;//错误S
}

解析:

       我们都知道数组名a是代表数组第一个元素的地址,但是&a是数组的地址,虽然a和&a的地址是相同的,但是二者有着不同的类型。

        a的类型是 int*

        &a的类型是 int(*p)[],即行指针。

为了证明a和&a有着不同的含义,我们同时对两个地址加1测试

输出

  •  可见数组第一个元素的地址+1后,实际上是加了4,对于16进制,c后是d,e,f,0然后进位a变为b,所以是ac变b0
  • 而数组的地址+1后,发现并没有+4,而是+12,12是3*4得来的,因为数组有3个int型数据,每个数据占4个字节。
  • 这也是行指针的作用,行指针+1后,实际上加上的是这一行数组组成的数组的总长度。

行指针

对于二维数组而言。

行指针格式: 数据类型 (*p)[列大小]

例如

int arry[2][3] = {{1,2,3},{4,5,6}};

int (*p)[3] = arry;

#include<iostream>int main(int argc, char const *argv[])
{int arry[2][3] = {{1,2,3},{4,5,6}};int (*p)[3] = arry;// 这种方式是一个行指针,也就是说该指针p指向的是二维数组中第一个包含三个int型数据的数组的地址// 对该地址进行解引用,就会得到该数组的首地址,再次解引用就会得到数组中具体的值。std::cout<<**p<<std::endl;//p为二维数组中每一行数组的地址,*p得到数组第一个元素的地址,**p得到数组元素的值std::cout<<*(*(p+1))<<std::endl;//p为二维数组中第0行数组的地址,再加1得到第1行数组的地址,解引用为第一行数组的第一个元素的地址,再解引用位第一个元素的值。std::cout<<*(*(p+1)+1)<<std::endl;//*(p+1)为第一行数组的第一个元素的地址,*(p+1)+1:再加1为第一行数组的第二个元素的地址,再解引用为第二个元素的值。std::cout<<*(p[1]+1)<<std::endl;//p[1]在c++中被解释为*(p+1)// 个人理解:// 地址 + n:// 应看这个地址的类型,即这个指针的类型,如果这个指针是行指针,那么加1就是加上这一行数组的总共的字节数//例如p+1,p是行指针,存储的是每一行数组的地址,加1其实是加上了4*3=12个字节//如果这个指针是普通指针,那么加1就是加上这个数组的单个元素的字节数//例如*(p+1)+1,*(p+1)是普通指针,存储的是数组第一个元素的地址,加1其实是加上了4个字节return 0;
}

输出

函数详解:

  • int (*p)[3] = arry;

        这种方式是一个行指针,也就是说该指针p指向的是二维数组中第一个包含三个int型数据的数组的地址

  • std::cout<<**p<<std::endl;

        p为二维数组中每一行数组的地址,*p得到数组第一个元素的地址,**p得到数组元素的值

  • std::cout<<*(*(p+1))<<std::endl;

        p为二维数组中第0行数组的地址,再加1得到第1行数组的地址,解引用为第一行数组的第一个元素的地址,再解引用位第一个元素的值。

  • std::cout<<*(*(p+1)+1)<<std::endl;

        *(p+1)为第一行数组的第一个元素的地址,*(p+1)+1:再加1为第一行数组的第二个元素的地址,再解引用为第二个元素的值。

  • std::cout<<*(p[1]+1)<<std::endl;

        p[1]在c++中被解释为*(p+1)

个人理解:

对于 地址 + n:

  •  应看这个地址的类型,即这个指针的类型,如果这个指针是行指针,那么加1就是加上这一行数组的总共的字节数。例如p+1,p是行指针,存储的是每一行数组的地址,加1其实是加上了4*3=12个字节
  • 如果这个指针是普通指针,那么加1就是加上这个数组的单个元素的字节数。例如*(p+1)+1,*(p+1)是普通指针,存储的是数组第一个元素的地址,加1其实是加上了4个字节

函数指针

指针用作函数参数

如果参数是一个 数组的话,必须传递数组的长度

下面用代码解释原因

#include<iostream>
void func(int* arr)
{std::cout<<"数组长度2="<<sizeof(arr)<<std::endl;for(int i =0;i<sizeof(arr)/sizeof(int);i++){std::cout<<*(arr+i)<<std::endl;}}
int main(int argc, char const *argv[])
{int arry[3] = {2,4,6};func(arry);std::cout<<"数组长度1="<<sizeof(arry)<<std::endl;  return 0;
}

输出

        在函数func中,参数是一个指针变量,使用sizeof运算符的时候,会返回这个指针的大小,而指针的大小是一个常数8(在64位操作系统);而在main函数中,arry是一个数组名,在使用sizeof运算符的时候,会返回这个数组的大小。

        所以在func函数中,sizeof(arr)/sizeof(int)的值是8/4等于2,所以数组只打印了索引为0和1的值。

正确的做法是参数中加上数组长度

#include<iostream>
void func(int* arr,int len)
{std::cout<<"数组长度2="<<sizeof(arr)<<std::endl;for(int i =0;i<len;i++){std::cout<<*(arr+i)<<std::endl;}}
int main(int argc, char const *argv[])
{int arry[3] = {2,4,6};func(arry,sizeof(arry)/sizeof(int));std::cout<<"数组长度1="<<sizeof(arry)<<std::endl;  return 0;
}

输出

用函数指针来传递函数

用途:可以用一个函数来调用别的函数.

做法:将该函数的参数设置为要调用函数的指针

声明一个函数指针:

格式:返回值类型 (*函数指针名)(参数1,参数2)

通过函数指针调用函数

函数指针名(参数1,2)

在C++中,函数的名称就是函数的地址

#include<iostream>int func2(int m)
{std::cout<<"函数2"<<std::endl;return m+1;
}
int func3(int m)
{std::cout<<"函数3"<<std::endl;return m-1;
}
void func(int(*pf)(int))
{std::cout<<"准备工作"<<std::endl;int n = pf(3);std::cout<<"返回值"<<n<<std::endl;std::cout<<"收尾工作"<<std::endl;
}
int main(int argc, char const *argv[])
{func(func2);func(func3);return 0;
}

函数解析:

这段代码实现了函数传递函数,通过修改参数可以让不同的函数被执行。  

     主要看的是void func(int(*pf)(int))

这个函数func的参数是一个函数指针

  • 名称:pf
  • 返回值:int
  • 参数:int 可不加变量名

    func(func2);func(func3);

这个是将需要传递的函数的名称传递过去,函数的名称就是函数的地址

 输出

若本文对你有帮助,你的支持是我创作莫大的动力!

    🔥🔥 个人主页 点击🔥🔥

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/74593.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JavaScript创建对象的多种方式

在JavaScript中&#xff0c;创建对象有多种方式&#xff0c;每种方式都有其优缺点。本文将介绍四种常见的对象创建模式&#xff1a;工厂模式、构造函数模式、原型模式和组合模式&#xff0c;并分析它们的特点以及如何优化。 1. 工厂模式 工厂模式是一种简单的对象创建方式&am…

muduo库的思路梳理

前言 对于muduo库源码的剖析我发现还是有些混乱的&#xff0c;所以这里再次梳理一下muduo网络库争取可以简单明了 首先对于muduo库来说&#xff0c;不能想的得太过于复杂&#xff0c;它无非就是一个线程池加上epoll组成的网络库 这里我们从用的角度出发理解muoduo网络库 #inc…

Keil5 安装全攻略

Keil5 安装全攻略 Keil5 是一款广泛用于嵌入式开发的 IDE&#xff0c;支持多种微控制器架构&#xff08;如 ARM、C51&#xff09;。本文将详细介绍 Keil5 的安装步骤、常见问题及解决方法&#xff0c;帮助您快速上手。 1. 安装前的准备工作 (1) 系统要求 操作系统&#xff1…

C语言do...while语句将数字反转后输出

一、题目引入 输入一个数字,将各位数字反转后输出? 参考代码: 二、分析代码 接着图片中的分析 第一 ->a 的值变为12 第二 ->进入while循环条件,a为12不等于0循环才停止(a的值为12,显然不等于0) 所以继续进行循环 第三 ->此时b的值为12取各位上的数字(即2) 打印…

优选算法系列(前缀和 _下) k

目录 五&#xff1a;和为 k 的子数组&#xff08;medium&#xff09; 题目链接&#xff1a;560. 和为 K 的子数组 - 力扣&#xff08;LeetCode&#xff09; 解法&#xff1a; 代码&#xff1a; 六&#xff1a;和可被 K 整除的子数组&#xff08;medium&#xff09; 题目链…

mac m3 pro 部署 stable diffusion webui

什么是Stable Diffusion WebUI &#xff1f; Stable Diffusion WebUI 是一个基于Stable Diffusion模型开发的图形用户界面&#xff08;GUI&#xff09;工具。通过这个工具&#xff0c;我们可以很方便的基于提示词&#xff0c;描述一段文本来指导模型生成相应的图像。相比较通过…

OpenCV图像拼接(6)根据权重图对源图像进行归一化处理函数normalizeUsingWeightMap()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 cv::detail::normalizeUsingWeightMap 是 OpenCV 中用于图像拼接细节处理的一个函数。它根据权重图对源图像进行归一化处理&#xff0c;通常用于…

23种设计模式-外观(Facade)设计模式

外观设计模式 &#x1f6a9;什么是外观设计模式&#xff1f;&#x1f6a9;外观设计模式的特点&#x1f6a9;外观设计模式的结构&#x1f6a9;外观设计模式的优缺点&#x1f6a9;外观设计模式的Java实现&#x1f6a9;代码总结&#x1f6a9;总结 &#x1f6a9;什么是外观设计模式…

capl语言基础语法(二)

1.strncpy&#xff1a;将字符串复制到另一个字符串中。 输入&#xff1a; dest 是目标字符串。 src 是源字符串。 n 是要复制的最大字符数。 语法&#xff1a; char *strncpy(char *dest, const char *src, size_t n); 例子&#xff1a; strncpy(gStringRep,"",…

QLoRA和LoRA 微调

QLoRA 其实是一种结合了量化和 LoRA 微调技术的统一方法&#xff0c;而不是同时使用两种不同的微调方式。换句话说&#xff0c;QLoRA 的意思就是&#xff1a;先把大模型的主权重用低精度&#xff08;例如 4-bit&#xff09;量化&#xff0c;从而大幅减少存储需求&#xff1b;然…

Qt Concurrent 并发 Map 和 Map-Reduce

并发 Map 和 Map-Reduce QtConcurrent::map()会对容器中的每个项目应用一个函数&#xff0c;对项目进行就地修改。QtConcurrent::mapped() 类似于 map()&#xff0c;但它返回的是一个包含修改内容的新容器。QtConcurrent::mappedReduced() 类似于 mapped()&#xff0c;只不过修…

RT-Thread-线程管理

一、线程管理 RT_Thread线程管理主要是实现线程管理和调度&#xff0c;线程分为用户线程和系统线程。RT_Thread的线程调度器是抢占式的&#xff0c;寻找就绪状态最高优先级线程。 线程管理的API函数 创建线程函数 rt_thread_t rt_thread_create( const char *name, //线程名称 …

【CC2530 教程 十二】CC2530 Z-Stack 硬件抽象层

目录 一、硬件抽象层简介&#xff1a; &#xff08;1&#xff09;HAL 硬件抽象层是什么&#xff1f; &#xff08;2&#xff09;通俗易懂的解释&#xff1a; &#xff08;3&#xff09;具体例子&#xff1a; 二、硬件抽象层HAL&#xff1a; &#xff08;1&#xff09;HAL…

Linux如何判断磁盘是否已分区?

在 Linux 系统中&#xff0c;判断磁盘是否已分区可通过以下方法实现&#xff1a; 方法 1&#xff1a;使用 fdisk -l 命令 此命令会列出所有磁盘及其分区的详细信息&#xff1a; sudo fdisk -l输出解读&#xff1a; 若磁盘&#xff08;如 /dev/sdb&#xff09;下有类似 /dev/…

《熔化焊接与热切割作业》考试注意事项

考试前的准备 携带必要的证件和材料&#xff1a;考生需携带身份证、准考证等有效证件&#xff0c;以及考试所需的焊接工具、材料等。确保证件齐全&#xff0c;避免因证件问题影响考试。 提前检查焊接设备和工具&#xff1a;在考试前&#xff0c;考生应仔细检查焊接设备和工具是…

Matlab Hessian矩阵计算(LoG算子)

文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 图像的Hessian矩阵用于描述图像灰度值的二阶导数,可以用来分析图像的局部曲率和变化。例如,在图像边缘检测、特征点检测等任务中,Hessian矩阵能帮助我们识别图像的结构。 Hessian矩阵定义 对于二维图像,Hessian…

selenium之处理弹框(alert、confirm、prompt)

弹框 WebDriver提供了一个API, 用于处理JavaScript提供的三种类型的原生弹窗消息. 这些弹窗由浏览器提供限定的样式.&#xff1b;分别为以下三种 alerts警告框confirm确认框prompt提示框 话不多说&#xff0c;开始实践下就知道怎么一回事了 alerts 警告框&#xff0c;显示…

Visual Studio 2019 Qt QML 项目环境搭建常见问题处理方法

在 Visual Studio 2019 运行 Qt/QML 项目比直接使用QtCreator环境麻烦&#xff0c;主要是有qmake 的一些配置项不能在 Visual Studio中设置。下面整理一些常见问题的处理方法&#xff0c;供参考&#xff1a; 搭建VS Qt 环境&#xff0c;在Visual Studios 2019下面安装 Qt Vis…

【Linux】POSIX信号量与基于环形队列的生产消费者模型

目录 一、POSIX信号量&#xff1a; 接口&#xff1a; 二、基于环形队列的生产消费者模型 环形队列&#xff1a; 单生产单消费实现代码&#xff1a; RingQueue.hpp&#xff1a; main.cc&#xff1a; 多生产多消费实现代码&#xff1a; RingQueue.hpp&#xff1a; main.…

RAG优化:python从零实现GraphRag 一场文档与知识的“恋爱”之旅

嘿,亲爱的算法工程师们,准备好迎接一场文档与知识的“恋爱”之旅了吗?今天我们要介绍的 Graph RAG,就像是一位“红娘”,帮助文档和知识在图的世界里找到彼此,擦出智慧的火花! 文章目录 为什么需要 Graph RAG?Graph RAG 的“恋爱秘籍”准备好了吗?让我们开始吧!环境设…