指针下篇,我想要把指针的基本知识全部说明白,所以本篇博客会比较长,有的部分文字也比较多,文字多有文字多的好处,可以帮助读者更加清楚的理解指针,篇幅较长,每一个举例代码都是在平台进行测试过的,希望读者也可以边阅读边测试理解过程。有什么问题可以在下面评论,多多指教。互相学习。
代码演示:变量的大小/变量地址的大小/取变量的地址
#include <stdio.h> int main() { char a = 1; short b = 2; int c = 3; double d = 1.234; printf("sizeof(a) = %d\n", sizeof(a)); printf("sizeof(b) = %d\n", sizeof(b)); printf("sizeof(c) = %d\n", sizeof(c)); printf("sizeof(d) = %d\n", sizeof(d)); printf("sizeof(&a) = %d\n", sizeof(&a)); printf("sizeof(&b) = %d\n", sizeof(&b)); printf("sizeof(&c) = %d\n", sizeof(&c)); printf("sizeof(&d) = %d\n", sizeof(&d)); printf("&a = %p\n", &a); printf("&b = %p\n", &b); printf("&c = %p\n", &c); printf("&d = %p\n", &d); return 0; }运行结果为: 但是上面a,b,c,d 的地址都是常量。 例如代码中如果出现:
&a = 0x1234;编译就会出错。 &a 就是物理内存上的一个地址,是一个常量,所以不允许被赋值。
我们已经在指针(中)篇博客中介绍到了指针变量。
凡是一个有类型的地址,都是可赋给同类型的指针变量。类型不同,编译器则可能报错。
接下来我们说明需要重要强调的一点,我们已经知道了可以自己定义一个指针变量来存放变量的地址。
但是以下方式是不允许的:
#include <stdio.h> int main() { char* p = (char*)0x12345678; //0x12345678 是数值,(char*)0x123456 是指针 printf("%c", *p); printf("%d", *p); return 0; }上面操作是不允许的,当然程序不会有任何结果,结果就是崩掉了,原因就是,我们整个内存中并不是所有的空间在创建变量的时候能够访问到的,之后我们会讲到C语言内存空间。其中有一部分是操作系统的内核区,内核中的空间是不允许用户访问的,而如果你直接给一个指针变量一个内存地址,那么这个地址很有可能是用户不允许访问的内存区域,那么我们之前已经讲到,我们可以定义指针变量对于内存进行操作,那么如果是不允许访问的区域使用指针变量进行了修改,可能会带来致命性的错误,所以编译器直接禁止通过以上方式初始化指针变量(我们的初始化可以直接理解为赋值,只不过这里赋值的是一个有类型的地址也就是指针)
所以通常作法是,把一个己经开辟空间的变量的地址赋给指针变量。 代码演示:
#include <stdio.h> int main() { int a = 100; int b = 200; int* pa = &a; int* pb = &b; printf("*pa = %d\t&a = %x\n", *pa, &a); *pb = 200; printf("*pb = %d\t&b = %x\n", *pb, &b); return 0; }运行结果为:
比较两个指针是否相同:地址相同并且类型相同才能说两个指针是同一指针。
那么在这里,我们已经通过以上打印找到了a和b的地址,通过指针用图解方式给大家演示一下,使得我们的认识更加清楚。 指针变量指向谁就是保存了谁的地址。 例如上面图解:指针变量pa保存了变量a的地址就是pa指向了a。a被pa所指向。
那么这里我们只是标出来了变量的地址,并没有直接写出指针变量的地址,这里的指针变量pa和pb也一定是有地址的,因为我们说过,指针变量是一个变量,是变量就有地址和大小,指针存放的是其他变量的地址,我们直接说明就是,指针pa指向的是a,a是一个int类型,指针pa是一个int * 类型,那么int * 表示指针的寻址能力为int类型大小,所以指针pa能够寻址到的大小就和a 的类型大小相等,一个是int 一个是int * 。那么就可以通过 *pa 访问到变量a的值,*pa就是解引用,也可以理解为取值,pa保存的是a的地址,所以就是取pa保存变量a的地址。也可以理解为pa指向变量a。
更改指向: 那么指针变量保存的是变量地址,那么我们如果让指针变量保存的地址发生改变,那么相应的我们在解引用的时候取到的变量就会改变,也就是指针变量更改指向的过程。
指针变量保存的地址发生改变,是指针变量更改指向的过程。
#include <stdio.h> int main() { int num = 10; int num2 = 20; int *p = # printf("%d\n",*p); p = &num2; //更改指向 printf("%d\n",*p); return 0; }打印结果为: 只要指针变量保存的地址发生改变,那么解引用的时候,保存的变量必然也发生改变,指针变量保存的地址是发生指向之后变量的地址,解引用的时候取到就是指针变量指向发生改变之后指向变量的值,这点很容易理解。就像你手机里面记住的是001房间,如果把001房间改成002房间,那么你寻找到的时候进入的就是002房间。
接下来我们介绍一种很危险的指针,操作不当造成的后果不可估量。 一个指针变量,如果指向一段无效的内存空间,则该指针称为野指针,也称为无效指针。 常见情况有两种: 一种是未初化的指针变量,一种是指针变量指向己经被释放的内存空间。
指针变量指向己经被释放的内存空间,我们之后在申请内存空间的时候进行说明,这篇博客我们主要说明未初始化的指针。
对野指针的读操作或许会崩溃尚可忍受,如果对野指针的写入成功,造成的后果是不可估量的。对野指针的读写操作,是危险而且是没有意义的。世上十之八九最难调的 bug 皆跟它有关系。
#include <stdio.h> int main() { int *pa; //未初始化的指针 printf("%x",*pa); return 0; }上面操作会直接崩掉,但是造成的损失还不算很严重,因为只是读取内存数据,读取的内存地址不确定,但是不会修改内存中的数据。但是很有可能读取到内核区域不允许用户读取的内存区域,操作系统会直接拦截。
#include <stdio.h> int main() { int *pa; *pa = 100; printf("%x",*pa); return 0; }上面操作也会崩掉,这个操作就会直接修改野指针所指向的地址对应内存的值,当定义的指针变量指向的地址不确定的时候,修改内存中的值,这是非常可怕的事情。所以操作系统也会直接拦截。崩溃的情况是正常的,系统会保护,不允许对于指向地址不确定的指针变量进行写入操作,因为一旦对于野指针的写入操作成功了,带来的损失可能是不可估量的,这就是一个可怕的事情!!!所以操作系统也会直接拦截。
如果一不小心使用了野指针,代码规模庞大的时候,如果里面有一个未初始化的变量被操作了,那查找起来就像是大海捞针一样。
所以我们在定义指针变量的时候要有一个良好的习惯就是在定义的时候一定要把指针变量置为NULL
那么NULL是什么呢? 我们先定义指针为空,然后通过编译器跳转进行查看:
#include <stdio.h> int main() { int *p = NULL; return 0; }上面解释是C++解释,但是这里理解不影响
在vs2019我们跳转到源码可以看到,NULL是一个宏定义的值为 0 ,如果现在不知道是宏定义的话,那么就把NULL理解为0就可以,这里的NULL指的是内存的0地址。
NULL 是一个宏,俗称空指针,等价于指针( void * ) 0。 ( void * ) 0 是一个很特别的指针,因为他是一个计算机黑洞,既读不出内容,也不写进东西去。 所以被赋值 NULL的指针变量,进行读写操作,是不会有内存数据损坏的。 那么我们就使用NULL起一个标记的作用,也就是说用 NULL 初始化未使用的指针。
c 标准中是这样定义的:
define NULL ((void *)0)故常用 NULL 来标记未初始化的指针。 或对己经被释放内存空间的指针变量赋值。
可以理解为 C 专门拿出了 NULL指针(零值无类型指针),用于作标志位使用。
void叫无类型。
接下来我们说明一下void,我们在见到的或者自己用到的函数在定义返回值的时候,或者参数里面都会有用到 void,具体的功能我们会在函数部分说明其功能,我们都知道char,int或者其他类型,void叫做无类型,我们首先提出: ①:void*指针可以赋值给任何类型的指针。 ②:void代表内存的最小单元,那么最小单元就是一个字节。
那么我们为什么没有用char类型来取代呢? 将来有可能char一个字节不够用了,我们可能就要升级为2个字节,但是void永远是一个字节。我们强调一点:void在 32 位机上地位等同于 char。
#include <stdio.h> int main() { printf("sizeof(char) = %d\n",sizeof(char)); printf("sizeof(void) = %d\n",sizeof(void)); return 0; }大家可以通过以上代码测试 void 的大小,但是并不是所有平台能都测出来,vs2019编译不能通过,Qt平台可以直接打印出来结果。我们在这里直接给出打印结果: 在这里我们需要强调一个小细节: 我们之前定义:
int a,b;上面代码定义的是两个int类型的变量。
如果我们:
int * pa,pb;那么这里,我们是不是定义了两个指针变量呢? 我们通过测试他们的大小来进行说明。
#include <stdio.h> int main() { int *p,q; printf("sizeof(p) = %d\n",sizeof(p)); printf("sizeof(q) = %d\n",sizeof(q)); return 0; }打印结果为: 两个类型大小相等,所以是两个指针变量,真实情况是这样吗? 我们改变以下类型:
#include <stdio.h> int main() { char* p, q; printf("sizeof(p) = %d\n", sizeof(p)); printf("sizeof(q) = %d\n", sizeof(q)); return 0; }打印结果为: 前面的 p 是指针变量,后面的 q 是一个 char 类型变量,那么为什么会出现之前的结果呢?因为我们在32位机上面测试指针大小为4个字节,int类型的大小也为4个字节,所以打印出来的结果相同。
如果我们要定义两个类型相同的指针变量就需要通过一下方式:
#include <stdio.h> int main() { char * p = NULL, * q = NULL; printf("sizeof(p) = %d\n", sizeof(p)); printf("sizeof(q) = %d\n", sizeof(q)); return 0; }打印结果为:
接下来我们来说明指针的运算,在说明指针的运算之前我们先复习一下之前运算符的部分内容: 我们已经知道在运算符中例如 i++ 和 ++i 的以及 j-- 以及 --j 的运算(关于这两个运算符需要注意的点已经在运算符博客中关于用法及注意作以说明。),这两种运算我们都能够轻松的使用,那么这里我们对于上面运算符的使用做一个简单的说明,对于我们在这里理解指针的运算有很大的帮助。
在说到i++和++i的使用的时候,我们在这里再给大家一种比较精简的验证方法大家可以自己分析结果,这里我们不给出解释,需要理解的话可以去(一维数组)博客有讲解。
#include <stdio.h> int main() { int num = 10; int num1 = 10; num++; printf("%d\t", num); printf("%d\t", num1++); printf("%d", num1); return 0; }打印结果为:
如果上面的运行结果,你不能理解的话,请停下来去看运算符篇的博客,然后看懂之后继续向下看。
不兼容类型赋值会发生类型丢失。为了避免隐式转化带来可能出现的错误,最好用强制转化显示的区别。
下面我们作以说明 C语言比较灵活。 我们在C语言这里可以直接这样写:
#include <stdio.h> int main() { int data = 0x12345678; char* p = &data; printf("%x\n", *p); return 0; }在C语言中是可以运行的,运行结果为:
上面赋值的过程只是把data的地址赋值给pc,类型还是由char来决定。我们可以认为在赋值的过程中丢失了类型。
但是在C++中必须这样:
#include <stdio.h> int main() { int data = 0x12345678; char* p = (char *)&data; printf("%x\n", *p); return 0; }打印结果为:
C++中必须使用强制类型转换,否则编译不能通过。
接下我们介绍指针的算术运算: 指针的乘法运算和除法运算时没有意义的,有兴趣的读者可以自行测试。 在这里我们说明指针的加法运算和减法运算,但是在加法运算中,我们指针+指针也是没有意义的。
指针的算术运算,不是简单的数值运算,而是一种数值加类型的运算。将指针加上或者减去某个整数值(以 n*sizeof(T)为单位进行操作的)。
我们给出指针的算数运算符及示例: 接下来我们说明指针的加法运算 首先我们给出这样的代码:
#include <stdio.h> int main() { int * p = (int*)0x0001; int pData = 0x0001; printf("p = %x p+1 = %x\n", p, p + 1); printf("pData = %x pData+1 = %x", pData, pData + 1); return 0; }我们在这里的目的就是定义一个指针变量,然后打印出来指针变量+1的结果进行分析。 打印结果为: 这里我们对比来看。 我们定义的数值加1是直接+1,指针加1的时候直接加上了4,加上了指针类型的大小,这里是int类型,占4个字节,所以+4。
我们已经说过很多次:指针就是类型+地址,地址由地址总线释放,类型代表了从当前地址的寻址能力,也就是步长(也就是指针一次+1能够变化的最小单元),那么不同的指针类型占几个字节,指针+1之后就加上几个字节。我们通过代码进行解释:
#include <stdio.h> int main() { int* p = (int*)0x0001; int pData = 0x0001; char* p1 = (char*)0x0001; short* p2 = (short*)0x0001; float* p3 = (float*)0x0001; double* p4 = (double *)0x0001; long long* p5 = (long long*)0x0001; printf("pData = %x pData+1 = %x\n", pData, pData + 1); printf("(char *)p1 = %x p1+1 = %x\n", p1, p1 + 1); printf("(short *)p2 = %x p2+1 = %x\n", p2, p2 + 1); printf("(int *)p = %x p1+1 = %x\n", p, p + 1); printf("(float *)p3 = %x p3+1 = %x\n", p3, p3 + 1); printf("(double *)p4 = %x p4+1 = %x\n", p4, p4 + 1); printf("(long long *)p5 = %x p5+1 = %x\n", p5, p5 + 1); return 0; }打印结果为: pData是一个数字 char * 类型+1 跳跃1个字节 short * 类型+1 跳跃2个字节 int * 类型+1 跳跃4个字节 float * 类型+1 跳跃4个字节 double * 类型+1 跳跃8个字节 long long * 类型+1 跳跃8个字节
我们再通过图解方式给大家解释:
代码演示:
#include <stdio.h> int main() { int* p = (int*)0x0001; int pData = 0x0001; printf("(double*)p = %x, (double*)p+1 = %x\n",(double*)p,(double*)p + 1); printf("(int)pData = %x, (int)pData+1 = %x\n",(int)pData,(int)pData + 1); return 0; }运行结果为: 上面 p 强转duoble * 类型之后指针加+1,加的是步长,也就是加上指针类型double所占字节数的大小为8个字节,所以结果为9。
pData 强转int类型之后加1,也就是数值加1,所以结果为2。
代码演示:
#include <stdio.h> int main() { int* p = (int*)0x0001; int pData = 0x0001; printf("p = %x, p+1 = %x\n", p,p + 1); printf("pData = %x, pData+ 1 = %x\n\n", pData, pData + 1); printf("(double*)p = %x, (double*)p+1 = %x\n", (double*)p, (double*)p + 1); //把 p 强制转换为double * 步长发生改变为8 printf("(int)p = %x, (int)p + 1 = %x\n\n", (int)p, (int)p + 1);//把 p 强制转换为int 步长发生改变为1 printf("++p = %x\n", ++p); printf("++pData = %x\n", ++pData); return 0; }运行结果为:
那么接下来我们说明指针的减法: 指针-1类似于指针+1,我们给简单说明:
#include <stdio.h> int main() { int* p = (int*)10; int pData = 10; printf("p = %x, p-1 = %x\n", p, p - 1); printf("pData = %x, pData - 1 = %x\n\n", pData, pData - 1); printf("(double*)p = %d, (double*)p - 1 = %d\n", (double*)p, (double*)p - 1); //把 p 强制转换为double * 步长发生改变为8 printf("(int)p = %d, (int)p - 1 = %d\n\n", (int)p, (int)p - 1);//把 p 强制转换为int 步长发生改变为1 printf("--p = %d\n", --p); printf("--pData = %d\n", --pData); return 0; }运行结果为:
下面我们看一种新的情况:
指针-指针:表示间隔的单元个数(正,负) 先算出间隔的字节数/sizeof(指针去掉一个 * )
那么如果来解释呢? 我们通过一维数组来解决:
#include <stdio.h> int main() { int arr[10];//x int* p = &arr[1];//x+4 int* q = &arr[9];//x+36 printf("%d\n", p - q);//-8 printf("%d\n", q - p);//8 printf("%d\n", (short*)q - (short*)p);//16 printf("%d\n", (long long*)q - (long long*)p);//4 printf("%d\n", (double**)q - (double**)p);//8 printf("%d\n", (char*)q - (char*)p);//32 printf("%d\n", (long)q - (long)p);//32 return 0; }我们在后面注释出来结果和我们打印出来的结果进行对比: 我们知道内存的编址顺序,当然也就很容易理解会出现负数,我们还是通过上面给出的方法计算和理解,间隔的单元个数,在数组中表示也就是中间间隔了几个数组。计算方法是,先计算出相差的字节数然后除以指针的类型大小,这里需要强调,既然是除以指针类型大小,那么这里必须首先是指针类型,然后才可以知道类型占几个字节。我们拆分上面代码逐个分析: 我们首先知道,int类型的数组,第一个元素和第九个元素相差32个字节。也就是8个int类型的变量。
printf("%d\n", p - q);//-8 printf("%d\n", q - p);//8那么这里为什么打印结果是8呢?我们刚才已经说过字节相差32个字节,当p-q,或者q-p的时候,我们就要用字节数除以去掉 * 之后类型的大小,那么我们定义指针变量时 int * p 和int * q,去掉 * 之后为int类型占4个字节,所以在计算的时候就用 32/4 = 8;所以一个结果为8,一个结果为-8。
printf("%d\n", (short*)q - (short*)p);//16 printf("%d\n", (long long*)q - (long long*)p);//4这里的我们也很容易解释: 间隔字节数为32 short去点* short大小为2个字节 所以是32/2 = 16 间隔字节数为32 long long去点* long long大小为8个字节 所以是32/8 = 4
printf("%d\n", (double**)q - (double**)p);//8 printf("%d\n", (char*)q - (char*)p);//32这里我们需要特别注意:double** 去掉一个*之后为 double * 而 double * 是4个字节 所以结果为 32/8 =8。 间隔字节数为32 char去点 * char大小为1个字节 所以是32/1 = 32
printf("%d\n", (long)q - (long)p);//32最后一点需要我们更加注意强转里面没有 * 可以去掉,那么不管里面是什么类型全部按照1字节处理所以结果为32/1 == 32
#include <stdio.h> int main() { int arr[10];//x int* p = &arr[1];//x+4 int* q = &arr[9];//x+36 printf("%d\n", (long)q - (long)p);//32 printf("%d\n", (int)q - (int)p);//32 printf("%d\n", (short)q - (short)p);//32 printf("%d\n", (char)q - (char)p);//32 return 0; }打印结果为: 得以证明。 我们这里帮助理解一下: 指针+i的含义:+i个格子,则为指针+isizeof(指针去掉一个)个字节 指针-i的含义:-i个格子,则为指针-isizeof(指针去掉一个)个字节 如果直接没有*直接当作1个字节
#include <stdio.h> int main() { int *p = (int *)2000; printf("%d\n",p+1);//2004 printf("%d\n",(char *)p+1);//2001 printf("%d\n",(long *)p+1);//2004 printf("%d\n",(float *)p+1);//2004 printf("%d\n",(double *)p+1);//2008 printf("%d\n",(short **)p+1);//2004 printf("%d\n",(char ***)p+1);//2004 printf("%d\n",(unsigned long long)p+1);//2001 return 0; }打印结果为: 再举一个例子进行理解:
#include <stdio.h> int main() { int* p = (int*)0x2010; printf("%x\n", p - 2);//2008 printf("%x\n", (short*)p - 2);//200c printf("%x\n", (long*)p - 2);//2008 printf("%x\n", (float*)p - 2);//2008 printf("%x\n", (double*)p - 2);//2000 printf("%x\n", (char**)p - 2);//2008 printf("%x\n", (char*)p - 2);//200e printf("%x\n", (unsigned long)p - 2);//200e return 0; }打印结果为: p-1 还是之前的int * 类型, char * * 去掉一个 * 之后还是一个指针类型所以大小为4字节,最后一行没有 *,所以不管是什么类型,都只是一个字节(我们之前有例子专门说明和解释)
我们已经在这里用很多例子说明这一点,读者可以多次复习查看不断理解。 我们在这里需要注意很重要的一点就是: 只有当指针指向一串连续的存储单元时,指针的移动才有意义。才可以将一个指针变量与一个整数 n 做加减运算。
我们先讨论一下指针相等的问题 指针相等有两个条件: ①类型相同 ②数值相等 我们通过代码来验证:
#include <stdio.h> int main() { int * p = (int *)0x0001; int * q = (int *)0x0005; if (p + 1 == q) { printf("p+1 = q"); } else { printf("p+1 != q"); } return 0; }打印结果为: 说明两者是相等的。
p+1本质就 p + 1 * sizeof(int) 也就是把*之前的类型大小。
我们再给出一些例子:
#include <stdio.h> int main() { char * p = (char *)0x0001; int * q = (int *)0x0001; if (p == q) { printf(" = q"); } else { printf(" != q"); } return 0; }上面代码,是写在C++里面的,我们这里只考虑是否相等,上面代码编译不能通过,因为在判断的时候p和q操作数不兼容。
在C语言中运行下面代码:
#include <stdio.h> int main() { char * p = 0x0001; int * q = 0x0001; if (p == q) { printf("p = q"); } else { printf("p != q"); } return 0; }运行结果为: **可以理解为char * 转为 int *
所以我们尽量避免不同类型的之间的比较。
我们再给出下面的例子:
#include <stdio.h> int main() { int *ptrnum1, *ptrnum2; int value = 1; ptrnum1 = &value; value += 10; ptrnum2 = &value; if (ptrnum1 == ptrnum2) printf("\n 两个指针指向同一个地址\n"); else printf("\n 两个指针指向不同的地址\n"); return 0; }打印结果为: 这个对于我们理解的程度来说已经很基础了,不管数值怎么变化,只要地址不变,两个指针指向同一个地址是可以的,就像不同的手机里面可以存同一个人的电话号码。但是不允许同一个电话号码表示两个人,否则就会出错,指针类似,通过一个指针指向不能同时指向两个地址,这也很容易理解。 指针的大小比较这里就不多说明,我们已经知道了内存的编址方式,指针的比较就很容易理解了。
有字符数组如下 char name[5] = {‘M’, ‘A’, ‘D’, ‘A’, ‘M’},判断其是否是回文串。
#include <stdio.h> #include <stdlib.h> int main() { char name[5] = { 'M', 'A', 'D', 'A', 'M' }; char* ph = &name[0]; char* pt = &name[4]; int flag = 1; //设计标志位 while (ph<=pt) { if (*ph == *pt) { ph++; pt--; } else { flag = 0; break; } } if (flag == 1) printf("回文\n"); else printf("非回文\n"); return 0; }运行结果为:
接下来我们了解以下二级指针 我们定义一级指针的时候用:
int num = 10; int * p = # printf(“%d”,*p);我们知道一级指针保存的是变量的地址,我们之前也已经提到过,指针也是变量,也有地址,那么二级指针就是存储一级指针的地址,我们通过图解方式加以理解: 我们先给出代码:
#include <stdio.h> int main() { int a = 10; int b = 20; int* p = &a; int* q = &b; int** pp = &p; int** qq = &q; printf("*p = %d\n",*p); printf("**pp = %d\n", ** pp); printf("*q = %d\n", *q); printf("**qq = %d\n",** qq); return 0; }打印结果为:
下面我们给出图解: 在这里,我们只是提出二级指针,具体的应用会在数据结构的内容说明到,这里我们首先了解知识作为补充理解,之后有很多应用,所以我们必须在开始的这一部分就要理解清楚,为之后更加深入的学习和应用做准备。
1.指针的运算只能发生在同类型或整型之间,否则会报错或是警告。 2.指针的运算,除了数值以外,还有类型在里面。
我们通常进行口述形表达时,说谁指向了谁,就是一种描述指针的指向关系。指向谁,即保存了谁的地址。
到这里所有C语言指针的基础内容就已经说明完了,整个内容比较多,读者可以多次阅读复习,不断理解。有问题的地方也欢迎大家在评论区留言一起交流学习。
注意:只有当指针指向一串连续的存储单元时,指针的移动才有意义。才可以将一 个指针变量与一个整数 n 做加减运算。