write in front
大家好,我是gugugu。希望你看完之后,能对你有所帮助,不足请指正!共同学习交流
本文由 gugugu 原创 CSDN首发 如需转载还请通知⚠
个人主页:gugugu—精品博客
欢迎各位→点赞 + 收藏⭐️ + 留言
系列专栏:gugugu的精品博客
✉️我们并非登上我们所选择的舞台,演出并非我们所选择的剧本
前言
今天这篇博客带来指针的分享,相信各位小伙伴在学习C语言之前就已经对指针有所耳闻,网上各种传言指针超级难,从而劝退C语言的学习,但是博主认为指针并不是难到不可以学会,多写代码,多多思考,就很容易学会指针的
指针的难点主要在于内容多,而并非难度大,努力啃每一个小版块,就很容易理解指针的
另外,本次博客将以讲解+代码+例题的方式讲解,从而加深对指针的理解
一、操作符&和*
在进入指针的学习之前,我们首先学习两个操作符 & 和 *,这两个操作符在指针中非常重要
1、操作符 &
&是取地址操作符,能够将一个元素的内存空间的地址给取出来
#include int main(){int a = 10;char ch = 'a';int arr[] = { 1,2,3,4 };printf("%p\n", &a);printf("%p\n", &ch);printf("%p\n", &arr);return 0;}
这里需要注意的是,对数组名进行取地址,比如举例中的&arr,是取了整个数组的地址,但是打印或者说在内存里面存放的还是数组首元素的地址 ,但本质仍然是取出了整个数组的地址
通过这个例子,可以看到&arr+1,并不是+4,而是增加了16,所以确实跳过了整个数组
2、操作符 *
*操作符叫做解引用操作符,是对地址进行解引用的,找到地址所对应的元素
#include int main(){int a = 10;int* p = &a;(*p)++;printf("%d\n", a);return 0;}
这里int * p=&a,这一步不用管,就是指针,后面会讲到,只用先理解解引用操作符
这里通过*p解引用,找到了p指向的元素a,之后进行++,所以得到11
二、指针
1、简单介绍一下指针
指针其实全称指针变量,也是一种变量,类似于整型变量,字符变量等变量的一种变量类型,为了便于称呼,习惯上直接叫做指针
整型变量存储整型,字符变量存储字符,那么指针变量储存什么呢?
相信,通过上面的两个操作符的例子中,聪明的大家能够猜出来,指针变量储存的是地址。
2、指针的定义方式
这个就不好文字描述了,上代码
int a = 10;int* p = &a;
首先得有对象(现阶段,先假设有对象,后期写代码可能先出指针,后出对象),对这个对象进行取地址,存到指针中。
注意,指针的写法
int 后面还跟了一个*
需要明确的是,这个 星号不是解引用操作符,而是指针的标志
3、稍微拆解一下,方便理解
以这一句代码为例
int * p = &a;
p是指针变量的名称,
星号*则是指针的标志,提示这是一个指针,
int 则是指针所指向的元素的类型
而int *是指针p的类型
当然啦,既然除了整型以外还有字符类型,指针也可以指向除整型以外的其他的数据类型啦
比如下列情况
char a;char *p1=&a;short b;short *p2=&b;double c;double *p3=&c;
除此之外还有很多,还可以指向数组,函数等,后面会讲到
4、指针的大小
经过以前的学习,我们知道int 类型是4个字节,char类型是1个字节,short类型是2个字节,float类型是4个字节
那么指针的大小是几个字节呢?
指针的好处来啦!
不用像其他的数据类型一样,记那么多的大小个数
所有类型的指针的大小都是4或8个字节
在32位机器上是4个字节,在64位机器上是8个字节
指针使用来存放地址的,只要是在32位机器下,一个地址由32个0或1组成
共计32个bit,一个字节就是8个bit,所以32位机器下,所有的指针大小都是4个字节,刚好放下一个地址
同理,64位机器同理
5、多种多样的指针存在的意义
既然在同一台机器下,指针的大小都是一样大,而且都是用来存放地址的,那指针要这么多类型干嘛?
这里就要结合我们上面学习到的解引用操作符啦
不同类型的指针在解引用 时可以支配的内存空间的大小是不同的
下面举两个例子看看
a、解引用
#include int main(){int a = 0x12345678;int* p = &a;*p = 0;printf("%x\n", a);a = 0x12345678;char* p2 = (char*)&a;*p2 = 0;printf("%x\n", a);return 0;}
比较代码和运行结果
我们发现int类型的指针可以支配4个字节,而char 类型的指针只能支配一个字节;
b、指针±
#include int main(){int a = 10;int* p1 = &a;char* p2 = (char*)&a;printf("%p\n", &a);printf("%p\n", &a+1);printf("%p\n", p1);printf("%p\n", p1+1);printf("%p\n", p2);printf("%p\n", p2+1);return 0;}
可以发现int类型的指针+1会跳过4个字节,char类型的指针+1会跳过1个字节,当然其他类型的指针同理
指针减法也是同理
三、丰富的指针世界
1、野指针
野指针呢,听起来还是挺恐怖的,野的,能有多野?
哈哈哈,活跃一下,缓解疲劳
在以后写代码时,当你写出了野指针时,够头疼的了
a、野指针的成因
野指针的成因主要有三个:
1.指针创建的时候没有进行初始化
int *a;*a=10;
2.指针访问数组时,发生了越界访问,指向了数组以外的空间
int arr[10] = {0};int *p = &arr[0];int i = 0;for(i=0; i<=11; i++){//当指针指向的范围超出数组arr的范围时,p就是野指针*(p++) = i;}
数组里下标最大为9,但是for循环,下标却可以到10和11,所以越界访问,形成了野指针
3.指针指向了一个已经释放了的空间,比如函数调用结束了,函数栈帧已经销毁,依然指向这个释放的空间。
#include int* test(){int n = 100;return &n;}int main(){int*p = test();printf("%d\n", *p);return 0;}
在函数test调用结束后,函数栈帧就会销毁,释放内存空间
但是仍然使用了*p指向test,所以形成了野指针
b、如何规避野指针
规避野指针,只需要根据上面三条成因,逐一注意即可
1.定义指针的时候要初始化,玩意没有想好对象,先赋值为NULL空指针
2.注意不要形成数组越界
3.指针使用之前,检查指针是否指向有效空间或对象
c、assert断言
我们在定义指针的时候,常常会使用NULL来初始化,
但是赋值为NULL之后,又很容易忘记已经赋值为NULL了,从而造成报错
这里介绍assert来解决这个问题
int* p = NULL;*p = 10;
当写出这样的代码是,编译器就会报错
所以在运用指针前就要进行检查
这里推荐检查方法为assert断言
#include #include int main(){int* p = NULL;assert(p != NULL);*p = 10;return 0;}
这样如果报错,会提醒在哪个文件,哪一行,从而方便快速寻找错误
注意,assert有头文件
除此之外,assert还有一个好处,就是可以通过定义NDEBUG宏来开启或关闭,
也就是在文件开头加一句#define NDEBUG
又变回了原样子
另外,assert在Windows中,debug版本才起作用,release版本会自动优化掉,不起作用,但是,在Linux操作系统下,debug版本和release版本,assert断言都会起作用
2、指针常量和常量指针
这两个很像的名字,听起来很抽象,哈哈,用起来也很抽象,(当代鲁迅在此qaq)
指针常量和常量指针的成因就在于const修饰的位置不同
a、指针常量
指针常量(pointer to constant)是指指针指向的数据是常量,不能通过指针修改数据的值。声明指针常量时,必须在类型前加上 const 关键字,也就是说const要在*前面
char const* p;const char* p;
这两种写法都是指针常量,指针指向的对象无法修改
int n = 10;int m = 20;const int* p = &n;*p = 20;//ok" />= &m; //ok?
*p=20是不可以的,但是p=&m是可以的
b、常量指针
常量指针(constant pointer)是指指针本身是常量,即不能修改指针的指向。声明常量指针时,在指针名前加上 const 关键字,也就是说const放在*后面
int *const ptr;
看一个例子
int n = 10;int m = 20;int *const p = &n;*p = 20; //ok?p = &m; //ok?
这里*p=20就是可以的,但是p=&m就是不行的
所以,想要确保代码的安全性,最好在*前面后面都加上const,但是实际代码的需求可能要改变变量值,也就不用加const,具体情况具体分析
这才两点,什么,你说你学了好多,哈哈哈,后面还多着呢,指针就是这样,内容很多,需要坚持不懈,一点点啃完
3、二级指针
二级指针和多级指针是一个道理,依次类推就ok啦
这里就只讲解二级指针啦!
我们来思考一个问题,指针存放的是地址对吧,
那么指针本身所占的内存空间的地址用什么储存呢?
这里,就要使用二级指针了
a、二级指针的理解
int a = 10;int* pa = &a;int** ppa = &pa;
依据这个图,可以更好的理解二级指针
b、二级指针解引用
通过二级指针也可以直接更改最底层的对象
int a = 10;int* pa = &a;int** ppa = &pa;(**ppa)++;printf("%d\n", **ppa);
4、指针数组和数组指针
分辨指针数组和数组指针的最好方法就是先明确,这是数组还是指针
a、指针数组
(1)数组指针的理解
指针数组本质上还是数组,既然是数组,那么就可以联系整型数组,字符数组等来理解
整型数组是存放整型的数组,字符数组是存放字符的数组,那么指针数组就是存放指针的数组
下面这个图,可以帮助大家更好的理解
(2)指针数组的结构
类似数组
数组有数组名,数组大小,数组存放的类型
同样,指针数组也是如此
以int * parr[5]为例
- parr是数组名
- [5]是数组大小
- int *是存放的元素的类型
- 数组的类型为int * [5]
b、数组指针
(1)数组指针的理解
通过指针数组的学习,运用一样的方法去学习数组指针,事半功倍
首先,数组指针的本质是指针,所以联系整型指针,字符指针去理解
整型指针指向整型变量,字符指针指向字符变量
因此,数组指针指向数组
(2)数组指针的结构
以int (*p)[5]为例
- p是指针变量的名字
- *是提示这是一个指针
- [5]是指p指向一个有五个元素的数组
- int 是指这个数组的元素是int类型
- 数组指针p的类型是int (*) [5]
(3)数组指针的初始化
int arr[5]={1,2,3,4,5};int (*parr)[5]=&arr;
c、指针数组和数组指针的对比
int * parr[5];int (*parr)[5];
从形式上看,主要差别在于()
为什么要括号呢?
*和p一起定义一个指针,
是因为[]的优先级高于(),
所以需要使用括号二者放在一起。
从内容上看,主要一个是数组,一个是指针
5、字符指针
字符指针有一个很神奇的一点,一般来说指针的作用是存储地址,但是根据字符的特殊性,可以使用字符指针打印字符串
存储字符的地址的方法就不讲了,具体讲讲字符指针打印字符串
int main(){const char* pstr = "hello bit.";//这是把整个字符串放到pstr指针变量里了吗?printf("%s\n", pstr);return 0;}
但是,原理是什么呢?又或者说这里是把整个字符串放到指针变量里面了吗?
其实并不是这样
pstr指针里面依然只存放了字符‘h’的地址,但是根据字符串的特性,在内存空间中连续存储,所以会继续往后打印,直到遇到了“\0”为止
注意,这里有几个小细节
- 注意要打印字符串需要使用%s
- 不需要使用*操作符
- 常量字符串不能够修改,比如*pstr=’a’就是错误的
- 这种方法可以处理空格,遇到’\0’才会停下来
- 如果要打印字符,需要用%c,且加上*操作符
6、函数指针
通过上面的类比学习,我们能够理解,函数指针就是存放函数地址的变量
1、函数的地址
函数也有地址吗?
有没有地址,&取地址一下不就ok了
做个测试看看吧
#include void test(){ printf("hehe\n");}int main(){ printf("test: %p\n", test); printf("&test: %p\n", &test);return 0;}
可以看到,函数的地址&函数名和直接函数名得到的地址是一样的
2、函数指针的写法
void test(){ printf("hehe\n");}void (*pf1)() = &test;void (*pf2)()= test;int Add(int x, int y){ return x+y;}int(*pf3)(int, int) = Add;int(*pf3)(int x, int y) = &Add;//x和y写上或者省略都是可以的
分别示范了两种返回值类型不同的函数的函数指针的写法
3、函数指针类型的结构
以int (*pf3)( int,int )为例
- pf3是函数指针的名称
- *是提示这是一个指针
- (int,int)这是指,指针指向了一个函数,函数有两个参数,都是int类型
- int 是指所指向的这个函数的返回类型是int类型
ok ,指针的大体内容基本分享结束,提升对指针的了解,就得靠自己多思考,多敲代码啦
下面,就和大家分享几道有意思的题目
四、分享几道有意思的题目
第一道
(*(void (*)())0)();
这段代码是什么意思呢?
题目还是很有些难度的,仔细思考哦
首先,使用强制转换,将0转换成一个void (*)()类型的一个函数指针,也就是将0 强制转换成了一个地址,接着对这个地址解引用,再去调用这个0处地址的那个函数
第二道
void (* signal(int, void(*)(int)))(int);
这道题目还是比较有意思的,仔细思考一下
这里是一个函数声明,函数名是signal,有两个参数,一个参数类型是int,另一个参数类型是一个函数指针类型,指向一个函数,这个函数只有一个int类型的参数,返回类型是int
参数说完了,该说返回类型了,返回类型是一个函数指针类型,指向的函数参数只有一个int类型,返回类型为void
okk ,今天的分享到这里先结束啦!
!!!!!!!!!谢谢观看!!!!!!!!!
!!!!!!!!记得三连哦!!!!!!!!!