C语言第十三课:初阶指针
目录
前言:
一、指针是什么:
1.那么指针到底是什么呢?
2.内存中的数据存储原理:
3.数据存储与指针使用实例:
4.存储编址原理:
二、指针和指针类型:
1.决定了指针的步长:
2.决定了对指针进行解引用时的权限大小:
三、野指针:
1.野指针的成因:
①.指针未初始化:
②.指针越界访问:
③.指针指向的空间被释放:
2.如何规避野指针:
四、指针运算:
1.指针+/-整数:
2.指针-指针:
3.指针的关系运算:
五、指针和数组:
六、二级指针:
七、指针数组:
八、总结:
前言:
在前面的三篇文章中,我和各位小伙伴们一起细致的学习了各种常用操作符的用法、属性以及各种常见的错误等等相关知识点。在掌握了操作符的相关使用后,本文我们将要一起学习关于指针的新知识。
一、指针是什么:
1.那么指针到底是什么呢?
在这里,关于指针的理解有两个要点:
1.指针是内存中一个最小单元的编号,也就是地址;
2.平时我们口中所说的指针,通常指的是指针变量,是用来存放内存地址的变量。
总结:指针就是地址,口语中的指针指的是指针变量。
2.内存中的数据存储原理:
我们的内存就如同下图,当我们使用内存进行存储时,存储空间被划分为一个一个的小的存储空间,在使用时按照我们的需求进行内存分配,而这个过程中被划分为最小储存单元的每一个存储单元的大小为一个字节,且每一个存储单元都按照顺序依次拥有自己的十六进制内存编号,即各个存储单元的存储地址:
内存 | 十六进制内存编号(地址) |
---|---|
一个字节 | 0xFFFFFFFF |
一个字节 | 0xFFFFFFFE |
一个字节 | 0xFFFFFFFD |
. . . . . . | . . . . . . |
一个字节 | 0x00000002 |
一个字节 | 0x00000001 |
一个字节 | 0x00000000 |
3.数据存储与指针使用实例:
例如下述代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int a = 10;
//定义整形变量a,在内存中的存储占用四个字节(连续的)的空间
int* b = &a;
//定义指针变量b,用取地址操作符&获取变量a的地址,并将其储存在指针变量b中
//整型变量a的存储空间大小为四个字节,这里是取出其中的第一个字节地址放入
//因为存储空间的分配是连续的,获取到第一个字节地址即可找到全部数据
printf("变量 a 的 值 为:%d\n", a);
printf("变量 a 的 存储地址 为:%p\n", b);
return 0;
}
我们看到我们的程序成功的打印出了变量a的值,也成功打印出了存储在指针变量b中的变量a的存储地址:
而这段代码的实现过程是这样的,当我们定义了一个整型变量a后,便在内存中为我们定义的整型变量分配了一块空间用于存储整型变量a的数据,而我们都知道,一个整型变量的大小是四个字节,也就是说此时在内存中为我们的整型变量a分配了一片大小为四个字节的空间用来存储数据。而后我们再使用取地址操作符获取到了整形变量a在内存中的存储空间的地址,要注意的是在这里使用取地址操作符&取出的是整型变量a的存储空间(共四个字节)中第一个字节的地址。接着我们定义了一个指针变量b接收并存储了我们获取到的地址,并最终将整形变量a与指针变量b中各自存储的数据打印出来:
内存 | 十六进制内存编号(地址) | |
---|---|---|
一个字节 | 0xFFFFFFFF | |
一个字节 | 0xFFFFFFFE | |
一个字节 | 0xFFFFFFFD | |
. . . | . . . | |
整型变量 int a | 第四字节 | 0x0012FF43 |
第三字节 | 0x0012FF42 | |
第二字节 | 0x0012FF41 | |
第一字节 | 0x0012FF40(指针变量存储) | |
. . . | . . . | |
一个字节 | 0x00000002 | |
一个字节 | 0x00000001 | |
一个字节 | 0x00000000 |
4.存储编址原理:
经过上面实例的演示和讲解,我们就能理解内存中的存储与指针的使用原理了,我们也知道了指针变量就是用来存储地址的变量(存储在指针变量中的值都将被作为地址进行处理)。那么在内存中我们的计算机到底是如何进行编址的呢?
对于32位的机器,我们可以假设它有32根地址线,那么假设每根地址线在进行寻址时,都产生高电平(高电压)与低电平(低电压),则每一位上都将是对应的1或0,这也就是计算机内存进行数据存储采用二进制的原理了。且此时32根地址线产生的地址就将是:
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
00000000 00000000 00000000 00000010
........ ........ ........ ........
11111111 11111111 11111111 11111111
共32位,且每一位上均为1或0两种可能,则在内存中将存在2^32个地址。
同时我们也都知道8比特位为1个字节,而每个地址32位又恰好是四个字节,即一个整型变量的大小。并且我们在第三课中讲解数据类型时为各位小伙伴们介绍过,整形int(分正负,有符号位)的取值范围是-2147483648到2147483647,而这32位除去最前面的一位最作为符号位外共有31位,每一位上有1或0两种可能,则共可以表示2^31个数字即共2147483648个,又恰好对应了0到2147483647的取值范围,负值部分同样如此,对应了-1到-2147483648的2147483648个数字。它们相互印证,不谋而合。
每一个地址标识一个字节,则我们就可以给2^32Byte即4GB大小的空间进行编址:
2^32Byte = 2^32/1024KB = 2^32/1024^2MB = 2^32/1024^3GB = 4GB
则使用同样的编码方式,在64位的机器上,有64根地址线,就可以对的空间进行编址。
到这里我们就明白:
★在32位机器上,地址为32个0或1组成的二进制序列,则地址就需要32比特位即4个字节的空间来进行存储。即在32位机器中一个指针变量的大小为4个字节
★在64位机器上,地址为64个0或1组成的二进制序列,则地址就需要64比特位即8个字节的空间来进行存储。即在64位机器中一个指针变量的大小为8个字节
而由这个原理所得出的结论也与我们之前所学习的相关知识相一致。
总结:
· 指针变量是用来存放地址的,地址是唯一标示一个内存单元的。
· 指针变量的大小在32位平台是4个字节,在64位平台是8个字节。
二、指针和指针类型:
我们都知道变量有许多中不同的类型,例如整形、浮点型等等,那么指针有没有类型之分呢?准确的来说:有的。
不同类型的指针变量的定义方式相同,均为 type + * :
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
char* pc = NULL;
int* pi = NULL;
short* ps = NULL;
long* pl = NULL;
float* pf = NULL;
double* pd = NULL;
return 0;
}
即在一般情况下,不同类型的指针变量用于存放不同类型数据的地址,但不管何种类型的指针变量内部,存储的都是数据的地址。那么既然都是用来存放地址的,这么多不同种类的指针类型存在的意义是什么呢?
其实不同类型的指针存在的意义主要是以下两点:
1.决定了指针的步长:
我们来看下面这段代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int n = 10;
int* pi = &n;
char* pc = (char*)&n;
//强制类型转换,n原本为int类型
printf("int型 n 的存储地址为:%p\n", &n);
printf("\n");
printf(" pi 指向的存储地址为:%p\n", pi);
printf("pi+1指向的存储地址为:%p\n", pi + 1);
printf("\n");
printf(" pc 指向的存储地址为:%p\n", pc);
printf("pc+1指向的存储地址为:%p\n", pc + 1);
return 0;
}
在这段代码中,我们看到,整型指针 pi 和经过强制类型转换后的字符型指针 pc 都获取并存储了变量 n 的存储地址,在这一点上没有区别。但是我们还看到,将 pi 与 pc 分别+1后,即分别让两个指针各向后“走一步”时,它们所跳过的内存空间不同,整型指针跳过了四个字节,而字符型指针只跳过了一个字节的空间:
这就是不同指针类型的第一个意义:决定了指针的步长。
2.决定了对指针进行解引用时的权限大小:
同样的我们结合代码实例来进行研究:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int n = 0x11223344;
int m = 0x11223344;
//此处不是书写错误,开头的0x表示十六进制整型
//int类型每次访问四个字节
int* pi = &n;
char* pc = (char*)&m;
*pi = 0;
//使用解引用操作符修改指针pi内存放地址对应地址内的数据
printf("变量 n 的值为:%d\n", n);
*pc = 0;
//使用解引用操作符修改指针pc内存放地址对应地址内的数据
printf("变量 m 的值为:%d\n", m);
return 0;
}
在这段代码中,指针 pi 为整型指针变量,pc 为字符型指针变量,变量n与变量m中存储的数据完全相同,均为十六进制的11223344,转换为十进制为287454020,接着我们让整型指针pi获取并存存储变量n的地址,让字符型指针获取并存储变量m的地址,然后我们通过使用解引用操作符,来修改不同类型指针变量 pi 与 pc 中存储的地址所对应地址内存储的数据。最后将被修改过的变量n与变量m的值打印出来进行反馈。我们观察到,n的值被修改为0,而m则被修改为了287453952(十六进制的11223300),即整个int类型的四个字节中仅有一个字节44被修改为了00。所以我们就知道,不同的指针类型决定了在进行一次操作时所能操作的空间大小(字节数):
这就是不同指针类型的另一个意义:决定了对指针进行解引用时的权限大小。
三、野指针:
顾名思义,野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。
1.野指针的成因:
①.指针未初始化:
指针未经初始化极易引发错误:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int* p;
//局部变量未初始化,默认为随机值
*p = 20;
return 0;
}
②.指针越界访问:
当我们使用数组时,若未仔细考量在数组使用中的指针指向问题,很有可能会导致指针越界的访问:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[5] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i <= 5; i++)
{
*(p + i) = i;
//当指针指向的范围超出数组啊让人的范围时,p将变为野指针
}
return 0;
}
③.指针指向的空间被释放:
这也是一类常见的野指针成因,由于牵扯到部分我们没有接触到过的知识,过这里仅做了解,将会在学习动态内存开辟时进行讲解。
2.如何规避野指针:
我们都知道野指针危害巨大,会对我们的程序造成巨大的负面影响,那么我们在便携性程序时就应当多多注意,尽可能地避免要求指针的出,我们主要从以下五个方面入手:
①.使用前及时将指针初始化
②.使用数组时,时刻小心指针越界访问
③.指针指向空间被释放,应及时置空
④.避免返回局部变量的地址
⑤.指针使用之前应当检查其有效性
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int* p1 = NULL;
int* p2 = NULL;
//及时进行初始化
int a = 10;
int arr[5] = { 0 };
p1 = &a;
p2 = arr;
int i = 0;
for (i = 0; i < 5; i++)
//使用数组时,时刻小心指针越界访问
{
*(p2++) = i;
printf("数组元素 arr[%d] 的值为:%d\n", i, arr[i]);
}
if (p1 != NULL)
//指针使用前检查其有效性
{
*p1 = 20;
}
printf("变量 a 的值为:%d\n", a);
return 0;
}
当每一个细节都注意到后,我们的程序成功且正确运行的可能性就大了许多:
四、指针运算:
指针的运算主要可以分为三类:指针+/-整数、指针-指针、指针的关系运算。
1.指针+/-整数:
我们再来看下面这一段代码:
#define _CRT_SECURE_NO_WARNINGS 1
#define N_VALUES 5
#include<stdio.h>
int main()
{
float values[N_VALUES];
float* vp;
for (vp = &values[0]; vp < &values[N_VALUES];)
{
*vp++ = 0;
//++的优先级高于*,且为后置++
//这里的起到的作用即为将每个数组元素均置0
}
int i = 0;
for (i = 0; i < N_VALUES; i++)
{
printf("数组元素 values[%d] 的值为:%d\n", i, values[i]);
}
return 0;
}
在这段代码中,置零功能的每次循环中的*vp++,即表示在每次使用并执行后,使指针vp继续向后按照float类型的步长指向一步:
即指针+/-整数表示:该指针按照当前指针类型所对应的步长,向后/前指向一(或多)步。
2.指针-指针:
指针-指针有一个前提要求:两只真均指向同一块内存空间。
有了这个前提,我们来看代码:
#define _CRT_SECURE_NO_WARNINGS 1
#define N_VALUES 5
#include<stdio.h>
int main()
{
int arr[N_VALUES] = { 0 };
int* p1 = &arr[0];
int* p2 = &arr[4];
//两指针指向同一块内存空间
printf("指针p2 - 指针p1的值为:%d", p2 - p1);
return 0;
}
在这段代码中我们使指针p1与指针p2均指向了数组arr在内存中所占的空间,只不过指针p1指向数组arr的首元素地址,而p2则指向了数组arr的尾元素地址,并将两指针相减后的结果打印反馈给了我们:
即指针-指针表示:得到的是两指针间的数据元素个数。
3.指针的关系运算:
通过指针的关系运算,我们可以将我们的代码进行简化:
#define _CRT_SECURE_NO_WARNINGS 1
#define N_VALUES 5
#include<stdio.h>
int main()
{
float values[N_VALUES];
float* vp;
for (vp = &values[N_VALUES]; vp > &values[0];)
{
*--vp = 0;
}
//通过指针的关系运算,可以将我们的代码简化为:
for (vp = &values[N_VALUES - 1]; vp > &values[0]; vp--)
{
*vp = 0;
}
return 0;
}
上述这段代码中两个循环语句的不同不仅仅在于指针进行减一操作的书写位置不同,更导致造成了需要对指针vp进行关系运算。
而实际上,这段代码在绝大部分编译器上都是可以顺利完成任务的,但我们在书写代码时还是应当尽可能的避免这样去书写,因为标准并不保证其可行:
★标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
五、指针和数组:
为了研究指针与数组间的关系,我们来看一个例子:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[5] = { 1,2,3,4,5, };
int* p1 = &arr;
int* p2 = &arr[0];
printf(" arr 指向的地址为:%p\n", arr);
printf("&arr[0] 指向的地址为:%p\n", &arr[0]);
printf("\n");
printf("指针 p1 指向的地址为:%p\n", p1);
printf("指针 p2 指向的地址为:%p\n", p2);
return 0;
}
运行结果显示,数组名与数组首元素地址是一样的,即数组名表示的就是数组首元素的地址(有两种特殊情况,在数组章节已经讲解过了):
而既然可以把数组名作为地址存放到一个指针当中,那我们使用指针来访问数组元素就成为了可能:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("数组arr[%d]的存储地址为:%p <====> p+%d 指向的地址为:%p\n", i, &arr[i], i, p + i);
}
return 0;
}
运行结果验证了我们的想法是正确可行的:
所以上述示例中的 p+i 就计算的是数组arr下标为 i 的地址。那么我们就可以直接通过指针来访问数组:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("数组元素 arr[%d] 的值为:%d\n", i, *(p + i));
}
return 0;
}
最终结果是访问成功:
六、二级指针:
那我们还有一个问题,指针变量它也是变量,而变量都有自己的地址,那么指针变量的地址该存放在哪里?答案是:二级指针:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int a = 10;
int* pa = &a;
//整型指针pa中存放的即为变量a的地址
int** pp = &pa;
//二级指针pp中存放的即为指针变量pa的地址
printf("变量 a 的值为:%d\n", **pp);
return 0;
}
运行结果:
则对于二级指针的运算还有:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int b = 20;
int* pb = &b;
printf("*p 指向空间内存储的值为:%d\n", *pb);
int** pp = &pb;
printf("**pp指向空间内存储的值为:%d\n", **pp);
**pp = 50;
//等价于*pa=50
//等价于a=50
printf("变 量 b 的 值 为:%d\n", b);
return 0;
}
其中 **pp 先通过 *pp 找到 pb,然后再对 pb 进行解引用操作,即 *pb,找到了 a,最终对a进行了重新赋值操作:
七、指针数组:
在数组章节中,我们已经知道了存在着整型数组、字符数组等等,那指针数组又是怎样的呢?指针数组究竟是指针还是数组呢?答案是:是数组。
指针数组的本质仍为数组,它是用来存放指针的数组:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int a = 0;
int b = 10;
int c = 20;
int d = 30;
int e = 40;
//定义五个0变量
int* arr[5] = { &a,&b,&c,&d,&e };
//定义指针数组
//使用指针数组进行打印:
int i = 0;
for (i = 0; i < 5; i++)
{
printf("数组元素arr[%d]中指针指向的空间存放的值为:%2d\n", i, *arr[i]);
}
return 0;
}
运行结果:
并且在这里我们千万千万要特别注意的是,指针数组中存放的各地址不一定是连续的!同样的我们写出代码,用事实来进行验证:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int a = 0;
int b = 10;
int c = 20;
int d = 30;
int e = 40;
//定义五个变量
int* arr[5] = { &a,&b,&c,&d,&e };
//定义指针数组
//使用指针数组进行打印:
int i = 0;
for (i = 0; i < 5; i++)
{
printf("数组元素arr[%d]的存储地址为:%p\n", i, arr[i]);
}
return 0;
}
由于指针数组内存放的是一系列的地址,而各地址又取决于各个常/变量在创建时内存进行的分配,所以指针数组内部存放的地址不一定是连续的:
八、总结:
至此,关于指针的介绍就结束啦,指针在我们进行内存访问时经常会用到,因此各位小伙伴们在课余时间最好能够多多练习,熟练的掌握关于指针的相关知识,争取能够在编写程序时驾轻就熟的使用指针工具,为自己的代码写作加油助力。
不知道各位小伙伴们从今天的文章中又学到了哪些知识呢?有没有哪怕一丝的进步呢?哪怕只进步一点点,今天的你也是成功的,而非碌碌无为。要么你去驾驭生命,要么是生命驾驭你,你的心态决定谁是坐骑,谁是骑师,心态的不同必然导致人格和作为的不同,因而也会谱写不同的人生!
新人初来乍到,辛苦各位小伙伴们动动小手,三连走一走 ~ ~ ~ 最后,本文仍有许多不足之处,欢迎各位看官老爷随时私信批评指正!