文章目录
- 1.什么是指针
- 2.指针和指针类型
- 2.1 指针+-整数
- 2.2 指针的解引用
- 3.野指针
- 3.1为什么会有野指针
- 3.2 如何规避野指针
- 4.指针运算
- 4.1 指针+-整数
- 4.2 指针减指针
- 4.3 指针的关系运算
- 5.指针与数组
- 6.二级指针
- 7.指针数组
1.什么是指针
指针的两个要点
1.指针是内存中的一个最小单元的编号,也就是地址。
2.平时口语所说的指针,通常指的是指针变量,是用来存放内存地址的变量。
总结
指针就是地址,口语所说的指针通常是指针变量
内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的。
所以为了有效的使用内存,就要把内存划分成一个个小的内存单元,每个内存单元的大小都是一个字节。
为了能够有效的访问到内存的每个单元,就要给内存单元进行编号,这些编号被称为内存单元的地址。
在写程序时,创建的变量、数组等都要在内存上开辟空间。
每个内存都有唯一的编号,这个编号也被称为地址 地址 == 编号
变量是创建内存中的(在内存中分配空间的),每个内存单位都有地址,所以变量也是有地址的。
可以利用&来取出变量的地址。
指针变量
通过&(取地址符)取出变量内存的地址,把地址可以存放在一个变量当中,这个变量就是指针变量。
#include <stdio.h>
int main()
{
int a = 0;
int* pa = &a;//这里的pa就是指针变量
*pa = 10;//*就是根据a的地址取找到a
//这样我们就可以间接的改变a的值
printf("%d\n",a);
return 0;
}
//打印结果:10
总结:
指针变量就是用来存放地址的变量。(存放在指针中的值会被当成地址处理)。
在内存当中是如何编址的呢?
上面我们提到了一个字节对应一个地址,为什么会这样呢?
其实在计算机当中会存在地址线,32位的机器上就存在32根地址线,这些地址线会发出高电压(高电平)和低电压(低电平)就是(1或者0);
那么32根地址线就可以产生2的32次方种情况。
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
…
11111111 11111111 11111111 11111111
2的32次方种情况,每种情况就对应着每个地址,就标识着一个字节。这里右2的32次方字节,大概是4G的空间。
同样的方法在64位机器,可以标识的空间就非常大了。
这里我们明白了:
- 在32位机器上,地址是32个0或者1组成的二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4字节。
- 因此在64位的机器上就是一个指针变量大小对应8个字节。
总结: - 指针是用来存放地址的,地址就是唯一标识一块地址的。
- 指针的大小在32位平台是4个字节,在64位平台是8个字节。
2.指针和指针类型
前面我学习了,整型,短整型,浮点型,字符型。这些都是变量的类型,那么指针有没有类型呢?
有的
int num = 10;
p = &num
要将&num(num的地址)保存到p中,我们知道p就是一个指针变量,那么它的类型是怎么样的呢?
我们给指针变量相应的类型。
char* pc = NULL;
int* pi = NULL;
short* ps = NULL;
long* pl = NULL;
long long* pll = NULL;
我们可以发现,指针的定义方式是type + *
但是我们又知道,指针变量的大小都是是固定的不是4个字节就是8个字节。那么为什么要搞出指针的类型呢?有什么意义吗?
意义就在于给*发出信息
指针类型可以决定指针解引用的时候访问多少字节
指针类型决定了指针解引用操作的权限
指针的类型决定了指针向前或者向后走一步有多大距离
2.1 指针±整数
#include <stdio.h>
int main()
{
int a = 0;
char* pc = (char*)&a;
int* pi = &a;
printf("%p\n",&a);
printf("%p\n",pc);
printf("%p\n",pc+1);
printf("%p\n",pi);
printf("%p\n",pi+1);
return 0;
}
//打印结果
/*
006FFE20
006FFE20
006FFE21
006FFE20
006FFE24
*/
可以发现用char*
作为指针类型的+1只能向后移动一个字节,而用int*
作为指针类型的+1却可以向后移动4个字节。
也就是说:
指针的类型决定了指针向前或者向后走一步有多大距离
2.2 指针的解引用
#include <stdio.h>
int main()
{
int n = 0x11223344;
char* pc = (char*)&n;
int* pi = &n;
*pc = 0;
*pi = 0;
return 0;
}
下面我们观察在调试过程当中内存的变化。
从这三张图我们可以了解到:
指针的类型决定了,对这种解引用有多大的权限(能操作几个字节)
比如 char*
的指针解引用就只能访问一个字节,而int*
的指针就能访问4个字节。
3.野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
3.1为什么会有野指针
1.指针未初始化
#include <stdio.h>
int main()
{
int* p;//局部变量指针未初始化,默认为随机值
*p = 100;
return 0;
}
2.指针越界访问
#include <stdio.h>
int main()
{
int arr[10] = {0};
int* p = arr;
for(int i = 0;i<=10;++i)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
3.指针指向的局部变量释放
#include <stdio.h>
int* test()
{
int a = 0;
return &a;
}
int main()
{
int* pa = test();
printf("%p\n",pa);
return 0;
}
3.2 如何规避野指针
1.指针初始化
2.小心指针越界
3.指针指向空间释放即置为NULL
4.避免返回局部变量的地址
5.指针使用前检查其有效性
#include <stdio.h>
int main()
{
int* p = NULL;
//明确知道指针应该初始化为谁的地址,就直接初始化
//不知道指针初始化为什么值,就暂时初始化为NULL;
//...
int a = 10;
p = &a;
if(p!=NULL)
{
*p = 100;
}
return 0;
}
4.指针运算
- 指针±整数
- 指针-指针
- 指针的关系运算
4.1 指针±整数
#include <stdio.h>
int main()
{
int arr[5] = {0};
for(int* p = arr;p<=&arr[4];)
{
*p++ = 0;
}
return 0;
}
4.2 指针减指针
指针-指针返回绝对值是它们间的元素个数,
#include <stdio.h>
int main()
{
int arr[5] = {0};
int* pa = &arr[0];
int* pb = &arr[4];
printf("%d\n",pb-pa);
return 0;
}
//打印结果
//4
4.3 指针的关系运算
for(vp = &values[N_VALUES];vp>&values[0];)
{
*--v = 0;
}
//代码简化
for(vp = &values[N_VALUES-1];vp>=&values[0];vp--)
{
*v = 0;
}
实际上大部分的编译器上都是可以完成上面的代码通过的,然而我们还要要避免这样写,因为标准不保证它可行。
规定:
允许指向数组元素的指针与指向数组最后元素的后面的那个内存的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
5.指针与数组
指针变量就是指针变量,不是数组。指针变量的大小是4/8字节,专门是用来存放地址的
数组就是数组,不是指针,数组是一块连续的空间,可以存放一个或多个类型相同的数据
数组中,数组名就是数组首元素的地址,数组名 == 地址 == 指针
当我们知道数组首元素的地址的时候,因为数组又是连续存放的,所以通过指针就可以遍历访问数组,数组是可以通过指针来访问的。
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
printf("%p\n",arr);
printf("%p\n",&arr[0]);
return 0;
}
//打印结果:
/*
012FFEB0
012FFEB0
*/
可见数组名和首元素的地址是一样的。
数组名表示的就是数组首元素的地址。(两种情况)
1.sizeof(数组名),计算的是整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。
2.&数组名,取出的整个数组的地址。&数组名,数组名表示整个数组,但是整个数组会以首元素的的地址显示。
既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问以数组就成为可能。
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int* p = arr;
for(int i = 0;i<10;++i)
{
printf("&arr[%d] = %p == p+%d = %p\n",i,&arr[i],i,p+i);
}
return 0;
}
//打印结果:
/*
&arr[0] = 00B3F708 == p+0 = 00B3F708
&arr[1] = 00B3F70C == p+1 = 00B3F70C
&arr[2] = 00B3F710 == p+2 = 00B3F710
&arr[3] = 00B3F714 == p+3 = 00B3F714
&arr[4] = 00B3F718 == p+4 = 00B3F718
&arr[5] = 00B3F71C == p+5 = 00B3F71C
&arr[6] = 00B3F720 == p+6 = 00B3F720
&arr[7] = 00B3F724 == p+7 = 00B3F724
&arr[8] = 00B3F728 == p+8 = 00B3F728
&arr[9] = 00B3F72C == p+9 = 00B3F72C
*/
所以p+i
就是计算的数组arr
下标为i的地址。
那我们就可以直接通过指针来访问数组。
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int* p = arr;
for(int i = 0;i<10;++i)
{
printf("%d ",*(p+i));
}
return 0;
}
//打印结果:1 2 3 4 5 6 7 8 9 10
也就是说arr[i] = *(p+i),这样的话,对于计算机来说,肯定是按*(p+i)来处理的,就是把arr[i]转换成*(p+i)。然后我们知道*(p+i)和*(i+p)是没有区别的。所以我们是可以写i[arr]来打印数组的。
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
for (int i = 0; i < 10; ++i)
{
printf("%d ",i[arr]);
}
return 0;
}
//打印结果:1 2 3 4 5 6 7 8 9 10
注意:不建议这样写,会有点装了。
6.二级指针
指针变量也是指针,是变量就有地址,那指针变量的地址存放在哪里呢?
a的地址存放在pa中,pa的地址存放在ppa中,pa是一级指针。而ppa是二级指针。
对于二级指针的运算有:
*ppa
通过对ppa中的地址进行解引用,这样找到的是pa
,*ppa
其实访问的是pa
int a = 10;
*ppa = &a;//等价于pa = &b
ppa
先通过*ppa
找到pa
进行解引用操作:*pa
,那找到的就是a
7.指针数组
指针数组就是存放指针的数组
比如整型数组是存放整型的数组,字符数组是存放字符的数组。
那么指针数组就是:
int* arr2[5];
arr2是一个数组,有5个元素,每一个元素是一个整型指针;
完