1.指针的概念
简单的来说,指针就是地址---数据的地址---数据在内存中的地址。
在计算机中,输入和读取数据都要对内存进行操作。现在我们的内存有2G,4G,8G,16G,32G,这是内存的空间,也就是程序运行的时候,CPU读取和存储数据的地方,实际上,只要我们的系统在运行,就会占用内存。再比如幻灯片桌面,循环播放图片,幻灯片桌面的程序在运行的时候,会对硬盘中的图片进行扫描,首先就是内存要从硬盘中拿到对应的图片的数据放在自己的某一块区域内,然后让cpu处理。那么计算机是由很多程序一起有条不紊的运行的,那么内存读/取数据必定是有规则的,即对应的数据都应该由对应的内存区域进行读取操作,就像仓库,或者说数据中转站,也可以用快递站点的货架形容。(或者门牌号)
快递货架有它自己的编号,叫取货码。内存空间也有它的编号,叫地址/指针。
快递货架有取货码,一个取货码只能取一个快递;指针/地址的单位是字节,最小一个字节只能放一个数据(char类型/字符)。
在计算机中,32位,64位,实际上是32根地址线,或者64根地址线的说法,(单片机51---8位///stm-32---32位)每一根地址线有0/1两种状态,即高电平/低电平两种状态。
32位的机器能产生2^32种不同的地址即下面左图从0X00000000——>0XFFFFFFFF,换成2进制就是00000000 00000000 00000000 00000000——>11111111 11111111 11111111 11111111。每个地址的大小是1字节(8bit)。
每次cpu存/取数据都要经过内存,通过地址/指针找到内存中的小格子,然后对这个小格子空间进行存/取数据的处理。
在C语言中,通常把地址叫做 指针。
1.1取地址操作符&
c语言中,创建变量的本质就是向内存申请一块空间,然后往这个变量里放数据。
在用scanf输入的时候,我们会用到取地址 & 操作符,实际上就是拿到某个变量的地址,然后往这个变量里存如你输入的数据。
一个char变量的大小是1字节,1个int变量的大小是4字节,也就是1个char变量需要1个内存单元来存储数据,int变量需要4个内存单元。用%p与&可以以16进制的形式打印地址。并且得到的地址是第一个字节的地址。下面的int a,占了4个字节,它的地址实际上是这4个字节中第一个字节的地址。
1.2指针变量与解引用(指针类型)
我们拿到的地址/指针是个数值(内存编号),指针是可以保存在变量中方便后续使用的。
既然地址也是一串数据,那么我们可以用指针变量来存储这个地址。
即储存指针/地址的变量叫做指针变量。
例如下图,我们用&a拿到了a的地址,存入了指针变量p中。其中,p是变量名,*代表p是个指针,int代表指针变量p所指的内存单元中的数据是int类型,这是整形指针变量,另外还有字符指针变量char * ,数组指针变量int * [],指针数组变量int (*)[],结构体指针变量。
存入指针变量之后,可以通过 * 解引用操作符来取出这个指针变量中的数据,即通过指针变量中存储的地址来找到这个内存单元中的数据。
在函数中,函数的形参都是临时变量,是实参的一份临时拷贝,想要改变函数中某个变量的值,就必须用到指针变量,通过内存编号修改内存中的数据。
1.3指针变量的大小
在32位系统和64位系统中,分别有2^32与2^64种不同的地址,分别需要32个和64个二进制位来表示地址,每8个二进制位表示一个字节,,那么32位和64位系统的指针(变量)大小分别就是32/8==4字节与64/8==8字节。
所有类型的指针变量大小相同,只是在不同系统环境下不同而已。
1.4指针的类型与指针的加减+-运算
指针变量是可以进行计算的。int * 是整形指针类型,指向的空间是4个字节,那么int *类型的指针变量+1,它会跳过4个字节。下图中int *类型的p加上1,跳过了1个int大小的空间,即跳过了4个字节。
将int * p强制转换为char*类型,此时的指针变量p指向的是1个内存单元---1字节的空间,那么它加1,跳过了一个char,即1个字节的空间。
扩展(两个指针相减)
已知,int * 类型的指针加上1,会跳过一个int大小的空间,即这个地址+4,那么两个int类型的指针变量相减,得到的就是这两个地址之间相距几个int的空间。
(注:数组名代表数组首元素的地址,&数组名代表整个数组的地址)
1.5指针的越界与置NULL以及assert断言
指针的越界是指指针跑到申请的内存空间之外去了,也就是指针指向了一块未知的内存空间,如果这块内存空间存的是系统运行中很关键的一个数据,修改了这个指针所指向的内存空间的数据,可能会对系统造成损坏。所以应该尽量避免指针越界(指针的自增自减容易越界)。
指针变量用完后,应该及时将指针变量赋值NULL,否则这个指针变量会成为野指针。
例如指针变量运算完毕,此时指针变量指向了一块未知的区域,这样就很危险,所以将这个指针变量赋NULL的值,来将这个指针变量保护起来。
assert( )断言,意思是——我断言括号里的表达式为真,否则就报错停止。需要包含assert.h头文件。
1.6指针模拟实现strlen
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
int strlen_plus(char * a)
{
assert(a); //保证传过来的不是空指针
char* b = a; //将a存入b,不改变a的值,只对b进行操作
while (*b) //字符串末尾有个\0,当b不为\0的时候循环
{
b++; //每次循环,b向后跳过一个char的空间
}
return b-a; //两个同类型指针相减,即为两指针之间的类型数量
} //函数结束后,指针变量b自动销毁,不会成为野指针
int main()
{
char a[]="lty520";
printf("%d\n", strlen_plus(a));
return 0;
}
2.数组指针&&指针数组
2.1一维数组(字符数组)指针
数组指针就是数组的指针,数组名代表首元素地址,&数组名代表整个元素的地址。
数组名+1,代表首元素的地址+1,即首元素跳过1个元素类型大小的空间。
&数组名+1,代表跳过整个数组大小的空间。
由下图可以得知,一维数组数组名就是首元素地址,&a拿到的是整个数组的地址。
sizeof(a),拿到的是整个数组的地址。
在一维数组中,&a与sizeof(a)拿到的是整个元素的地址,除此之外的数组名,就是首元素的地址。
数组指针与字符数组指针是相同的。
在函数的数组的传参中,传的就是数组首元素的地址。
2.2冒泡排序模拟实现
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
void paixu(int * a, int b)//接收一个数组首元素地址和数组的元素个数
{
assert(a); //判断不为空指针
int q = 0;
int w = 0;
int e = 0;
for (q = 0; q < b-1; q++)
{
int flag = 1;
w = 0;
for (; w < b - q - 1; w++)
{
if (a[w] > a[w + 1])
{
e = a[w];
a[w] = a[w + 1];
a[w + 1] = e;
flag = 0; //如果交换了,说明数组不是升序的
}
}
if (flag == 1) //如果一轮内循环都没有交换,那么这个数组就是已经排序好的
{
break; //跳出循环
}
}
}
int main()
{
int lty[] = { 5,3,1,6,2,9 };
int size = sizeof(lty) / sizeof(lty[0]);
printf("排序前:");
for (int m = 0; m < size; m++)
{
printf("%d ", lty[m]);
}
paixu(lty, size);
printf("\n排序后:");
for (int m = 0; m < size; m++)
{
printf("%d ", lty[m]);
}
return 0;
}
2.3指针数组
根据标题名可知,是指针的数组,也就是数组中的元素是指针,即存放指针的数组。它的每个元素类型都是指针类型,包括int*,char*等等。
2.4二维数组的指针
二维数组的本质是一个一维数组中,存放了多个一维数组。
在下图中可以看到,a[3][4]二维数组实际上是3个拥有4个元素的一维数组的集合。
二维数组的地址最后分别为8c,9c,ac,说明二维数组中各个一维数组是连续存放的。
对于二维数组来说,数组名代表的是它的首元素的地址,即第一个一维数组的地址。数组名+1跳过的是一个一维数组的大小的空间。下图的0x002efca0与0x002efcb0,相差16个字节,即4个整形的空间。
&数组名拿到的则是整个二维数组的地址。
我们已经知道了数组指针的加减规则和二维数组数组名的含义,那么二维数组数组名加上一个整数n,指针就跳过n个一维数组的空间大小,拿到的是跳过n个一维数组之后的那个数组的指针,拿到的是整个一维数组的指针,也就是相当于&了这个一维数组的数组名。&(取地址)数组名,再*(解引用)数组名,拿到的是数组名/首元素地址,它的类型是int *,拿到首元素地址后再加上一个整数m,即跳过m个整形,拿到了 * ( *(二维数组数组名+n) ) +m,即 [n][m]。
回顾一维数组,一维数组中数组名+n是指针跳过n个元素空间大小,数组名[n]是拿到第n个元素。
可以得知,*(数组名+n)==数组名[n].由此可以推出在二维数组的指针使用规则(下图).
2.5指针模拟二维数组
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
int main()
{ //下标: 0 1 2
int a0[] = { 1,2,3 };
int b1[] = { 4,5,6 };
int c2[] = { 7,8,9 }; //数组名就是数组首元素地址,是int *类型的
int* arr[] = { a0,b1,c2 };//arr数组的每个元素为int *,整形指针变量
//下标: 0 1 2
for (int i = 0; i < 3; i++)
{
for (int u = 0; u < 3; u++)
{
printf("%d ", arr[i][u]); //1 2 3
} //4 5 6
printf("\n"); //7 8 9
}
return 0;
}
2.6数组指针变量与指针数组
首先我们先要明确 [ ] 的优先级比 * (解引用操作符)高。
int (*p)[10] *p代表p是个指针,它有10个元素,它的每个元素类型是int。
int * p [10] p先与 [10] 结合,代表p是个数组,p[10],这个数组有10个元素,每个元素是int*类型。
前者是数组指针(数组的指针),p是个指针,指向一个数组。
后者是指针数组(指针的数组),p是个数组,每个元素是指针。
拓展:函数中二维数组的传参
二维数组在函数中传参,传的是第一个一维数组的地址。
int p[2][3]与int(*p)[3]是相同的。
拓展:字符串指针
字符串本质上来说就是一个字符数组,只不过必须以\0结尾,它的变量名也是首元素地址,&变量名拿到的是整个字符串的地址。字符串指针变量在数组指针变量的基础上只需要将int改为char。
(数组的两种初始化方式,” “初始化的是常量字符串,内容不可更改,末尾隐藏\0;{}初始化必须在结尾加上\0)
3.函数指针变量
函数在调用的时候会创建一块空间,这块空间的起始地址就是函数的指针/地址。
它的类型是 返回类型---指针变量---函数参数
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
void test(int (*p)[10])
{
printf("%p\n", p);
printf("%p\n\n", p+1);
}
int main()
{
printf("%p\n", test); //001713CF
void (*p)(int(*)[10]) = test;
printf("%p\n", p); //001713CF
return 0;
}
3.1函数指针数组
首先,明确是 函数指针 的 数组,它是个数组,根据指针数组的含义可知,函数指针数组,这个数组的每个元素是函数指针。
那么它的类型是 返回类型---指针变量(数组)---参数类型
a[2]是个有2个元素的数组,它的每个元素是void(*)(int(*)[10])类型的。
拓展:转移表
为什么要有函数指针数组呢?
拿计算器程序举例(转移表)
它可以增加程序的可读性,维护难度,如果需要增加功能,只需要增加新的函数,在函数指针数组中增加一个元素。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
int jiafa(int a, int b) //定义多个函数,函数返回类型和参数都相同
{
return a + b;
}
int jianfa(int a, int b)
{
return a - b;
}
int chengfa(int a, int b)
{
return a * b;
}
int main()
{
int (*a[4])(int,int) = {0,jiafa,jianfa,chengfa};//将各相同类型的函数指针放到同一个数组里
int lty = 1; //前面多加一个0,可以让选项和下表对齐
int e = 0; // l
int r = 0; // l————————————————————————————————————————————l
while (lty) // l
{ // l
printf("请输入模式:\n1.加法\n2.减法\n3.乘法\n0.退出\n");
scanf("%d", <y);
if (lty > 0 && lty < 4)
{
printf("请输入两个操作数\n");
scanf("%d %d", &e, &r);
printf("%d\n",a[lty](e,r)); //a[lty]拿到的是对应下标的函数地址。
//将输入的两个参数传给这个地址(函数),打印它的返回值
}
}
return 0;
拓展:回调函数
回调函数,就是通过函数指针在函数中调用另一个函数;
4.多级指针,二级指针——1000级指针
有时候,我们需要把指针变量存起来方便后续使用,那么就要存到二级指针变量里去,二级变量又可以存到3级指针里去,具体可以存多少层,我也不知道,你可以试试;
二级指针的写法是这样的
int main()
{
int a = 0;
int* aa = &a; //1级指针
int** aaa = &aa; //2级指针
int*** aaaa = &aaa; //3级指针
//int*****………… aaaaaa………… //99999999999999级指针
return 0;
}