指针就是地址
视频讲解:抖音剑哥聊技术
指针的引入
访问变量的两种方式
通过变量名字访问
通过地址来访问
代码:
#include<stdio.h>
int main()
{
//指针就是地址
int a=10;
printf("a=%d\n",a);
printf("a变量的地址是%p\n",&a);
//*号是取内容的运算符,可以把地址里面的东西取出来
printf("通过地址找到变量a里面的数据%d\n",*(&a));
return 0;
}
运行结果:*号的作用是取内容的意思
可以把地址里面的内容给取出来
指针变量的定义和使用
指针变量是能保存地址的变量,根据类型保存可以保存不同类型数据的地址
指针变量的定义:
类型 * 指针变量的名字 = 地址;
int *p=&a;//保存整数类型变量地址
char *p2=&d;//保存字符地址
使用方式:
代码:
#include<stdio.h>
int main()
{
//指针就是地址
int a=10;
int *p=&a;
printf("a=%d\n",a);
printf("a变量的地址是%p\n",&a);
//*号是取内容的运算符,可以把地址里面的东西取出来
printf("通过地址找到变量a里面的数据%d\n",*(&a));
//指针变量p里面存放的是变量a的地址,通过*号来读取
printf("通过指针变量p找到里面的数据是%d\n",*p);
return 0;
}
运行结果:
根据指针类型保存相同类型的地址
例子:char*类型的指针保存了int型变量的地址的结果
代码:
#include<stdio.h>
int main()
{
int a=0x1234;
int *p=&a;
char *c=&a;
//打印出指针变量p和c的地址
printf("指针变量p存放的地址是%p\n",p);
printf("指针变量c里面存放的地址是%p\n",c);
//打印出指针变量p和c的里面的值的结果
//%x是16进制的数
printf("通过指针变量p访问的a的值是%x\n",*p);
printf("通过指针变量c访问的a的值是%x\n",*c);
//地址+1后的结果:
printf("指针变量++p存放的地址是%p\n",++p);
printf("指针变量++c里面存放的地址是%p\n",++c);
return 0;
}
运行结果:
- 可以看到通过指针c访问的变量a得到数据不完整,指针p访问的空间是4个字节,因为整数在内存中占用4个字节,指针c是字符类型的指针访问的空间是一个字节,所以访问的结果缺失
- 通过对指针的偏移也可以说明,指针p偏移了4个字节,而字符指针c
只是偏移了一个字节 - 同类型指针要保存同类型地址
为什么需要指针
因为可以直接对地址进行操作
练习:通过函数分装的方式交换两个数的值
代码:
#include<stdio.h>
void print(int data,int data2)
{
printf("交换前data=%d,data2=%d\n",data,data2);
}
void exchange1(int data,int data2)
{
int tmp;//临时变量
tmp=data2;
data2=data;
data=tmp;
}
void exchange2(int *pdata,int *pdata2)//传递的是地址,用指针变量来匹配
{
int tmp;
tmp=*pdata2;//*号来取出里面的值
*pdata2=*pdata;
*pdata=tmp;
}
int main()
{
//通过函数分装的方式交换两个数的值
int data=10;
int data2=20;
//打印data,data2的函数
print(data,data2);
//不用指针直接传递值进行交换
exchange1(data,data2);
printf("通过函数exchange1交换后的结果:data=%d,data2=%d\n",data,data2);
//通过传递地址进行交换的结果
exchange2(&data,&data2);
printf("通过函数exchange2交换后的结果:data=%d,data2=%d\n",data,data2);
return 0;
}
两个函数分别传递值和地址,直接传递值的函数里面虽然也进行了交换,但是在main函数中没有体现出来,因为exchange1函数传递的值只是在使用该函数时向内存申请了新的内存,函数调用结束后内存被回收了所以不会影响main函数,因为是局部变量,只是在该函数里面进行交换,而exchange2函数传递的时地址,地址是唯一的,传递地址就直接对当前地址里面的数进行变化也会影响到main函数里面的值,因为该函数是对main函数里面变量data和data2的地址进行变化所以才会成功
运行结果:
可以指向一个固定的地址
#include<stdio.h>
int main()
{
int a=10;
int *p=NULL;
printf("a的地址是0x%p\n",&a);
p=(int *)0x000000000061FE18;
printf("p=%p\n",p);
return 0;
}
运行结果:
练习:三个数,函数分装的方式实现从小到大进行交换
代码:
#include<stdio.h>
void exchange(int *p1,int *p2,int *p3)
{
int tmp;//临时变量
if(*p1<*p2)
{
tmp=*p2;
*p2=*p1;
*p1=tmp;
}
if(*p2<*p3)
{
tmp=*p3;
*p3=*p2;
*p2=tmp;
}
if(*p1<*p2)
{
tmp=*p2;
*p2=*p1;
*p1=tmp;
}
}
int main()
{
//三个数找到从大到小进行交换
int data,data2,data3;
printf("请输入三个数\n");
scanf("%d%d%d",&data,&data2,&data3);
exchange(&data,&data2,&data3);
printf("从大到小排序后是%d,%d,%d\n",data,data2,data3);
return 0;
}
运行结果:
指针和数组
指向数组的时
指针指向数组的时候指向的是数组的首地址
数组的首地址:两种表示方式
int arry[3]={1,2,3};
- 首个元素的地址,例子:
arry[0],int *p=&arry[0];
- 数组的名字就是数组的首地址,例子:
int *p=arry;
指针增量和数组
数组是一个类型数据的集合,其中里面的数据地址是连续的,指针增量就是像一个游标卡尺一样偏移到了下一个元素
代码:
#include<stdio.h>
int main()
{
int arry[3]={1,2,3};
int i;//控制循环
int *p=arry;
for(i=0;i<3;i++)
{
printf("0x%p,%d\n",p+i,*(p+i));
}
return 0;
}
运行结果:
指针偏移后要记得回到数组的首个地址
代码:
#include<stdio.h>
int main()
{
int arry[3]={1,2,3};
int i;//控制循环
int *p=arry;
for(i=0;i<3;i++)
{
printf("%d\t",*p++);//另一种写法
}
p=arry;//回到数组首地址
for(i=0;i<3;i++)
{
printf("%d\t",*p++);//另一种写法
}
return 0;
}
*(p+i)也可以写成 *p++;
第一次循环完成之后要回到数组的首个地址,不然会发生越界
第一次循环已经偏移到了数组的最后一个元素如果不回到首地址继续偏移会发生错误
结果:
正确结果:
见怪不怪的指针和数组名字用法
#include<stdio.h>
int main()
{
int arry[3]={1,2,3};
int *p=arry;//指向数组arry
//指针变量可以当作数组名字来使用
for(int i=0;i<3;i++)
{
printf("%d\t",p[i]);
}
p=arry;//回到首地址
putchar('\n');
//数组名字+1
for(int i=0;i<3;i++)
{
printf("%d\t",*(arry+i));
}
p=arry;
putchar('\n');
//数组名字不可以自增
/*for(int i=0;i<3;i++)
{
printf("%d\t",*arry);
arry++;
}
p=arry;
putchar('\n');
*/
return 0;
}
- 指针可以当作数组名字当下标法进行访问,因为指针存储的是数组的名字
- 数组名字可以+1因为地址是连续的+1就是和指针一样进行偏移了
- 数组名字不能进行++操作,数组名字是一个地址,地址相当于常量,只有变量可以进行++(自增)的操作,常量是不行的
详细解释:为啥数组名字不可以自增
运行结果:
练习:使用指针和函数来输出数组
代码:
#include<stdio.h>
void initarry(int *parry,int len)//传递地址用指针变量来承接
{
int i;//控制虚幻的变量
for(i=0;i<len;i++)
{
printf("请输入数组arry中第%d个数\n",i+1);
scanf("%d",parry);//指针就是地址
parry++;
}
}
void printarry(int *parry,int len)
{
int i;
for(i=0;i<len;i++)
{
printf("%d\t",*parry);
parry++;
}
}
int main()
{
//使用指针和函数来遍历数组
int arry[5];
int len;//计算数组的长度
len=sizeof(arry)/sizeof(arry[0]);
//分装一个可以往数组里面输入数据的函数
initarry(arry,len);//把数组名字当作实参进行传递
//打印数组中数据的函数
printarry(&arry[0],len);//数组首个元素地址当作实际参数进行传递
return 0;
}
运行结果:
练习:数组的逆序输出
#include<stdio.h>
void initarry(int *parry,int len)
{
int i;//控制循环
for(i=0;i<len;i++)
{
printf("请输入数组中第%d个数\n",i+1);
scanf("%d",parry);
parry++;//指针偏移
}
}
void printarry(int *parry,int len)
{
int i;
for(i=0;i<len;i++)
{
printf("%d\t",*parry++);
}
putchar('\n');
}
void reverse(int *arry,int len)
{
printf("开始进行交换\n");
int i,j;//i变量控制循环,j变量控制下标
int tmp;//临时变量
//逆序循环根据奇数和偶数除以2
for(i=0;i<len/2;i++)
{
j=len-1-i;//j是最后一个数的下标
tmp=*(arry+i);
*(arry+i)=*(arry+j);
*(arry+j)=tmp;
}
}
int main()
{
//数组的逆序输出
int arry[5];
int len;//数组的长度
len=sizeof(arry)/sizeof(arry[0]);
//初始化数组的函数
initarry(arry,len);
//打印数组的函数
printarry(arry,len);
//逆序输出的函数
reverse(arry,len);
//再次调用打印数组的函数
printarry(arry,len);
return 0;
}
运行结果:
指针和二维数组
二维数组地址的理解
数组名字就是地址!!!
二维数组是一个特殊的数组,一维数组里每个元素都是一个值,二维数组里面的每个元素是个数组
例子:
int arry[3][4]={{1,2,3,4},
{5,6,7,8},
{9,10,11,12}};
1.二维数组的名字就是地址,里面的每个元素也有一个地址,把二维数组的名字理解为父数组,里面每个元素的数组名字理解为子数组
2. 父数组是arry,子数组的名字是arry[0],arry[1],arry[2]。
3.arry+1和arry[0]+1是不一样的,如果把arry+1当作一个一维数组来理解arry+1就是偏移了一个元素,arry是二维数组的名字也就是首地址,偏移了一个元素就是从里面第一个数组到第二个数组,也就是arry[0]到了arry[1],因为二维数组每个元素都是数组,而arry[0]是第一个元素数组的名字,这里arry[0]+1,就是在子数组中偏移到了下一个元素,arry[0][0]->arry[0][1]
代码验证上面的话
#include<stdio.h>
int main()
{
//验证二维数组父数组和子数组+1后偏移多少
int arry[3][4]={{1,2,3,4},
{5,6,7,8},
{9,10,11,12}};
printf("父数组名字arry的地址%p,+1后的地址是%p\n",arry,arry+1);
printf("子数组名字arry[0]的地址是%p,+1后的地址是%p\n",arry[0],arry[0]+1);
//另一种写法,二维数组的首地址是首个元素的地址,arry=arry[0],
printf("arry[0]=*(arry+0)地址是%p,+1的写法*(arry+0)+1的地址是%p\n",*arry,*(arry+0)+1);
return 0;
}
运行结果:
父数组+1偏移了16个字节,
子数组+1偏移了4个字节
通过对二维数组父数组和子数组的名字进行取值的方式
1.通过下标法进行取值
2. 通过找到子数组方式进行取值,arry[i]+j,取值*(arry[i]+j);
3.通过找到二维数组的名字(地址),进行取值,*(arry+i)+j, 找到数组中每个元素的名字地址, * ( *(arry+i)+j);,左边的 *号是取值,右边的 *号是取地址
代码:
#include<stdio.h>
int main()
{
//二维数组地址写法应用
int arry[3][4]={{1,2,3,4},
{5,6,7,8},
{9,10,11,12}};
int i;//控制循环
for(i=0;i<3;i++)
{
for(int j=0;j<4;j++)
{
//通过下标法来访问二维数组
printf("arry的地址%p,%d\n",&arry[i][j],arry[i][j]);
//通过二维数组中的元素来访问,子数组的名字arry[i],
printf("arry[i]+j的方式,地址是%p,%d\n",arry[i]+j,*(arry[i]+j));
//通过地址访问
printf("*(arry+i)+j的地址是%p,%d\n",*(arry+i)+j,*(*(arry+i)+j));
puts("=================================");
}
}
return 0;
}
运行结果:
数组指针,匹配二维数组的指针
如何定义: 类型 (*指针名字)[常量];例子:int ( *p)[4],
可以指向一个二维数组,但是二维数组里面必须是4个元素,
代码:
#include<stdio.h>
void initarry(int(*p)[4])
{
int i;
int j;
for(i=0;i<3;i++)
{
for(j=0;j<4;j++)
{
printf("请输入第%d行,第%d列数\n",i+1,j+1);
scanf("%d",*(p+i)+j);
}
}
}
void printarry(int (*p)[4])
{
int i;
int j;
for(i=0;i<3;i++)
{
for(j=0;j<4;j++)
{
printf("%d\t",*(*(p+i)+j));
}
printf("\n");
}
}
int main()
{
//数组指针
int arry[3][4];
int (*p)[4]=arry;//数组指针指向数组arry
//输入数组的函数
initarry(arry);
//打印数组的函数
printarry(arry);
return 0;
}
运行结果:
数组和数组指针练习:输入行和列找到其中对应的值
代码:
#include<stdio.h>
void tips(int *hang,int *lie)
{
printf("请输入行和列\n");
scanf("%d%d",hang,lie);
puts("done!");
}
void initarry(int (*p)[4],int hang,int lie)
{
int i,j;//控制循环
for(i=0;i<hang;i++)
{
for(j=0;j<lie;j++)
{
printf("请输入第%d行,第%d列数\n",i+1,j+1);
scanf("%d",*(p+i)+j);
}
}
}
void printarry(int (*p)[4],int hang,int lie)
{
int i,j;//控制循环
for(i=0;i<hang;i++)
{
for(j=0;j<lie;j++)
{
printf("%d\t",*(*(p+i)+j));
}
printf("\n");
}
printf("\n");
}
void tipss(int *p,int *p2)
{
printf("请输入行和列\n");
scanf("%d%d",p,p2);
}
int getdata(int (*p)[4],int hang,int lie)
{
int data;//返回值
data=*(*(p+hang)+lie);
return data;
}
int main()
{
//函数分装,输入行和列找到对应的值
int arry[3][4];//定义一个二维数组
int hang,lie;//行和列
int data;//对应的数
//数组初始化的函数
initarry(arry,3,4);
//数组打印的函数
printarry(arry,3,4);
tipss(&hang,&lie);//提示用户输入行和列的函数
//找到输入对应行和列的数
data=getdata(arry,hang,lie);
printf("输入%d行,%d列的数是%d\n",hang,lie,data);
return 0;
}
运行结果:
函数指针
函数指针的定义和使用
如何定义一个函数指针
1.类型 (*指针名字)(实际参数);
例子:void (*p)(a);
函数的名字就是函数的地址,和数组名字理解起来差不多
代码:
#include<stdio.h>
void tipss()
{
printf("hellow,world\n");
}
int getdata(int data)
{
return data+1;
}
int main()
{
//函数指针的使用
void (*p)();//定义了一个无参数无返回值函数指针
p=tipss;//保存函数的地址,函数名字就是地址
//可以直接调用
(*p)();//打印你好世界的函数
//定义一个有参数有返回值的函数指针
int (*p2)(int);//函数指针后面括号要确定带那种类型实际参数
p2=getdata;//指针指向函数getdata //把常量10当作实际参数
printf("通过p2指针保存函数地址最终返回的值是%d\n",(*p2)(10));//调用过程就是先对指针p2进行取内容找到函数然后就和调用一个函数差不多
return 0;
}
运行结果:
为啥要使用函数指针,可以根据需要调用某个函数
例子:输入1-3,根据输入的数来打印出两个数之间大数,小数,和
输入1打印大数,输入2打印小数,输入3打印和
代码:
#include<stdio.h>
#include<stdlib.h>
void initcmd(int *p)
{
printf("请输入1——3之间的数\n");
scanf("%d",p);
}
int getmax(int data,int data2)//返回最大值的函数
{
int ret;
ret=data>data2?data:data2;
return ret;
}
int getmin(int data,int data2)
{
int ret;
ret=data<data2?data:data2;
return ret;
}
int getsum(int data,int data2)//返回两个数之和
{
return data+data2;
}
int getret(int data,int data2,int (*pfunc)(int,int))//最终确定结果的函数
{
int ret;
ret=(*pfunc)(data,data2);
return ret;
}
int main()
{
//函数指针的使用,用户输入1,2,3根据用户输入的数进行选择调用那个函数,
//1(两个数大数),2(两个数小数)3.(两个数的和)
int cmd;//用户输入的数
int data=10;
int data2=20;//两个整数
int ret;//结果
//提示用户输入的函数
initcmd(&cmd);
int (*func)(int,int);//定义一个函数指针指向两个实际参数的类型的函数
//根据switch进行选择
switch(cmd)
{
case 1:
func=getmax;
break;
case 2:
func=getmin;
break;
case 3:
func=getsum;
break;
default:
printf("(输入错误)请输入1——3之间的数\n");
exit(-1);
break;
}
//这里的func传递是个函数的地址,地址的形式参数写成函数指针
ret=getret(data,data2,func);
printf("最终结果ret=%d\n",ret);
return 0;
}
运行结果:
指针数组
指针数组实际上也是一个数组,这个数组里面内容是地址
定义方式:类型 *指针名字[常量]={地址};
例子:int *p[4]={&a,&b,&c,&d};
代码:
#include<stdio.h>
int main()
{
//指针数组
int a=10;
int b=20;
int c=30;
int d=40;
//定义一个指针数组保存上面变量的地址
int *p[4]={&a,&b,&c,&d};
//建立循环来输出上面变量
for(int i=0;i<4;i++)
{
//通过下标法来打印
printf("%d\t",*p[i]);//p[i]是个地址,需要用*号来取内容
}
putchar('\n');//换行
//使用指针偏移来打印
for(int i=0;i<4;i++)
{
printf("%d\t",*(*(p+i)));
}
return 0;
}
运行结果:
函数指针数组
和指针数组的区别就是,定义方式和里面内容不同
定义方式(*指针名字[常量])(类型)={函数地址};
函数指针数组里面保存的是多个函数地址
例子:(*pfunc[3])(int,int)={getmin,getmax,getret};
代码:
#include<stdio.h>
int getmin(int data,int data2)//返回小数
{
return data<data2?data:data2;
}
int getmax(int data,int data2)//返回大数
{
return data>data2?data:data2;
}
int getsum(int data,int data2)//返回整数的函数
{
return data+data2;
}
int main()
{
//使用函数指针数组输出这两个数中的大数,小数,和
int data=10;
int data2=20;
int ret;
int (*pfunc[3])(int , int)={getmin,getmax,getsum};
//建立循环来访问里面的函数
for(int i=0;i<3;i++)
{
ret=(*(pfunc+i))(data,data2);
printf("ret=%d\n",ret);
}
return 0;
}
运行结果:
指针函数
指针函数是可以返回地址的函数,该函数调用结束时返回一个地址
练习:3个学生每个学生4门课程,要求使用指针函数,输入学号输出该学号学生的成绩
代码:
#include<stdio.h>
int *getnumber(int(*p)[4],int data)//定义一个指针函数用来返回位置
{
int *p2;//局部指针存放数组中某个元素
p2=(int *)(p+data);//指针偏移的位置
return p2;
}
int main()
{
//输入学号,找到其中学生的成绩
int arry[3][4]={{66,77,57,89},
{34,56,78,99},
{88,67,75,46}};//3个学生,4门成绩
int number;//学号
printf("请输入学号0-2\n");
scanf("%d",&number);//输入学号,
int *p=getnumber(arry,number);//指向数组中某个元素,用函数返回数组中某个元素的位置
for(int i=0;i<4;i++)
{
printf("%d\t",*p++);
}
return 0;
}
运行结果:
基于上面例题,找到不及格学生的学号并输出成绩
代码:
#include<stdio.h>
int* getarry(int(*p)[4],int data)
{
int *p2;
p2=(int *)(p+data);//指针偏移到里面学生的位置
for(int i=0;i<4;i++)
{
if(*p2<60)//如果学生成绩有小于60的就返回当前学生的位置
{
return p2;
}
}
return NULL;//没有不及格返回空地址
}
int main()
{
//三个学生每个学生4门课程,找到其中有不及格课程的成绩和学号
int arry[3][4]={{66,77,56,88},{78,75,60,100},{33,65,64,61}};//学生和成绩
int *p=NULL;
for(int i=0;i<3;i++)//三个学生判断三次
{
p=getarry(arry,i);//返回学生位置的函数
if(p!=NULL)//如果返回的地址不是空说明有不及格的科目
{
printf("学号为%d的学生成绩不及格,成绩是\n",i);
for(int j=0;j<4;j++)//四门课程就循环四次
{
printf("%d\t",*p++);
}
printf("\n");
}
}
return 0;
}
运行结果:
二级指针
二级指针介绍
二级指针和指针的区别不大,指针可以保存变量的地址,而二级指针可以保存指针变量的地址
二级指针保存一级指针变量地址,n级指针保存n-1级指针地址
通过*号数量判断几级
二级指针:int **p2=&p;
三级指针:int ***p3=&p2;
#include<stdio.h>
int main()
{
//二级和多级指针
int data=100;
//访问变量的两种方式
//1.变量名字
printf("data的地址是%p,里面的值是%d\n",&data,data);
//2.通过指针
int *p=&data;
printf("p里面存放的地址是%p,通过指针取出来的值是%d\n",p,*p);
printf("指针变量p本身的地址是%p\n",&p);
//二级指针可以存放指针变量的地址
int **p2=&p;//保存变量p的地址
printf("p2指针里面的地址是p的地址%p,*p2的地址是data的地址%p\n",p2,*p2);
printf("p2本身的地址是%p\n",&p2);
//正确的打印出指针p2里面的值的方式是两个**号
printf("**p2的打印出里面的值是data的%d\n",**p2);
//多级指针可以存放二级指针变量的地址,三级指针
int ***p3=&p2;
printf("p3里面存放的地址是%p,*p3里面存放的地址是p的地址%p\n",p3,*p3);
printf("**p3里面存放的是data的地址%p,***p3是data的值%d\n",**p3,***p3);
return 0;
}
运行结果:可以观察到*号是取值作用,通过多级指针一级一级的进行取值
为啥使用二级指针
二级指针可以保存指针变量地址在把指针当作实际参数进行传递时可以间接改变主函数里面保存的地址,不用返回
代码:
#include<stdio.h>
void initarry(int (*p)[4])
{
int i,j;//控制循环变量
for(i=0;i<3;i++)
{
for(j=0;j<4;j++)
{
printf("请输入第%d个学生第%d门课程\n",i+1,j+1);
scanf("%d",*(p+i)+j);
}
}
}
void printarry(int (*p)[4])
{
int i,j;
for(i=0;i<3;i++)
{
printf("第%d个学生的成绩:\n",i+1);
for(j=0;j<4;j++)
{
//printf("第%d个学生的成绩:\n",i+1);
printf("%d\t",*(*(p+i)+j));
}
putchar('\n');
}
}
void seekarry(int (*p)[4],int **p2,int i)
{
*p2=(int *)(p+i);//找到当中成绩地址
}
int main()
{
//二级指针的使用
int arry[3][4];//3个学生每个学生4门课程
int *ppos=NULL;//一个空指针
//1.初始化函数
initarry(arry);
//2.打印出学生的成绩
printarry(arry);
//3.找到其中不及格学生和成绩
int i;
for(i=0;i<3;i++)//3个学生三次循环
{
//ppos=NULL;//每次进来指针都为空
seekarry(arry,&ppos,i);//在该函数中改变指针ppos的里面存放的地址
//if(ppos!=NULL){//如果不是空指针就打印里面成绩
for(int j=0;j<4;j++)//4门成绩就是4次循环
{
if(*(ppos+j)<60)//找到地址后进行偏移取值
{
printf("学号为%d的学生不及格成绩:",i+1);
printf("%d\t",*(ppos+j));
}
}
putchar('\n');
}
return 0;
}
上面定义的函数没有用到返回某个数据和地址,而是直接在定义函数中修改地址存放的内容
运行结果:找到每个学生不及格的成绩