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