指针的内容比较难理解,也比较多 ; 这里是为了方便后续更新的系列的文章更容易理解一些 ;
在竞赛里用的比较少!!!!!!!。如果想详细学习指针 ,可以去找找相关的学习资料;这里为了方便观察 , 我们使用VS2022
一、指针
1.1 内存和地址
讲内存和地址之前 , 我们联想一下生活案例:
假设有⼀栋宿舍楼,你就住在其中一间,楼上有100个房间, 但是房间没有编号 ,你的一个朋友来找你玩; 如果想找到你,就得挨个房子去找,这样效率很低,但是我们如果根据楼层和楼层的房间的情况,给每个房间编上号,如:
有了房间号 , 就能快速找到目的地;
把这个例子对标到 计算机中 :我们知道计算机上 CPU (中央处理器)在处理数据的时候 , 需要的数据是在内存中读取的 , 处理后的数据也会放回在内存中;电脑内存有 8GB/16GB/32GB等等,那这些内存空间是如何高效管理的?
其实也是把 内存划分为一个个的内存单元,每个内存单元的大小为 1个字节。
这里补充一个小知识点:计算机的存储单位
1.2 指针变量
1.2.1 取地址操作符(&)
C++中创建变量的本质是 向内存申请空间 。
例如:
#define _CRT_SECURE_NO_WARNINGS 1
#include <cstdio>
#include <iostream>
using namespace std;int main()
{int a;return 0;
}
上述的代码就是创建了整型变量a , 在内存中申请 4 个字节 , 用于存放整数 10 , 申请到的每一个空间都有地址 。如果想知道变量的存储地址 , 我们就可以使用 & 操作符 , & 操作符取出来的是占4个字节中地址较小的字节的地址 。
---> 虽然整型变量占用 4 个字节 , 但是只要我们知道了第一个字节的地址 , 顺藤摸瓜就找到后面 3个字节的地址了。
1.2.2 指针变量
指针变量就是 存储地址的变量 。我们通过 & 操作符拿到了一个数值的地址 , 例如:0x0117FAD8 , 这个数值有时候也需要存储起来 , 方便后续继续使用 ,那么这样的地址值应该存放在哪里呢 ? ----> 指针变量中!!!
#include <cstdio>
#include <iostream>
using namespace std;int main()
{int a = 10;int* pa = &a;//取出a的地址并存放到指针变量pa中cout << a << endl;cout << pa << endl;return 0;
}
指针变量也是一种变量 , 这种变量就是用来存放地址的 。存放在指针变量中的值都会理解为地址 。指针变量中存放了谁的地址 , 我们就说这个指针变量指向了谁 ; 上面的代码中 , pa 就是存放a 的地址 , 我们就可以理解成 pa 指向了 a 变量 ;
但是有时候 一个指针变量创建的时候 , 还不知道应该存储谁的地址 , 那怎么办?
---> 在C++中这时候 , 我们会给指针变量赋值为NULL , NULL的值其实是 0 , 表示空指针 , 意思是没有指向任何有效的变量 。
当然 0 也是作为地址编号的 , 这个地址是无法使用的 , 读写该地址会报错 。
1.2.3 如何拆解指针类型
pa 的 类型是 int* ,如何理解指针的类型呢?
1.2.4 解引用操作符
在现实生活中,我们使用 地址 要 找到一个房间 ,在房间里可以拿去或者存放物品。C++语言中其实也是一样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象。那么,这里需要学习⼀个操作符叫解引用操作符( * )。
#include <cstdio>
#include <iostream>
using namespace std;int main()
{int a = 10;cout << a << endl;int* pa = &a;*pa = 100;//*解引用操作符,通过地址找到acout << a << endl;return 0;
}
*pa 的意思就是通过pa 中存放的地址 , 找到指向的空间(*pa 其实就是 a 变量了) 。
---> 为啥不能直接把 a 改为 100 , 而是要通过地址找到 a , 然后再修改 (这不就是脱裤子放屁,多此一举吗?....) 。这里其实把a 的修改交给了 pa 来操作 , 这样对 a 的修改 , 就多了一种途径 , 编写代码会更加灵活 ,后期会慢慢理解了 。
注意:如果一个指针变量是NULL时候 , 表示 这个指针变量没有指向有效的空间 , 所以一个指针变量是NULL的时候 , 是不能解引用操作的 。
所以我们在编写代码的时候 , 需要避免对空指针的访问:
#include <cstdio>
#include <iostream>
using namespace std;int main()
{int* pa = NULL;if(pa != NULL)*pa = 100;//*解引用操作符,通过地址找到areturn 0;
}
1.3 指针类型的意义
指针类型的意义体现在两个方面:
1)指针的解引用
2)指针 +-整数
1.3.1 指针的解引用
对比一下下面的代码 , 观察在调试时 ---> 内存的变化
//代码一
#include <iostream>
using namespace std;int main()
{int n = 0x11223344;int* pn = &n;*pn = 0;return 0;
}
//代码二:
#include <iostream>
using namespace std;int main()
{int n = 0x11223344;char* pn =(char*) & n;*pn = 0;return 0;
}
调试中 , 我们可以看到 , 代码1 会将 n 的 4 个字节全部改为 0 ; 但是代码2 只是将第1个字节改为 0 。
结论 : 指针 的 类型决定了 , 对指针解引用的时候 有多大权限 (一次能操作几个字节)。
比如: char* 的指针解引用就只能访问 一个字节 , 而 int* 的指针的解引用就能访问四个字节。
1.3.2 指针 +- 整数
观察一下代码的执行结果 :
#include <cstdio>
#include <iostream>
using namespace std;int main()
{int n = 10;char* pc = (char*)&n;int* pi = (int*)&n;printf("&n = %p\n", &n);printf("pc = %p\n", pc);printf("pc+1 = %p\n", pc+1);printf("pi = %p\n", pi);printf("pi+1 = %p\n", pi + 1);return 0;
}
注意:这里我们用 printf 来打印地址 , 因为cout 中 , 编译器会识别为字符串来打印
从控制台打印结果看 , char* 类型的指针变量 +1 跳过一个字节 , int* 类型的指针变量+1 跳过了4个字节 。这就是指针变量的类型差异带来的变化 。 指针+1 , 其实跳过一个指针指向的元素 。指针可以 +1 , 那也可以 -1。
结论:指针 的 类型决定了指针向前 或者 向后走一步有多大(距离)
1.3.3 void* 指针
在指针类型中 , 有一种特殊的类型是 void* 类型 , 可以理解为无具体类型的指针(或者叫叫泛型指针),这种类型的指针可以用来接受任意类型地址 。 但是也有局限性 , void * 类型的指针不能直接进行指针的 +- 整数和解引用的计算。
举例:
#include <iostream>
using namespace std;int main()
{int a = 10;int* pa = &a;char* pc = &a;return 0;
}
编译器报错 , 为啥?
因为将一个 int 类型的变量的地址赋值给一个 char*类型的指针变量 , 类型不兼容 ~ 。而使用void* 类型就不会有这样的问题 。
使用 void* 类型的指针接收地址:
#include <iostream>
using namespace std;int main()
{int a = 10;void * pa = &a;void* pc = &a;return 0;
}
但是void* 类型的指针并不能解引用 和 +- 整数的计算
#include <iostream>
using namespace std;int main()
{int a = 10;void * pa = &a;void* pc = &a;*pa = 100;*pc = 10;return 0;
}
这里我们可以看到 , void* 类型可以接收不同类型的地址 , 但是无法直接进行指针计算。
那么,void* 类型的指针到底有什么用?
---> 一般void* 类型的指针是使用在函数参数的部分 , 用来接收不同类型数据的地址 , 这样的设计可以实现泛型编程的效果。指针在竞赛中用的不是很多,但是在工程中用得非常多 。
#include <iostream>
using namespace std;void test(void* p)
{//
}
int main()
{int a = 0;test(&a);double b = 3.14;test(&b);return 0;
}
1.4 指针访问数组
可以使用指针来访问内存 , 这就会涉及到指针运算 。
常见的指针运算:
1)指针 +- 整数
2)指针关系运算
---> 指针之所以能 用来访问数组 :
1) 数组在内存中是连续存放的
2)随着数组下标的增长 , 地址是由高到低变化的
3)指针能够进行 +-整数运算 和 解引用计算
1)数组的形式:
#include <iostream>
using namespace std;int main()
{int arr[10] = { 0 };for (int i = 0; i < 10; i++){arr[i] = i + 1;}for (int i = 0; i < 10; i++){cout << arr[i] << " ";}cout << endl;return 0;
}
2)指针的形式:
优化:
#include <iostream>
using namespace std;int main()
{int arr[10] = { 0 };int* p = arr;for (int i = 0; i < 10; i++){*(p + i) = i + 1;}for (int i = 1; i <= 10; i++){cout << *p << " ";p++;}cout << endl;return 0;
}
我们在代码中使用 for 循环 , 通过元素个数控制循环的次数 。 其实指针就是地址 , 是一串编号 , 这个编号是由大小的 , 那么我们就可以进行大小比较 , 这就是指针的关系运算:
二、动态内存管理
之前学习了变量、数组等知识,我们知道 变量的创建会为变量申请⼀块内存空间 , 数组的创建其实也向内存申请一块连续的内存空间。
C++提供了另外⼀种方式 ---> 动态内存管理。允许程序员在适当的时候,自己申请空间,自己释放空间,自主维护这块空间的生命周期。动态内存管理所开辟到的空间是在内存的 堆区 。
2.1 new/delete
C++中通过 new 和 delete 操作符进行动态内存管理。
- new 负责申请内存 , new 操作符 返回的是申请到的内存空间的起始地址 , 需要指针存放 。
- new 申请一个变量的空间 , new[ ] 申请一个数组的空间
- delete 负责释放( 回收 ) 内存
- delete 负责释放一个变量的空间 , delete[ ] 释放一个数组的空间
- new 和 delete 配对 , new[ ] 和 delete[ ] 配对使用
2.2 代码举例
#include <iostream>
using namespace std;int main()
{int* ptr1 = new int;*ptr1 = 10;cout << *ptr1 << endl;delete ptr1;int* ptr2 = new int[10];for (int i = 0; i < 10; i++){*(ptr2 + i) = i + 1;}for (int i = 0; i < 10; i++){cout << *(ptr2 + i) << " ";}cout << endl;
//释放数组delete[] ptr2;ptr2 = NULL;return 0;
}
其实数组是连续的空间 , new[ ] 申请到的空间也是连续的 , 那上述代码中 ptr2 指向的空间也可以使用数组的形式进行访问 --->
这个章节就了解一下就好了 , 竞赛不太会出指针~