1. 指针

指针是一种C++数据类型,用来描述内存地址。

什么是内存地址?

内存中的每一个存储单元格都有自己的地址,地址是使用二进制进行编码。地址从形态上看是一个整型数据类型。但是,它的数据含义并不表示数字,而是一个位置标志,类似于门牌号。

指针类型数据的算术运算:

  • 在地址上加上减去一个正整数,表示向前或向后移动地址。移动地址的意义:可实现从一个存储位置到达另一个存储位置。
  • 地址与地址之间也可以相减,表示两个地址之间的差距。
  • 地址与地址之间不可以相加、相乘、相除运算。对地址进行相加、相乘、相除类似门牌号门牌号之间相加、相乘、相除,没有任何意义可言。

2. 指针变量

变量是一个存储块,为了能访问到变量中的数据,开发者需要为变量指定一个名字,即变量名。编译器会在分配变量后,把变量变量名进行关联。

变量名和变量地址有什么关系?

变量名是变量的逻辑地址,由开发者提供。而变量地址是变量的物理地址,指变量在内存中的具体位置。如下声明语句,在编译时,编译器会做一些细碎的底层工作。

int num=20;
  • 根据数据类型的约定,在内存中找到一个可用的内存块。int一般大小为 4B
  • 获取到内存块的物理地址,并把物理地址和开发者提供的变量名(逻辑名)进行关联,并保存在映射表中。
  • 把数字 20保存在num变量中。

在使用 num访问变量时,需要借助映射表,找到变量名对应的内存地址,方能访问变量中的数据。变量名是变量地址的逻辑名。

std::cout<<num;
//输出结果:20

能不能获取到变量在内存的地址,通过地址访问变量?

当然可以,前提是需要声明一个指针变量,保存变量的物理地址。

用来保存地址(指针)类型数据的变量称为指针变量

指针变量也是内存中的一个存储块,只是变量中存储的是另一个变量在内存中的地址。如下代码,保存 num变量在内存的地址。

//整型类型变量
int num=20;
//指针类型的变量
int* num_p=&num;

代码说明:

  • int *表示指针类型。声明指针变量时,需要指定变量是用来保存指针类型数据。

int *表示指针变量是用来保存一个 int类型变量的地址,并不是指变量用来保存一个整型数据。

  • &运算符,取地址运算符。&num表示获取num变量的内存(物理)地址。

既然是变量,指针变量在内存也有属于自己的存储位置。如下图所示,只是指针变量中保存的是地址信息。

指针变量实际占用内存大小是多少,由底层编译器决定。

如何通过指针变量中的地址访问 num 变量?

如下代码,先试着直接输出指针变量 num_p 中的数据。

std::cout<<num_p;

输出结果:0x70fe14。很明显这是内存地址的 16进制格式,也证实指针变量中存储的是地址。

千万别问我为什么输出的不是1000。图片只是一个演示。

有了这个地址后,可以通过这个地址访问num变量中的数据。

std::cout<<*num_p;
//输出:20

需要注意:在声明通过地址访问数据时,都要使用 *符号:

  • 声明时*表示指针类型。int* num_p;
  • 使用指针变量时,表示通过地址找到变量中的数据。

*num_pnum是访问同一个变量的两种方案。前者是使用物理名(内存地址)访问变量的语法,后者是使用逻辑名(变量名)访问变量。

同样的也能够使用指针变量对其引用的变量进行赋值。

int num=20;
int* num_p=&num;
//通过指针变量赋值,和 num=30 等同
*num_p=30;
std::cout<<*num_p<<std::endl;
std::cout<<num<<std::endl;
//输出结果:
30
30

3. 几个问题

3.1 为什么要使用指针变量

在使用指针变量时,总会有一个疑问,既然能够使用变量名访问变量,为什么还要搞一个指针变量。指针变量不仅要占用内存空间,且语法繁琐,是不是有点啰嗦了。

其实,指针变量C系列语言的特色,是演化过程中保留下来的原始特性:

  • 访问速度。

指针访问是直接硬件访问,速度较快。

遍历数组时,通过指针的加法、减法运算法则,可以向前或向后快速移动指针。

int nums[4]={1,2,3,4};
int* nums_p=nums;
for(int i=0;i<4;i++){
std::cout<<*(nums_p+i)<<std::endl;
}
//输出
1
2
3
4

数组变量本质是指针变量,保存着数组在内存中的首地址。所以在把数组的地址赋值另一个指针变量时,int* nums_p=nums;是不需要使用&符号的。

上述代码nums_p+i让指针变量能加上一个正整数,实现指针的移动,这里要注意,加上 1 不是表示只移动一个存储单元格,而是移动int大小。

如果知道数据在数组中的位置,可以直接在首指针基础上加上一个移动单位,便能快速访问数组中的数据。

  • 访问 new 创建的内存块。

如下语句:

int *num01=new int;

new运算符会在堆中开辟一个用来保存int类型数据的存储块,返回存储块的内存地址(指针类型数据) ,这时只能使用指针变量保存,并且通过指针变量使用这个存储块 。

指针变量的存在为使用堆提供了必要条件,C++称堆为动态内存区域,开发者可随时根据自己的需求在程序运行时申请、使用。

理论上讲,编译器也可以让开发者提供变量名,然后把变量名和new返回的地址进行映射。

显然,省略映射环节,直接指针访问,即减轻了编译器的负担,又提升了访问速度。

int *num01=new int;
*num01=40;
std::cout<<*num01<<std::endl;
//输出:40
  • 使用指针变量作为函数的参数,用来影响函数调用处变量中的值。

如果现在有一个需求,使用一个函数交换 2 个变量中的数据。先看一下下面的代码是否能实现这个效果。

#include <iostream>
//交换函数
void swap(int num1,int num2){
int tmp=num1;
num1=num2;
num2=tmp;
} int main(int argc, char** argv) {
int num1=20;
int num2=30;
std::cout<<"交换前:"<<num1<<":"<<num2<<std::endl;
swap(num1,num2);
std::cout<<"交换后:"<<num1<<":"<<num2<<std::endl;
return 0;
}

输出结果:

交换前:20:30
交换后:20:30

主函数中的 num1num2变量中的数据根本没有交换。

原因在于调用函数swap时,参数是值传递。所谓值传递,指把主函数中num1num2变量的值传递给swap函数中的 num1num2变量。swap的交换逻辑仅修改了自身 2 个变量中的值。

如下图所示,主函数变量中的数据没有改变。

如果希望通过调用swap后直接修改主函数中num1num2中的值,可以使用指针变量作参数。

#include <iostream>
//形参为指针类型
void swap(int* num1,int* num2){
//*num1 通过地址访问主函数中的 num1 变量
int tmp=*num1;
//交换的是主函数中变量中的值
*num1=*num2;
*num2=tmp;
}
int main(int argc, char** argv) {
int num1=20;
int num2=30;
std::cout<<"交换前:"<<num1<<":"<<num2<<std::endl;
//主函数把变量的地址传递给 swap 函数
swap(&num1,&num2);
std::cout<<"交换后:"<<num1<<":"<<num2<<std::endl;
return 0;
}

输出结果:

交换前:20:30
交换后:30:20

指针作为参数,传递的是变量地址,意味着,swap函数中两个变量引用了主函数中两个变量的物理地址。可以实现修改主函数中变量值的目的。

相当于主函数把变量房间的钥匙传递给 swap 函数,swap 再使用钥匙进入主函数中的变量,进行数据维护。

3.2 指针潜在的风险

3.2.1 初始化风险

必须初始化: 如下代码,编译器不会报任何错误,但实际上是没有任何意义的代码。

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

输出结果:

0x40ebd9
264275272

当声明指针变量 p时,如果没有指定初始值,编译器会随意指定一个值。指望把这个值当成一个有效地址,是没有意义的。如果把这指针变量用于代码逻辑,会产生无中生有的数据,显然是违背数据的准确性和可靠性。

所以,在声明指针变量后,一定要对其进行初始化。

不能使用整型常量初始化: 使用整型数字常量初始化指针变量,编译层面是通不过的。

//语法错误
int* p=0x40aed9;

0x40aed9即使是一个有效的内存地址数据,因为类型不同,也不能把整型数据赋值给一个指针类型变量。

但是,可以强制类型转换后再赋值。

地址形态上是数字,也仅是形态上是,本质上不是数字类型,不具有数字语义,也不具有数字运算操作能力,不能把地址类型与数字类型混淆。

//正确
int* p=(int*)0x44eb99;

虽然,通过强制转换可以成功初始化指针变量,但是存在潜在风险:

  • 0x44eb99地址不一定是一个有效的地址。
  • 0x44eb99即使是一个有效地址,有可能此地址正被其它变量使用。如此,你便修改了其它变量的值。误打误撞,跑到了别人家里。

如下代码,本意并不是想让p保存score变量的内存地址。如果恰好0x70fddc就是score的内存地址。则通过*p对变量的修改最终会导致score变量中的数据被修改。

int score=89;
//本意是想使用一个空闲的空间,误打误撞引用了 score 的地址
int* p=(int*)0x70fddc;
//会修改 score 中的值
*p=56;
std::cout<<score<<std::endl;
std::cout<<*p<<std::endl;
//输出
56
56

可以认为指针访问变量名访问的另一种形式,所以在初始化指针变量时, 需要使用 &new 运算符合理计算出来的地址。指针变量必须是一个已经存在的、合法变量的内存地址。

类型一致初始化: 如下代码是错误的,千万不要认为会发生自动类型转换。num_p只能引用double类型变量的地址,这是语法层面约定。

int num=34;
//语法错误,声明指针时的数据类型,严格规定了指针变量能引用的变量类型
double* num_p=&num;

3.2.2 越界风险

指针越界: 指指针移动到了非法区域,如下代码:

int num=34;
int* num_p=&num;
std::cout<<"正常输出:"<<*num_p<<std::endl;
//指针移到了一个没有声明的区域
std::cout<<"移动指针输出:"<<*(num_p+1)<<std::endl;

输出结果:

正常输出:34
移动指针输出:7405060

虽然指针变量可以通过加上一个整型数字进行移动。但是一定要控制合法范围,否则会发生如上的非法访问,非法访问到的数据一旦用于数据逻辑,会存在很大的风险。

3.3 多级指针

指针变量本身也是一个存储块,它所在内存地址是否还可以保存在另一个指针变量中?

显然,这是可以的,如下代码:

//声明常规变量
int num=20;
//一级指针变量:用来保存 num 变量的地址
int* num_p=&num;
//二给指针变量,用来保存 num_p 变量的地址
int** num_p_p=&num_p;

int**表示二级指针类型,本质还是内存地址,是另一个指针变量的内存地址。



使用二级指针访问 num变量中的数据。

//……
*(*num_p_p)=30;
std::cout<<"输出:"<<num<<std::endl;

代码解释:

  • *num_p_p获取到 num_p变量中的内存地址值 1000
  • *(*num_p_p)利用上面返回的 1000 地址,找到变量 num位置,并返回变量 num的值。

同理可以使用多维指针,如下是三维指针。

int num=20;
int* num_p=&num;
int** num_p_p=&num_p;
int*** num_p_p_p=&num_p_p;
*(*(*num_p_p_p))=30;
std::cout<<"输出:"<<num<<std::endl;
//输出:30

4. 总结

虽然可以通过使用指针提升内存的访问性能,但也因存在指针的自由性,易出现潜在风险。如JAVA在语法层面对指针使用做了限制,权衡利弊,虽然消弱了指针的自由性,同时也降低了代码的潜在风险。

C++ 练气期之指针所指何处的更多相关文章

  1. C++ 练气期之一文看懂字符串

    C++ 练气期之细聊字符串 1. 概念 程序不仅仅用于数字计算,现代企业级项目中更多流转着充满了烟火气的人间话语.这些话语,在计算机语言称为字符串. 从字面上理解字符串,类似于用一根竹签串起了很多字符 ...

  2. C++ 练气期之二维数组与矩阵运算

    1. 前言 C++中的一维数组可以存储线性结构的数据,二维数组可以存储平面结构的数据.如班上所有学生的各科目成绩就有二个维度,学生姓名维度和科目成绩维度. 这样的表格数据可以使用二维数组进行存储. 当 ...

  3. C++ 练气期之函数探幽

    1. 函数基础 一个C++程序中,往往需要包含若干个函数,可以说函数是C++程序的基础组成元件,是程序中的头等公民. 如果要理解程序中使用函数的具体意义,则需要了解语言发展过程中致力要解决的 2 问题 ...

  4. 实现一个Memcpy函数:将源指针所指的区域从起始地址开始的n个字节复制到目的指针所指区域

    首先肯定要先看看这两部分是不是有内存重叠?为什么? 1.因为如果有内存重叠(目的地址起始位置处于源指针所指区域之中),你再从起始位置复制的话,这样目的地址改变的时候将源地址内存里面存的东西给改变了,所 ...

  5. [Windows] [VS] [C] [取得指针所指内存的二进制形式字符]

    // 取得指针所指内存的十六进制形式字符串,size指定字节长度#define Mem_toString(address, size) _Mem_toString((PBYTE)address, si ...

  6. [Windows] [VS] [C] [取得指针所指内存的十六进制形式字符串]

    接口定义如下: #include <Windows.h> // 取得指针所指内存的十六进制形式字符串,size指定字节长度 #define Mem_toString(address, si ...

  7. Spring Security使用数据库数据完成认证--练气后期2

    写在前面 没错,这篇文章还是练气后期!但作者我相信筑基指日可待! 在前一篇文章当中,我们简单地分析了一下Spring Security的认证流程,知道了如果想要实现对自己用户数据(账户.角色.权限)的 ...

  8. Shiro入门学习---使用自定义Realm完成认证|练气中期

    写在前面 在上一篇文章<shiro认证流程源码分析--练气初期>当中,我们简单分析了一下shiro的认证流程.不难发现,如果我们需要使用其他数据源的信息完成认证操作,我们需要自定义Real ...

  9. shiro入门学习--使用MD5和salt进行加密|练气后期

    写在前面 在上一篇文章<Shiro入门学习---使用自定义Realm完成认证|练气中期>当中,我们学会了使用自定义Realm实现shiro数据源的切换,我们可以切换成从关系数据库如MySQ ...

随机推荐

  1. Django显示本地图片,注意事项

    1.在url.py文件中的配置 导入相关的库,在Python2.0后,要用re_path from django.urls import path,re_path from django.views. ...

  2. JavaWeb和WebGIS学习笔记(七)——MapGuide Open Source安装、配置以及MapGuide Maestro发布地图——超详细!目前最保姆级的MapGuide上手指南!

    JavaWeb和WebGIS学习笔记(七)--MapGuide Open Source安装.配置以及MapGuide Maestro发布地图 超详细!目前最保姆级的MapGuide上手指南! 系列链接 ...

  3. ThinkPhP $map用法

    ThinkPHP内置了非常灵活的查询方法,可以快速的进行数据查询操作,查询条件可以用于CURD等任何操作,作为where方法的参数传入即可,下面来一一讲解查询语言的内涵.查询方式ThinkPHP可以支 ...

  4. 漏洞复现:MS10-046漏洞

    漏洞复现:MS10-046漏洞 实验工具1.VMware虚拟机2.Windows7系统虚拟机3.Kali 2021 系统虚拟机 1.在VMware中打开Windows7虚拟机和Kali 2021虚拟机 ...

  5. XCTF练习题---MISC---easycap

    XCTF练习题---MISC---easycap flag:FLAG:385b87afc8671dee07550290d16a8071 解题步骤: 1.观察题目,下载附件 2.拿到手以后发现是一个流量 ...

  6. XSS攻击&CSRF攻击 ----Django解决方案

    XSS攻击: XSS又叫CSS (Cross Site Script) ,跨站脚本攻击.它指的是恶意攻击者往Web页面里插入恶意html代码,当用户浏览该页之时,嵌入其中Web里面的html代码会被执 ...

  7. VSCODE调试时在cygwin.S中发生段错误

    起因: C++实现矩阵类和向量类 当看了我实现的矩阵类后,一个同学问我: 然后我就试了试1000维,结果运行时在cygwin.S里引发了奇奇怪怪的Segmentation fault,而且这个文件还是 ...

  8. 团队Arpha6

    队名:观光队 链接 组长博客 作业博客 组员实践情况 黄恒杰 - **过去两天完成了哪些任务 ** - 文字/口头描述 地图功能增加.博客 - 展示GitHub当日代码/文档签入记录 - 接下来的计划 ...

  9. iptables系列教程(一)| iptables入门篇

    一个执着于技术的公众号 前言 在早期的 Linux 系统中,默认使用的是 iptables 配置防火墙.尽管新型 的 firewalld 防火墙已经被投入使用多年,但是大量的企业在生产环境中依然出于各 ...

  10. Nacos源码系列—关于服务注册的那些事

    点赞再看,养成习惯,微信搜索[牧小农]关注我获取更多资讯,风里雨里,小农等你,很高兴能够成为你的朋友. 项目源码地址:公众号回复 nacos,即可免费获取源码 简介 首先我们在看Nacos源码之前,要 ...