👦个人主页:Weraphael
✍🏻作者简介:目前正在回炉重造C语言(2023暑假)
✈️专栏:【C语言航路】
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍
目录
- 一、什么是指针
- 1.1 内存
- 1.2 内存的管理与使用
- 1.3 指针变量的使用
- 1.4 指针的大小
- 二、指针和指针类型
- 2.1 指针类型的意义
- 2.2 指针+ 或 - 整数
- 2.3 指针解引用
- 三、野指针
- 3.1 野指针成因
- 3.1.1 指针未初始化
- 3.1.2 指针越界访问
- 3.1.3 指针指向的空间被释放
- 3.2 如何规避野指针
- 四、指针运算
- 4.1 指针+-整数
- 4.2 指针 - 指针
- 4.2.1 指针-指针的运用
- 4.3 指针的关系运算(比较大小)
- 五、指针和数组
- 六、二级指针
- 6.1 什么是二级指针
- 6.2 二级指针的使用
- 七、指针数组
- 7.1 什么是指针数组
- 7.2 用一维数组模拟二维数组
一、什么是指针
想要理解什么是指针,必须先了解什么是 内存。
1.1 内存
内存是电脑上的存储设备,一般都是4G/8G/16G
等,程序运行时会加载到内存中,也会使用内存空间。我们可以看看电脑的任务管理器:
1.2 内存的管理与使用
我们将内存划分为一个个小格子,每一个格子是一个 内存单元,大小为 一个字节,对每一个内存单元进行 编号,假设未来要找一个内存单元,就可以通过编号(地址)很快的找到,我们把这些 编号叫做地址,而地址在C语言中又叫做指针。
举一个例子,在下图中,假设定义一个变量
int a = 10
,一个int
类型的变量,需要占4
个字节的空间,而每个字节都有地址,&a
取出的是4个字节中的哪一个的地址呢?其实取出的是第一个字节的地址(也就是较小的地址),也就是说,&a
最终取出来的地址是0x0012ff40
。当然,可以把这个地址存到一个变量中,int* pa = &a
,*
表示pa
是一个指针,int
代表pa
所指向的类型是int
类型,这个pa
也叫做指针变量(它是专门用来存放地址的)。
总结指针理解的2个要点:
- 指针是内存中一个最小的单元编号,也就是地址。
- 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量。
总结:指针就是地址,口语中说的指针通常指的是指针变量。
1.3 指针变量的使用
通过以上代码就验证了,指针变量p
存放的就是a
的地址。
接下来,可以使用*
解引用操作符来对其使用
1.4 指针的大小
- 指针变量,就是用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
- 那么问题来了:一个内存单元到底是多大?刚刚讲过,就是一个字节。那它又是如何编址的呢?
经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。 对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0)
那么32根地址线产生的地址就会是:
00000000000000000000000000000000
00000000000000000000000000000001
.....
11111111111111111111111111111111
这里就有232个地址。 一个地址管理一个内存单元,那么232个地址就能管理232个内存单元,也就是232个字节,那2的32次方个字节又是多大空间呢?根据进制转化:
232 Byte = 232÷1024 KB ÷1024 MB ÷ 1024 GB = 4GB
同样的方法,64位机器,也可以计算出来。
264 Byte = 8 GB
这里我们就明白:
- 在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以 一个指针变量的大小就应该是4个字节。
- 那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。
总结:
- 指针变量是用来存放地址的,地址是唯一标示一个内存单元的。
- 指针的大小在32位平台是4个字节,在64位平台是8个字节。
二、指针和指针类型
2.1 指针类型的意义
假设在32位或64位机器上,指针大小都是4个字节或8个字节,直接搞一个通用指针不就完事了,那为什么要区分int*
、char*
、double*
这些呢?那它肯定就有特殊的意义。
先看看下面的代码:
我们可以通过调试来观察变量a
在 内存 中的变化
为了方便观察,我们把它调成4列
我们发现,内存中确实存的是44332211
,只不过是倒着放的(为什么是在内存中倒着存放会在后期讲解)
接着按F10再往下走
我们发现,4个字节全部被改为0,这说明a
的值确实被修改成0了
假设我把指针变量pa
的类型改为char*
结果又会是如何呢?
可以继续通过观察其内存变化
继续按F10
它只改变了1个字节!!
总结:指针类型的意义
- 指针类型决定了,指针在进行解引用操作的时候,一次性访问几个字节
如果是char*
类型的指针,解引用访问内存中的一个字节
如果是int*
类型的指针,解引用访问内存中的四个字节
float*
和double*
也同样如此…
指针类型还要其它意义,接着往下看:
我们发现,pa + 1
跳过了4个字节,pc + 1
跳过了1个字节
总结:指针类型的意义:
2. 指针类型决定指针的步长(指针+1到底跳过几个字节)
字符指针+1,跳过1个字节
整型指针+1,跳过4个字节
其它类型的指针也是如此…
2.2 指针+ 或 - 整数
注意:指针
+
一个数,是往高地址走,指针-
一个数,是往低地址走
2.3 指针解引用
指针的类型决定了,对指针解引用的时候有多大权限(能操作几个字节)。比如:
char*
类型的指针解引用就只能访问1个字节,int*
解引用就只能访问4个字节
三、野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
3.1 野指针成因
3.1.1 指针未初始化
3.1.2 指针越界访问
3.1.3 指针指向的空间被释放
首先test函数中的
a
是一个局部变量,根据局部变量的生命周期,出了它的作用域,生命周期结束。返回的时候a的地址就已经被销毁了,此时的指针变量p
就是一个野指针。但是运行的时候结果还是10,这只是侥幸,这是因为之前的内存空间还没有被覆盖。我们加上个输出语句就能破坏掉这个内存空间。
3.2 如何规避野指针
- 指针初始化
- 小心指针越界
- 指针指向空间释放,及时置NULL
- 避免放回局部变量的地址
- 指针使用之前要检查有效性
四、指针运算
4.1 指针±整数
#include <stdio.h>
#define N 5 // #define定义的标识符常量
float a[N];
float* p; // 全局变量默认初始化为0,而NULL本质上就是0
int main()
{
for (p = &a[0]; p < &a[N]; )
{
*p++;
}
return 0;
}
看下图就很容易理解代码循环了,但这里要主要循环体内的代码,
*
的优先级是比++
操作符要高的,而++
是后置的(先使用,后++),所以这代码的意思是把数组内5个元素全部赋值成0
4.2 指针 - 指针
- 运算的前提条件:两个指针要指向同一块空间(同个数组)
- 运算结果是:相减绝对值的结果就是两个指针之间的元素个数
【解析】
4.2.1 指针-指针的运用
之前我们求字符串长度是这么写的(不使用库函数strlen
的情况下),计数法
#include <stdio.h>
int my_strlen(char* str)
{
// 计数法
int count = 0;
while (*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
char a[] = "abcdef";
int len = my_strlen(a);
printf("%d\n", len);
return 0;
}
当然还可以使用递归
#include <stdio.h>
int my_strlen(char* str)
{
if (*str == '\0')
return 0;
else
return 1 + my_strlen(str + 1);
}
int main()
{
char a[] = "abcdef";
int len = my_strlen(a);
printf("%d\n", len);
return 0;
}
除了以上两种方法,还可以使用 指针 - 指针
#include <stdio.h>
int my_strlen(char* str)
{
// 记录起始地址
char* start = str;
// 记录'\0'的位置
while (*str != '\0')
{
str++;
}
return str - start;
}
int main()
{
char a[] = "abcdef";
int len = my_strlen(a);
printf("%d\n", len);
return 0;
}
【解析】
注意:
++
千万不要放到循环表达式中,因为不管是前置还是后置++
,它都会有副作用,会导致return
的结果有误差。
4.3 指针的关系运算(比较大小)
#include <stdio.h>
#define N 5 // #define定义的标识符常量
float a[N];
float* p; // 全局变量默认初始化为0,而NULL本质上就是0
int main()
{
for (p = &a[N]; p > &a[0]; )
{
*--p;
}
return 0;
}
【详解】
上面的代码的写法有点难以理解,如果写成以下这样,结果还会是一样吗?
#include <stdio.h>
#define N 5 // #define定义的标识符常量
float a[N];
float* p; // 全局变量默认初始化为0,而NULL本质上就是0
int main()
{
for (p = &a[N - 1]; p >= &a[0];p-- )
{
*p = 0;
}
return 0;
}
【解析】
其实,简化过的代码实际在绝大部分的编译器上是可以顺利完成任务的,然而我们应该避免这样书写,因为标准规定并不保证它可行
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
五、指针和数组
- 指针和数组是不同的对象
- 指针是一种变量,存放地址的,大小4/8字节的
- 数组是一组相同类型元素的集合,是可以放多个元素的,大小是取决于元素个数和元素类型
- 数组的数组名是数组首元素的地址,地址是可以放在指针变量中,可以通过指针访问数组
如何用指针来访问数组呢?
或者还能这么写
【总结】
int arr[10];
int* p = arr;//首元素地址
这里有一层等价关系
arr[i] == *(arr + i) == *(i + arr) == i [arr]
i[arr]是可以正常使用的,因为[]只是一个操作符,i和arr是[ ]的操作数而已
(就像a + b同样也能写成b + a)。在编译的过程中,arr[i]也会被翻译成*(arr+i)
六、二级指针
6.1 什么是二级指针
变量a
的地址存放在指针变量pa
中,我们称pa
是一级指针。指针变量也是变量,是变量就得有地址,pa
的的地址存放在ppa
中,所以我们称ppa
是二级指针。
这里再解释一下,pa
前面一颗的*
是告诉我们pa
是指针变量,而pa
指向的a
是int
类型,所以pa
的类型就是int*
;同样的,ppa
前一颗的*
告诉我们ppa
是指针变量,而ppa
指向的pa
是int*
类型的,所以ppa
的类型就是int**
。
a
、pa
、ppa
关系如下:
形象点就是如下所示:
6.2 二级指针的使用
七、指针数组
7.1 什么是指针数组
指针数组是指针还是数组?
--- 是数组(存放指针的数组)
可以类比整型数组和字符数组
整型数组是存放整型的数组
字符数组是存放字符的数组
那么指针数组就是存放指针(地址)的数组
举一个简单的例子:
7.2 用一维数组模拟二维数组
假设要模拟三行四列的数组
思路:开辟一个数组用来存放3个数组首元素的地址,因为数组在内存中是连续存放的,所以知道首元素的地址,后面也自然而然也就跟着知道了。
【代码实现】
#include <stdio.h>
int main()
{
int a[] = { 1,2,3,4 };
int b[] = { 5,6,7,8 };
int c[] = { 8,10,11,12 };
int* arr[] = { a,b,c };
for (int i = 0; i < 3; i++)
{
// 偏移量
for (int j = 0; j < 4; j++)
{
printf("%d ", *(arr[i] + j));
}
printf("\n");
}
return 0;
}