还在为不懂指针而感到烦恼么?那还在等什么呢快来看看吧!冲冲冲!
文章目录
- 1,认识指针
- 2,指针变量
- 指针变量的定义
- 指针变量的大小
- const修饰指针变量
- 3,指针的运算
- 4, 野指针
- 概念
- 成因
- 规避
- 5, assert断言
- 6, 结尾
1,认识指针
什么是指针?
首先我们都知道数据是储存在内存之中的,我们可以把内存当成一座公寓,里面有许多房间,每个房间中储存着数据,但是我们会发现一个问题,我们根本不知道我们想要的数据在哪个房间,那么如果我们给每个房间都装上一个门牌号,只要告诉我们数据所在的房间的房间号,那么我们就可以很快的找到所需要的数据,而在计算机中这个门牌号我们有一个名字来称呼它, “地址”。
而所谓的指针也就是内存地址,我们常说的指针一般指的是指针变量,指针变量是用来存放内存地址的变量。
2,指针变量
指针变量的定义
既然指针变量是个变量那么它肯定可以储存数据,指针变量的内容存储的是其指向的对象的首地址,指向的对象可以是变量(指针变量也是变量),数组,函数等占据存储空间的实体。
我们先来写一个最简单的指针变量。
int a = 10;
int* pa = &a;&是取地址操作符可以取出a的地址
printf(“%p\n%d”,pa,*pa);打印地址时需要用的占位符是%p
我们可以看到输出的结果是
第一行打印的就是变量a的地址(十六进制),而第二行打印的是a的地址里存放的整形数据,*是解引用操作符,的作用是引用指针指向的变量值,引用其实就是引用该变量的地址,“解”就是把该地址对应的东西解开,解出来,就像打开一个包裹一样,那就是该变量的值了,所以称为“解引用”。
int pa = &a;其中pa是指针变量的名字,*的意思是指pa是指针变量,而int的意思是pa所指向的地址中存放的数据的类型是整形,那么我们发现a的类型是int时我们要用 int * pa = &a;那么如果a是字符类型的话我们就要这样定义 char * pa = &a;
如果a是浮点型我们就要用 float * pa = &a;
如果我们需要修改a的值我们可以直接用指针变量来进行
*pa = 20;//*pa等价于a因此这一句等价于a=20;
指针变量的大小
想知道指针变量的大小我们可以尝试用如下代码
#include<stdio.h>
int main()
{
int a = 10;
int* pa = &a;
char b = '1';
char* pb = &b;
float c = 1.0;
float* pc = &c;
printf("%zd %zd %zd", sizeof(pa), sizeof(pb), sizeof(pc));
return 0;
}
在x86的环境下输出的结果为
在x64的环境下输出的结果为
我们可以发现指针变量的大小与所存放的地址指向的数据的数据类型无关,在x86环境下地址是由32位2进制数字组成的每一位占1个bit所以它的大小为4个字节,在x64的环境下地址是由64位2进制数字组成的每一位占1个bit所以它的大小为8个字节.
const修饰指针变量
我们知道const可以修饰普通变量如:
const int a = 10;
经过修饰后a这个变量会获得常量属性使a的值无法再更改,同理const在修饰指针变量的时候也就类似的效果。
根据不同需求,有三种不同效果的书写方式
1,const int* pa = &a;
2, int* const pa = &a;
3, const int* const pa = &a;
首先第一种我们可以理解为const和a结合了,无法更改pa中储存的地址,但是我们仍然可以更改a的值。
如pa=20;但如果写int b = 20;pa = &b;那么就会报错。
第二种我们可以理解为const与a结合了,无法更改a的值但是我们可以更改pa储存的地址。
如int b = 20; pa=&b;但是如果写*pa=20时会显示报错
第三种则是前两种写法的结合既无法更改pa中储存的地址,又不可以更改a的值。
3,指针的运算
为了方便大家理解指针的运算我们可以先写出如下代码
#include<stdio.h>
int main()
{
int a = 10;
int* pa = &a;
char b = '1';
char* pb = &b;
float c = 1.0;
float* pc = &c;
long long d = 0;
long long* pd = &d;
double e = 0.0;
double* pe = &e;
printf("%p %p %p %p %p\n", pa, pb, pc,pd, pe);//打印指针变量的地址
printf("%p %p %p %p %p\n", pa+1, pb+1, pc+1, pd+1, pe+1);//打印指针变量+1之后的地址
return 0;
}
我们运行之后的结果为(每次运行的时候结果会不一样因为每次数据不一定都分配到同一个地址)
对于int和float类型的指针每+1其地址会+4,
对于char类型的指针每+1其地址会+1,
对于double和龙long long*类型的指针每+1其地址会+8,
这是为什么呢?我们知道int和float类型的数据占4个字节大小,对指针变量+1会使其跳过当前数据类型大小的字节跳到一个新地址,所以指针的加减运算与其是什么类型的指针有关。也就是说我们可以通过±整形来改变指针中储存的地址。
那么指针与指针之间可不可以进行运算呢?回答当然是可以,但是我们暂且先不讲之后再讲。(#^ . ^#)
4, 野指针
概念
野指针,正如它的名字,“野”,就像野鸡野鸭野狗一样不受人类的控制,野指针你也没法控制,因为你根本不知道它指向了那一位置。
定义:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)指针变量在定义时如果未初始化,其值是随机的,指针变量的值是别的变量的地址,意味着指针指向了一个地址是不确定的变量,此时去解引用就是去访问了一个不确定的地址,所以结果是不可知的。
由此可见野指针是十分危险的,那让我了解了解野指针的成因。
成因
1,指针变量未初始化
任何指针变量刚被创建时,它的值是随机的,它会乱指。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。如果没有初始化,编译器会报错“ ‘point’ may be uninitialized in the function ”。
2,指针释放后之后未置空
有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。(读起来比较拗口,简单来说就是指针中所储存的地址指向的数据被清除了,但是指针并没有被清除,那么指针指向的东西就是无意义的垃圾内存,这时候它就变为了野指针。)
3,数组越界
打个比方比如有个数组 int a[10];而你却这么定义指针int* pa = &a[11];这样数组就越界了,指针所指的位置不知道有什么所以也变成了野指针。
规避
1,初始化时置 NULL
指针变量一定要初始化为NULL或者已知的值,因为任何指针变量(除了static修饰的指针变量)刚被创建时不会自动成为NULL指针,它的值是随机的。
2,释放时置 NULL
当指针p指向的内存空间释放时,没有设置指针p的值为NULL。delete和free只是把内存空间释放了,但是并没有将指针p的值赋为NULL。通常判断一个指针是否合法,都是使用if语句测试该指针是否为NULL,或者使用assert断言(马上讲)。
5, assert断言
什么是断言?
断言是一种用于调试的语句,可以用来检查程序运行时一些前提条件是否满足,如果不满足条件在运行时就会报错。
那么怎么使用呢?我们先用如下代码让大家认识一下断言语句的写法。
#include<stdio.h>
#include<assert.h>//断言的头文件
int main()
{
int a = 10;
assert(a == 20);//判断a是否等于20
return 0;
}
显然条件不成立运行时会出现如下结果。
那我们为什么要在指针章节来介绍这个语句呢?众所周知野指针是非常危险的所以为了避免出现野指针的情况我们通常要用assert语句判断一下该指针是否为NULL,以防止野指针。当条件成立时assert就不会使程序报错,但是一段代码中可能会用许多次assert在调试完成后我们要一个一个的把他们删除就会变得非常麻烦,其实想解决这个问题也非常简单,只需要加上一句#define NDEBUG就可以禁用assert如下。
`#define NDEBUG
#include<stdio.h>
#include<assert.h>
int main()
{
int a = 10;
assert(a == 20);
return 0;
}`
这样再运行assert语句就会失效不会再报错。
6, 结尾
关于指针的基本知识还有很多所以我决定拆成三期来讲,如果觉得讲的不错的小伙伴希望也能再看看我之后要写的两期,我尽量加快更新(学习太紧张了o(╥﹏╥)o)。希望小伙伴们都能学好指针,为了自己的目标继续前进吧!
第二期链接,冲冲冲!