C语言初阶——7指针


前言

作者写本篇文章旨在复习自己所学知识,并把我在这个过程中遇到的困难的将解决方法和心得分享给大家。由于作者本人还是一个刚入门的菜鸟,不可避免的会出现一些错误和观点片面的地方,非常感谢读者指正!希望大家能一同进步,成为大牛,拿到好offer。
本系列(初识C语言指针),是为了与大家分享自己学习经验和所遇到的困难,同大家一起进步。


日志

2024.5.16首发

1.指针是什么?

  1. 指针是内存中一个最小单元的编号,也就是地址
  2. 口头上说的指针,通常指指针变量,是用来存放内存地址的变量

1.1计算机如何管理内存?

现实生活中,国家把地址大致划分为了省、市、县、街道、门牌号等,通过一个人的家庭住址就能精确找到他家位置。如此,就方便国家管理各个地区。
计算机也是如此,为了方便管理计算机的内存,人们就把内存分成了一块块的内存单元,对内存单元进行编号,这个编号就是地址,也是指针
在这里插入图片描述

1.2 内存单元如何编址?

  1. 计算机是通过地址线所产生的电压以二进制的形式来存储数据的
    32/64位计算机上有32/64根地址线,每根地址线可以产生高电平(高电压1)和低电平(低电压0)这一个数,32/64根就会产生32/64个1/0。
    在这里插入图片描述
  2. 人们就把这32/64根地址线所产生的32/64个1/0所组成的二进制序列,作为一个内存单元的编号。
    每个内存单元的编号就作为,这块内存空间的地址。通过编号能找到这块空间。就像现实生活把地区划分为省市这种编号,通过省、市、县、街道、门牌号等这一串编号,而通过身份证就能找到你家。这样身份证上的地址就相当于有了指向的作用。
    而在计算机中通过编号,也就是地址,就能找到这个地址所指向的空间。说明这个地址也是具有指向作用的,所以也叫做指针。就有了
    编号\==地址\==指针
    在这里插入图片描述

1.3内存单元的大小应该是多少?

  1. 每个编号对应一个地址,每个地址表示一个内存单元。那32/64根地址线能产生2^32/2^64个地址,也就是说对应有2^32/2^64个内存单元。每个内存单元是多大合适?

  2. 假设每个内存单元大小为1bit
    在这里插入图片描述

  3. 假设内存单元的大小为1kb
    在这里插入图片描述

  4. 假设内存单元的大小为1byte
    在这里插入图片描述
    所以最终我们得出一个结论说,内存单元的大小就是1个字节。所以32/64位机器就能管理4GB/8GB内存。

注意上面说的统统都是管理内存,而不是说内存就这么大。比如说有一台512GB的32位机器,所以能通过编址管理512GB中的4GB的内存,而不是说内存只有4GB。内存有多大取决有硬件,也就是你买多大就是多大。

1.4指针的大小是多少?

我们说在32位机器上,产生的地址是32个bit的二进制序列
在这里插入图片描述
所以32/64位机器上无论是什么类型的指针,大小都是4/8字节。
在32位机器上。左上角的x86表示32位平台
在这里插入图片描述
左上角x64表示64位平台
在这里插入图片描述

2.指针和指针类型

2.1指针类型的意义

前面说不论什么类型的指针大小都是4字节,那肯定会有人问:那指针类型为什么还要存在?在创建指针变量的时候,类型干脆全部都是int*好了。

  1. 看下面的例子。并且打开内存观察,在内存窗口,把列调成4,方便观察
    在这里插入图片描述

  2. 输入&a,看变量a空间内容的变化
    在这里插入图片描述

  3. 执行int a = 0x11223344;
    在这里插入图片描述

  4. 数据在内存中是倒着存的
    在这里插入图片描述
    为什么倒着存,现在先不要纠结,后面介绍

  5. 一个16进制位能转化为4个二进制位
    在这里插入图片描述

  6. 把a的地址存到两个指针里
    在这里插入图片描述

  7. 执行\*pa\=0;,观察变量a的空间内容的变化
    在这里插入图片描述

  8. 把a的值改回来,赋值0x11223344;
    在这里插入图片描述

  9. 执行*ch = 0;,观察变量a的空间内容的变化
    在这里插入图片描述
    后面三个地址45,46,47没有被修改

通过上面的观察我们发现了一个区别。同样对指针解引用访问变量a操作,int*修改了4个字节,而char*只修改了一个字节。说明int*访问了变量a的4个字节,而char*只访问了变量a的一个字节。从而得出一个结论
在这里插入图片描述
从而我们可以推出
在这里插入图片描述

最后也就是说指针的类型决定了指针解引用访问的对象的字节个数,也就是指针的权限

2.2指针+-整数

我们知道了指针类型存在的意义,那再来看指针+-整数的现象

  1. 看下面的例子。
    在这里插入图片描述
  2. 指针类型决定了指针加减n操作时的步长
    在这里插入图片描述
  3. 指针访问字节数应该与指针+-1的步长相等
    在这里插入图片描述

3.野指针

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

3.1野指针的成因

  1. 指针未初始化
    先看我们正常使用指针的方式
    在这里插入图片描述
    如果现在你不小心写错了,就会出错。
    在这里插入图片描述
    而此时的pa就是野指针
    在这里插入图片描述
    因为它的地址是随机值,指向的空间是不确定的。就好比大街上的一条野狗,没有主人,到处乱窜,逮住一个人可能就要一口很危险。

  2. 指针越界访问

int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	int i = 0;
	for (i = 0; i <= 11; i++)
	{
		//当指针指向的范围超出数组arr的范围时,p就是野指针
		*(p++) = i;
	}
	return 0;
}

如果i==0的时候
在这里插入图片描述
执行完*p=0;后,后置++使得p向后移动一个元素;指针p所指向的位置变成了arr[1]
在这里插入图片描述

一直到p指向arr[9]。假设后面第一块和第二块空间有下标,而且分别为10,11。但是这个下标为10和11的空间已经不是数组arr的空间了
在这里插入图片描述
再往后走
在这里插入图片描述
循环已经进行了10次,把下标0~9的元素的内容都修改了。而这个循环要循环12次,当循环再继续往下走了的时候,下标为10和11的这两块空间不属于arr,就发生了数组越界。这时的p就变成了野指针。
比如本来你这条狗有主人,但是中途看到别人丢的实物跑出去了,没有栓绳了。这时候这条狗此时就相当于一条野狗很危险

  1. 指针所指向的空间释放了
    在这里插入图片描述
    虽然这里确实打印了10,但是这种方式是非法的。
    就好比你去酒店租了个房,觉得非常不错。这个时候,你把这个消息告诉了张三,叫他也过来住住。但是张三是第二天来的,第二天早上你已经退房了。这个时候张三强行进去你租的这个房间,里面还留着你住过的痕迹,一片狼藉。而张三强行进去这个房间,是不合法的,可能会挨保安打一顿。

3.2如何避免野指针

我们说野指针是非法的,使用野指针可能带来一些问题,那现在我们就来说如何规避野指针

  1. 指针初始化
    明确指针该初始化为谁的地址,就直接初始化
    不知道指针该初始化为什么值,暂时初始化为NULL
    在这里插入图片描述
    这就是是大街上,有一条野狗,有人把它拴到一棵树上了。如果你不靠近那棵树,就没有被咬的风险。而把指针置为空指针就是这个意思。空指针当然比野指针好,因为野指针就像没有栓起来的狗,你不惹它都可能咬你。而空指针,则是不使用它是不会有危险的。
    而在你有初始化指针的习惯之后,我们就有了一个意识
    在这里插入图片描述
    当ptr不是空指针,说明本来是空指针的ptr有了指向,可以使用了。

  2. 小心数组越界

  3. 指针指向的空间释放后,及时置为空指针
    在指针指向的空间后,应该加上p=NULL;

  4. 避免返回局部变量的地址
    在这里插入图片描述
    因为局部变量的地址在出了函数之后,就销毁了。本身就危险。

  5. 指针使用前进行有效检查
    在这里插入图片描述
    当然能这么写的前提是你有初始化指针为空指针的习惯,否则野指针也是会被使用的,没有意义

4.指针的运算

  1. 指针+-整数
  2. 指针-指针
  3. 指针的关系运算

4.1指针+-整数

4.1.1通过指针访问数组

  1. 不使用数组下标访问数组
    因为数组的是连续存放的,当指针指向的是arr[0]的时候,只要指针p不断+1,就能偏移遍历数组
    在这里插入图片描述

  2. 可以打印里面的值来看看是不是真用指针修改了,这次用下标打印
    在这里插入图片描述
    两个for循环,第一个通过指针+1修改了数组的内容,第二个通过数组下标打印数组内容。而这么一看数组果然被指针修改成功了。

  3. 修改数组的时候也可以写成这种形式
    在这里插入图片描述

  4. 也可以换成用指针打印
    当然前面这里因为修改指针的时候,p已经指向了arr[9],所以还要对p再次赋值,使它指向arr[0]
    在这里插入图片描述

4.1.2数组本质上(在底层的实现上)是通过指针的形式去写的

  1. 假设我们现在,有这样一个前提
    在这里插入图片描述
  2. 那么就可以退出这么一个结论
    在这里插入图片描述
  3. 再推导下去
    在这里插入图片描述
  4. 去试了之后发现果然行得通
    在这里插入图片描述
  5. 当然说了那么多不是讲可以写成这样,而是说对于[]来说,arr和i是它的两个操作数。就像2+3,2和3是+的两个操作数可以换位置,所以arr和i也可以换位置。
    在这里插入图片描述

4.1.3允许指针访问数组最后一个元素的下一个元素

  1. 先来看一段代码
    在这里插入图片描述
    这里假设values[4]下标后面的空间有下标5和6,但实际上这两块空间不属于values数组

  2. 第一次循环
    在这里插入图片描述

  3. 第二次循环,此时vp已经指向了values[1]
    在这里插入图片描述

  4. 第五次循环,此时vp已经指向了values[4]
    在这里插入图片描述
    当vp=&values[5]时,vp<&values[N_VALUES]不再进入循环

4.2指针-指针

  1. 我们说指针是地址,那地址-地址,肯定能减,来看看指针-指针
    在这里插入图片描述

  2. 指针-指针得到的绝对值是两个指针之间的元素个数
    当换了一边之后,数值变成负的了
    在这里插入图片描述

  3. 指针-指针的前提是两个指针同时指向了一块空间
    在这里插入图片描述
    这两个问题都是不确定的,所以如果不是指向同一块空间是没有意义的

  4. 前面我们写过strlen函数的实现,今天来复习一下

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

//版本2
int my_strlen(char* s)
{
	if (*s == '\0')
		return 0;
	else
		return 1 + my_strlen(s + 1);
}
int main()
{
	char ch[] = "abcdef";
	int len = my_strlen(ch);//传的是数组名,形参用指针接收
	printf("%d\n", len);
	return 0;
}

注意有些人在写版本1的时候,指针s++,写的是*s++

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

在这里插入图片描述
当我们学了指针-指针这个操作的时候,我们就可以用’\0’的地址-a的地址就能算出中间的元素个数,也就是长度了

//版本3
int my_strlen(char* s)
{
	char* start = s;//首元素a的地址
	//停下来的时候,说明s指向\0
	while (*s != '\0')
	{
		s++;
	}
	return s - start;
}

尝试走一遍,也是能跑出结果的
在这里插入图片描述
因为’\0’的ASCII码值是0,所以也可以写成这样
在这里插入图片描述
还有人这样写
在这里插入图片描述
这样写长度为7,是因为s指向\0的时候,还会后置++一次,使得s+1。而s++写在里面,压根就不进去了,没有++。所以如果要写成这个形式,返回值要多减一个1
在这里插入图片描述

4.3指针的关系运算

因为指针是地址,地址是一个值,有大小。所以指针的关系运算,就是比较地址的大小

  1. 还是之前那个例子,只不过之前是从前往后走,现在是从后往前走
    在这里插入图片描述
    之后往前走
    在这里插入图片描述
    再往前
    在这里插入图片描述

一直走到vp指向values[1]的时候

在这里插入图片描述
vp == &values[0] ,所以vp > &values[0]不成立。这就走完了这个循环,它是倒着往前走的。而在这个循环的过程中就用到了比较指针的大小,这就是指针的关系运算

  1. 如果这段代码稍作简化
    在这里插入图片描述
    第一次进入循环。循环后,判断还是符合,再次进入
    在这里插入图片描述
    第二次
    在这里插入图片描述
    就这样一直走到vp指向values[0]时
    在这里插入图片描述
    这串代码在绝大多数编译器上也能正常运行,但是它还是错误的,我们要避免这样写。因为C语言标准不保证它可行

  2. 允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
    在这里插入图片描述

5.指针和数组

我们前面说数组在编译器底层的实现,实际上还是用指针来做的,但他们之间不是一回事。

5.1指针和数组是什么关系?

  1. 指针就是指针变量,不是数组。指针变量的大小是4/8个字节,是专门用来存放地址的
  2. 数组就是数组,不是指针。数组是一块连续的空间,可以存放一个或多个类型相同的数据。数组的类型也是多种多样,去掉数组名就是数组类型。数组的大小是sizeof(类型 [元素个数])

他们不是一回事,不要混为一谈

5.2指针和数组的联系

  1. 数组中,数组名是数组首元素的地址。地址又等价为指针,所以在一定程度上可以这么讲
    数组名 == 地址 == 指针
    用数组名访问数组元素的时候,其实和用指针访问这个数组是一样的。

  2. 当我们知道数组首元素地址的时候,因为数组又是连续存放的,所以通过指针就可以遍历访问数组。数组可以通过指针来访问,这就是他们之间的联系

  3. 可以打印数组每个元素的地址来看看
    在这里插入图片描述
    可以看到数组每个元素的地址之间相差4字节,而数组元素的大小就是4字节。说明它们之间没有空隙,所以是连续存放的

  4. 当我们知道数组是连续存放的之后,我们就可以通过上面学的指针+整数的操作访问数组
    数组名是首元素地址,把数组名赋值给指针p,p就能找到首元素1
    在这里插入图片描述

  5. 所以当我们通过p+i拿到下标为i的地址的时候,解引用p+i,就可以不用数组名来打印数组元素了
    在这里插入图片描述

6.二级指针

我们之前说的指针都是一级指针,那么二级指针又是什么?
在这里插入图片描述

6.1什么是二级指针?

  1. p是一级指针变量,指针变量也是变量,变量要在内存中开辟空间,是变量就有地址。所以p也应该有自己的地址
    在这里插入图片描述

  2. a是int类型,占4个字节。而指针p再x86环境下也是4字节
    在这里插入图片描述

  3. 指针p里存的是a的地址0x00cffc98,所以p能找变量a的空间。
    在这里插入图片描述

  4. 变量p指向了a的空间,所以p叫做指针变量。是变量就得在内存中开辟空间,所以p也有地址。&p可以看到p的地址,p的地址存起来也需要一块空间,假设这块空间叫做pp。pp能通过p的地址0x00cffc98找到p所在空间。
    在这里插入图片描述

  5. pp要存起来的话要类型是int**
    在这里插入图片描述

  6. 可以这样理解
    在这里插入图片描述
    如果此时把pp的地址存到指针ppp里面去就可以这么理解
    在这里插入图片描述

6.2二级指针的作用

  1. pp指向p,p指向a。通过pp就可以修改a的值
    在这里插入图片描述
    当然括号也能省略,加括号只是为了好理解
    在这里插入图片描述
  2. 二级指针和数组之间的关系
    在这里插入图片描述
    我们现在将三个数组名存在一起,使他们之间联系起来。而数组名是首元素地址,而且要存三个数组名,所以用char*的数组存放
    在这里插入图片描述
    而parr是数组名,数组名是首元素地址,也就是arr1的地址。把parr存到一个指针p里,p此时此刻就是一个二级指针。而p指向的对象parr的类型是char*,所以p的类型是char
    **
    在这里插入图片描述

他们的指向关系大概就是:parr指针存放了三个数组的首元素地址。而二级指针存放了p的地址。通过这样就能将数组和指针联系起来,这时候的parr其实就是指针数组。
在这里插入图片描述

7.指针数组

  1. 指针数组是数组。比如好马,主语讲的是马,而好是说明马的状态。而指针数组就是存放了指针(地址)的数组。
    在这里插入图片描述

  2. 通过指针parr就管理了这三个数组,也就能通过指针遍历这三个数组。
    假设现在要打印arr1数组,只要拿到arr1数组的首元素地址(数组名),就能顺着打印下去了
    在这里插入图片描述
    也就是说只要拿到这三个数组的首元素地址也就能打印他们了。而parr指针数组里存的就是他们的数组名,也就是首元素的地址
    在这里插入图片描述
    这里就可以看到本来三个数组没有什么关联,而用parr这个数组将他们的数组名放到一起,就把三个数组维护起来,放到了一起

  3. 为了更好理解,现在换整型数组来试试。刚刚字符数组可以用%s来打印整个数组里面的字符,而整型数组可不能用%s来打印,因此必须要访问到整型数组里面的每个元素才能打印整个数组。
    在这里插入图片描述
    在这里插入图片描述

  4. 这只是模拟出来的二维数组,不是真正的二维数组。真正的二维数组是连续的,一行接着一行。而模拟出来的二维数组,只是把一维数组的数组名(首元素地址)放到一起,管理起来,并不一定是连续的
    在这里插入图片描述

  5. 我们知道parr[i]可以访问到数组名,也就是首元素地址。地址又是指针,所以parr[i]是指针。因此就可以通过指针+整数访问到一维数组里每个元素的地址,从而打印
    在这里插入图片描述

  6. 那可能会有人这么想parr[i+j]行不行?
    在这里插入图片描述

  7. *(parr[i + j])也不行
    在这里插入图片描述
    死循环是因为发生了数组越界,具体错误原因,下一节介绍

8.总结

还没想好

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值