目录
一、指针的定义
0x00 何为指针
❓ 我们先来看看定义:
指针是编程语言中的一个对象,利用地址,他的值直接指向存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。
📚 简单地说:指针就是地址,地址就是指针;
📌 注意事项:
① 指针就是变量,用来存放地址的变量(存放在之阵中的值都被当成地址处理);
② 一个小的内存单元大小为 1 个字节;
③ 指针是用来存放地址的,地址是唯一标识一块内存空间的;
④ 指针的大小在 32 位平台上是 4 个字节,在 64 位平台上是 8 个字节;
💬 指针的创建:
int main() { int a = 10; // 在内存中开辟一块空间 int* pa = &a; // 使用解引用操作符&,取出变量a的地址 // 👆 将a的地址存放在pa变量中,此时pa就是一个指针变量 return 0; }
❓ 什么是指针变量
💡 指针变量就是存放指针的变量,这里的 int* pa 就是一个整型指针变量,里面存放了 a 的地址;
0x02 指针的大小
💻 32位平台:4 bit , 64位平台:8 bit ;
💬 验证当前系统的指针大小:
int main() { printf("%d\n", sizeof(char*)); printf("%d\n", sizeof(short*)); printf("%d\n", sizeof(int*)); printf("%d\n", sizeof(double*)); return 0; }
二、指针的指针类型
0x00 指针类型
📚 int 型指针和 char 型指针都可以存储 a ;
int main() { int a = 0x11223344; int* pa = &a; char* pc = &a; printf("%p\n", pa); printf("%p\n", pc); return 0; }
🚩 运行结果:他们的运行结果是一样的
0x01 指针类型的意义
📚 指针类型决定了指针进行解引用时,能够访问的内存大小是多少;
💬 不同的指针类型,访问的大小不同:
int main() { int a = 0x11223344; int* pa = &a; // 44 33 22 11 (至于为什么是倒着的,后面会讲。) *pa = 0;// 00 00 00 00 char* pc = &a; // 44 33 22 11 *pc = 0; // 00 33 22 11 // 在内存中仅仅改变了一个字节 // 解引用操作时就不一样了 // 整型指针操作了4个字节,让四个字节变为0 // 字符指针能把地址交到内存中, // 但是解引用操作时,只敢动1个字节 return 0; }
0x02 指针加减整数
📚 定理:指针类型决定指针步长(指针走一步走多远);
💬 代码验证:指针类型决定指针步长;
int main() { int a = 0x11223344; int* pa = &a; char* pc = &a; printf("%p\n", pa); // 0095FB58 printf("%p\n", pa+1); // 0095FB5C +4 printf("%p\n", pc); // 0095FB58 printf("%p\n", pc+1); // 0095FB59 +1 return 0; }
0x03 指针修改数组元素
💬 把数组里的元素都改成1
1. 使用整型指针:
int main() { int arr[10] = {0}; int* p = arr; //数组名 - 首元素地址 /* 修改 */ int i = 0; for(i=0; i<10; i++) { *(p+i) = 1; //成功,arr里的元素都变为了1 } /* 打印 */ for(i=0; i<10; i++) { printf("%d ", arr[i]); } return 0; }
🚩 1 1 1 1 1 1 1 1 1 1
2. 使用字符指针:
int main() { int arr[10] = {0}; char* p = arr; //数组名 - 首元素地址 /* 修改 */ int i = 0; for(i=0; i<10; i++) { *(p+i) = 1; // 一个一个字节改,只改了十个字节 } return 0; }
💡 解析:
🔺 总结:
① 指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节);
② 譬如,char* 的指针解引用只能访问1个字节,而 int* 的指针解引用就能够访问4个字节
三、野指针(Wild pointer)
0x00 野指针的概念
📚 概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的);
野指针指向了一块随机的内存空间,不受程序控制;
0x01 野指针的成因
📚 原因:
① 指针未初始化;
② 指针越界访问;
③ 指针指向的空间已释放;
💬 指针未初始化
∵ 局部变量不初始化默认为随机值:
int main() { int a;//局部变量不初始化默认为随机值 printf("%d", a); return 0; }
∴ 同理,局部的指针变量,如果不初始化,默认为随机值:
int main() { int *p; //局部的指针变量,就被初始化随机值 *p = 20; //内存中随便找个地址存进去 return 0; }
💬 指针越界访问
指针越界,越出arr管理范围时会产生野指针:
int main() { int arr[10] = {0}; int *p = arr; int i = 0; for(i=0; i<12; i++) { //当指针越出arr管理的范围时,p就称为野指针 p++; } return 0; }
💬 指针指向的空间已释放
int* test() { int a = 10; return &a; } int main() { int *pa = test(); *pa = 20; return 0; }
🔑 解析:
① 一进入test 函数内时就创建一个临时变量 a(10 - 0x0012ff44),这个a是局部变量,进入范围时创建,一旦出去就销毁,销毁就意味着这个内存空间还给了操作系统,这块空间(0x0012ff44)就不再是 a 的了;
② 进入这个函数时创建了 a,有了地址,ruturn &a 把地址返回去了,但是这个函数一结束,这块空间就不属于自己了,当你使用时,这块空间已经释放了,指针指向的空间被指放了,这种情况就会导致野指针的问题;
③ 只要是返回临时变量的地址,都会存在问题(除非这个变量出了这个范围不销毁);
0x02 如何规避野指针
💬 指针初始化
int main() { int a = 10; int* pa = &a; // 初始化 int* p = NULL; // 当你不知道给什么值的时候用NULL return 0; }
💬 指针指向空间释放及时置 NULL
int main() { int a = 10; int *pa = &a; *pa = 20; //假设已经把a操作好了,pa指针已经不打算用它了 pa = NULL; //置成空指针 return 0; }
💬 指针使用之前检查有效性
int main() { int a = 10; int *pa = &a; *pa = 20; pa = NULL; //*pa = 10; 崩溃,访问发生错误,指针为空时不能访问 if(pa != NULL) { // 检查 如果指针不是空指针 *pa = 10; // 检查通过才执行 } return 0; }
四、指针运算
0x00 指针加整数
💬 指针加整数:打印 1 2 3 4 5 6 7 8 9 10
int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int i = 0; int sz = sizeof(arr) / sizeof(arr[0]); int* p = arr; // 指向数组的首元素 - 1 for(i=0; i<sz; i++) { printf("%d ", *p); p = p + 1; //p++ 第一次循环+1之后指向2 } return 0; }
🚩 1 2 3 4 5 6 7 8 9 10
0x01 指针减整数
💬 指针减整数:打印 10 8 6 4 2
int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int i = 0; int sz = sizeof(arr) / sizeof(arr[0]); int* p = &arr[9]; // 取出数组最后一个元素的地址 for(i=0; i<sz/2; i++) { printf("%d ", *p); p = p - 2; } return 0; }
0x02 指针后置++
#include <stdio.h> #define N_VALUES 5 int main() { float value[N_VALUES]; float* vp; for (vp = &value[0]; vp < &value[N_VALUES];) { *vp++ = 0; printf("%d ", *vp); } return 0; }
0x03 指针减指针
📚 说明:指针减指针得到的是元素之间元素的个数;
📌 注意事项:当指针减指针时,他们必须指向同一空间(比如同一个数组的空间);
💬 指针减指针:
int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; printf("%d\n", &arr[9] - &arr[0]); // 得到指针和指针之间元素的个数 return 0; }
🚩 9
❌ 错误演示:不在同一内存空间
int ch[5] = {0}; int arr[10] = {1,2,3,4,5,6,7,8,9,10}; printf("%d\n", &arr[9] - &ch[0]); // 没有意义,结果是不可预知的
💬 手写 strlen 函数(用指针方法实现):
int my_strlen(char* str) { char* start = str; char* end = str; while(*end != '\0') { end++; } return end - start; //return } int main() { //strlen - 求字符串长度 //递归 - 模拟实现了strlen - 计数器方式1, 递归的方式2 char arr[] = "abcdef"; int len = my_strlen(arr); //arr是首元素的地址 printf("%d\n", len); return 0; }
⚡ 简化(库函数的写法):
int my_strlen(const char* str) { const char* end = str; while(*end++); return (end - str - 1); } int main() { char arr[] = "abcdef"; int len = my_strlen(arr); printf("%d\n", len); return 0; }
0x04 指针的关系运算(比较大小)
💬 指针减减指针:
#define N_VALUES 5 int main() { float values[N_VALUES]; float *vp; for(vp=&values[N_VALUES]; vp> &values[0]; ) { *--vp = 0; //前置-- } return 0; }
⚡ 简化(这么写更容易理解,上面代码 *--vp在最大索引后的位置开始访问的):
int main() { float values[5]; float *vp; for(vp=&values[N_VALUES]; vp> &values[0]; vp--) { *vp = 0; } return 0; }
❗ 实际在绝大部分编译器上是可行的,但是我们应该避免这么写,因为标准并不保证它可执行;、
🔑 解析:标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的拿个内存位置的指针比较,但是不允许与指向第一个元素之前的拿个内存位置的指针进行比较;
五、指针和数组
0x00 数组名
📚 数组名在 绝大部分情况下都是首元素地址;
💬 大多数情况下数组名是首元素地址:arr 等同于 &arr[0]
int main() { int arr[10] = {0}; printf("%p\n", arr); // 数组名是地址,首元素地址 printf("%p\n", &arr[0]); // 结果同上 printf("%p\n", &arr); // 看下面的 “例外” return 0; }
🚩 00EFF8E0 00EFF8E0 00EFF8E0(这是整个元素的地址)
🌂 例外:
1. &数组名( &arr ):数组名不是首元素地址,而是表示整个数组:
💬 &数组名 - 数组名表示的是整个数组:
int main() { int arr[10] = {0}; printf("%p\n", arr); printf("%p\n", arr+1); printf("%p\n", &arr[0]); printf("%p\n", &arr[0]+1); printf("%p\n", &arr); printf("%p\n", &arr + 1); // +1,以整个元素为单位 return 0; }
🚩 运行结果如下:
2. sizeof(数组名):计算的是整个数组的大小,单位是字节
💬 sizeof(数组名):数组名表示的是整个数组:
int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int sz = sizeof(arr) / sizeof(arr[0]); printf("%d\n", sz); return 0; }
🚩 10
🔺 总结:数组名是首元素地址( 除&数组名 和 sizeof 数组名 外 );
0x01 使用指针访问数组
💬 p+i 计算的是数组 arr 下标为 i 的地址:
int main() { int arr[10] = {0}; int* p = arr; //这时arr数组就可以通过指针进行访问了 int i = 0; for(i=0; i<10; i++) { printf("%p == %p\n", p+i, &arr[i]); } return 0; }
🚩 运行结果如下:
💬 生成 0 1 2 3 4 5 6 7 8 9
int main() { int arr[10] = {0}; int* p = arr; // 这时arr数组就可以通过指针进行访问了 int i = 0; printf("生成前:\n"); for(i=0; i<10; i++) { printf("%d ", arr[i]); } for(i=0; i<10; i++) { *(p+i) = i; // p+1=1, p+2=2, p+3=3... // 👆 arr[i] = i; 等价于 } printf("\n生成后:\n"); for(i=0; i<10; i++) { printf("%d ", arr[i]); // 👆 printf("%d ", *(p + i)); 等价于 } return 0; }
🚩 运行结果如下:
💬 生成 6 6 6 6 6 6 6 6 6 6
int main() { int arr[10] = {0}; int* p = arr; // 这时arr数组就可以通过指针进行访问了 int i = 0; for(i=0; i<10; i++) { *(p+i) = 8; // p+1=1, p+2=2, p+3=3... } for(i=0; i<10; i++) { printf("%d ", *(p + i)); } return 0; }
🚩 6 6 6 6 6 6 6 6 6 6
六、二级指针(Second Rank Pointer)
0x00 二级指针的概念
📚 概念:指针变量也是变量,是变量就有地址,指针的地址存放在二级指针;
💬 二级指针:
int main() { int a = 10; int* pa = &a; int** ppa = &pa; // ppa就是二级指针 int*** pppa = &ppa; // pppa就是三级指针 ... **ppa = 20; printf("%d\n", *ppa); // 20 printf("%d\n", a); // 20 return 0; }
🔑 对于二级指针的运算:
① *ppa 通过对 ppa 中的地址进行解引用,找到了的是 pa,*ppa 其实访问的就是pa;
② **ppa 先通过 *ppa 找到 pa,然后对 pa 进行解引用操作,*pa 找到的就是 a;
七、指针数组(Pointer to Array)
0x00 指针数组的概念
📚 概念:指针数组本质上是数组,存放指针的数组;
📌 注意:不要和数组指针混淆,数组指针本质上是指针;
❓ 分析下面的数组:
int arr1[5]; char arr2[6]; int* arr3[5];
🔑 解析:
① arr1 是一个整型数组,有 5 个元素,每个元素都是一个 整型;
② arr2 是一个字符数组,有 6 个元素,每个元素都是一个 char 型;
③ arr3 是一个整型指针数组,有 5 个元素,每个元素是一个 整型指针;