大家好,上次我们给大家带来了一个用C语言实现三子棋的小游戏,相信大家对数组以及循环也有了一些深刻的理解,那么今天,我们将开启一个新的篇章---指针。
相信计算机专业的同学都对指针这个名词并不陌生,也都知道指针是C语言中相对重要且难度较大的一块内容,当然,不要慌,相信通过熊哥今天的讲解,让你对指针的理解更上一层楼,我们话不多说,开始今天的讲解。
目录
1. 指针的定义
首先,我们要明确的一点,指针就是地址。
我们都知道,平时在写C语言的程序中,创建的变量等都存放在内存中,但是如果我们想访问这个变量,应该通过什么方式来进行访问呢?
这个时候,自然地就引出了地址的概念。恰巧我们知道,内存中的地址是连续存放的,所以我们可以通过地址来访问变量,从而进行一系列操作。
int a = 10;
int* pa = &a; // pa 是一个指针变量
// a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量
我们创建了一个整型变量,把他赋值成10,再用一个指针变量来存放a变量的地址,&符号的意思是取出a变量的地址,用一个指针变量 int* 的数据类型进行接受,我们可以打印pa来看一下内部存放的地址
如图所示,图中的值就是变量a 的地址
注意:pa是一个指针变量,变量类型为整型指针,用int* 来表示,我们平时所说的指针指的其实就是指针变量,只是为了说起来方便,其实指针就是地址,指针变量是用来存放地址(指针)的变量。
那有的同学可能会问了,那一个指针变量占多少字节呢?
实践出真知,我们可以让编译器告诉我们答案
int main()
{
int a = 10;
int* pa = &a;
printf("%d\n", sizeof(pa));
return 0;
}
我们得出结论:指针变量所占据的内存数跟不同机器的位数是有关系的,那么造成这种结果的原因是什么呢?
我们以32位的机器来举例,32位代表机器我们可以理想化机器有32根导线,那么每根导线都会产生高电平和低电平,也就是我们常说的0和1,那么总共就会有2^32次方个地址,用16进制表示就是0x00000000 - 0xFFFFFFFF。
我们同时知道,1Byte = 8bit,那么32个地址为就应该等于4个字节,所以在32位的机器下,一个指针变量的大小为4个字节就很容易理解了。
同理可得,64位的机器下,一个指针变量占据8个字节。
这里要提一下, 2^32Byte == 4GB
那么2^64Byte == 16GB,现在可以看出来32位机器和64位机器的区别了吧,同时也能理解为什么指针变量在不同的机器上所占用的内存空间也不同了吧?
总结:
1. 指针变量是用来存放地址的(无论什么,存在指针变量中的值都会被当做是地址)。
2. 指针变量的大小在32位机器下是4个字节,在64位的机器下是8个字节。
3. 指针变量的大小只跟机器的位数有关,跟其他因素无关。
2. 指针和指针类型
在这里我们谈一下指针的类型。
我们都知道,变量有不同的类型,那么指针作为一种特殊的变量,是否也有多种类型呢?
int a = 10;
*p = &a;
我们将a的地址保存在指针变量p中,那么指针变量p的类型应该是什么呢?
int* p = NULL;
char* p = NULL;
float* p = NULL;
double* p = NULL;
......
我们可以看到,指针变量的定义方式是 数据类型 + * + 变量名,*前面的数据类型就代表指针中存放的是什么类型的数据,例如:
int num = 10;
int* p1 = # // 存放整型变量的地址
double sum = 18.88;
double* p2 = ∑ // 存放浮点型变量的地址
char c = 'a';
char* p3 = &c; // 存放字符型变量的地址
以上分别是整型指针变量,浮点型指针变量以及字符型指针变量,在写代码的时候,我们要做到指针变量和他存放地址的数据类型要保持一致
那么肯定有小朋友会问,我一定要用整型指针来存放整数吗,用浮点型指针不可以吗,反正所有的指针变量大小都为4个字节。
答案是:不可以
下面让我用代码来演示一下
#include <stdio.h>
int main()
{
int a = 10;
int* p1 = &a;
char* p2 = &a;
printf("%p\n", p1);
printf("%p\n", p2);
printf("\n");
printf("%p", p1 + 1);
printf("%p", p2 + 1);
return 0;
}
让我们运行来看一下结果
我们其实能看到 p1 和 p2 里面存放的地址是相同的,这会给初学者造成一个假象,不同的指针变量可以混用,但是当我们把指针向后移动一个单位,也就是+1的时候,我们会发现 p1 + 1比 p1多了四个字节,而 p2 + 1 只比 p2 多了一个字节,这是为什么呢?
原因很简单,整型变量的地址只应该用整形指针来存放,当直接打印两个指针的值的时候,其实我们会发现没什么区别,但是当我们将指针向后移动一个单位的时候,我们就会发现,整型指针移动了四个字节,也就是一个整型变量的大小,因为你是用整型指针存储的数据啊,所以编译器默认你里面存放的就是整数,一个整数占四个字节,所以向后移动四个单位。但是当你用字符指针来存放整型变量的地址的时候,同理,编译器会认为你的字符指针里面存放的是一个一个的字符,因为在C语言中,一个字符只占一个字节,所以当你移动字符指针的时候,只向后移动一个单位。
总结:指针的类型决定了指针向前或者向后走一步有多大。
2.2 指针的解引用
同理,在指针解引用的过程中,如果使用了不恰当的指针类型,也会导致出现一些错误。
#include <stdio.h>
int main()
{
int a = 0x11223344;
int* p1 = &a;
char* p2 = (char*) & a;
*p1 = 0;
printf("%d", a);
return 0;
}
p1 是整型指针,所以对他解引用的时候可以访问4个字节,当我们打印结果,发现 a 变成了0。
相反,我们如果对字符指针 p2 进行解引用,让 *p2 = 0,会出现什么结果呢
#include <stdio.h>
int main()
{
int a = 0x11223344;
int* p1 = &a;
char* p2 = (char*) & a;
*p2 = 0;
printf("%d", a);
return 0;
}
我们不难看出,由于 a 是整型变量占据4个字节,而 p2 是一个字符指针,所以当他解引用的时候,只能操作一个字节,导致a当中的字节 ‘44’ 变成了 ‘00’
总结:指针的类型决定了在解引用的时候有多大的权限(能操作多少字节数)
3. 野指针
概念:野指针就是指向方向不确定的,未初始化的指针
3.1 野指针成因
1. 指针未初始化
#include <stdio.h>
int main()
{
int* p;
*p = 20;
return 0;
}
上图,指针变量 p 就是一个野指针
我们未对指针变量 p 进行初始化,导致在对 p 进行解引用的时候,找不到 p 所指向的内存单元,导致程序出现错误
3.2 指针越界访问
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = arr;
for (int i = 0; i <= 10; i++)
{
printf("%d", *(p + i)); // p + 10 的时候发生越界访问
}
return 0;
}
如上图所示,指针变量 p 也是一个野指针
3.2 如何规避野指针
1. 指针初始化
2. 小心指针越界
3. 指针指向的空间释放时及时置空
4. 避免返回局部变量的地址
5. 指针使用之前检查有效性
#include <stdio.h>
int main()
{
int *p = NULL;
int a = 10;
p = &a;
// 指针使用之前判断是否为空,若不为空再对其进行解引用
if(p != NULL)
{
*p = 20;
}
下面,就是今天所要讲的重中之重了-----指针运算
4. 指针运算
4.1 指针加减整数
#include <stdio.h>
int main()
{
int arr[5];
int* pa;
for (pa = &arr[0]; pa < &arr[5]; pa++)
{
*(pa++) = 0;
}
for (int i = 0; i < 5; i++) printf("%d ", arr[0]);
return 0;
}
首先,我们定义了一个长度为 5 的整型数组,然后我们定义了一个指针变量 pa 指向数组首元素的地址,通过指针的解引用来对数组内部进行赋值
相信大家已经对上述的知识掌握的非常透彻了,那么今天我想给大家带来一些好玩的
我们都知道,数组名是首元素地址,那么我们对数组的每一个元素进行访问的时候,用到的操作符是 [ ],所以我们可以得出 arr[ i ] == *(arr + i)。
那么,我们根据加法交换律就可以得出,*(arr + i) == *(i + arr)
所以我们可以得出*(i + arr) == i[ arr ]
??? 是不是看起来有一点荒谬,打破了你们的认知呢?
那么这种方式是否是合理的呢?我们让编译器帮助我们证明
#include <stdio.h>
int main()
{
// arr[i] == *(arr + i) == *(i + arr) == i[arr] Yes or No?
int arr[5] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < 5; i++)
{
printf("%d ", i[arr]);
}
return 0;
}
打印结果真的还是1 2 3 4 5,说明我们的猜想正确了。
那么我们可以从中收获到什么知识呢?
其实,在访问数组中的元素时,[ ]他只是一个操作符,并没有什么真实的含义,所以arr[ i ] == i[arr]
就好比我们进行加法运算 2 + 3 和 3 + 2 也是等价的。
4.2 指针 - 指针
我们都知道,指针是存放数组中元素的地址的,那么两个指针相减得到的值又有什么含义呢?
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int* p1 = &arr[0];
int* p2 = &arr[9];
printf("p2 - p1 = %d", p2 - p1);
return 0;
}
大家先来猜一猜,p2 - p1的值会是多少呢?
我相信一定会有两种答案,有的同学可能认为是9,因为两个指针中间经过了九个元素。
也有的同学可能认为是36,因为一个整型变量占据4个字节。
那么答案到底是多少呢?
我们可以看到,编译器给出的答案是9,于是我们可以得出结论:
两个指针相减得到的值是两个指针之间元素的个数
4.3 指针的关系运算
让我们来看这么一段代码
#include <stdio.h>
int main()
{
int values[5];
int* vp;
for (vp = &values[5]; vp > &values[0];)
{
*--vp = 0;
}
return 0;
}
那么,如果我们将它进行简化呢?
#include <stdio.h>
int main()
{
int values[5];
int* vp;
for (vp = &values[N_VALUES - 1]; vp >= &values[0]; vp--)
{
*vp = 0;
}
return 0;
}
第二段代码相比于第一段代码,就出现了一些问题。
首先我们要明确,C语言标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置进行比较,但是不允许与指向第一个元素之前的那个内存空间进行比较。
所以,第二段代码,访问了指向第一个元素之前的那个内存空间,这对编译器来说是非法的,索然大多数编译器也都能给出正确结果。
好了,今天有关C语言指针的讲解就到这里了,你学会了吗~
我们下期再见,拜拜!