目录
一.指针是什么
1.定义
1. 指针是内存中一个最小单元的编号,也就是地址
2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
总结:指针就是地址,口语中说的指针通常指的是指针变量。
那我们就可以这样理解:
内存——电脑上的存储设备,程序运行的时候会加加载到内存中,也会使用内存空间
2.指针变量
我们可以通过& (取地址操作符)取出变量的内存其实地址,把地址可以存放到一个变量中,这个变量就是指针变量
#include <stdio.h>
int main()
{
int a = 10;//在内存中开辟一块空间
int* p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
//a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量中,p就是一个之指针变量。
return 0;
}
&a对a取地址,a是一个整型变量,是四个字节,每个字节都有他的地址,&a去取出的是第一个字节的地址,也就是最小的那个
总结:指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
一个小的单元到底是多大?(1个字节) 如何编址?
经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。
对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0);
那么32根地址线产生的地址就会是:
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
...
11111111 11111111 11111111 11111111
这里就有2的32次方个地址。
每个地址标识一个字节,那我们就可以给 (2 ^ 32Byte == 2 ^ 32 / 1024KB ==2 ^ 32 / 1024 / 1024MB == 2 ^ 32 / 1024 / 1024 / 1024GB == 4GB) 4G的空间进行编址。
同样的方法,那64位机器,如果给64根地址线,那能编址多大空间,自己计算。
这里我们就明白:
在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。
总结:
指针变量是用来存放地址的,地址是唯一标示一个内存单元的。
指针的大小在32位平台是4个字节,在64位平台是8个字节。
二.指针类型
1.定义
指针的类型跟数据的类型一样,但是也有区别,我们用代码来看
int main()
{
int a = 0x11223344;
int* pa = &a;
*pa = 0;
char* pc = &a;
*pc = 0;
return 0;
}
我们发现我们监视的时候,我们发现,pa地址里面的值四个字节全部变为了0,而pc里面的只有第一个变为了0
所以我们知道了,指针的类型决定访问的时候,访问的字节数量是不一样的
int 4个字节
char 1个字节
float 4个字节
short 2个字节
所以不同的指针类型对应不同的访问权限,所以我们用NULL进行空指针的初始化
char* pc = NULL;
int* pi = NULL;
short* ps = NULL;
long* pl = NULL;
float* pf = NULL;
double* pd = NULL;
这里可以看到,指针的定义方式是: type + *。
char* 类型的指针是为了存放 char 类型变量的地址。
short* 类型的指针是为了存放 short 类型变量的地址。
int* 类型的指针是为了存放 int 类型变量的地址。
但是我们不是跟数据类型一样,数据类型是必须要放对应的数据,但是指针类型就是,方便我们去访问内存,不用对应数据的类型来定义指针的类型
我们来看加减整数的时候
int main()
{
int a = 0x11223344;
int* pa = &a;
char* pc = &a;
printf("%p\n", pa);
printf("%p\n", pa+1);
printf("%p\n", pc);
printf("%p\n", pc+1);
return 0;
}
我们可以看到pa向后跳了四个字节,pc向后跳了一个字节
类型决定访问的步长(加一跳过去几个字符)
指针类型是为了我们方便去访问内存是一个还是两个还是四个,就要看我们的指针类型
方便我们进行操作,而不是要必须对应这个数据类型,灵活访问,方便我们访问内存
假如我们要更改四个,我们用char的指针类型,可以循环四次去访问
char* pc = (char*)&a;//int*
int i = 0;
for (i = 0; i < 4; i++)
{
*pc = 1;
pc++;
}
指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。
2.意义
1. 指针类型决定了,指针进行解引用操作的时候,一次性访问几个字节,访问权限的大小
如果是char*的指针,解引用访问1个字节
如果是int*的指针,解引用访问4个字节
如果是float*的指针, 解引用访问4个字节
2. 指针类型决定指针的步长(指针+1到底跳过几个字节)
字符指针+1,跳过1个字节
整型指针+1,跳过4个字节
浮点型指针+1,跳过4个字节
总结一下,指针类型,就是方便我们去访问内存,一个字节两个字节,四个字节,灵活运用,而不是什么类型必须对应什么类型,做题也需要看清楚是什么类型,看是改变几个字节还是跳过几个
三.野指针
1.概念
概念:野指针就是指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)
(1)未初始化
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
(2)越界访问
#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = arr;
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
(3)空间释放
比如在一个程序中调用函数,而函数的返回值是地址,函数调用后会自行结束,此时再调用这个地址,如调用这个地址之前没有进行任何占用空间的动作,则此地址所存放的东西不会改变,否则为未知
int* test()
{
int a = 10;
return &a;
}
int main()
{
int *p = test();
printf("hehe\n");
//这个函数栈帧,调用完函数之后,test有个函数栈帧,里面是a,没有人破坏这个栈帧,*p还是等到a的值
//但是加上这个printf,这个printf返回之后,就可以把前面那个函数栈帧破环了,就把a给覆盖了
printf("%d\n", *p);
return 0;
}
这个就是我们先进行函数里面的,然后a的地址,主函数里面是p的地址,然后是一样的对应的
但是我函数返回的时候,是局部变量,用完就销毁了,这个a就没有了,这个时候我们主函数里面的地址,找不到这个a了,有头尾无尾,就是野指针了
2.规避
1. 指针初始化
2. 小心指针越界
3. 指针指向空间释放,及时置NULL
4. 避免返回局部变量的地址
5. 指针使用之前检查有效性
NULL就是专门来定义空指针的,但是也不能直接对其进行赋值,先要用if判断,再去看
int main()
{
int a = 10;
int* pa = &a;//指针的初始化
int* p = NULL;//NULL - 空指针,专门用来初始化指针
if (p != NULL)
{
}
return 0;
}
四.指针运算
1.加减整数
上面就分析了,就是跳过几个字节,需要看是什么类型的指针来判断
#define N_VALUES 5
float values[N_VALUES];//这个数组就是五个元素
float* vp;
//指针+-整数;指针的关系运算
for (vp = &values[0]; vp < &values[N_VALUES];)
{
*vp++ = 0;
//这个是后置加加,是先用的vp的值,然后再去加加,先解引用改值为0,然后再加加,到下面一个地址,再次解引用改值为1
//这个指针是float类型,每次跳过四个字节,但是我们定义数组的时候也是float类型的,所以我们加一就是跳过一个float元素
//等价于:
* vp = 0;
vp++;
}
for里面vp就是第一元素地址,随着下标的增长,地址是从低的到高的,然后小于下标为5的元素里的地址
后置加加,是先用的vp的值,然后再去加加,先解引用改值为0,然后再加加,到下面一个地址,再次解引用改值为1,这个指针是float类型,每次跳过四个字节,但是我们定义数组的时候也是float类型的,所以我们加一就是跳过一个float元素
2.指针减指针
前提:两个指针要指向同一块空间
指针减指针的绝对值得到的是两个指针之间的元素个数
int main()
{
char ch[5] = {0};
int arr[10] = { 0 };
printf("%d\n", &arr[9] - &arr[0]);//9
printf("%d\n", &arr[0] - &arr[9]);//-9
//指针减去指针的绝对值得到是两个指针之间的元素个数
printf("%d\n", &arr[0] - &ch[4]);
//这个是没道理的,两个指针类型也需要相同,也需要指向同一个地址
return 0;
}
用一个例子来看用法,写一个函数求一个字符串的长度
计数器—— 递归—— 库函数strlen——等等方法
求字符串长度,就是统计\0之前的字符个数,先写出主函数
int main()
{
char arr[] = "abcdef";
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
用指针来数个数 ,str++,就是指针加加,往后找
int my_strlen(char* str)
{
int count = 0;
while (*str != '\0')
{
count++;
str++;
}
return count;
}
指针减指针 先把初始的str赋值给start,然后我们开始去数,循环直到\0后,我们停止
然后我们最后str结束的地址,减去我们刚开始的地址,就是指针减指针,就是里面字符的个数
int my_strlen(char* str)
{
char* start = str;
while (*str != '\0')
{
str++;
}
return str - start;
}
这样写要小心一点,把加加写进去,然后一个一个判断,但是要记得减去一,最后的时候str++,就会多往后走了一步,所以我们最后减去一就行,也不能把++写到前面,因为会直接跳走第一个元素
int my_strlen(char* str)
{
char* start = str;
while (*str++ != '\0')
;
return str - start - 1;
}
3.关系运算
就是指针去比较大小
for (vp = &values[N_VALUES]; vp > &values[0];)
{
*--vp = 0;
}
先是5的时候,然后向前走,大于下标为0的元素,然后解引用改值为0,然后再往前走,减减然后解引用改值,从后往前,把数组初始化为0,指针的比较大小的运算
改写一下
for (vp = &values[N_VALUES - 1]; vp >= &values[0]; vp--)
{
*vp = 0;
}
实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
五.指针和数组
1.指针和数组是不同的对象
指针是一种变量,存放地址的,大小是4或者8个字节
数组是一种相同类型元素的集合,可以放多个元素的,大小是取决于元素的个数,和元素的类型的
2.数组的数组名是数组首元素的地址,地址可以放在指针变量里,可以通过指针去访问数组
但是不能把他们画等号
#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;
}
可见数组名和数组首元素的地址是一样的。 结论:数组名表示的是数组首元素的地址。
我们也可以用指针访问数组
#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;
for(i=0; i<sz; i++)
{
printf("&arr[%d] = %p <====> p+%d = %p\n", i, &arr[i], i, p+i);
}
return 0;
}
也能直接给数组赋值打印数组
int main()
{
//1~10
int arr[10] = { 0 };
int*p = arr;
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
//赋值
for (i = 0; i < sz; i++)
{
*p = i + 1;
//从开始往里面赋值123...
p++;
//然后加加,用地址往后给里面赋值
}
//打印
p = arr;
for (i = 0; i < sz; i++)
{
printf("%d ", *p);
//打印的时候解引用找到里面的值
p++;
//然后加加,用地址往后找,然后打印
}
return 0;
}
也可以把p++直接放到里面,有很多种写法
int main()
{
//1~10
int arr[10] = { 0 };
int* p = arr;
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
//赋值
for (i = 0; i < sz; i++)
{
*p++ = i + 1;
}
//打印
p = arr;
for (i = 0; i < sz; i++)
{
printf("%d ", *p++);
}
return 0;
}
指针的用法很灵活,下面的几种写法正确,外面加加,里面加加,直接加 i
int main()
{
//1~10
int arr[10] = { 0 };
int* p = arr;
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
//赋值
for (i = 0; i < sz; i++)
{
*(p + i) = i + 1;//也可以这也写,这时候p没动,所以p = arr;没用了
}
//打印
for (i = 0; i < sz; i++)
{
printf("%d ", *(p+i));
}
return 0;
}
下面这几种都是等价的,着重理解
//arr[i]等价于* (arr + i)
for (i = 0; i < sz; i++)
{
printf("%d ", *(arr+i));
printf("%d ", *(i+arr));
printf("%d ", arr[i]);
printf("%d ", i[arr]);
//最后一个也是可以的,[]是操作符,i和arr是操作数
}
六.二级指针
先看一级指针
a的四个字节肯定占用内存,我们取地址然后放到pa里面,pa里面放的就是a对的地址,指向的是a的值
int main()
{
int a = 10;
int* pa = &a;
*pa = 20;//a=20
printf("%d\n", a);
return 0;
}
a类型是int pa的类型是int* ,pa也有自己的地址,我们也可以取地址pa放到ppa
ppa的类型就是int** ,就是二级指针变量
int main()
{
int a = 10;
int * pa = &a;//pa是一级指针变量
int* * ppa = &pa;//ppa就是一个二级指针变量
int*** pppa = &ppa;//pppa是一个三级指针变量
//*ppa——*pa
//**ppa——a
**ppa = 50;
printf("%d\n", **ppa);//50
printf("%d\n", a);//50
return 0;
}
pa指向的是a,a的类型是int,所以加个*,就是int* , *前就是指向的值的类型
ppa指向的是pa,pa的类型是int*,所以加个*,就是int * * ,三级指针也一样道理
pppa指向的是ppa,ppa的类型是int * * ,所以加个*,就是int * * *
七.指针数组
指针数组的意思就是数组里面放的是地址也就是指针,定义上还是数组
字符数组——存放字符的数组
char [10]
整型数组——存放整型的数组
int [10]
指针数组——存放地址的数组
int* [10]
也可以是其他类型的指针
int main()
{
int a = 10;
int b = 20;
int c = 30;
int d = 40;
int e = 50;
//但是这abcde的地址可不是连续排列的
int* arr[5] = {&a, &b, &c, &d, &e};
int i = 0;
for (i = 0; i < 5; i++)
{
printf("%d ", *(arr[i]));
}
return 0;
}
存放指针的数组对应解引用就是打印出来abcde的值
使用一维数组来模拟二维数组
int main()
{
//假设我想模拟出一个3行4列的数组
int a[] = { 1,2,3,4 };
int b[] = { 2,3,4,5 };
int c[] = { 3,4,5,6 };
int* arr[3] = { a, b, c };
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 4; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
先写出三个一维数组a b c,再写个指针数组,然后把abc地址放进来,就成了二维数组
我们打印的时候就是,找到值是*(arr[i]+j)——arr[i][j]这俩是等价的
看两个例题
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5 };
short* p = (short*)arr;
int i = 0;
for (i = 0; i < 4; i++)
{
*(p + i) = 0;
}
for (i = 0; i < 5; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
看输出结果
arr数组在内存中的存储格式为:
0x00ECFBF4: 01 00 00 00
0x00ECFBF8: 02 00 00 00
0x00ECFBFC: 03 00 00 00
0x00ECFC00: 04 00 00 00
0x00ECFC04: 05 00 00 00
指针p的类型为short*类型的,因此p每次只能所有两个字节,for循环对数组中内容进行修改时,一次访问的是:
arr[0]的低两个字节,arr[0]的高两个字节,arr[1]的低两个字节,arr[1]的高两个字节,故改变之后,数组中内容如下:
0x00ECFBF4: 00 00 00 00
0x00ECFBF8: 00 00 00 00
0x00ECFBFC: 03 00 00 00
0x00ECFC00: 04 00 00 00
0x00ECFC04: 05 00 00 00
故最后打印:0 0 3 4 5
#include <stdio.h>
int main()
{
int a = 0x11223344;
char* pc = (char*)&a;
*pc = 0;
printf("%x\n", a);
return 0;
}
这个是以后要讲的小端机器里面的——小端字节序存储和大端字节序存储
假设,a变量的地址为0x64,则a变量在内存中的模型为:
0x64| 44 |
0x65| 33 |
0x66| 22 |
0x67| 11 |
char*类型的指针变量pc指向只能指向字符类型的空间,如果是非char类型的空间,必须要将该空间的地址强转为char*类型。
char *pc = (char*)&a; pc实际指向的是整形变量a的空间,即pc的内容为0x64,即44,
*pc=0,即将44位置中内容改为0,修改完成之后,a中内容为:0x11223300
结束语
讲完了指针,要注意好好理解