[C语言]指针简介
前言
指针是C语言中的精髓,意味着学好指针才能发挥出C语言的强大作用。要看一个程序员用C的能力强不强,就要看其对指针的理解到不到位。
指针
数据存储在内存中。为了高效地访问数据,内存中的每个字节都被赋予一个唯一的地址。通过该地址,我们可以定位到对应的数据并进行相应操作。
取地址操作符、指针变量
&:取地址操作符。用来取出变量存储在内存中的地址指针变量:指针变量可以用来存储取出来的地址
如下图中的变量p1就是指针变量,变量的类型是int*,也就是指向整形的指针。
inta=0;int*pA=&a;// &a: 取变量 a 的地址 pA:类型为int* 的指针变量,pA的意思是point a。return0;指针变量本身也是一个变量,说明变量pA本身也占用字节,指针变量在32位操作环境下大小为4字节,而在64位操作环境下大小则为8个字节。不管指针指向什么数据,在同一环境下,它存储地址所需要的字节数都是固定的。
上述int*是指向整形的指针变量,还有各种其他的指针变量,如char**和double*以及void*(无类型指针,通用指针)…。
解引用操作符
得到了数据的存储地址,那么要怎么获得指针指向的实际数据呢?*:解引用操作符,解读地址的内容,以读取数据或者是修改数据。
例如:
#include<cstdio>intmain(){chara='a';// 创建了一个字符变量 a,并将字符的值初始化为 'a'char*pA=&a;printf("%c\n",*pA);//}运行结果:
a指针的加减运算
指针变量可以存储的地址,也可以进行加减运算,那么指针进行加减操作有什么意义呢?下面请看一段代码和运行结果
示例代码:
#include<stdio.h>intmain(){inta=0;int*pA=&a;printf("%d\n",*pA);printf("这是p1的地址: %p\n",pA);printf("这是p1+1的地址: %p\n",pA+1);printf("这是p1+2的地址: %p\n",pA+2);putchar('\n');charb='a';char*pB=&b;printf("%c\n",*pB);printf("这是p2的地址: %p\n",pB);printf("这是p2+1的地址: %p\n",pB+1);printf("这是p2+2的地址: %p\n",pB+2);return0;}运行结果:
0 这是p1的地址: 004FF9DC 这是p1+1的地址: 004FF9E0 这是p1+2的地址: 004FF9E4 a 这是p2的地址: 004FF9C7 这是p2+1的地址: 004FF9C8 这是p2+2的地址: 004FF9C9在内存中,int和char类型分别占用4字节和1字节的内存大小。 当我们对其类型的指针变量进行加减操作时,指针会按照其类型的大小进行位移,如int*的指针加上1就会从当前地址位移4个字节,char*指针加上1就会位移一个字节。如果我们指针p指向的是一个数组的起始元素地址,通过数组元素在内存中存储是连续的特性,可以通过加和减来定位数组中的每个元素的地址(计算偏移量时要小心,避免越界)。
数组名的理解
指针可以指向数组的元素,下面我们来研究一下指向数组的指针该如何使用吧。
下面来看具体例子说明数组变量名:
代码示例:
#include<cstdio>intmain(){intarr[10]={0};// arr 的类型: int[10]printf("%-20s %-14s %-10s\n","变量","地址(16进制)","地址(10进制)");printf("%-20s %-14p %-10d\n","arr:",arr,(int)arr);printf("%-20s %-14p %-10d\n","&arr[0]:",&arr[0],(int)&arr[0]);printf("%-20s %-14p %-10d\n","&arr:",&arr,(int)&arr);printf("%-20s %-14p %-10d\n","&arr[0] + 1:",&arr[0]+1,(int)(&arr[0]+1));printf("%-20s %-14p %-10d\n","&arr + 1:",&arr+1,(int)(&arr+1));printf("-------------------------------------\n");printf("%-20s %-14s\n","变量","大小");printf("%-20s %-14zu\n","sizeof(arr):",sizeof(arr));printf("%-20s %-14zu\n","sizeof(arr[0]):",sizeof(arr[0]));return0;}运行结果:
变量 地址(16进制) 地址(10进制) arr: 006FF7A8 7337896 &arr[0]: 006FF7A8 7337896 &arr: 006FF7A8 7337896 &arr[0] + 1: 006FF7AC 7337900 &arr + 1: 006FF7D0 7337936 ------------------------------------- 变量 大小 sizeof(arr): 40 sizeof(arr[0]): 4运行结果显示:
- 数组名其实是一个地址,它不仅代表数组首元素的地址还可以代表整个数组。
- 数组名只有在两种情况下才代表整个数组的地址。
- 第一种情况:&arr,这个操作是将整个数组的地址取出来,其值是数组的首元素地址,观察上面的运行结果中
arr、arr1 + 1和&arr + 1的十进制地址,发现arr1 + 1相比arr的偏移量是 4(一个int变量的大小),而&arr + 1跳相比arr的偏移量是 40(一个数组的大小,也就是10个int的大小)。 - 第二种情况:arr1放在sizeof()的括号里头时,代表的是整个数组,计算的内存是整个数组占用的内存大小。
- 第一种情况:&arr,这个操作是将整个数组的地址取出来,其值是数组的首元素地址,观察上面的运行结果中
既然数组名是首元素的地址,我们可以尝试用指针来访问数组的每一个元素而不用下标操作符“[]”来操作符,下面我们来看代码的具体实现。
#include<cstdio>intmain(){intarr[10]={10,9,8,7,6,5,4,3,2,1};int*p=arr;size_t size=sizeof(arr)/sizeof(arr[0]);// 此表达式可以计算数组中单个元素的大小for(inti=0;i<size;i++){printf("%d ",*(p+i));}putchar('\n');for(inti=0;i<10;i++){printf("%d ",p[i]);}return0;}运行结果:
10 9 8 7 6 5 4 3 2 1 10 9 8 7 6 5 4 3 2 1想要获取数组中的任意一个元素,可以用*(p + i)的方式或p[i]的方式获取。换句话说*(p1+i)和p1[i]是同一个意思。循环里变量i每增加一次,我们的指针就指向下一个元素的起始位置,然后根据指针类型进行解引用操作,就可以访问数组的每个元素。
指针数组
指针数组就是指针的数组,这个数组存放的变量是指针变量。我们通过指针数组模拟二维数组的示例代码来理解指针数组。
示例代码:
#include<cstdio>//指针数组模拟实现二维数组intmain(){//初始化三个一维数组intarr1[5]={1,2,3,4,5};intarr2[5]={6,7,8,9,10};intarr3[5]={11,12,13,14,15};//指针数组//arr数组存放的变量类型是int*的指针//存放的是一维数组的首元素地址int*arr[3]={arr1,arr2,arr3};//遍历数组,模拟一个三行五列的二维数组for(inti=0;i<3;i++){for(intj=0;j<5;j++){printf("%2d ",*(arr[i]+j));}//arr[i]可写成*(arr+i)printf("\n");}return0;}运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15我们所创建的指针数组里三个元素存放的是指针变量,每个指针变量指向三个一维数组的起始位置,当我们读取数组中第一个元素其实是得到第一个一维数组的首元素地址,我们对其+ i指针偏移并进行解引用,就可以访问到每一个元素了。
我们可以将上面代码的for循环中的i<5条件改为i<20,这样做虽然是不允许的,但是为了深入探讨问题我们可以试一试。
修改的代码:
for(intj=0;j<20;j++)// 循环条件 j < 5 改为 j < 20{printf("%2d ",*(arr[i]+j));}运行结果:
1 2 3 4 5 -858993460 230655617 13630200 6234595 1 17117464 17120824 1 17117464 17120824 13630292 6234154 230655757 6230051 6230051 6 7 8 9 10 -858993460 -858993460 1 2 3 4 5 -858993460 230655617 13630200 6234595 1 17117464 17120824 1 11 12 13 14 15 -858993460 -858993460 6 7 8 9 10 -858993460 -858993460 1 2 3 4 5 -858993460我们会发现访问完一维数组中的五个元素后,后面访问到的元素中有莫名其妙的值也有访问完的上一个一维数组存储的值。这是使用指针数组模拟二维数组与直接开辟二维数组的区别。开辟空间时是依据从高地址向低地址的顺序开辟的,当我们继续向高地址进行访问时,就有可能访问到之前开辟的一维数组的元素。直接创建的二维数组在内存中是连续的,而我们用指针数组和一维数组模拟的二维数组,在内存中并不是连续的。
字符指针
我们来看一段代码
#include<cstdio>// 字符指针和数组指针intmain(){charstr1[20]={"I love you!"};printf("%s\n",str1);constchar*str2="I love you!";printf("%s\n",str2);return0;}看到这里你是不是以为第二种方式是将字符串“I love you”传入了字符指针里了?其实它是将字符串中首元素地址“I”传入了str2中,只要找到字符串数组的首元素地址就可以将字符串打印出来了,\0为字符串的结束标志。
数组指针
数组指针就是指针,指向的内容是一个数组(细品这句话)。
下面我们用二维数组传参的方式来认识一下数组指针。
#include<cstdio>intmain(){//数组指针 指向数组intarr[3][5]={{1,2,3,4,5},{6,7,8,9,10},{11,12,13,14,15}};int(*pArr)[5]=arr;//这里放入的是二维数组首元素即一维数组的地址for(inti=0;i<3;i++){for(intj=0;j<5;j++){// pArr指向的是一个一维数组// (pArr + i) 理解为指向数组指针中的第 i 个一维数组,类型为 (*pArr)[5]// *(pArr + i) 解引用第 i 个一维数组,指向一维数组的首元素,类型为 int*// *(pArr + i) + j) 理解为指向一维数组中的第 j 个元素的起始位置,类型为 int*// *(*(pArr + i) + j) 解引用第 j 个元素的起始位置,获取到 int 元素printf("%2d ",*(*(pArr+i)+j));}putchar('\n');}printf("-----------------等价的访问方式-----------------\n");for(inti=0;i<3;i++){for(intj=0;j<5;j++){// *(*(parr1+i)+j)printf("%2d ",pArr[i][j]);}putchar('\n');}return0;}运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -----------------等价的访问方式----------------- 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15代码中我们声明了一个数组指针即int (*pArr)[5]。因为下标操作符[]比解引用操作符优先级更高,防止被理解为int*类型的指针数组,所以我们需要给指针*pArr加上括号来保证其所代表的是指针而不是数组。指针pArr指向的是int [5]类型的数组。
所谓的二维数组我们可以理解为一个存储了一维数组的数组,也就是二维数组中每个元素即为一个一维数组,所以二维数组的数组名代表了第一个整个一维数组的地址,即二维数组名指向二维数组的第一行元素(一个一维数组的起始地址).
我们将二维数组的第一个元素的地址存入数组指针pArr中,当我们进行pArr + i操作时,数组指针就会指向二维数组的下一个元素(下一个一维数组)。当我们对这一个元素的地址进行解引用的时候,我们就得到了这一个元素中的第一个元素(该元素就是int)的地址(元素的元素,降维),此时的*(pArr + i)指向的就是一维数组中的首个int元素的地址,当我们再进行+ j操作时,指针就会往后移动j个元素,解引用后得到第这个一维数组的第j个元素。
使用这样的方法,这样我们就能用数组指针遍历二维数组里每一个元素了,并将其打印出来。
函数指针
基本用法
函数指针也就是指向函数的指针,我们可以通过函数指针来调用函数。
函数由函数名、参数、返回值组成,其中函数名不仅仅是函数名,还代表着函数的地址。我们可以通过函数名和(&函数名)的方法得到函数的地址,这两种方法都是一样的,没有什么区别。下面我们通过代码来看看函数指针是如何声明和使用的吧!
示例代码:
#include<cstdio>//函数指针intmax(inta,intb){returna>b?a:b;//返回a和b中的最大值}intmain(){int(*p)(int,int)=max;// 函数指针inta=3,b=4;printf("%d和%d的最大值是max(a, b) = %d。\n",a,b,max(a,b));printf("%d和%d的最大值是p(a, b) = %d。\n",a,b,p(a,b));return0;}运行结果:
3和4的最大值是max(a, b) = 4。 3和4的最大值是p(a, b) = 4。在上述代码中,我们声明了一个函数指针p,它指向的函数类型的返回值是int型,传入的形参是两个int的数据(int, int)。
在后面的代码中,我们用指针的方式调用了函数max来求出两个数的最大值,即max(a, b)。如果不用常规函数名方式调用函数,也可以用函数指针,也就是p(a, b)。容易观察到,函数名起始就是一个地址,我们将函数地址存入函数指针中,我们就可以通过函数指针的方式来调用该函数了。
结合 qsort 排序任意类型的数组
函数指针的一个用法:c 标准库中有一个快速排序的函数叫做qsort,如果想要对自定义类型的数组(如自己定义的结构体的数组)进行排序,那么就要告诉qsort排序规则。首先要自己定义一个函数,根据比较方法来返回比较的结果,然后把这个比较规则的函数的函数指针传给qsort供其内部调用,这样就可以实现对任意类型的数据进行排序了!
代码示例:
#include<stdio.h>#include<stdlib.h>#include<string.h>// 定义学生结构体typedefstruct{charname[20];intage;floatscore;}Student;// 按年龄升序排序intcompareByAge(constvoid*a,constvoid*b){Student*s1=(Student*)a;Student*s2=(Student*)b;returns1->age-s2->age;// 升序}// 按分数降序排序intcompareByScoreDesc(constvoid*a,constvoid*b){Student*s1=(Student*)a;Student*s2=(Student*)b;// 分数高的排前面(降序)if(s1->score>s2->score)return-1;if(s1->score<s2->score)return1;return0;}// 按姓名升序排序intcompareByName(constvoid*a,constvoid*b){Student*s1=(Student*)a;Student*s2=(Student*)b;returnstrcmp(s1->name,s2->name);// 字符串比较}// 打印学生信息voidprintStudents(Student*students,intn){for(inti=0;i<n;i++){printf("%-10s\t%d岁\t%.1f分\n",students[i].name,students[i].age,students[i].score);}}intmain(){// 初始化学生数组Student students[]={{"Max Caulfield",20,85.5},{"Chloe Price",18,92.0},{"Rachel Amber",22,78.5},{"Kate Marsh",19,88.0},{"Warren Graham",21,95.5}};intn=sizeof(students)/sizeof(students[0]);printf("========== 原始数据 ==========\n");printStudents(students,n);// 按年龄排序qsort(students,n,sizeof(Student),compareByAge);printf("\n========== 按年龄升序 ==========\n");printStudents(students,n);// 按分数降序排序qsort(students,n,sizeof(Student),compareByScoreDesc);printf("\n========== 按分数降序 ==========\n");printStudents(students,n);// 按姓名排序qsort(students,n,sizeof(Student),compareByName);printf("\n========== 按姓名升序 ==========\n");printStudents(students,n);return0;}运行结果:
========== 原始数据 ========== Max Caulfield 20岁 85.5分 Chloe Price 18岁 92.0分 Rachel Amber 22岁 78.5分 Kate Marsh 19岁 88.0分 Warren Graham 21岁 95.5分 ========== 按年龄升序 ========== Chloe Price 18岁 92.0分 Kate Marsh 19岁 88.0分 Max Caulfield 20岁 85.5分 Warren Graham 21岁 95.5分 Rachel Amber 22岁 78.5分 ========== 按分数降序 ========== Warren Graham 21岁 95.5分 Chloe Price 18岁 92.0分 Kate Marsh 19岁 88.0分 Max Caulfield 20岁 85.5分 Rachel Amber 22岁 78.5分 ========== 按姓名升序 ========== Chloe Price 18岁 92.0分 Kate Marsh 19岁 88.0分 Max Caulfield 20岁 85.5分 Rachel Amber 22岁 78.5分 Warren Graham 21岁 95.5分转移表
我们可以创建一个指针数组,用来存放我们的函数的地址,然后通过不同的条件,调用数组中的不同函数,这种方式的调用就叫转移表。
如下是简单的创建函数指针数组实现简易的转移表代码:
示例代码:
#define_CRT_SECURE_NO_WARNINGS1#include<cstdio>intmax(inta,intb){returna>b?a:b;// 返回a和b中的最大值}intmin(inta,intb){returna<b?a:b;// 返回a和b中的最小值}intmain(){int(*p[3])(int,int)={NULL,max,min};inta,b,choice;do{printf("请输入两个整数: \n");scanf("%d %d",&a,&b);printf("请输入你的操作: 1---求最大值 2---求最小值\n");scanf("%d",&choice);if(choice==1)printf("%d和%d的最大值为: ",a,b);elseif(choice==2)printf("%d和%d的最小值为: ",a,b);else{printf("错误:请输入正确的操作!\n");return-1;}printf("%d\n",p[choice](a,b));printf("是否继续? 继续---Y, 退出---N\n");getchar();// 用于接收换行符}while('Y'==getchar());return0;}运行结果:
请输入两个整数: 520 1314 请输入你的操作: 1---求最大值 2---求最小值 1 520和1314的最大值为: 1314 是否继续? 继续---Y, 退出---N Y 请输入两个整数: 520 1314 请输入你的操作: 1---求最大值 2---求最小值 2 520和1314的最小值为: 520 是否继续? 继续---Y, 退出---N N以上就是我对于指针的理解
