梦开始的地方—— C语言指针入门


指针入门

1.指针概念

指针(Pointer) 是编程语言中的一个对象,利用地址,它的值直接指向存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元 。

指针是个变量,存放内存单元的地址(编号) ,一个内存单元的大小是一字节,也就是8比特,是一串8比特的二进制数。

#include <stdio.h>
int main()
{
	int num = 10;
	int* p = &num; //取出num的地址,p里面存放的就是num变量的首地址


	return 0;
}

在这里插入图片描述

在32位的机器上存在着32根地址线,64位机器上存在着64根地址线。每根地址线对应1个比特位,通电后产生了0和1的正电和负电,对应着二进制位。也就是说32位机器上一个地址是32个比特位,也就是4个字节。而64位机器上地址是64个比特位,也就是8个字节

我们知道指针存放的是地址,那么这样说的话32位机器和64位机器上的指针大小是不一样的。

  • 在32位机器上,有32根地址线,所以指针大小要4个字节才能存放地址
  • 在64位机器上,如果有64根地址线,那么指针要是8个字节才能把地址存放起来

2. 指针和指针类型

我们都知道变量有不同的类型,那么指针也是有不同的类型的。

	int* p1 = NULL;
	short* p2 = NULL;
	float* p3 = NULL;
	double* p3 = NULL;
	char* p4 = NULL;
	long* p5 = NULL;

以上可以看到,指针变量的定义是 type+*,分别对应着各种变量的类型。

那么指针类型的意义又是什么呢?

来看代码

#include <stdio.h>
int main()
{
	int* a = 10;
	int* p1 = &a;
	char c = '1';
	char* p2 = &c;

	printf("%p\n",p1);
	printf("%p\n",p1+1);
	printf("====================\n");
	printf("%p\n",p2);
	printf("%p\n",p2+1);

	return 0;
}

运行结果,从下面的运行结果我们发现int型的指针+1跳过了4个字节,而char类型的指针只跳过了1个字节。

00B5FE30
00B5FE34
====================
00B5FE1B
00B5FE1C

也就是说指针的类型决定了指针+1/-1能走多大距离。

再来看一段代码,我们通过解引用来修改变量的值

#include <stdio.h>
int main()
{
	int a = 0x11223344;
	int* p1 = &a;
	*p1 = 0;


	return 0;
}

这是修改前a在内存中的值

在这里插入图片描述

这是修改后内存中的值,我们发现4个字节的值全被改为0了

在这里插入图片描述

接着来看这么一段代码,,同样是&a,不过这里是哪char类型的指针来接受int类型的地址,因为指针大小是相同的所以是可以放得下的

#include <stdio.h>
int main()
{
	int a = 0x11223344;
	char* p1 = &a;
	*p1 = 0;


	return 0;
}

在这里插入图片描述

同样是修改值,我们发现在内存中只有一个字节的值被修改了。

在这里插入图片描述

所以指针的类型的又一个作用就是:决定了指针的访问权限,也就是能访问几个字节。

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	int i = 0;
	for (i = 0; i < 10; i ++)
	{
		*(p + i) = i;
	}
	i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}

	return 0;
}

这段代码输出

0 1 2 3 4 5 6 7 8 9

小结

  • 指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节) 。
  • 指针的类型决定了指针+1/-1能走多大距离

3. 野指针

什么是野指针?

野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

造成野指针的原因

  1. 指针指向的空间已经释放

    下面这段代码正确输出10,貌似没什么问题

    #include <stdio.h>
    int* test()
    {
    	int a = 10;
    
    
    	return &a;
    }
    int main()
    {
    	int* p = test();
    	printf("%d", *p);
    
    
    	return 0;
    }
    

    再来看看,我们加上语句打印的代码

    #include <stdio.h>
    int* test()
    {
    	int a = 10;
    
    
    	return &a;
    }
    int main()
    {
    	int* p = test();
    	printf("hello world!\n");
    	printf("%d", *p);
    
    
    	return 0;
    }
    
    //输出???
    

    这段代码的打印结果就不能确定了,打印的就是随机值。这就是个典型的野指针

    p得到的地址之后,地址指向的空间已经释放了,所以这个时候的p就是野指针

  2. 指针未初始化

    下面这段代码也是野指针,指针变量未初始化,里面放的就是随机值,也就是编译器给我们分配的地址。这段代码运行是直击报错的

    #include <stdio.h>
    
    int main()
    {
    	int* p;
    	*p = 10;
    	printf("%d\n", *p);
    
    	return 0;
    }
    
  3. 指针越界访问

    假设我们对一个指针越界访问,也就是访问程序外的内存地址。也是会造成野指针的。

    下面数组最后一个元素是arr[4],尝试使用指针越界访问,打印的就是随机值。

    当指针指向的范围超出数组的范围的时候,此时的p就是野指针

    #include <stdio.h>
    
    int main()
    {
    	int arr[5] = { 0 };
    	int* p = &arr;
    	int i = 0;
    	for (i = 0; i < 6; i++)
    	{
    		printf("%d ", *(p + i));
    	}
    
    	return 0;
    }
    

如何避免野指针

  1. 定义指针变量前记得初始化或者让其指向NULL
  2. 注意不要让指针越界访问
  3. 指针指向的空间释放时,及时让该指针指向NULL

来看一段代码

#include <stdio.h>

int main()
{
	int arr[5] = { 0 };
	int* p = &arr;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		printf("%d ", *(p + i));
	}
	//假设指针已经不使用
	int num = 20;
	p = NULL;
	if (p != NULL)
	{
		*p = 10;
	}
	else
	{
		p = &num;
	}
	printf("%d\n", *p);

	return 0;
}

注意,NULL本身是等于0的,在源代码中定义了,只是将其强转成了 viod*

#define NULL ((void *)0)

NULL指向的空间是不能访问的

其实指针变量p本身存的就是一个16进制的地址,语法上是支持直接赋值地址给指针变量的,虽然语法上是支持,但这属于非法访问。

#include <stdio.h>

int main()
{
	int* p = 0x00112233F5;

	printf("%p\n", p);
	return 0;
}

4. 指针的运算

指针加减整数

前面已经提到过对指针进行加减操作,指针能走多远取决于指针的类型。比如int类型的指针+1就是跳过4个字节,char类型的指针+1就是跳过1个字节,同理-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;
	}
	for (i = 0; i < 5; i++)
	{
		printf("%d ", *(p + i));
	}

	return 0;
}

在这里插入图片描述

指针的运算关系

来看一段代码

#include <stdio.h>

int main()
{
	int arr[10] = { 0 };
	char ch[5] = { 0 };

	printf("%d\n", &arr[9] - &arr[0]);
	printf("%d\n", &arr[0] - &arr[9]);
	printf("%d\n", &ch[4] - &ch[0]);
	printf("%d\n", &ch[0] - &ch[4]);

	return 0;
}

运行结果

9
-9
4
-4

&arr[index]是一个地址,本质上就是一个指针,两个指针相减得到了数组元素的个数。

所以

  • 指针-指针 绝对值的是指针和指针之间的元素个数
  • 注意:指针-指针 计算的前提条件是:两个指针指向的是同一块连续的空间的

通过指针-指针模拟实现strlen函数

int my_strlen(char* str)
{
	char* index = str;
	while (*index != '\0')
	{
		index++;
	}

	return index - str;
}

指针的关系运算

我们知道指针可以加减操作,来看这么一段代码,这个代码的指针p指向数组的最后一个元素,然后不断减减,判断当前地址是否要大于等于数组第一元素的地址,完成了数组的赋值。

#include <stdio.h>

int main()
{
	int arr[10] = { 0 };
	int* p = arr[10];

	for (p = &arr[9]; p >= &arr[0]; p--)
	{
		*p = 10;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

这个代码输出10个10,表面上好像没有问题,大部分编译器都是这个结果。

但是需要注意的是C语言标准规定

**允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许
与指向第一个元素之前的那个内存位置的指针进行比较 **

因为这里for循环最后一次判断比较的是数组首元素前面的那块地址,但是C语言标准不建议这么做。这么做可能会出现问题

我们可以把代码改进一下,这样就是拿数组最后一个元素的地址和它后面的地址比较了

#include <stdio.h>

int main()
{
	int arr[10] = { 0 };
	int* p = arr[10];

	for (p = &arr[0]; p < &arr[10]; p++)
	{
		*p = 10;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

5.指针和数组

指针不是数组,数组也不是指针。但是数组可以通过指针来访问

来看一段代码

#include <stdio.h>

int main()
{
	int arr[5] = { 0 };
	printf("arr == %p\n", arr);
	printf("&arr[0] == %p\n", &arr[0]);
	
	return 0;
}

输出

arr == 0000005DD98FF578
&arr[0] == 0000005DD98FF578

可以说名数组名就是数组的首元素地址.

既然数组名可以当做一个内存地址放到指针中,那么就可以通过指针来访问。

#include <stdio.h>

int main()
{
	int arr[5] = { 0 };
	int* p = arr;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		printf("&arr[%d] = %p <===> p+%d = %p\n",i ,&arr[i],i,p+i);
	}
	return 0;
}

输出

&arr[0] = 000000B9FD36FC58 <===> p+0 = 000000B9FD36FC58
&arr[1] = 000000B9FD36FC5C <===> p+1 = 000000B9FD36FC5C
&arr[2] = 000000B9FD36FC60 <===> p+2 = 000000B9FD36FC60
&arr[3] = 000000B9FD36FC64 <===> p+3 = 000000B9FD36FC64
&arr[4] = 000000B9FD36FC68 <===> p+4 = 000000B9FD36FC68

二维数组名的地址也是首元素地址,二维数组的首元素是一位数组,所以二维数组名是二维数组第一行的地址。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qwQgYSeK-1666788548464)(assets/1666615950740.png)]

6. 二级指针

指针是一个变量,我们知道是个变量在内存中就会有一个地址。那么指针变量的地址怎么存放?那么就可以使用二级指针来存放。

#include <stdio.h>

int main()
{
	int a = 100;
	int* p = &a;
	int** pp = &p;
	printf("%p\n", p);//存的是a的地址
	printf("%p\n", pp);//存的是p的地址
	printf("%p\n", &pp);//取出二级指针pp的地址
	
	return 0;
}

运行结果

00000094626FFA24
00000094626FFA48
00000094626FFA68

pp是一个二级指针变量里面存的是一级指针p的地址,通过对pp进行解引用操作就能拿到p的地址,而p里面存的是变量a的地址,再次对p进行解引用操作就能得到变量a的值

#include <stdio.h>

int main()
{
	int a = 100;
	int* p = &a;
	int** pp = &p;
	printf("%d\n", **pp);
	
	return 0;
}

多级指针

#include <stdio.h>

int main()
{
	int a = 100;
	int* p = &a;
	int** pp = &p;
	int*** ppp = &pp;
	int**** pppp = &ppp;

	printf("%d\n", ****pppp);
	
	return 0;
}


//输出100

7. 指针数组

指针数组是指针还是数组?指针数组是存放指针的数组。

我们整形数组和字符型数组,里面存的是整形变量和字符变量。

int arr[5] = { 0 };
char str[] = "hello";

在这里插入图片描述

那么指针数组又是什么样子的呢?

下面这段代码我们知道对变量取地址得到的是一个地址也就是指针,把这个指针放到一个数组里,这个数组就是指针数组,[]表示是数组,int*是一个整形指针,所以这是一个整形的指针数组。

#include <stdio.h>

int main()
{
	int a = 10;
	int b = 20;
	int c = 30;

	int* arr[] = { &a,&b,&c };
	
	return 0;
}

字符指针数组

我们知道字符数组可以写成指针的形式,存的就是字符串的首地址。那么我们把几个字符串放到一个数组中,就可以让一个字符指针数组来接收了。

字符指针数组里面存的都是字符串的首元素地址,可以直击通过%s来打印

#include <stdio.h>

int main()
{
	char* str = "hhh";//存放的是字符串的首地址

	char* arr[] = { "hello","world","linux" };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%s ", arr[i]);
	}
	
	return 0;
}

在这里插入图片描述

那么指针数组又是什么样子的呢?

下面这段代码我们知道对变量取地址得到的是一个地址也就是指针,把这个指针放到一个数组里,这个数组就是指针数组,[]表示是数组,int*是一个整形指针,所以这是一个整形的指针数组。

#include <stdio.h>

int main()
{
	int a = 10;
	int b = 20;
	int c = 30;

	int* arr[] = { &a,&b,&c };
	
	return 0;
}

字符指针数组

我们知道字符数组可以写成指针的形式,存的就是字符串的首地址。那么我们把几个字符串放到一个数组中,就可以让一个字符指针数组来接收了。

字符指针数组里面存的都是字符串的首元素地址,可以直击通过%s来打印

#include <stdio.h>

int main()
{
	char* str = "hhh";//存放的是字符串的首地址

	char* arr[] = { "hello","world","linux" };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%s ", arr[i]);
	}
	
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MaNgbR3H-1666788548465)(assets/1666666525408.png)]

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱敲代码的三毛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值