目录
注:
本笔记参考B站up鹏哥C语言的视
指针是什么?
在计算机科学中,指针(pointer)是编程语言中的一个对象,利用地址,它的值直接指向(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该内存单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。(所以平时提到的内存单元的编号、内存单元的地址和指针其实是同一个东西)
内存 | 地址(即内存单元的编号) |
一个字节 | 0xFFFFFFFF |
一个字节 | 0xFFFFFFFE |
…… | …… |
一个字节 | 0x00000002 |
一个字节 | 0x00000001 |
一个字节 | 0x00000000 |
通过地址,就可以方便地找到内存单元。
(ps:如果是32位机器,由正负电位产生的32位数字就可以作为内存单元的编号。)
------
int main()
{
int a = 10;
int * pa = &a;
*pa = 20;
return 0;
}
在上述代码中:
- a占了4个字节 - 有4个地址
- 拿到的是a的4个字节中第一个字节(起始)的地址
- 如果要存放地址(指针),就需要一个变量,因此存放地址的变量又被称为指针变量
故:指针就是变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)
小知识点:
- 在32位机器上,地址是32个0或者1组成的二进制序列,那地址就得用 4个字节 的科技来存储,所以一个指针变量的大小就应该是4个字节。
- 如果在64位机器上,有64根地址线,那一个指针变量的大小就得是 8个字节 才能存放一个地址。
总结:指针的大小在32位平台上是4个字节,在64位平台上是8个字节。
指针和指针类型
引例
int main()
{
int* pa;
char* pc;
float* pf;
printf("%d\n", sizeof(pa));
printf("%d\n", sizeof(pc));
printf("%d\n", sizeof(pf));
return 0;
}
存在问题:无论什么类型的指针都是4/8个字节,既然如此,为什么不创造一种通用类型的指针,而还会存在指针类型的不同?
探讨指针类型的意义
小知识:
一个十六进制数字对应四个二进制位数字
例1(解引用)
int main()
{
int a = 0x11223344;//十六进制数字
int* pa = &a;
*pa = 0;
return 0;
}
- 执行命令,按F10,在执行到[int* pa = &a]时,&a可见:
- 而在执行到[return 0]时,&a可见:
程序正常执行。
---
int main()
{
int a = 0x11223344;
char* pc = &a;
*pc = 0;
return 0;
}
改变存储a地址的指针变量的类型,按F10:
- 在执行到[char* pc = &a]时,有
- 在执行到[return 0]时,有
此处发生了变化。
注意:
int类型的指针,解引用时访问4个字节;
char类型的指针,解引用时访问1个字节。
类型发生了变化,访问权限也发生了变化。
例2(+ 或 - 整数)
int main()
{
int arr[10] = { 0 };
int* p = arr;
char* pc = arr;
return 0;
}
按F10,可见:
p 和 pc 内存入地址相同。
---
但是,打印二者:
int main()
{
int arr[10] = { 0 };
int* p = arr;
char* pc = arr;
printf("%p\n", p);
printf("%p\n", p + 1);
printf("%p\n", pc);
printf("%p\n", pc + 1);
return 0;
}
打印结果为:
0000005DB731F9A8(p)
0000005DB731F9AC(p + 1)
0000005DB731F9A8(pc)
0000005DB731F9A9(pc + 1)可见:
p 到 p + 1,跳过了4(整型)
pc 到 pc + 1,跳过了1(字符)
造成区别的原因:指针类型的不同
总结
- (例1)指针类型决定了:指针解引用的权限大小(能操作几个字节)
- (例2)指针类型决定了:指针走一步,能走多远(步长/距离)
应用
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for ( i = 0; i < 10; i++)
{
*(p + i) = 1;
}
return 0;
}
数组arr:
按F10,得:
如果指针类型改为 char*,则如:
是一个一个字节访问。(二者存在区别,可以区别使用)
野指针
||| 概念:野指针就是这种的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针成因
1.指针未初始化
int main()
{
int* p;
*p = 20;
return 0;
}
- p是一个局部的指针变量,而局部变量不初始化的话,默认是随机值
- 所以此时p内存在一个随机值
- 当把数值存入*p内时,实际上是把p内原本的随机值当作地址,访问了随机值对应的空间
也就是说,p非法访问了内存
此时强行运行代码会报错(VS2022):
2.指针越界访问
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for ( i = 0; i <= 10; i++)
{
*p = i;
p++;
}
return 0;
}
分析:
[i <= 10],说明循环进行11次
arr
3.指针指向的空间释放
如果p原本指向某一空间,则当该空间被释放时(还给操作系统),p记住了原本的地址,但指向的空间不存在,p就变成了野指针。
int* test()
{
int a = 10;
return &a;
}
int main()
{
int* p = test();
*p = 20;
return 0;
}
当 a 超出 test函数 的范围时,a 的空间返回给操作系统,此时就算[*p = 20]执行,空间也不属于 p,也就是发生非法访问内存。
如何规避野指针
- 进行指针初始化
int main() { //当前不知道p应该初始化为什么地址时,直接初始化为NULL int* p = NULL; //当明确知道初始化的值时 int a = 10; int* ptr = &a; return 0; }
- 小心指针越界(C语言本身是不会检查数组的越界行为的)
- 指针指向空间释放及时置NULL(空)
- 指针使用之前检查有效性,如:
int main()
{
int* p = NULL;
*p = 10;
return 0;
}
按F10,发现:
检查有效性:
int main()
{
int* p = NULL;
if (p != NULL)
*p = 10;
return 0;
}
指针运算
- 指针 + 或 - 整数
- 指针 - 指针
- 指针的关系运算
指针 + 或 - 整数
int main()
{
#define N_VALUES 5
float values[N_VALUES];
float* vp;
for ( vp = &values[0]; vp < &values[N_VALUES];)
//指针变量vp在for循环内初始化
{
*vp++ = 0;
}
}
values
注意:
[*vp++] --- 指针 + 整数
[vp < &values[N_VALUES]] --- 指针的关系运算
运用:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int* pend = arr + 9;
while (p <= pend)
{
printf("%d\n", *p);
p++;
}
return 0;
}
打印结果为:
1
2
3
4
5
6
7
8
9
10
指针 - 指针
基础知识
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", &arr[9] - &arr[0]);
return 0;
}
问:打印结果为多少?
打印结果为:9
解析:
指针 - 指针,得到的是指针和指针之间的元素个数
(指针 + 指针 是没有的)
---
反例
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
char c[5];
printf("%d\n", &arr[9] - &c[9]);
return 0;
}
注意
指针和指针相减的前提是:两个指针指向同一块空间。否则,会出现问题。
运用
#include<stdio.h>
#include<string.h>
//指针 - 指针的思路
int my_strlen(char* str)
{
char* start = str;
while (*str != '\0')
{
str++;
}
return str - start;
}
int main()
{
//strlen(); - 求字符串长度
//递归模拟实现strlen函数
int len = my_strlen("abc");
printf("%d\n", len);
return 0;
}
打印结果为:3
ps:计数器思路
int my_strlen(char* str) { int count = 0; while (*str != '\0') { count++; str++; } return count; }
指针的关系运算
int main()
{
#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]; vp > &values[0];)
{
*--vp = 0;
}
values:
[*--vp]:先--,再出值
ps:如果for循环部分改为
for ( vp = &values[N_VALUES - 1]; vp >= &values[0]; vp--) { *vp = 0; }
values:
但是,这个写法还是要尽量避免,尽管大部分编译器可以顺利完成任务,但是标准并不保证它可行。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较
指针与数组
数组名是什么?
数组名是数组首元素的(首)地址。(首元素可能占不止一个字节)
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", &arr[0]);
return 0;
}
打印结果 完全一致
例1
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for ( i = 0; i < 10; i++)
{
printf("%p <==> %p\n", &arr[i], p + i);
}
return 0;
}
打印结果参考:
- 000000E5A3B2FB88 <==> 000000E5A3B2FB88
- 000000E5A3B2FB8C <==> 000000E5A3B2FB8C
- 000000E5A3B2FB90 <==> 000000E5A3B2FB90
- 000000E5A3B2FB94 <==> 000000E5A3B2FB94
- 000000E5A3B2FB98 <==> 000000E5A3B2FB98
- 000000E5A3B2FB9C <==> 000000E5A3B2FB9C
- 000000E5A3B2FBA0 <==> 000000E5A3B2FBA0
- 000000E5A3B2FBA4 <==> 000000E5A3B2FBA4
- 000000E5A3B2FBA8 <==> 000000E5A3B2FBA8
- 000000E5A3B2FBAC <==> 000000E5A3B2FBAC
可知:
&arr[i] = p + 1
运用
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for ( i = 0; i < 10; i++)
{
*(p + i) = i;
}
for ( i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
打印结果为:0 1 2 3 4 5 6 7 8 9
---
扩展
int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int* p = arr;//数组名 return 0; }
上述数组中:
- arr[2]
- *(arr + 2)
- *(p + 2)
- *(2 + P)
- *(2 + arr)
- 2[arr]
上述表达式相等。
故存在 arr[2] == 2[arr]。证明:
int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int* p = arr; printf("%d\n", 2[arr]); printf("%d\n", arr[2]); return 0; }
打印结果都为 3。
解析(原理):
[ ] 是一个操作符, 2 和 arr 是两个操作数。在编译器运算 arr[2] 时,arr[2] 会被转化为 *(arr + 2),而加法支持交换律,所以*(arr + 2) 可以转化为 *(2 + arr),最后转化为 2[arr]。(并且 p[2] 也是可以的)
二级指针
指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?这就是 二级指针。
int mainO()
{
int a = 10;
int* pa = &a;//pa是指针变量(一级指针)
int** ppa = pa;//pa也是个变量,&a取出pa在内存中起始地址
return 0;
}
此处 ppa 就是一个二级指针变量
- int* - ppa的类型
- 第二个* - 说明ppa是指针变量
二级指针和一级指针的关系
int mainO()
{
int a = 10;
int* pa = &a;
int** ppa = pa;
return 0;
}
设:
- [*ppa] 找到 [pa]
- [*pa] 找到 [a]
由1和2可得 [* *ppa] == [a]
ps:ppa也可以存起来,有
int** * pppa = &ppa;
指针数组
指针数组是什么?是数组,是存放指针的数组。
int main()
{
int arr[10];//整型数组 - 存放整型的数组
char ch[5];//字符数组 - 存放的是字符
return 0;
}
由此可知,指针数组 - 存放指针的数组。
int* parr[5];//整型指针的数组
char* pch[5];//字符指针的数组