@toc
1.指针是什么
在计算机科学中,指针 (Pointer)是编程语言中的一个对象,利用地址,它的值直接指向 points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以 说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元
那我们就可以这样理解:
内存:
指针
指针是个变量,存放内存单元的地址(编号)。
那对应到代码:#include <stdio.h>
int main()
{
int a = 10;//在内存中开辟一块空间
int p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
//将a的地址存放在p变量中,p就是一个之指针变量。
return 0;
}
总结∶指针就是变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
思考以下的问题∶
(1)一个小的单元到底是多大 ? (1个字节)
(2)如何编址 ?
经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。
对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的是产生一个电信号正电 / 负电(1或者0)
那么32根地址线产生的地址就会是∶
00000000 00000000 00000000 00000000
0000000 00000000 00000000 00000001
…….
11111111 1111111111111111 11111111
这里就有2的32次方个地址。 每个地址标识一个字节,那我们就可以给(2 ^ 32Byte == 2 ^ 32 / 1024KB == 2 ^
32 / 1024 / 1024MB == 2 ^ 32 / 1024 / 1024 / 1024GB == 4GB)4G的空闲进行编址。
同样的方法,那64位机器,如果给64根地址线,那能编址多大空间,自己计算。
这里我们就明白 :
(1)在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
(2)那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。
总结 :
(1)指针是用来存放地址的,地址是唯一标示一块地址空间的。
(2)指针的大小在32位平台是4个字节,在64位平台是8个字节。
2.指针和指针类型
我们先看这样一段代码的执行效果:
在32位 (x32)系统下:
可以看到不管什么类型的指针,大小都是4,这时候我们心里可能产生疑问,为什么大小都是4,还要区分不同类型的指针,那这种区分是不是没有意义的?不同类型的指针是不是可以互用?为了验证这些问题,请看以下代码及运行结果 。
我们可以先屏蔽其它代码内容,保留前三行内容,再加上 p = 0 ; 即调用指针来更改指针指向地址中的内容。通过F10调试,打开窗口-- - 内存监视器(类似变量监视窗口),可以看到a的内容确实改变了
现在我们*将char pc来重复上面的操作,看能否实现相应变更指针指向地址内容的功能。
通过F10调试,打开窗口-- - 内存监视器(类似变量监视窗口) ,我们发现这里仅前两个变成了00,后面几位没有变化。**
通过这个我们可以发现指针的类型还是意义的,其意义在于解引用操作时,对字节的操作数量不同。
比如int 类型的指针,可以改动4个字节的内容,而char 类型的指针只能更改一个字节的内容。
指针的意义1:指针的解引用
总结:指针类型决定了指针进行解引用操作的时候,能够访问空间的大小
举例:
int p; p能够访问4个字节
char p; p能够访问1个字节
double p; p能够访问8个字节
指针的意义2:指针 + - 整数
看下面这个例子:pa + 1 实际地址 + 4, pc + 1 实际地址 + 1
总结:指针的类型决定了指针向前或者向后走一步有多大(距离、指针的步长)。
举例:
int p; p + 1 – > 往后跳4字节
char p; p + 1 – >往后跳1字节
double p; p + 1 – > 往后跳8字节
思考:那么指针这两个意义的价值是什么呢?
把arr[10]数组所有元素依次加1 :↓↓↓↓
如果我们将int p = arr; 改成char pc = arr;会发生什么呢?
是不是只会将10个字节改成1,而我们数组int arr[10]有40个字节,10个字节的大小相当于2个半int类型的大小,我们可以打开内存窗口调试,看一下是不是这样的情况。
只改了前十个字节
3.野指针
野指针概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针成因:
1.指针未初始化 #include<stdio.h>
int main()
{
int a;//局部变量不初始化,默认是随机值
int* p;//局部的指针变量不初始化,默认也会给随机值
*p = 20;//这时候对指针进行解引用操作,实际上并不知道更改的内存是在哪里
return 0;
}</code></pre>
2.指针越界访问 #include<stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i < 12; i++)
{
//当指针指向的范围超出数组arr的范围时,就越界访问了,此时p就是野指针
*(p++)=1;
}
return 0;
}</code></pre>
3.指针指向的空间释放 #include<stdio.h>
int test()
{
int a = 10;
return &a;
}
//1.a 创建的空间在函数结束的时候返回给系统了,不属于当前程序的内容
int main()
{int* p = test();
//2.这时候通过*p 去访问一个返还系统的空间,该空间有可能已经存放其它内容了
//3.所以p是一个野指针
*p = 20;
return 0;
}</code></pre>
那么我们如何能够避免野指针呢?
1.指针初始化
2.小心指针越界
3.指针指向空间释放,即置为NULLint*pa=NULL; //空指针
4.指针使用之前检查有效性
4.指针运算
指针运算有三种,分别为:
①指针 + -整数
②指针 - 指针
③指针的关系运算
下面对这三种运算进行详细的讲解:
①指针 + - 整数 #include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10};
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;
for (i = 0; i < sz; i++)
{
printf("%d ", *p);
//p = p + 1; //指针 + 整数
p++;
}
return 0;
}</code></pre>
这里我们使用的时指针 + 1,那么指针 + 2或指针 + 3是什么样子的呢?请看下面的举例;
指针除了可以 + 整数,也可以 - 整数,比如:
intp=arr[sz-1]-->访问下标
练习:
解析:
②指针 - 指针
我们知道指针变量是用于存放地址的,那么指针 - 指针 就是 地址 - 地址,那指针 - 指针的结果又是什么呢?请看下面的例子:
打印 & arr[9] - &arr[0] 结果为9,代表从arr[0]到arr[9], 其中间有9个元素。
总结:指针 - 指针得到的结果是中间元素的个数
当然如果我们将 & arr[9] - &arr[0] 的顺序调换以下,变成 & arr[0] - &arr[9],得到的又是什么呢?
结果是: - 9
所以如果我们要得到元素的个数,应该用大地址 - 小地址。
看下面这个 错误做法:
指针 + - 要使他们都指向一个空间
练习:模拟strlen的功能函数 (第三种方法)-->见' 递归 ' int my_strlen(char str)
{
char* start = str;
char end = str;
while (end != '\0')
{
end++;
}
return end - start;
}
int main()
{
char arr[] = "hello,world";
int len = my_strlen(arr);
printf("%d", len);
return 0;
}
③指针的关系运算
关系运算,简单来说就是比较大小,有 > 、 >= 、 == 、 != 、 < 、 <=
看下面例子:#define N_VALUES 5
float values[N_VALUES]; //代表右5个元素
float *vp;
//指针+-整数;指针的关系运算
for (vp = &values[N_VALUES]; vp > &values[0];)
{
--vp = 0;
}
将上面代码改造一下↓↓↓↓↓for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
{
vp = 0;
}
实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样第二种写,因为标准并不保证它可行。
标准规定∶
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
这句话看到这里,还是不太明白,所以就画个图来演示一下:
(画图演示很重要)
5.数组和指针
数组名是什么?
在之前的学习中,我们可能已经知道了数组名就是首元素的地址,这里我们验证一下这个说法是否正确,请看下面这个例子:
可以看到 arr 和& arr[0] 结果是一样,说明 “数组名就是首元素的地址” 是正确的!
但是,数组名就一直是首元素地址吗?
数组名在绝大多数情况下是首元素地址,但有两个例外:
1、& arr --- &数组名 - 数组名不是首元素的地址,数组名表示整个数组 &
数组名 , 取出的是整个数组的地址。
整个数组的地址和首元素地址有什么区别呢?
2、sizeof(arr)----sizeof(数组名)-- - 数组名表示的整个数组–sizeof(数组名)计算的是整个数组的大小。
我们回到刚刚说的问题:整个数组的地址和首元素地址有什么区别呢?
在打印的时候我们发现打印出来的值是相等的。
解析:
举例:我们让它们都进行 + 1, 首元素地址加1就到了第二个元素的地址,而数组地址 + 1则是直接跳过该数组
上面我们说了 数组名就是首元素的地址,那么我们通过使用指针来访问数组就成为了可能:#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
printf("&arr[%d] = %p <===> p+%d = %p\n", i, &arr[i], i, p + i);
}
return 0;
}</code></pre>
p + i其实计算的是数组arr下标为i的地址。那么我们就可以直接通过指针来访问数组,比如:
6.二级指针
指针变量也是变量,是变量就有存储空间,就有地址,那么指针变量的地址存放在哪里那?
这就是二级指针,指向内容是存放地址的指针。
通常我们所说的指向数组的指针是一级指针。#include<stdio.h>
int main()
{
int a = 10;
int* pa = &a;//pa 一级指针
int** ppa = &pa;//ppa 二级指针 ......后面还有 三级指针、四级指针....n级指针
return 0;
}
这里我们进行画图演示:
好了,现在我们对二级指针应该理解清楚了,但是二级指针怎么应用呢?
如果我们对 ppa 进行解引用操作 * ppa就 能找到 pa 也就是a的地址,再次进行解引用操作** ppa,就能找到a了!
如下图:
7.指针数组
在我们学习指针和数组的概念时,会接触到两个这样的概念
1.指针数组-- - 数组-- - 存放指针的数组
2.数组指针-- - 指针
那么我们怎么来理解1.指针数组呢?我们来看下面的这个问题。#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
int c = 30;
int pa = &a;
int pc = &c;
//这里只有三个变量,我们创建了三个指针分别存放它们的地址
//假如有10个变量,我们要一口气创建10个指针来存放它们的地址吗?
return 0;
}
这个时候我们就会思考,这10个指针变量能不能利用数组的方式来创建呢,就跟10个int 类型的变量一样,通过创建一个 int arr[10]就能实现。
整型数组-- - 存放整型
字符数组-- - 存放字符
指针数组-- - 存放指针
按照这个思路,我们创建一个指针数组 int* arr[3] = { &a,&b,&c };
我们可以通过指针数组访问其内容并打印: