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”为止

注意,这里有几个小细节

  1. 注意要打印字符串需要使用%s
  2. 不需要使用*操作符
  3. 常量字符串不能够修改,比如*pstr=’a’就是错误的
  4. 这种方法可以处理空格,遇到’\0’才会停下来
  5. 如果要打印字符,需要用%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 ,今天的分享到这里先结束啦!
!!!!!!!!!谢谢观看!!!!!!!!!
!!!!!!!!记得三连哦!!!!!!!!!