大家好,我是苏貝,本篇博客带大家了解C语言中令人头疼的指针,如果大家觉得我写的不错的话,可以给我一个赞吗,感谢❤️
使用的是VS2019编译器,默认为32位平台
指针理解的2个要点:
指针变量:
我们可以通过&(取地址操作符)取出变量的内存起始地址,把地址可以存放到一个变量中,这个变量就是指针变量
要知道
1.内存被划分为一个个的内存单元,每个内存单元的大小是一个字节
2.每个字节的内存单元都有一个编号,这个编号就是地址,地址在C语言中被称为指针
3.每个内存单元都有唯一的地址来标识
4.地址要存储的话,存放在指针变量中
5.在32位机器上,地址的大小为4个字节,所以指针变量的大小也为4个字节
在64位机器上,地址的大小为8个字节,所以指针变量的大小也为8个字节
对第5点进行解释
对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0);
那么32根地址线产生的地址就会是:
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
…
11111111 11111111 11111111 11111111
每个地址有32个比特位,即32/8=4个字节
同理:对于64位的机器,有8个字节
那32和64位机器能编址多大空间呢?
由上方解释可知,32位机器就有2的32次方个地址。每个地址标识一个字节,那我们就可以给 (2^ 32Byte = 2^ 32/1024KB =2^ 32/1024/1024MB=2^32/1024/1024/1024GB = 4GB) 4G的空闲进行编址。
同理,64位机器就可以给 2^ 64Byte 的空闲进行编址。
这里我们在讨论一下:指针的类型
我们都知道,变量有不同的类型,整形,浮点型等。那指针有没有类型呢?
准确的说:有的。当有这样的代码:
(1) int* p;
(2) char* p;
(3) int** p;
(1)* 代表p是指针变量,int* 是指针变量p的类型,int 是p所指向的类型,即p所指向的是int类型的变量
(2)* 代表p是指针变量,char* 是指针变量p的类型,int 是p所指向的类型
(3)离p最近的 * 代表p是指针变量,int* * 是指针变量p的类型,int* 是p所指向的类型
这里的&是取地址操作符,*是间接访问操作符
int a=10;
int* p=&a;
p=&a的意思是:用&操作符取出a的地址,放在指针变量p中,指针变量的类型是 a的类型 + 一个 * 号,例如:a是int类型的,p的类型为int * ; a是char类型的,p的类型为char * ……
int a=10;
int *p=&a;
*p=0;
*p=0;意思是通过 *间接访问操作符解引用找到p指向的那个变量并将之赋值为0(即将0赋值给a)*p==a
int main()
{
int a = 10;//在内存中开辟一块空间存储a
int* p = &a;//用&操作符取出a的地址,放在指针变量p中
*p = 20;//p通过*解引用找到a并将之赋值为20,*p==a
printf("a=%d\n", a);
return 0;
}
//a=20
先看下面代码的结果,正如我们上面所说的,在32位平台下地址的大小为4个字节,那既然大家字节数都一样,为什么还要区分char* ,int* 等类型呢?直接用一个笼统的类型如all*类型不可以吗?
答案是不可以。为什么呢?
因为指针类型是有意义的,指针类型决定了指针进行解引用操作时访问几个字节
示例1:
因为下图的p是整型指针,指向的是一个int类型的变量,所以解引用时访问4个字节,所以在进行*p=0操作后,0x11223344全部变成0
示例2:
将上图的int* p=&a;改为char* p=&a;
因为下图的p是 char* 指针,指向的是一个char类型的变量,所以解引用时访问1个字节,所以在进行*p=0操作后,0x11223344全部变成0x11223300
示例3:
指针类型决定了指针 +1 / -1跳过几个字节(与指针类型进行解引用操作时访问的字节数相同)
总结
指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
比如: char* 的指针解引用就只能访问1个字节,而 int* 的指针的解引用就能访问4个字节,double* 的指针解引用就只能访问8个字节……
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
(1). 指针未初始化:
指针未初始化,默认为随机值
int main()
{
int* p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
(2).指针越界访问:
数组arr只有10个元素,但却循环了11次,循环第11次时,针指向的范围超出数组arr的范围时,p成为野指针
解释*p++ = i ;
++(后缀自增操作符)的优先级高于*(间接访问操作符),所以p先后置++,再与 * 结合,但由于后置++是先使用再自增,所以执行*p=i操作后再自增,所以数组首元素被赋值为0后p自增1,使得p指向数组的第二个元素(跳了4个字节,原因在上面指针类型)
int main()
{
int arr[10] = { 0 };
int* p = arr;//p指向arr的首元素
int i = 0;
for (i = 0; i <= 10; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*p++ = i;
}
return 0;
}
(3).指针指向的空间释放:
a是函数内的局部变量,在函数调用结束后,a所开辟的内存空间会被释放,即使在释放前将a的地址传了出去,但p接收地址的时候a所开辟的内存空间已被释放,此时再用* 对p进行解引用,即使找到了该地址也属于非法访问
int* text()
{
int a = 10;
return &a;
}
int main()
{
int* p = text();
//p就是野指针
printf("*p=%d", *p);
return 0;
}
int main()
{
int* p = NULL;//1
//....
int a = 10;
p = &a;
if (p != NULL)//5
{
*p = 20;
}
return 0;
}
int main()
{
int arr[9] = { 1,2,3,4,5,6,7,8,9 };
//使用指针打印数组的内容
int* p = arr;//arr是首元素地址,所以p指向数组首元素
int i = 0;
for (i = 0; i < 9; i++)
{
printf("%d ", *(p + i));
//p指向数组首元素,即下标为0的元素
//p+i指向数组下标为i的元素
//*(p+i)是通过*解引用找到下标为i的元素
//p+i 与p相比,跳过了i*sizeof(int)个字节
}
return 0;
}
//1 2 3 4 5 6 7 8 9
上面arr是首元素地址,指针变量p里面存的也是首元素地址,所以arr==p
因此arr+i == p+i, arr[i] == p[i](下标为i的元素的地址)
*(arr+i) == *(p+i) = arr[i]
下面表达式中,左边数组的这种形式在编译器处理时会转化为右边的表达式,又因为[ ] 和+ 一样只是一个操作符,所以i写在[ ]里面或外面都可以
arr[ i ] == *(arr+i) //当然,更建议写成 arr[ i ] 这种形式
i [ arr] == *(i+arr)
拓展:
数组名是数组首元素地址,除了以下2种情况:
(1)sizeof(数组名),此时的数组名代表整个数组,所以计算结果是整个数组的大小
(2)&数组名,此时的数组名也代表整个数组,取出的是整个数组的地址,返回的是数组首元素的地址,但绝不代表&数组名==arr(首元素地址),如下:
arr和&arr[0]等价,都是首元素地址,类型是int*,所以+1跳过4个字节
&arr是取出了整个数组的地址,但返回首元素地址,所以+1跳过整个数组即10* 4=40个字节
sizeof(arr)计算结果是整个数组的大小即10* 4=40个字节
指针-指针的前提:两个指针指向同一块区域,指针类型相同
指针-指针差值的绝对值是:指针和指针之间的元素个数
了解了这些之后,我们是不是又多了一种计算字符个数的功能的自定义函数my_strlen()实现的方法
点击链接了解:自定义实现strlen函数的3种方法
请看下面代码:
int main()
{
int arr[5];
int* vp = NULL;
for (vp = &arr[5]; vp > &arr[0];)
{
*--vp = 0;
}
return 0;
}
若不想一开始就越界,有人可能会修改成这样的代码:
int main()
{
int arr[5];
int* vp = NULL;
for (vp = &arr[4]; vp >= &arr[0];vp--)
{
*vp = 0;
}
return 0;
}
那这两种方法哪个更好呢?答案是第一种,第二种在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较
指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?
存放在 二级指针 中
*pp 通过对pp中的地址进行解引用,这样找到的是 p , *pp 其实访问的就是 p
**pp 先通过 *pp 找到 p ,然后对 p 进行解引用操作: *p ,那找到的是 a
int main()
{
int a = 10;
int* p = NULL;
int** pp = NULL;
*pp = &a;//等价于 p = &b;
**pp = 30;
//等价于*p = 30;
//等价于a = 30;
return 0;
}
指针就是指针,指针变量就是一个变量,存放的是地址,指针变量的大小取决与32位平台还是64位平台
数组就是数组,可以存放一组相同类型的数,数组的大小取决于元素的类型和个数
数组的数组名是首元素地址,通过指针可以访问一个数组的每一个元素
int main()
{
int arr[5] = { 1,2,3,4,5 };
int* p = arr;
//p=arr
//arr[i]=p[i]=*(arr+i)=*(p+i)都是下标为i的元素
int i = 0;
for (i = 0; i < 5; i++)
{
printf("%d ", p[i]);//arr[i],*(arr+i),*(p+i)
}
return 0;
}
//1 2 3 4 5
指针数组是指针还是数组?我们知道,整型数组是数组,存放的是int 类型的元素;字符数组是数组,存放的是char类型的元素……
指针数组是数组,是存放指针的数组
例如:int* arr[5];
arr是数组名,5代表数组有5个元素,int* 代表每个元素都是一个整型指针