文章目录
前言
在之前的文章中有简单介绍指针,但在C语言初识这个专栏里也不会深入,在未来C语言进阶这个专栏会详细了解
一、指针是什么
官方来说:在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向(Points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”,意思是通过它能找到以它为地址的内存单元
#include<stdio.h> int main() { int a = 10;//a占4个字节,1个字节对应1个编号,a有4个地址,如果用4个地址去访问a比较麻烦 int* pa = &a;//&a拿到的是a的4个字节中每一个字节的地址。通过int*类型的pa存储a的地址 *pa = 20;//再通过*解引用操作去访问a return 0; }
一个小的单元是多大?以及如何编址?
经过仔细计算和权衡我们发现一个字节对应一个地址是比较合适的
对于32位的机器,假设有32根地址线,那么假设每根地址线寻址产生一个电信号正电/负电(1或0)
假设一个内存单元是1bit - 2^32bit:
如果给每个bit都有一个地址 - 太浪费了。经过平衡后,以一个字节为内存单元,然后分配地址
这里就有2的32次方个地址。 每个地址标识一个字节,那么:
同理64位也是一样
在32位机器上,地址是32个0或者1组成的二进制序列,那地址就得用4个字节的空间来存储所以一个指针变量的大小就是4个字节
在64位机器上,如果有64根地址线,只有一个指针变量的大小是8个字节,才能存放一个地址
小结:1.每个地址标识一个字节    2.指针的大小在32位平台是4个字节,在64位平台是8个字节
二、指针和指针类型
1、指针类型
不同类型的数据交给指针时也要使用不同的指针类型去存储
由以下代码发现:不同的指针类型都是4个字节或8个字节,那么指针类型是否有存在的必要?
#include<stdio.h> int main() { int* pa; char* pc; float* pf; printf("%d\n", sizeof(pa));//4 printf("%d\n", sizeof(pc));//4 printf("%d\n", sizeof(pf));//4 return 0; }
2、指针类型的意义
先了解一下:1个十六进制位是4个二进制位
0 1 2 3 4 5 6 7 8 9 a b c d e f -> 十六进制位
1 1 1 1 1 1 1 1 -> 二进制位
8 4 2 1 = 15 = f
所以说4个二进制位是1个十六进制位,1个字节是2个十六进制位
#include<stdio.h> int main01() { int a = 0x11223344; int* pa = &a; *pa = 0; return 0; } //-------------------------------------------------- int main02() { int a = 0x11223344; char* pc = &a; *pc = 0; return 0; }
这里使用int和char类型的指针存储a的值。调试发现:同一个值,被不同类型指针存储后,在解引用操作时,它们的访问权限不一样
为了能够更直观的认识指针类型的意义,再看以下代码:
#include<stdio.h> int main() { int arr[10] = {0}; int* p1 = arr; char* p2 = arr; printf("%p\n", p1); printf("%p\n", p1 + 1); printf("----------分割线---------\n"); printf("%p\n", p2); printf("%p\n", p2 + 1); return 0; }
这里使用int和char类型的指针存储arr数组首元素的地址。运行发现:被不同类型指针存储后,统一加1后的结果不同
小结:1、指针类型决定了指针解引用的权限有多大    2、指针类型决定了指针走一步,能走多远(步长)**    **3、int类型指针+1跳过4个字节;char类型指针+1跳过1个字节;数组类型指针+1跳过过1个数组(暂时不了解)
3、简单应用
/***********************************************************************
目的:借助指针输出将数组里的元素1 - 10输出
分析:使用int*类型的指针存储数组首地址,并利用解引用操作符
平台:Visual studio 2017 && windows
***********************************************************************/
#include<stdio.h> int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int i = 0; int* p = arr; for(i = 0; i < 10; i++) { printf("%d ", *(p++)); } return 0; }
输出结果:
相反,如果使用char*类型的指针去存储:每次加1后所走的步长不同
#include<stdio.h> int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int i = 0; char* p = arr; for(i = 0; i < 10; i++) { printf("%d ", *(p++)); } return 0; }
输出结果:
三、野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。以下代码列举几个野指针
:
1、野指针的产生
#include<stdio.h> //1、 int main01() { int* p;//p是一个局部的指针变量,局部变量不初始化的话,默认是随机值 *p = 20;//非法访问内存 return 0; } //2、 int main02() { int arr[10] = {0}; int* p = arr; int i = 0; for(i = 0; i <= 10; i++)//当i = 10时,此时的p就是野指针,再去解引用的话就是非法访问内存 { *p = i; p++; } } //3、 int* test() { int a = 10; return &a; } int main03() { int* p = test(); //a是局部变量,一旦test函数执行完成,test函数被销毁,变量a被释放。p虽然拿到了a的地址,但是指向的空间就是未知的 *p = 20;//非法访问内存 return 0; }
小结: 1、指针变量未初始化的情况是野指针
2、指针指向的数组越界后是野指针
3、指针去接收一个函数的局部变量的返回地址时,这个程序结束,函数销毁,指针指向的空间被释放,也会导致野指针
2、如何规避野指针
#include<stdio.h> //1、 int main01() { //`1.明确知道要初始化的值时: int a1 = 10; int* p1 = &a1; //2.不明确知道要初始化的值时 //养成好的习惯,定义变量的时候对变量初始化为0 int a = 0; //而指针可以初始化为NULL(空) int* p = NULL; return 0; } //2、 int main02() { int arr[10] = {0}; int* p = arr; int i = 0; for(i = 0; i < 10; i++)//C语言本身是不会检查数组越界的,要保证数组不越界 { *p = i; p++; } } //3、 int* test() { int a = 10; return &a; } int main03() { int* p = test(); p = NULL;//将p置为空指针 *p = 20;//err return 0; } //4、 int main04() { int* p = NULL; if(p != NULL) *P = 20; return 0; }
总结: 1、初始化变量
2、注意不要数组越界
3、指针指向的空间释放后及时置为NULL
4、指针使用之前检查有效性
四、指针运算
1、利用指针进行简单运算
#define N_VALUES 5 #include<stdio.h> //1、使用指针运算将数组从前往后被初始化为0 int main01() { float values[N_VALUES]; float* vp; for(vp = &values[0]; vp < &values[N_VALUES];)//指针的关系运算 { *vp++ = 0;//指针加减运算 } return 0; } //2、使用指针运算将数组从后往前被初始化为0 int main02() { float values[N_VALUES]; float* vp; for(vp = &values[N_VALUES]; vp > &values[0];) { *--vp = 0; } return 0; } //3、将main02代码简化一点 int main03() { float values[N_VALUES]; float* vp; for(vp = &values[N_VALUES - 1]; vp >= &values[0]; vp--) { *vp = 0; } return 0; }
观察main02和main03
main02和main03的功能是一样的,都能初始化数组为0,且main03相对来说更容易理解
实际上main03在绝大部分的编译器上是可以完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行
标准规定:允许指向数组元素的指针与指向数组最后1个元素后面的那个内存位置的指针比较,但是不允许与指向第1个元素之前的那个内存位置的指针进行比较
#include<stdio.h> //1、利用指针运算打印数组元素 int main01() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int* p = arr;//将数组的第1个元素的地址交给p int* pend = arr + 9;//将数组的最后1个元素的地址交给pend while(p <= pend)//利用数组的地址 -> 从低到高 { printf("%d ", *p);//1 2 3 4 5 6 7 8 9 10 p++; } return 0; } //2、指针相减 int main02() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; //指针相减运算 -> 指针-指针 = 2个指针之间的元素个数 -> 但前提是2个指针指向同一块空间 printf("%d\n", &arr[9] - &arr[0]);//9 //细想一下,其实指针+指针其实是没有意义的 return 0; }
2、简单应用
/***********************************************************************
目的:使用指针与指针的运算模拟strlen
分析:找到目标字符串\0的位置和首元素地址相减即可
平台:Visual studio 2017 && windows
*************************************************************************/
#include<stdio.h> int my_strlen(char* str) { //备份1份首地址 char* start = str; while(*str) { str++; } return str - start; } int main() { int len = my_strlen("abc");//传过去的仅仅是a的地址 printf("%d\n", len); return 0; }
五、指针和数组
1、指针和数组的关联
#include<stdio.h> int main() { int arr[10] = {0}; int* p = arr; int i = 0; for(i = 0; i < 10; i++) { //printf("%p <==> %p\n", &arr[i], p + i);//&arr[i] <==> p + i *(p+i) = i; } for(i = 0; i < 10; i++) { printf("%d ", *(p + i)); } return 0; }
2、更深层次的了解指针和数组
#include<stdio.h> int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int* p = arr; //打印第3个元素: printf("%d\n", arr[2]); printf("%d\n", 2[arr]); printf("%d\n", p[2]); //arr[2] --> *(arr + 2) --> *(2 + arr) --> 2[arr] //p[2] --> *(p + 2) //可推出: //arr[2] <==> *(arr + 2) <==> *(p + 2) <==> *(2 + p) <==> *(2 + arr) <==> 2[arr] return 0; }
六、二级指针
#include<stdio.h> int main() { int a = 10; int* pa = &a;//此时pa指向变量a的地址时,pa为一级指针变量 int** ppa = &pa;//同时pa也是个变量||地址,此时ppa去指向pa的地址时,ppa为二级指针变量 int*** pppa = &ppa;//此时pppa为三级指针变量 //当然还有四级指针、五级指针...。语法是支持的,但是并不常用(三级指针也很少用到) //怎么通过ppa找到a -> *ppa <=> pa, *pa <=> a, **ppa <=> a printf("%d\n", **(ppa)); return 0; }
图解:
七、指针数组
#include<stdio. h> int main() { int arr[10];//整型数组 - 存放整型的数组就是整型数组 char ch[5];//字符数组 - 存放字符的数组就是字符数组 //指针数组 - 存放指针的数组就是指针数组 int* parr1[5];//整型指针数组 char* parr2[5];//字符指针数组 return 0; }
八、指针进阶
这里将附上对于指针更深层次的文章