指针
目录
- 内存变量
- 指针变量
- 对指针进行赋值
- 指针占用的内存
- 使用指针
- 指针用于函数参数
- 用const修饰指针
- void关键字
- C++内存模型
- 动态分配内存new 和 delete
- 二级指针
- 空指针
- 野指针
- 一位数组和指针
- 数组的地址
- 数组的本质
- 数组名不一定会被解释为地址
- 一维数组作为函数参数
- 用new动态创建一维数组
- 二维数组用于函数参数
- 二维数组名是行地址
- 把二维数组作为参数
- 使用new动态创建二维数组
- 多维数组
- 函数指针和回调函数
- 回调函数
- 指针函数
内存变量
变量的全称是内存变量
以上例子中,cout
会将地址转换成字符串进行输出,就有可能出现乱码的的情况
void*
类型返回的是16进制的地址,longlong
类型返回的是10进制的地址
指针变量
指针变量简称指针,是一种特殊的变量,专门用来存放变量在内存中的起始地址
对指针进行赋值
地址都是16进制的数,但是指针是有类型的标识符,指针的名字也是标识符,不能与其他变量重名
int *pa = &a;
对指针赋值的操作可以叫做指针指向某个变量,被指向的类型叫做基类型
指针占用的内存
在64位操作系统中,都是8字节
可以把int*
当成一种数据类型
使用指针
声明指针变量后,不初始化的话,其中是乱七八糟的值,这时候不能使用指针
*运算符是解引用运算符,用于指针,可以得到该地址的内存中存储的值。
变量和指向变量的指针就像同一枚硬币的两面
每次运行程序的时候,变量的地址都是随机分配的,不一样
指针只是记录了变量在内存中的起始地址,类型决定了数据占用内存的大小以及如何操作这个数据
指针用于函数参数
指针作为参数的时候,在函数中通过解引用的方法可以修改实参的值
地址传递
- 可以在函数中修改实参的值,作为传出参数
- 减少内存拷贝,提升性能,每个指针都是8字节,但是要拷贝的内容可能很大,像字符串,数组什么的
用const
修饰指针
常量指针
const 数据类型* 变量名
不能通过解引用的方法修改内存地址中的值,用原始的变量名是可以修改的
- 可以修改指针的指向,但是没有什么意义
- 一般用于函数形参,表示在函数中不能用解引用的方式修改常量的值
指针常量
数据类型* const 变量名
指向的对象不能改变,所以在声明的时候必须初始化
- 可以通过解引用的方式修改内存地址中的值
- C++编译把指针常量做了一些特别的处理,改头换面之后,新名字叫做引用
常指针常量
指向的对象不能改变,不能通过解引用的方法修改内存地址中的值
常引用
void关键字
- 表示无类型,用于函数返回值
- 用于函数形参,表示无参数
- 函数的形参使用void *,表示接受任意数据类型的指针
注意
- 不能用void 来声明变量,不能表示一个真实的变量
- 不能对void*指针直接解引用(需要转换成其他类型的指针)
- 把其他类型的指针赋值给void*指针不需要转换
- 把void*指针赋值给其他类型的指针需要转换
在c++中返回字符类型的时候会有乱码,可以先转成void*类型再输出
C++内存模型
程序运行后,代码段中的内容是不会改变的
- 栈:存储局部变量、函数参数和返回值
- 堆:存储动态开辟内存的变量
- 数据段:存储全局变量和静态变量
- 代码段:存储可执行程序的代码和常量
堆和栈的主要区别
动态分配内存new 和 delete
使用堆区内存有四个步骤
- 声明一个指针
- 使用new运算符向系统中申请一块内存,让指针指向这块内存
- 通过对指针解引用的方法,像使用变量一样使用这块内存
- 如果这块内存不存在了,使用delete运算符来释放
内存只分配不释放,后果是非常严重的,可能会用尽系统的内存
动态分配出来的内存没有变量名,只能通过指向它的指针来操作内存中的数据
动态分配的内存生命周期跟程序相同,程序退出时,如果没有释放,系统将自动回收
就算指针的作用域已经失效,所指向的内存也不会释放
用指针跟踪已分配的内存时,不能跟丢
二级指针
二级指针的变量中存放的是指针的地址
使用指针的目的
- 传递地址
- 存放动态分配的内存的地址
在函数中,如果传递普通变量的地址,形参使用指针,传递指针变量的地址,形参使用二级指针→为了修改指针的地址
多级指针除了套娃没有任何意义
空指针
- 如果对空指针解引用,程序会崩溃。
- 如果对空指针使用delete运算符,系统将忽略该操作,不会出现异常。所以,内存被释放后,也应该把指针指向空。
- 在函数中,应该有判断形参是否为空指针的代码,目的是保证程序的健壮性。
为什么空指针访问会出现异常
NULL指针分配的分区:其范围是从 0x00000000
到0x0000FFFF
。这段空间是空闲的,对于空闲的空间而言,没有相应的物理存储器与之相对应,所以对这段空间来说,任何读写操作都是会引起异常的。空指针是程序无论在何时都没有物理存储器与之对应的地址。为了保障“无论何时”这个条件,需要人为划分一个空指针的区域,固有上面NULL指针分区。
简而言之,系统中有专门的空指针分区,这段空间是空闲的,没有相应的物理存储对应,所以读写操作都会出错。说白了就是NULL指针没有真实的内存数据对应
nullptr
等价于(void *)0
野指针
指针指向的不是一个有效或者说合法的地址
在程序中,如果访问野指针,可能会造成程序的崩溃
为什么是可能?
打人犯法吗,犯法;一定会近派出所吗,不一定;如果野指针指向的内存是内核或者其他非空闲的内存,就会导致程序的崩溃,如果是空闲的内存,就不会崩溃,但是没有人能保证那块内存是不是空闲的
出现的情况
- 指针在定义的时候,如果没有进行初始化,它的值是不确定的
- 如果用指针指向了动态分配的内存,内存被释放后,指针不会置空,但是指向的地址已经失效
- 指针指向的变量已经超越变量的作用域,让指针指向了函数的局部变量,或者把函数的局部变量的地址作为返回值赋值给了指针
规避方法
针对上面三种情况
- 初始化,没有地方指的话就初始化为
nullptr
- 动态分配的内存被释放后,指针指向
nullptr
- 函数不要返回局部变量的地址
一位数组和指针
对地址加1操作往后移动的长度跟数据类型有关,地址往后移动的长度是 1 * sizeof(数据类型)
数组的地址
double a[5];
cout << a << &a << &a[0] << endl;
以上三个值是相同的,都是数组a的地址
C++编译器把数组名[下标]
解释为*(数组首地址 + 下标)
C++编译器把**地址[下标]
解释为*(地址 + 下标)
**
数组的本质
数组是占用连续空间的一块内存,数据名被解释成数组第0个元素的地址,C++操作这块内存有两种方法:数组解释法和指针表示法,它们是等价的。
数组名不一定会被解释为地址
在**sizeof
**运算符运用于数组名的时候,返回的是整个数组占用内存空间的字节数
可以修改指针的值,数组名是常量,不可以修改
可以p++,但是不能a++
一维数组作为函数参数
只能传数组的地址,并且把数组长度也传递进去
书写方法有以下两种
void func(int* arr, int len);
void func(int arr[], int len);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wd3yuzEn-1683617233351)(image/image_R_EISMeJ1S.png)]
指针作为参数传入函数的时候,进行**sizeof
**运算,返回就永远是8了,因为被当作是指针,而不再是数组了
用new动态创建一维数组
创建语法:数据类型* 指针 = new 数据类型[数组长度]
;
释放语法:delete[] 指针
注意
- 动态创建的数组没有数组名,不能使用sizeof运算符,得到的永远是8
// 如果内存不够,程序不会崩溃,而是会返回空地址
int *a = new (std::nothrow) int[10000001];
二维数组用于函数参数
行指针(数组指针)
对一维数组取地址,得到的是行地址
这里有一个比较有意思的现象,a和&a的值是相同的,但是a+1与&a+1的值是不同的,原因在于&a是一个行地址。在方阵中,一个人的下一个位置是她旁边的那个,下一行的位置是下一行中正后面的那个人。
二维数组名是行地址
int bh[2][3] = {{11, 12, 13}, {21, 22, 23}};
其中,bh
是二维数组名,该数组有两个元素,每个元素本身又是一个数组长度为3的整型数组。
bh
被解释为数组长度为3的整形数组的行指针。
如果存放bh
的值,要用数组长度为3的整型数组类型的行指针
int (*p)[3] = bh;
把二维数组作为参数
声明如下
void func(int (*p)[3], int len);
void func(int p[][3], int len);
使用new动态创建二维数组
int **mat = new int*[n];
for (int i = 0; i < n; i ++)
mat[i] = new int[n];
多维数组
在实际开发中应用场景很少
**memset
**函数
memset(数组名, 0, sizeof(数组名));
函数指针和回调函数
函数指针的主要用途是函数的回调
函数的二进制代码存放在内存四区中的代码段,函数的地址是它在内存中的起始地址,如果把函数的地址作为参数传递给函数,就可以在函数中灵活的调用其他函数。
声明函数指针的时候也必须提供函数的类型,函数的类型指的是返回值和参数列表,函数名和形参名不是
// 原函数
int func1(int bh, string str);
bool func2(int id, string info);
// 函数指针
int (*pf1)(int, string);
bool (*pf2)(int, string);
// 对函数指针赋值
pf1 = func1;
// 使用函数指针调用函数
int no = 1;
string message = "测试";
pf1(no, message);
应用场景,回调函数
回调函数
需要在封装好的函数中调用一个自定义的个性化函数
如视频中的表白问题
表白公司可以提供一个封装好的表白流程,处理准备工作和收尾工作,但是中间你自己要操作的部分是你自己需要定义好的,所以表白流程的函数用一个函数指针作为参数,在其函数体内部调用自定义的表白函数。
这种情况下只能使用函数指针,没有别的方法
回调函数在多线程和网络通讯中很常用
指针函数
返回值是地址的函数是指针函数,感觉这个概念是一个伪命题。