目录
1.指针是什么?
2. 指针和指针类型
2.1 指针 +- 整数
2.2 指针的解引用
3. 野指针
3.1 野指针成因
3.2 如何规避野指针
4. 常量指针和指针常量 (const)
4.1.常量指针
4.2.指针常量
5. 指针运算
5.1 指针+-整数
5.2 指针-指针
5.3指针的关系运算
6. 指针和数组
7.二级指针
8.指针数组
1.指针是什么?
理解指针的两个要点:1. 指针是内存中一个最小单元的编号,也就是地址。2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量。总结:指针就是地址,口语中说的指针通常指的是指针变量
我们可以理解:
内存单元 -> 编号 -> 地址 -> 指针
int a = 10;
如果VS2019中的地址存储的内容如下:
地址: 0x032FFB24 -> 地址里面存放的数据(0a 00 00 00)
说明此VS2019是按照小端字节序存储的。
//00000000 00000000 00000000 00000101
// 00 00 00 0a
也可以这样理解地址的产生:
32位CPU - 32根地址线 - 物理的电线 - 通电 - 1/0 (产生32个数)
64位CPU - 64根地址线 - 物理的电线 - 通电 - 1/0 (产生64个数)
对CPU来说,他执行到某个特定的编码数值,就会执行特定的操作。比如2+3,其实就是通过总线把数据2和3从内存里读入,然后放到寄存器上,再用加法器相加这两个数值并将结果放入到寄存器里,最后将这个数值回写到内存中,以此循环往复,一行行执行机器码直到退出。
指针变量我们可以通过&(取地址操作符)取出变量的内存起始地址,把地址可以存放到一个变量中,这个变量就是 指针变量.
#include <stdio.h>
int main()
{
int a = 10;//在内存中开辟一块空间
int* p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
//a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址
//存放在p变量中,p就是一个之指针变量。
*p = 1;
printf("%d\n", a);
return 0;
}
- 一个小的单元到底是多大?(1个字节)
- 如何编址?
经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。
对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0);
那么32根地址线产生的地址就会是:00000000 00000000 00000000 0000000000000000 00000000 00000000 00000001.......11111111 11111111 11111111 11111111
总结:指针是用来存放地址的,地址是唯一标示一块地址空间的。指针的大小在 32 位平台是 4 个字节,在 64 位平台是 8 个字节 。
2. 指针和指针类型
指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。要搞清一个指针需要搞清指针的四方面的内容:指针的类型、指针所指向的类型、指针的值或者叫指针所指向的内存区、指针本身所占据的内存区。让我们分别说明。
先声明几个指针放着做例子:
例一:
- (1)int*ptr;
- (2)char*ptr;
- (3)int**ptr;
- (4)int(*ptr)[3];
- (5)int*(*ptr)[4];
2.1.指针的类型
从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。让我们看看例一中各个指针的类型:
(1)int*ptr; //指针的类型是int*
(2)char*ptr; //指针的类型是char*
(3)int**ptr; //指针的类型是int**
(4)int(*ptr)[3]; //指针的类型是int(*)[3]
(5)int*(*ptr)[4]; //指针的类型是int*(*)[4]
怎么样?找出指针的类型的方法是不是很简单?
2.2.指针所指向的类型
当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。
从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。例如:
(1)int*ptr; //指针所指向的类型是int
(2)char*ptr; //指针所指向的的类型是char
(3)int**ptr; //指针所指向的的类型是int*
(4)int(*ptr)[3]; //指针所指向的的类型是int()[3]
(5)int*(*ptr)[4]; //指针所指向的的类型是int*()[4]
这里可以看到,指针的定义方式是: type + * 。其实:char* 类型的指针是为了存放 char 类型变量的地址。short* 类型的指针是为了存放 short 类型变量的地址。int* 类型的指针是为了存放 int 类型变量的地址。
在指针的算术运算中,指针所指向的类型有很大的作用。
指针的类型(即指针本身的类型)和指针所指向的类型是两个概念。当你对C越来越熟悉时,你会发现,把与指针搅和在一起的"类型"这个概念分成"指针的类型"和"指针所指向的类型"两个概念,是精通指针的关键点之一。
2.3.指针的值----或者叫指针所指向的内存区或地址
指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。在32 位程序里,所有类型的指针的值都是一个32 位整数,因为32 位程序里内存地址全都是32 位长。
指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。
以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。
指针所指向的内存区和指针所指向的类型是两个完全不同的概念。在例一中,指针所指向的类型已经有了,但由于指针还未初始化,所以它所指向的内存区是不存在的,或者说是无意义的。
以后,每遇到一个指针,都应该问问:这个指针的类型是什么?指针指的类型是什么?该指针指向了哪里?(重点注意)
那指针类型的意义是什么?指针类型决定了指针向前或向后走一步,走多大距离(单位是字节)。
2.4 指针本身所占据的内存区
指针本身占了多大的内存?你只要用函数sizeof(指针的类型)测一下就知道了。在32 位平台里,指针本身占据了4 个字节的长度。指针本身占据的内存这个概念在判断一个指针表达式(后面会解释)是否是左值时很有用。
2.5 指针 +- 整数
#include <stdio.h>
//演示实例
int main()
{
int n = 10;
char* pc = (char*)&n;
int* pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc + 1);//增加了1个字节
printf("%p\n", pi);
printf("%p\n", pi + 1);//增加了4个字节
return 0;
}
总结:指针的类型决定了指针向前或者向后走一步有多大(距离)。
举例说明:创建一个数组,存入10个数,利用指针倒着打印出来。
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i + 1;
printf("%d ", *(p+i));
}
printf("\n");
//倒着打印
int* q = &arr[9];
for (i = 0; i < 10; i++)
{
printf("%d ", *q);
q--;
}
return 0;
}
int* 类型的指针+1 就等于指针向前4个字节。
2.6 指针的解引用
1.指针类型决定了在解引用的是一次能访问几个字节(指针的权限);
字符指针解引用会访问1个字节
char* p -> 1个字节
整形指针解引用会访问4个字节
//演示实例
#include <stdio.h>
int main()
{
int n = 0x11223344;
char* pc = (char*)&n;
printf("%x\n", *pc);
int* pi = &n;
printf("%x\n", *pi);
*pc = 0; //重点在调试的过程中观察内存的变化。
*pi = 0; //重点在调试的过程中观察内存的变化。
return 0;
}
总结:指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。
3. 野指针
3.1 野指针成因
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
代码int* p没有初始化,所以p变量中存的地址并没有指向我们当前程序的空间,而是指向内存中随机的空间,因此要*p要访问这块内存空间肯定是出错的!
这就是p的野指针。
(2) 指针越界访问
越界访问也很常见,我们平常在遍历数组中,不注意就会遇到这样的问题:
#include <stdio.h>
int main()
{
int arr[5] = {0};
int *p = arr;
int i = 0;
for(i=0; i<6; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
#include<stdio.h>
int* test()
{
int x = 20;
return &x;
}
int main()
{
int* p = test();
*p = 30;
return 0;
}
test函数返回值是x的地址,main函数中用指针变量p接收x的地址,但是x变量进入test函数创建,而出了test函数会销毁,这时在改变*p的值,即使用x的地址,则是非法访问了,也会造成野指针的问题。
3.2 如何规避野指针
1. 指针初始化比如上方的例子中,不能直接int* p, 必须要初始化,int a = 10; int* p = &a;从而规避野指针问题。2. 小心指针越界问题在我们使用数组时,一定要注意数组的 元素个数 以及我们所 循环的次数 ,避免粗心而导致的越界访问。3. 指针指向空间释放及时置 NULL在我们 不使用指针变量p时 ,int* p = NULL; 置为空,在接下来想要使用p时,用if语句:if(p!=NULL) .........能够很好的避免野指针。4. 避免返回局部变量的地址就像上面也指针成因的第三条所举的例子,避免返回局部变量的地址。5. 指针使用之前检查有效性像if(p!=NULL) .........就是在检查指针的有效性。#include <stdio.h> int main() { int *p = NULL; //.... int a = 10; p = &a; if(p != NULL) { *p = 20; } return 0; }
4. 常量指针和指针常量 (const)
常量指针和指针常量,ta们都与 const 这个关键字有关,下面举几个例子,看看大家能不能分清其中的区别:const int* pa = &a;int const* pa = &a;int* const pa = &a;const int* const pa = &a;4.1.常量指针
常量指针的概念:指针 所指空间的值不能发生改变,不能通过指针解引用修改指针所指向空间的值,但是 指针的指向是可以改的。4.2.指针常量
指针常量的概念:指针本身就是一个常量,即 指针的指向不能发生改变,但是 指针所指向空间的值是可以改变的,可以通过解引用改变指针所指向空间的值。具体可以理解为:常量指针这个名字,以指针结尾,所以是具有指针性质的,因此可以改变指向,又因为是常量指针,所以指向的值是常量的,即是不能改变的。再看指针常量这个名字,以常量结尾,应该具有常量的一些性质,所以自然不能改变指针的指向,只能修改指针所指向的值。所以怎么区分 const 加到那个位置才叫常量指针,加到哪个位置叫指针常量?const 如果在*的左边,那就是常量指针;const 如果在*的右边,那就是指针常量;为什么要这样记呢,可以思考一下,*在指针中有解引用的作用//const 放在*的左边(可以理解为const修饰*)//const 修饰的指针指向的内容,表示指针指向的内容不能通过指针来改变;//但是指针变量本身是可以改变的。(不能修改指针所指向空间的值, 但是指针的指向确是没有限制的)//cosnt 放在*的右边(可以理解为const修饰的是pa)//const修饰的指针变量本身,指针变量的内容不能被修改;//但是指针指向的内容是可以通过指针来改变的。(指针变量不能改变他的指向,但是指针所指向空间的值是不受限制的)那么上面的几个例子已经有答案了,只需要看cosnt修饰的是*还是指针变量。常量指针:const int* pa = pa; int const *pa = &a;指针常量:int* const pa = &a;如果*左右两边都加上cosnt , 说明这个指针指向和内容都不可改变。
5. 指针运算
5.1 指针+-整数
举例说明指针加减整数的情况:
#include<stdio.h>
int main()
{
int arr[8] = { 1,2,3,4,5,6,7,8 };
int* p = &arr[0];
int i = 0;
for (i = 0; i < 8; i++)
{
printf("%d ", *p++);
}
return 0;
}
指针变量p是数组第一个元素的地址,用for循环打印数组各个元素,*的优先级高于++,所以每次执行*p打印完数组中元素后,p++在指向数组下一个元素,从而循环5次打印出数组中5个元素。
5.2 指针-指针
指针-指针的操作得到的是指针和指针之间元素个数,当然前提是两个指针必须指向同一块空间;举个栗子说明:
#include<stdio.h>
int main()
{
int arr[8] = { 1,2,3,4,5,6,7,8 };
int* p1 = &arr[0];
int* p2 = &arr[7];
printf("%d\n", p2 - p1);
return 0;
}
如图所示表示数组中8个元素在内存中的存储,其中p1指向数组的第一个元素的地址,p2指向数组的第八个元素的地址,p2-p1,表示两个指针之间的元素个数,可以看到间隔7个元素,所以打印结果为7。
5.3指针的关系运算
指针的运算关系就是指针之间进行大小的比较,从而实现某些功能,例如:
#include<stdio.h>
int main()
{
int arr[8] = { 1,2,3,4,5,6,7,8 };
int* p = &arr[0];
int i = 0;
for (i = 0; p < &arr[8]; p++)
{
printf("%d ", *p);
}
return 0;
}
如图所示即为指针的关系运算,下方所画的图可以清楚的解释:
p刚开始是数组首元素的地址,接着通过p<&arr[8]的比较,满足则打印该地址的下的元素,接着p++,进行下一轮的比较,直到不满足p<&arr[8]为止。
这里可能有人会疑惑,上文说到不能越界吗,数组只有8个元素,下标最大为7,怎么可以使用arr[8]呢,其实这里并没有越界,上文是使用了越界的地址,已经非法访问内存了,而这里只是将这个地址写出来,和指针变量p进行关系运算,并没有使用这个地址,所以并不存在野指针的问题。
规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较(如上方例子),但是不允许与指向第一个元素之前的内存位置的指针相比较。
6. 指针和数组
看一个例子:
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
printf("%p\n", arr);
printf("%p\n", &arr[0]);
return 0;
}
运行结果:
可以看见数组名和数组首元素的地址是一样的。
结论: 数组名表示数组首元素的地址。(2种情况除外,数组章节讲解了)
这样写代码是可以的:
int arr[10] = { 1,2,3,4,5,6,7,8,9,10};
int* p = arr; //p存放的是数组首元素的地址
既然可以把数组名当成地址存放到一个指针中,那我们使用指针来访问一个元素就成为可能。
例如:
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
int* p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("&arr[%d] = %p <====> p+%d = %p\n", i, &arr[i], i, p + i);
}
return 0;
}
运行结果:
所以p+i其实计算的是数组arr下标为i的地址。
那我们就可以直接通过指针来访问数组。
如下:
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
int *p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i<sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
7.二级指针
对于二级指针的运算有:
- *ppa通过对ppa中的地址进行解引用,这样找到的是pa,*ppa其实访问的就是pa.
int b = 20;
*ppa = &b; //等价于 pa = &b;
- **ppa先通过*ppa 找到pa ,然后对pa 进行解引用操作:*pa, 那找到的是a。
**ppa = 30;
//等价于 *pa = 30
//等价于 a = 30
8.指针数组
指针数组是指针还是数组?
答案:是数组。是存放指针的数组。
数组我们已经知道整形数组,字符数组。
int arr1[5]; //整形数组 - 存放整形的数组
char arr2[6]; //字符数组 - 存放字符的数组
指针数组 - 存放指针的数组
int a = 10;
int b = 20;
int c = 30;
int* arr[5] = { &a, &b, &c}; //存放整形指针的数组
int i = 0;
for(i=0; i<3; i++)
{
printf("%d ", *(arr[i]));}
那指针数组是怎样的?
int* arr3[5]; //是什么?
arr3是一个数组,有五个元素,每个元素是一个整形是指针。
指针就介绍到这里。
补充:
CPU位数的含义
上面这个流程里,最重要的几个关键词,分别是CPU寄存器,总线,内存。CPU的寄存器,说白了就是个存放数值的小盒子,盒子的大小,叫位宽。32位CPU能放入最大2^32的数值。64位就是最大2^64的值。这里的32位位宽的CPU就是我们常说的32位CPU,同理64位CPU也是一样。
而CPU跟内存之间,是用总线来进行信号传输的,总线可以分为数据总线,控制总线,地址总线。功能如其名,举个例子说明下他们的作用吧。在一个进程的运行过程中,CPU会根据进程的机器码一行行执行操作。
比如现在有一行是将A地址的数据与B地址的数据相加,那么CPU就会通过控制总线,发送信号给内存这个设备,告诉它,现在CPU要通过地址总线在内存中找到A数据的地址,然后取得A数据的值,假设是100,那么这个100,就会通过数据总线回传到CPU的某个寄存器中。B也一样,假设B=200,放到另一个寄存器中,此时A和B相加后,结果是300,然后控制CPU通过地址总线找到返回的参数地址,再把数据结果通过数据总线传回内存中。这一存一取,CPU都是通过控制总线对内存发出指令的。
而总线,也可以理解为有个宽度,比如宽度是32位,那么一次可以传32个0或1的信号,那么这个宽度能表达的数值范围就是0到2^32这么多。
32位CPU的总线宽度一般是32位,因为刚刚上面提到了,CPU可以利用地址总线在内存中进行寻址操作,那么现在这根地址总线,最大能寻址的范围,也就到2^32,其实就是4G。
64位CPU,按理说总线宽度是64位,但实际上是48位(也有看到说是40位或46位的,没关系,你知道它很大就行了),所以寻址范围能到2^48次方,也就是256T。
本小节到这里就结束了。