C语言——指针——第1篇——(第19篇)

本文详细介绍了指针的基本概念、不同类型的指针、指针与整数的运算、指针解引用的重要性以及野指针的形成原因和规避策略。特别强调了指针初始化、越界访问的注意事项以及如何检查指针的有效性,以确保程序的健壮性。
摘要由CSDN通过智能技术生成

坚持就是胜利

1.指针是什么

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

指针是个变量,存放内存单元的地址(编号)。指针 = 地址 = 编号

# include <stdio.h>

int main()
{
	int a = 10;  //在内存中开辟一块空间
	int* p = &a;  //这里我们对变量 a ,取出它的地址,可以使用 & 操作符
	              //将 a 的地址存放在 p 变量中,p 就是一个指针变量
	return 0;
}

总结:指针就是变量,用来存放地址的变量。(存放在 指针中的值 都被当成 地址 处理)

回答以下两个问题:
1.一个小的单元到底是多大?
答:1 个字节,1 Byte  
2.如何编址?
答:1 个字节给 1 个对应的地址(见以下内容)

经过仔细的计算和权衡,我们发现 一个字节 给 一个对应的地址 是比较合适的。
对于 32位 的机器,假设有 32根 地址线,那么假设每根地址线在寻址的是产生一个电信号正电/负电(1或者0)。
那么 32 根地址线产生的地址就会是:
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001

11111111 11111111 11111111 11111111
这里就有 2 的 32 次方个地址。

每个地址标识 1 个字节,那我们就可以给 4GB的空闲进行编址。

2^32Byte = 2^32/1024KB= 2^32/1024/1024MB = 2^32/1024/1024/1024GB = 4GB)

同样的方法,那64位机器,就非常非常大了。

这里就知道了:
1.在32位的机器上,地址是32个 0 和 1 组成的二进制序列,那地址就得用 4 个字节的空间来存储,所以一个指针变量的大小就应该是 4 个字节。
2.那如果在64位机器上,如果有 64 个地址线,那一个指针变量的大小是 8 个字节,才能存放一个地址。

总结:
1.指针是用来存放地址的,地址是唯一标示一块空间的。
2.指针的大小在 32 位平台是 4 个字节,在 64 位平台是 8 个字节。

2.指针和指针类型

#include <stdio.h>

int main()
{
	char* pc = NULL;  //用来存放 char 类型变量的地址
	int* pi = NULL;   //用来存放 int 类型变量的地址
	short* ps = NULL;
	long* pl = NULL;
	float* pf = NULL;
	double* pd = NULL;
	printf("%d\n", sizeof(pc));  //4
	printf("%d\n", sizeof(pi));  //4
	printf("%d\n", sizeof(ps));  //4
	printf("%d\n", sizeof(pl));  //4 
	printf("%d\n", sizeof(pf));  //4
	printf("%d\n", sizeof(pd));  //4
	return 0;
}

(1)指针 + - 整数

总结:指针的类型决定了指针向前或者向后走一步有多大(距离)。

#include <stdio.h>

int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	char* pc = arr;
	printf("%p\n", p);          //0095F9C0
	printf("%p\n", p + 1);      //0095F9C4
	printf("%p\n", pc);         //0095F9C0
	printf("%p\n", pc + 1);     //0095F9C1
	return 0;
}
#include <stdio.h>

int main()
{
	int n = 10;
	char* pc = (char*)&n;
	int* pi = &n;

	printf("%p\n", &n);      //010FF890
	printf("%p\n", pc);      //010FF890
	printf("%p\n", pc + 1);  //010FF891
	printf("%p\n", pi);      //010FF890
	printf("%p\n", pi + 1);  //010FF894
	return 0;
}

(2)指针 的 解引用

总结:
指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
比如:char* 的指针解引用就只能访问 1 个字节,而 int* 的指针的解引用就能访问 4 个字节。

#include <stdio.h>

int main()
{
	int n = 0x11223344;
	char* pc = (char*)&n;
	int* pi = &n;  
	*pc = 0;     //重点在调试的过程中,观察内存的变化
	             //由于是 char* 类型,只能改变 1 个字节的内存大小
	*pi = 0;     //重点在调试的过程中,观察内存的变化
				 //由于是 int* 类型,则可以改变 4 个字节的内存大小
	return 0;
}

在这里插入图片描述

由于是 char* 类型,只能改变 1 个字节的内存大小

在这里插入图片描述

由于是 int* 类型,则可以改变 4 个字节的内存大小

在这里插入图片描述

#include <stdio.h>

int main()
{
	int arr[10] = { 0 };
	char* p = arr;    //由于 char* ,只能一个字节一个字节去访问内存

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = 1;
	}
	return 0;
}

在这里插入图片描述
在这里插入图片描述

3.野指针

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

(1)野指针成因

1.指针未初始化

#include <stdio.h>

int main()
{
	int* p;   //局部变量指针未初始化,默认为随机的
	          //局部变量不初始化的时候,内容是随机值
	*p = 20;  
	return 0;
}

2.指针越界访问

#include <stdio.h>

int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	int i = 0;
	for (i = 0; i <= 11; i++)
	{
		*(p+i) = i;  //这样做不会改变  p 中的地址
	}
	
	int j = 0;
	for (j = 0; j <= 11; j++)   //arr数组只有10个空间,已经超出了
	{
		printf("%d\n", *(p + j));
	}
	return 0;
}
#include <stdio.h>

int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	int i = 0;
	for (i = 0; i <= 9; i++)
	{
		*(p++) = i;  //这样做改变了 p 中的地址
	}
	p = arr;  //所以要将 p 中的地址保存为 arr 的 初始地址
	int j = 0;
	for (j = 0; j <= 9; j++)
	{
		printf("%d\n", *(p + j));
	}
	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;
		p++;
	}
	int j = 0;
	for (j = 0; j < 10; j++)
	{
		printf("%d\n", arr[j]);  //直接输出数组中的元素
	}
	return 0;
}

3.指针指向的空间释放

原先指针是指向这块空间的,但是后来这块空间被释放了。
这里放在动态内存开辟的时候讲解,这里可以简单提示一下。

//当程序运行完 int* p = test();时,由于 a 只是局部变量,它的空间就被释放了,
//但是赋给 p 的地址却是已经被释放了的空间地址,此时,p 就成了“野指针”

#include <stdio.h>

int* test()   //由于返回的是 &a ,所以返回类型是 int*    //int 表明指针所指对象的类型是 int 类型    //* 表明是指针
{
	int a = 10;
	return &a;  //变量 a 的空间,是进入函数创建,出函数还给操作系统。
}

int main()
{
	int* p = test();   //当程序运行完 int* p = test();时,由于 a 只是局部变量,它的空间就被释放了,但是赋给 p 的地址却是已经被释放了的空间地址,此时,p 就成了“野指针”
	printf("%d\n",*p);
	return 0;
}

虽然最后程序的返回值是正确的,得到10,但是这种访问是非法的。

在这里插入图片描述

(2)如何规避野指针

1.指针初始化

(1)明确知道指针应该初始化为谁的地址,就直接初始化。
(2)不知道指针初始化为什么值时,暂时初始化为 NULL 。

2.小心指针越界

3.指针指向的空间被释放时,及时置NULL

当前不知道 指针p 应该初始化为什么地址的时候,直接初始化为 NULL

NULL 讲解

通过查看 NULL 的定义:

#define NULL 0
else if
#define NULL ((void *)0)

本质:NULL 就是 0
#include <stdio.h>

int main()
{
	int* p = NULL;  //当前不知道 p 应该初始化为什么地址的时候,直接初始化为 NULL
	                //p 是一个空指针,没有指向任何有效的空间。这个指针不能直接使用。
	//........
	int a = 10;
	p = &a;
	if (p!=NULL)
	{
		*p = 20;
	}
	return 0;
}

4.避免返回局部变量的地址

//当程序运行完 int* p = test();时,由于 a 只是局部变量,它的空间就被释放了,
//但是赋给 p 的地址却是已经被释放了的空间地址,此时,p 就成了“野指针”

#include <stdio.h>

int* test()   //由于返回的是 &a ,所以返回类型是 int*    //int 表明指针所指对象的类型是 int 类型    //* 表明是指针
{
	int a = 10;
	return &a;  //变量 a 的空间,是进入函数创建,出函数还给操作系统。
}

int main()
{
	int* p = test();   //当程序运行完 int* p = test();时,由于 a 只是局部变量,它的空间就被释放了,但是赋给 p 的地址却是已经被释放了的空间地址,此时,p 就成了“野指针”
	printf("%d\n",*p);
	return 0;
}

虽然最后程序的返回值是正确的,得到10,但是这种访问是非法的。

在这里插入图片描述

5.指针使用之前检查有效性

#include <stdio.h>

int main()
{
	int* p = NULL;  //p 是一个空指针,没有指向任何有效的空间。这个指针不能直接使用。
	*p = 10;   //指针使用之前检查有效性。
	           //这样使用就是错误的。
	return 0;
}
#include <stdio.h>

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	int* pend = arr + 9;  //指向数组arr的最后一个元素
	while (p <= pend)  //地址
	{
		printf("%d\n", *p);
		p++;
	}
	return 0;
}

int arr[10];
int* p=arr;
*(p+i) == arr[i];
*(arr+i) == arr[i];
这两行代码好好理解。

arr[i] == (arr+i)==(i+arr)==i[arr]

#include <stdio.h>

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int i = 0;
	int* p = arr;
	for (i = 0; i < 10; i++)
	{
		printf("%d  ", i[arr]);  //这里强调:"[]"只是操作符而已,i和arr只是"[]"两端的操作数而已
		                         //就像,a+b,a和b只是操作数而已
		                         // i[arr]  ==  arr[i]
		printf("%d  ", i[arr]);
		printf("%d  ", *(p+i));
		printf("%d  ", *(arr+i));
	}
	return 0;
}

"[ ]“只是操作符而已,i和arr只是”[ ]"两端的操作数而已
就像,a+b,a和b只是操作数而已
i[arr] == arr[i]

4.指针运算

(1)指针 + - 整数

	*vp++ = 1;   //虽然 ++ 的运算优先级 高于 * ,
	             //但是,++ 是后置的,
	             //先进行:*vp  再进行:vp++

    *--vp = 0 ;   //先是  --vp
                  //再是 *(--vp)
#define N_value 5
#include <stdio.h>

int main()
{
	int* vp;
	int arr[N_value] = { 0 };
	for (vp = &arr[0]; vp < &arr[N_value];)
	{
		*vp++ = 1;   //虽然 ++ 的运算优先级 高于 * ,
		             //但是,++ 是后置的,
		             //先进行:*vp  再进行:vp++
	}

	vp = arr;  //在第一个循环中,vp 中的地址已经改变了,所以要将地址重新赋值 arr 的初始地址

	int j = 0;
	for (j = 0; j < N_value; j++)
	{
		printf("%d\n", *(vp + j));
	}
	return 0;
}
#include <stdio.h>

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	int* pend = arr + 9;  //指向数组arr的最后一个元素
	while (p <= pend)  //地址
	{
		printf("%d\n", *p);
		p++;
	}
	return 0;
}

(2)指针 - 指针

(1)得到的是“两个指针之间的元素个数。
(2)前提:两个指针指向同一块内存空间。
(3)指针 - 指针:有意义---------两者之间的元素个数
指针 + 指针 :无意义
类比:日期 + 日期:无意义
日期 - 日期:天数
在这里插入图片描述

(1) 指针 - 指针 得到的数值的绝对值:是指针和指针之间的元素个数

//指针 - 指针 得到的数值的绝对值:是指针和指针之间的元素个数

#include <stdio.h>

int main()
{
	int arr[10] = { 0 };
	printf("%d\n", &arr[9] - &arr[0]);  //结果:9
	printf("%d\n", &arr[0] - &arr[9]);  //结果:-9
	return 0;
}

(2) 指针 - 指针 相减的前提条件是:指针和指针指向了同一块空间。

//指针 - 指针 相减的前提条件是:指针和指针指向了同一块空间。

#include <stdio.h>

int main()   //此代码无法运行
{
	int arr[10] = { 0 };
	char ch[5] = { 0 };
	printf("%d\n", &ch[4] - &arr[1]);
	return 0;
}

(3)用函数写 计数器

#include <stdio.h>

int my_strlen(char* s)
{
	int count = 0;
	while (*s != '\0')
	{
		count++;
		s++;
	}
	return count;
}

int main()
{
	char arr[10] = "abcdef";
	my_strlen(arr);
	printf("%d\n", my_strlen(arr));
	return 0;
}
#include <stdio.h>

int my_strlen(char* str)
{
	char* start = str;
	while (*str != '\0')
	{
		str++;  //到最后 str 指着 '\0';
		        //当 str - start 时,得到的是 “两个指针之间的元素个数”
	}
	return str - start;
}

int main()
{
	char arr[10] = "abcdef";
	printf("%d\n", my_strlen(arr));
	return 0;
}
#include <stdio.h>

int my_strlen(char* str)
{
	if (*str != '\0')
	{
		return 1 + my_strlen(str + 1);   //递归
	}
	else
		return 0;
}

int main()
{
	char arr[10] = "abcdef";
	printf("%d\n", my_strlen(arr));
	return 0;
}

(3)指针的关系运算

地址是有大小的。
指针的关系运算,就是:比较指针的大小

#define N_VALUES 5
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;
	}

实际在绝大多数的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许指向第一个元素之前的那个内存位置的指针进行比较。
在这里插入图片描述

微软雅黑字体
黑体
3号字
4号字
红色
绿色
蓝色

  • 34
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值