前言
指针是我们学习c语言的重要环节之一,可以说学好指针,你才能学好c语言。对于很多初学者来说,指针之前的内容就是“洒洒水”,从指针开始就什么也搞不懂了。希望通过我的讲解,能够加深你对指针的理解。
一、指针是什么
首先,让我们举一个生活中的例子:假设有一个酒店,这个酒店当中有一百个房间,每一个房间都有一个唯一的编号(001,002,003......100),现在你在这个酒店订房,成交之后前台会告诉你房间的具体编号,这就便于你找到该房间然后入住。
我们将上述例子运用到计算机当中:计算机也是用类似的方式高效管理内存。内存被计算机划分为一个个的内存单元,一个单元就是一个字节。而这些内存单元就相当于酒店的房间,房间的编号就是内存的地址,也叫做指针。地址本质是一个十六进制数字,每一个字节的内存都有其唯一的地址。
二、指针变量
1.取地址操作符:&
我们都知道,在c语言中,要创建一个变量,就会申请对应字节的内存空间。比如:
int main() { int a = 10; return 0; }
我们通过内存窗口来观察一下它们的地址:
可以看到,在我们定义变量a时,向系统申请了四个字节的内存空间,然后将10这个值存入其中。这四个字节的的地址分别是:
0x012FFE0C
0x012FFE0D
0x012FFE0E
0x012FFE0F
在这四个地址当中,a的地址就是其中第一个字节的地址,也就是最小的0x012FFE0C。
那么,我们该如何得到它的地址呢?这就需要我们学习一个新的操作符——&(取地址操作符)。
我们在使用它时,在变量名之前加上&符号,就表示这个变量的地址。我们尝试打印一下变量a的地址:
int main() { int a = 10; printf("%p\n", &a);//打印地址用%p return 0; }
运行结果:
屏幕上出现了一个16进制数字,这就是它的地址。不过这个值在程序每次运行时都是不同的,因为为变量开辟的内存空间是不一定的。
2.指针变量的定义
我们刚才使用&操作符来取得变量的地址,但是为了方便后期使用,我们可以将这个地址存入到一个变量中,而这个变量就叫做指针变量。也就是说,指针变量是专门存放地址的变量。
指针变量的定义方式:
int main() { int a = 0; int* p = &a;//指针变量p当中存放a的地址 return 0; }
在定义的变量名前加一个 * 号,这个 * 号说明这是一个指针变量。接着用&取出a的地址,将其赋值给p变量即可。
注意:这里的变量怕,它的类型是 int*,说明是个整形指针变量。如果定义一个浮点型变量,就用float* 类型的指针去指向(存放该变量的地址)它。
3.解引用操作符:*
既然我们已经定义了一个指针变量,那么该如何使用它呢?这就像我们订了一间房需要入住,那就需要用钥匙。解引用操作符就相当于钥匙,通过钥匙就能通过地址找到变量,然后间接性地对变量进行操作。我们尝试使用以下解引用操作符:
int main() { int a = 0; int* p = &a; *p = 10;//解引用操作 printf("%d\n", a); return 0; }
运行结果:
不难看出,p存了a的地址,解引用操作符通过地址找到了相应的值并且改为10,此时*p就等价于a。这里要注意:解引用操作符的 * 和定义指针变量时的 * 是不一样的,定义变量时 * 只是表示它是一个指针变量。
想必你会有疑问了:想要改变a的值,直接改不就可以了嘛,为什么还要这么麻烦地定义一个指针去改它呢?我们这里写一个程序来说明指针的作用:
void swap(int a,int b) { int t = 0; t = a; a = b; b = t; } int main() { int a = 3; int b = 5; swap(a, b); printf("a=%d,b=%d", a, b); }
swap函数用于交换a和b的值,可以想一想,这个函数能否实现功能呢?让我们看看运行结果:
a和b的值并没有交换过来。为什么呢?还记得函数传参的本质吗?函数传参时,形参只是实参的一份临时拷贝,你在函数内改变形参是无法影响到实参的。要做到改变实参,我们就需要传入实参的地址,然后在函数中通过地址来找到值去改变,这叫做传址调用。让我们用代码实现一下:
void swap(int* pa, int* pb)//传入地址,用指针去接收 { int t = 0; t = *pa; *pa = *pb; *pb = t; } int main() { int a = 3; int b = 5; swap(&a, &b);//地址作为函数参数 printf("a=%d,b=%d", a, b); }
运行结果:
可以看到,a和b的值交换过来了。
4.指针变量的大小
接下来,我们来探讨一下指针变量的大小。指针变量既然是一个变量,那么它也肯定是占用内存空间的,它占用的内存大小是多少呢?我们使用sizeof操作符查看一下:
int main() { printf("%zd\n", sizeof(char*)); printf("%zd\n", sizeof(short*)); printf("%zd\n", sizeof(int*)); printf("%zd\n", sizeof(float*)); return 0; }
首先在X86环境下运行:
然后再X64环境下运行:
可以看出,在X86环境下,指针无论是什么类型,它的大小都是4个字节,而X64环境下大小是8个字节。所以指针的大小和它的类型是无关的。
既然指针大小与类型无关,那为什么还有这么多种类型的指针变量?其实,指针变量类型是有它独特的意义的。
三、指针类型的意义
1.指针解引用访问的字节数
我们写一段代码,观察一下不同类型指针指向相同类型变量的结果:
int main() { int a = 0x11223344;//这里设置成十六进制数字方便调试观察 int b = 0x11223344; int* pa = &a; char* pb = (char*)&b;//用char指针接收int型变量,为保证严谨性将地址强制转换为char*类型 *pa = 0x0; *pb = 0x0; printf("a=%#x b=%#x", a, b);//%#x可以在输出的十六进制数字前带0x return 0; }
运行结果:
可以发现,a的值直接被改为0,而b的最后两位被改为了0。这里要注意:这是一个十六进制数字,十六进制数字的一位就代表四位二进制数字,两位被改成0,就说明八位2进制数字被改成0,而八位刚好是一个字节。这说明我们使用char*类型的指针去修改变量的值时,修改的是一个字节的内容。而int*类型的指针修改了四个字节。
结论:指针变量的类型决定了它访问变量时的权限(能够操作的字节个数)。
为了便于让我们理解,我们画图表示一下:
2.指针+-整数
我们写一段代码观察对指针加或减一个整数的效果:
int main() { int a = 0; char* p1 = (char*)&a; int* p2 = &a; printf("%p\n", p1); printf("%p\n", p2); printf("%p\n", p1 + 1); printf("%p\n", p2 + 1); return 0; }
运行结果:
不难看出,char型的指针+1,它存放的地址就+1,相当于就向前走了一个字节,而int型指针+1就走了4个字节。
结论:指针变量的类型决定了它向前或者向后走的步长有多大。
四、const修饰
1.const修饰变量
在我们编写程序的时候,如果我们想要一个变量不可被修改,那么就可以在变量定义时加一个const,例如:
可以看到,当我想要重新给a赋值时,会报错。
但是如果我们使用指针去修改呢?
int main() { const int a = 0; int* p = &a; *p = 10; printf("%d\n", a); return 0; }
运行结果:
如果使用指针去间接修改a的值,const就形同虚设了。很显然,这样做就相当于是锁了门却从窗户爬出去的行为。那么,我们是否可以对指针变量使用const修饰,关上“这扇窗”呢?
2.const修饰指针
const修饰指针变量的形式有三种,分别是:
const int * p;//const放在 * 前边 int * const p;//const放在 * 后边 const int * const p;// * 前后都放const
2.1 const放在 * 前面
const放在 * 前面时,指针所指向的内容不能改变,但是指针变量可以改变。例如:
想要通过指针去修改变量a的值就会报错。
2.2 const放在 * 后面
const放在 * 后面时,指针所指向的内容可以改变,但是指针变量不能改变。例如:
当使用指针去指向变量b时,就会报错。
3.3 * 左右两边都放const
当 * 左右两边都放const时,指针变量本身和其所指向的内容都不能改变。例如:
可以看到,无论想要通过指针改变a的值,还是让指针指向b,都是无法进行的。
五、指针的运算
接下来,我们学习一下指针的三种运算方式,便于我们加深对指针的理解,易于上手指针操作。
1.指针+-整数
刚才已经提到,指针+-整数可以跳过特定单位的字节。那么它的实际作用是什么?我们都知道,数组中的元素在内存中是连续存放的,那么我们就可以利用指针跳过特定字节的特性,来访问数组当中的每一个元素。示例如下:
int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int sz = sizeof(arr) / sizeof(arr[0]); int* p = &arr[0];//p中存放第一个元素的地址 int i = 0; for (i = 0; i < sz; i++) { printf("%d ", *p);//通过指针访问元素并打印 p++;//指针自增1,跳过一个整形元素(4个字节) } return 0; }
我们通过循环的方式,运用指针访问元素并且打印,之后让指针自增1,就跳过了四个字节,也就是一个整形元素,就可以访问下一个元素了。
以上代码也可以这样写:
int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int sz = sizeof(arr) / sizeof(arr[0]); int* p = &arr[0]; int i = 0; for (i = 0; i < sz; i++) { printf("%d ", *(p + i)); } return 0; }
指针变量p先加i之后再解引用,就能跳过特定数量来访问元素。
注意:第一段代码中指针变量p由于自增,所以它在循环结束之后已经不再指向第一个元素了,如果要重新访问这个数组,就需要先将第一个元素的地址赋值给p,防止越界访问。
2.指针-指针
指针-指针,也就是两个地址相减,得到的是两个指针之间的元素个数。
注意:在进行指针相减运算时,一定要保证这两个指针指向的是一块连续的空间,比如数组。否则就是无意义的。
举个例子:
int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int* p1 = &arr[0]; int* p2 = &arr[9]; printf("%d\n", p2 - p1); return 0; }
运行结果:
在数组arr当中,从第一个元素的地址开始,到第十个元素,中间确实有九个元素。
既然有指针-指针的操作,那么有没有指针+指针呢?这个问题很简单,比如我们有一本日历,我们要算算两个日期之间有多少天,那么就用大的日期减去小的日期。但是两个日期相加,就是没有意义的。同样的,指针+指针也是无意义的。
3.指针的关系运算(大小比较)
由于地址有大小,所以指针也可以进行大小比较。我们改造一下刚才打印数组元素的代码:
int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int* p = &arr[0]; while (p <= &arr[9])//指针的关系运算 { printf("%d ", *p); p++; } return 0; }
可以看出,指针的相关操作还是十分灵活的。
六、野指针
接下来我们学习一个概念:野指针。
1.野指针的概念
如果一个指针指向的位置不可知或者并非自己申请的,那么它就是一个野指针。
2.野指针出现的情况
2.1 指针变量未初始化
int main() { int a = 0; int* p;//未初始化的指针 *p = 20;//使用了野指针 return 0; }
2.2 使用指针越界访问
int main() { int arr[5] = { 0 }; int* p = &arr[10]; *p = 20;//越界访问,此时p为野指针 return 0; }
2.3 指针指向的空间已经释放
int* fun()//int*表示函数返回值是一个int*类型的地址 { int a = 0; int* p = &a; return p; } int main() { int* p = fun();//函数退出,返回的地址已经被释放 printf("%d\n", *p); return 0; }
野指针总是会在不经意的地方出现,并可能产生意想不到的后果。我们在实际编程的时候,要学会规避野指针。
3.如何规避野指针
3.1 指针初始化赋值NULL
在指针变量初始化时,如果不知道存入谁的地址,那么就先制成空指针(NULL)。
NULL是C语言中定义的一个常量,它的值是0,同时也是一个地址,表示内存地址为0的地方。0地址处的空间是不可使用的。如果对NULL进行解引用操作,就会发生报错。
例如:
int *p = NULL;
3.2 防止指针越界
在使用指针访问一片连续的内存时,一定要检查指针是否会越界,防止出现野指针。
3.3 避免函数返回局部变量的地址
我们在定义一个函数的时候,要避免返回局部变量的地址,这样就不会在外部函数中出现使用野指针的情况。
总结
以上就是对指针的定义和基本操作,真正运用指针的方式还有很多很多。后续博主还会继续和大家探索指针的更多知识。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤