C语言中指针的运用最为重要,在熟练使用一级指针之后,高级指针便接踵而来,今天就来介绍高级指针。
二级指针 (int ** p)
对于一级指针 int * q 来说,q是一个int * 类型的变量,指向一个 int 类型的变量。对于二级指针 int ** p 来说,p是一个 int ** 类型的变量,指向一个 int * 类型的变量,即 p 指向一个一级指针变量。
#include <stdio.h>
int main()
{
int **p = NULL;
int *q = NULL;
int a = 3;
q = &a; //q指向a
p = &q; //p指向q, *p的值为q(即*p=q), 所以*p指向a
printf("&a=%p, q=%p\n", &a, q); //q指向a
printf("&q=%p, p=%p\n", &q, p); //p指向q
printf("a=%d, *q=%d, **p=%d\n", a, *q, **p);
//*q=a, **p=a
return 0;
}
运行结果如下:
指针数组 (int * a[5])
指针数组,即指针的数组,是由指针构成的数组,数组中每个元素都是指针变量。
#include <stdio.h>
int main()
{
int *a[5];//指针数组,每个元素都指向int类型的变量
int b[5]={1, 2, 3, 4, 5};
for(int i=0; i<5; i++)
{//把int类型的变量b[i]的地址赋值给a[i]
a[i] = &b[i];
}
for(int i=0; i<5; i++)
{//通过指针变量a[i]修改b[i]
*a[i] = 5-i;
}
for(int i=0; i<5; i++)
{//打印数组b中的内容
printf("%d ", b[i]);
}
printf("\n");
for(int i=0; i<5; i++)
{//通过指针变量a[i]打印数组b中的内容
printf("%d ", *a[i]);
}
printf("\n");
return 0;
}
运行结果:
数组指针 (int ( * p)[5])
数组指针,就是指数组的指针,它是一个指针变量,但是指向的不是一个 int 类型的变量,而是一个数组。
这里,为了理解数组指针,我们先来看一看数组。对于一个一维数组 int a[5], 数组名是a,数组长度是 5,我们知道,a 既是数组名也是数组首元素的地址,记住是首元素,即 a = &a[0],也就是说 a 是指向 a[0] 的。当你定义 int a[5] 时,会在内存中分配一块连续的内存空间,只需要知道第一个变量的地址(a),再加上偏移量(数组下标)就可以访问数组中的任意一个元素。好了,既然 a 是数组首元素的地址,那么什么是数组的地址呢?
对于一个变量,我们直接使用"&“取地址, 对于数组也是如此,直接对数组名使用”&",取的就是数组的地址。
#include <stdio.h>
int main()
{
int a[5];
printf("%p %p\n", a, &a);
return 0;
}
这里发现,数组的地址和数组的首元素地址是相同的,那么,a 和 &a 是一样的吗?显然不是,a 指向a[0],是int * 类型的;&a 指向一个长度为5的数组,是 int (*)[5] 类型的,它是一个数组指针。那么,指向不同类型数据的指针变量有什么不同呢?
我们通过指针的加减法来理解不同类型的指针变量的不同。先区别一下以下两个概念:
偏移量
指针变量在进行加减法时加上或减去的值就是偏移量,如 (p-i 或 p+i) 表示取指针 p 向左或者向右偏移 i 个偏移单位的地址。
偏移单位
指针变量在进行偏移时,每偏移一个偏移量所偏移的字节个数。
#include <stdio.h>
int main()
{
int a=3;
int *p=&a;
//这里不关心p指向的值,只单纯的对它做加法,看看地址的变化
printf("int*: %u %u %u\n", p, p+1, p+2);
//这里观察偏移量为1时、为2时的地址分别是多少
char b='c';
char *q=&b;
printf("char*: %u %u %u\n", q, q+1, q+2);
return 0;
}
这里很好的发现,p 是 int * 类型,q 是 char * 类型,它们的偏移量相同,但是偏移单位不同,分别是4个字节和1个字节,所以,指针的类型不同,指针的偏移单位也不同。那么之前的问题也随之而解了。
#include <stdio.h>
int main()
{
int a[5]={};
int (*p)[5] = &a; //对p初始化,让其指向数组a
printf("int a[5]: %u %u\n", a, a+1);
printf("int (*p)[5]: %u %u\n", p, p+1);
return 0;
}
可以知道,由于 a 指向的是 int 类型的数据,所以偏移单位是4个字节;p 指向的是长度为5的存储 int 类型数据的数组,所以它的偏移单位是5 * 4 = 20个字节。
那么,如何通过数组指针访问数组元素呢?
#include <stdio.h>
int main()
{
int a[5]={1, 2, 3, 4, 5};
int (*p)[5] = &a; //对数组指针初始化,p和&a的类型是相同的,所以*p可以起到a的作用
printf("a[3]: %d %d\n", a[3], (*p)[3])
//p和&a的类型是相同的,所以*p可以起到a的作用
printf("a[2]: %d %d\n", *(a+2), *(*p+2));
//a也是指针,a[2]本质上是通过指针访问元素,即 a[2]=*(a+2),同理,(*p)[2]=*(*p+2)
return 0;
}
接下来,谈一谈一维数组和二维数组的相同点与不同点。
在定义一个一维数组时(int a[5]) ,会分配一块地址连续的内存块(5 * 4=20字节)。在定义一个二维数组时 (int mat[3][5]),同样分配一个地址连续的内存块(3 * 5 * 4=60字节)。所以,二维数组和一位数组具有同样的物理结构,都可以通过对首元素地址进行偏移来访问元素。
#include <stdio.h>
int main()
{
int mat[3][5]={};
for(int i=0; i<3*5; i++)
{//给数组赋值
mat[i/5][i%5]=i+1;
}
//二维数组也是连续的内存块,取它的首元素地址
int *m = &mat[0][0];
for(int i=0; i<3*5; i++)
{//通过首元素地址访问
printf("%d\t", m[i]);
i%5==4 && printf("\n");
}
return 0;
}
但是它们也有不同点。对于一维数组 int a[5], a[i] = * (a+i),即一维数组名是一维数组首元素地址;对于二维数组 int mat[3][5],mat[i][j] = * ( * (mat+i)+j),这里发现,它和之前的数组指针的写法相同,* ( * p+i)= * (* (p+0)+i)。所以,对于一个二维数组数 int mat[3][5],它的数组名 mat 是 int ( * )[5] 类型的。
二维数组是包含若干个一维数组的一维数组,在内存中是连续的。它的数组名是数组指针类型,对数组名加1(mat+1),则地址会偏移一个一维数组,此时 mat+1 指向行下标为1的一维数组,* (mat+1) 指向行下标为1的一维数组的首元素,(* (mat+1)+1)指向这个一维数组中下标为1的元素,* (* (mat+1)+1)为这个元素的值。
#include <stdio.h>
int main()
{
int mat[3][5]={};
int (*p)[5]=mat;
//p指向长度为5的一维数组,p的偏移单位是一维数组的内存长度
for(int i=0; i<3*5; i++)
{//给数组赋值
mat[i/5][i%5]=i+1;
}
for(int i=0; i<3; i++)
{//通过p访问元素
for(int j=0; j<5; j++)
{
printf("%d\t", *(*(p+i)+j));
//也可以写作p[i][j]
}
printf("\n");
}
return 0;
}
看到这里,发现对于任何指针来说,我们必须明确它的偏移单位,这样才能在使用时不出错。
#include <stdio.h>
int main()
{
int a=0;
int *p1 = &a;
printf("%u %u\n", p1, p1+1);
//指针p1的偏移单位是1个int数据的长度,是4个字节
int **ps;
printf("%u %u\n", ps, ps+1);
//指针ps的偏移单位是1个int*数据的长度,是4或8个字节
int (*p2)[1];
printf("%u %u\n", p2, p2+1);
//指针p2的偏移单位是1个int数据的长度,是4个字节
int (*p3)[0];
printf("%u %u\n", p3, p3+1);
//指针p3的偏移单位是0个int数据的长度,无论偏移量多少,都不偏移
return 0;
}
指针函数
指针函数是一个函数,这个函数的返回值是一个指针。例如:char * strcpy(char * dest, const char * src); 这是C语言的字符串拷贝函数,它在拷贝结束后将拷贝完成的字符串的地址return返回。指针函数并不难理解,就不介绍了。
函数指针
函数指针是一个指针,这个指针指向一个函数。定义一个函数指针变量时,需要明确指针指向函数的类型(函数返回值类型,函数形参类型)。当让函数指针指向一个函数后,就可以通过函数指针调用函数了。
这里要知道,定义一个函数后(是定义不是声明),函数名就是函数的地址,在调用函数时,需要通过函数地址进入函数。
#include <stdio.h>
void func(void)
{
printf("function!\n");
}
int bar(void)
{
printf("bar!\n");
}
void goo(int a)
{
printf("goo!\n");
}
int main()
{
//打印func()函数的地址
printf("%p\n", func);
//定义函数指针变量pf,指向的函数返回值为void,形参列表为void
void (*pf)(void) = func;//让pf指向相同类型的
//定义函数指针pb,指向的函数返回值为int类型,形参列表为void
int (*pb)(void) = bar;
//定义函数指针pg,指向的函数返回值为void,形参列表只有一个形参,为int类型
void (*pg)(int) = goo;
//调用函数
pf();
pb();
pg(1);
return 0;
}
我们还可以声明函数指针数组,即一个数组中的元素是一类相同的函数指针。
#include <stdio.h>
void f1(void)
{
printf("f1\n");
}
void f2(void)
{
printf("f2\n");
}
void f3(void)
{
printf("f3\n");
}
void f4(void)
{
printf("f4\n");
}
void f5(void)
{
printf("f5\n");
}
int main(){
//给函数指针取别名PF
typedef void (*PF)(void);
PF arr[5]={f1,f2,f3,f4,f5};
//void (*parr[5])(void) = {f1,f2,f3,f4,f5}也是一种声明的方法
for(int i=0; i<5; i++)
{//调用函数
arr[i]();
}
return 0;
}