前言
今天有收获,别期待明天,活着本来就挺累挺难的,别想太远了,专注现在吧,谁也不能预料接下来会发生什么!难得来世界走一遭,那就朝着自己觉得有趣的方向前进吧,感谢自己的陪伴了我这一生
一、指针是什么
指针是什么?
指针理解的2个要点:
(1)指针是内存中一个最小单元的编号,也就是地址。
(2)平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量。
总结:指针就是地址,口语中说的指针通常指的是指针变量。
内存:
- 首先在16位操作系统上,指针的大小占2个字节;在32位操作系统上,指针的大小占4个字节;在64位操作系统上,指针的大小占8个字节。
- 我们知道一个字节是有8个比特位,4个比特位可以表示一个十六进制的一个位置上的数,例如二进制数1111,它对应的十进制数就是1*2^3+1*2^2+1*2^1+1*2^0 == 15。15也是对应了十六进制一个位上的最大表示数了(不管是十六进制数,还是八进制数和二进制数都是通过表示数*对应的权重得到的值——该值也是对应了十进制数的值)
- 所以二进制的4个位也就是4个比特位就可以表示一位十六进制数。例如在32位操作系统上的指针大小是4个字节(32位),那么也就是可以表示8个十六进制的表示数(32/4 == 8)。如下图:在32位操作系统上,一个字节地址就可以存放到一个以十六进制表示的指针中。(编号->地址->指针)。一个字节的内存地址由一个8位的十六进制(32位的二进制)指针变量中。
指针变量
我们可以通过&(取地址操作符)取出变量的内存的地址,把该地址存放到一个变量中,这个变量就是指针变量。
#include <stdio.h> int main() { int a = 10;//在内存中开辟一块空间 int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。 /*a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量 中,p就是一个之指针变量。*/ return 0; }
总结:
- 指针是用来存放地址的,地址是唯一标识一块地址空间的。
- 指针的大小在32位平台是4个字节,在64位平台是8个字节。
二、指针和指针类型
我们都知道,变量有不同的类型,那么要对应上这些不同的变量类型,也就衍生出了对应不同的变量类型的指针变量类型。
char *pc = NULL; int *pi = NULL; short *ps = NULL; long *pl = NULL; float *pf = NULL; double *pd = NULL;
(1)char* 类型的指针是为了存放 char 类型变量的地址。
(2)short* 类型的指针是为了存放 short 类型变量的地址。
(3)int* 类型的指针是为了存放 int 类型变量的地址。
那指针类型的意义是什么呢?
(1)指针+-整数
总结:指针的类型决定了指针向前或者向后走一步有多大(跨过多少字节)
(2)指针的解引用
图1:用int指针变量指向int变量
图2:用char指针变量指向int变量
观察图1和图2,不难发现,char型的指针变量在解引用时,只是解了1个字节的大小;而int型指针变量在解引用时,就全部解了4个字节的大小。
总结:指针的类型决定了对指针解引用的时候有多大的权限(能操作几个字节)。比如:char*的指针解引用就只能访问一个字节,而int*就能访问四个字节。
三、野指针
概念:野指针就是指针指向的位置是不可知(随机的、不正确的、没有明确限制的)
1.野指针的成因
1.1 指针未初始化
#include <stdio.h> int main() { int *p;//局部变量指针未初始化,默认为随机值 *p = 20; return 0; }
2.2 指针越界访问
#include <stdio.h> int main() { int arr[10] = {0}; int *p = arr; int i = 0; for(i=0; i<=11; i++) { //当指针指向的范围超出数组arr的范围时,p就是野指针 *(p++) = i; } return 0; }
3.3 指针指向的空间已经释放了
2.如何规避野指针
(1)指针初始化
(2)小心指针越界
(3)指针指向空间释放及时置NULL
(4)避免返回局部变量的地址
(5)指针使用之前检查有效性,如下代码:
#include <stdio.h> int main() { int *p = NULL; int a = 10; p = &a; if(p != NULL) { *p = 20; } return 0; }
四、指针运算
- 指针+-整数
- 指针-指针
- 指针的关系运算
1.指针+-整数
例1:
#define N_VALUES 5 void main(){ float values[N_VALUES] = {1.0,2.0,3.0,4.0,5.0}; float *vp; //指针+-整数;指针的关系运算 for (vp = &values[0]; vp < &values[N_VALUES];) { *vp++ = 0; } }
执行过程
例2:思路如上
int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int* p = arr; int* pend = arr + 9; while (p<=pend) { printf("%d\n", *p); p++; } return 0; }
2.指针-指针
int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; //指针和指针相减的前提: //两个指针指向同一块空间(数组) printf("%d\n", &arr[9] - &arr[0]); return 0; }
如果是两个指向不同空间的指针就会出问题,所以指针-指针大多数用在数组!如下代码是错误代码:
int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; char c[5]; //指针和指针相减的前提: //两个指针指向同一块空间 printf("%d\n", &arr[9] - &c[0]);//err return 0; }
为什么没有指针+指针呢?我们可以在这里把指针理解为日期,日期-日期和日期+天数都有意义,但是日期+日期就没啥意义了。
例题:运用指针-指针的知识写出计算字符串长度的功能
int my_strlen(char* str) { char* start = str; while (*str != '\0') { str++; } return str - start; } int main() { //strlen(); - 求字符串长度 //递归 int len = my_strlen("abc"); printf("%d\n", len); return 0; }
3.指针的关系运算
void main(){ for(vp = &values[5]; vp > &values[0];) { *--vp = 0; } }
代码简化, 这将代码修改如下:
void main(){ for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--) { *vp = 0; } }
实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证 它可行。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
五、指针和数组
我们先看一个例子:
可见数组名和数组的首元素的地址是一样的
结论:数组名表示的是数组首元素的地址(有两种情况除外,请观看我的数组文章,里面有详细介绍)
那么代码写成这样就是可行:
int arr[10] = {1,2,3,4,5,6,7,8,9,0}; int *p = arr;//p存放的是数组首元素的地址
既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问也是可能的,例如:
所以p+i其实计算的就是数组arr下标为i的地址
那我们就可以直接通过指针来访问数组,例如:
int main() { int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; int *p = arr; //指针存放数组首元素的地址 int sz = sizeof(arr) / sizeof(arr[0]); int i = 0; for (i = 0; i<sz; i++) { printf("%d ", *(p + i)); } return 0; }
这里还有一个很好玩的地方:
int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10}; int* p = arr;//数组名 printf("%d\n", arr[2]); printf("%d\n", p[2]);//p[2] --> *(p+2) //[] 是一个操作符 2和arr是两个操作数 //a+b //b+a printf("%d\n", 2[arr]); printf("%d\n", arr[2]); //arr[2] --> *(arr+2)-->*(2+arr)-->2[arr] //arr[2] <==> *(arr+2) <==> *(p+2) <==> *(2+p) <==> *(2+arr) == 2[arr] //2[arr] <==> *(2+arr) return 0; }
先定义有int arr[10];int* p = arr
(1)因为arr(数组名)其实也是一个一个地址,而且是首元素的地址,p也是指向首元素的地址,那么arr+i和p+i都是相等的。
(2)这里刷新了我对[ ]的认识,不管是arr[2]、2[arr]、p[2]、2[p],在编译时都是转换成了*(arr+2)或者*(p+2)。
六、二级指针
int main() { int a = 10; int* pa = &a;//pa是指针变量,一级指针 //ppa就是一个二级指针变量 int ** ppa = &pa;//pa也是个变量,&pa取出pa在内存中起始地址 int*** pppa = &ppa;//pppa是个三级指针 return 0; }
找代码其中的int**ppa = &举例,*ppa表示ppa是一个指针变量,而int*表示该指针变量是指向int*类型的。
七、指针数组
我们直接用一段代码来理解就行了
//指针数组 - 数组 // //好孩子 int main() { int arr[10];//整形数组 - 存放整形的数组就是整形数组 char ch[5];//字符数组 - 存放的是字符 //指针数组 - 存放指针的数组 int* parr[5];//整形指针的数组 char* pch[5]; return 0; }
有些人问我,为什么要写博客,你难道不怕别人知道这些知识后,让自己的竞争压力变得更大吗?
其实吧,写博客主要是位了巩固自己的知识体现,这是我写的博客,是我的东西,别人不可能比我更熟悉,写博客对于我来说是一件有成就感的事情。别人来看来学,学不学得会是别人的事,我只是想让自己刚到高兴的同时让学习C语言变得更加有趣。