一、地址的相关介绍
1、指针
指针变量是为了储存内存地址的一个变量,在我们创建一个变量的时候,就会在内存中申请一块区域,这块区域就是这个变量的地址。以整形变量为例,我们创建一个变量a,就会在内存空间申请四个字节的地址:
0x006FFD70
0x006FFD71
0x006FFD72
0x006FFD73
由于这个四个地址的序号都是连着一块的,当我们知道第一个字节的地址时,就可以顺便地推出另外三个字节的地址。所以为了方便,指针指向a时,实际上指向的是a的第一个字节。
我们如何去取出一个元素的地址呢?这里就要介绍一个操作符了:&,取地址操作符。
int main()
{
int a = 10;
&a;//取出a的地址
printf("%p\n", &a);//%p专门用来打印地址
return 0;
}
这样就会打印出a所占四个字节的第一个字节的地址,比如:0x006FFD70,我们一般使用一个指针变量来接收 ,指针变量也是⼀种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址。
int *p=&a;
这里的我们用了int * p=&a来定义一个p指针。
*表示p是一个指针,前面的int则表示这个指针指向的是一个int类型的对象,所以,当我们要存储的地址是一个char类型的数据时,我们就应该使用:char* 。
我们拿到了地址,又改如何去使用呢?这里也有一个操作符:*,解引用操作符
int main()
{
int a = 100;
int* pa = &a;
*pa = 0;
return 0;
}
我们把a的地址给了指针pa,指针变量就是指向的a的地址,于是我们用解引用操作符*来通过pa中存放的地址,找到指向的空间,也就是说,*pa就相当于是a了,我们这时将*pa赋值于0,就相当于把a的值改成了0 。
指针变量也是有类型的,比如int*,char*,甚至还有void*这一可以接收任意数据类型的指针,但不论是是什么类型的指针变量,在相同的平台,他们的大小也是一定的,在64位操作系统下是8,在32位操作系统下是4。
指针变量的大小既然在同一操作系统下是一样的,那为什么还要分这么多类型呢?
#include <stdio.h>
//代码1
int main()
{
int n = 0x11223344;
int *pi = &n;
*pi = 0;
return 0;
}
//代码2
int main()
{
int n = 0x11223344;
char *pc = (char *)&n;
*pc = 0;
return 0;
}
当然,指针类型也会影响指针进行加减操作时跳过的字节大小。
int main()
{
int n = 1;
int* pn = &n;
char* pc = (char*)&n;
printf("%p\n",&n);
printf("%p\n",pn);
printf("%p\n",pc);
printf("%p\n",pn+1);
printf("%p\n",pc+1);
return 0;
}
2、传值调用与传址调用
在下面的的代码中:
int swap(int a, int b)
{
int c = a;
a = b;
b = c;
}
int main()
{
int a = 10;
int b = 20;
swap(a, b);
printf("%d,%d\n", a, b);
return 0;
}
我们再swap函数中对a,b的值进行了交换,但是打印出来的a,b的值仍然是原理的值,这是为什么呢?
这是因为形参与实参是两个互不影响的参数,实参与形参的地址不同,而当我们使用swap函数时i,我们只是将实参的数值拷贝到了形参上,并不是把实参的地址传过去,所以我们对形参进行交换,并不会找到实参的地址,从而对实参的数值进行交换。
那要如何才能让实参交换呢,这里就要运用指针的知识了:
void swap2(int* a, int* b)
{
int num = *a;
*a = *b;
*b = num;
}
int main()
{
int a = 10;
int b = 20;
swap1(a, b);
printf("%d,%d\n", a, b);
swap2(&a, &b);
printf("%d,%d\n", a, b);
return 0;
}
像这样,我们把a与b的地址当做是实参,传递到形参指针变量里面,通过对a,b地址上面存储的值进行交换,完成了交换a,b值的功能。
3、数组中的地址
数组中的变量也都有地址,例如
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0]
中,我们定义了一个arr数组,然后又定义了一个指针p来指向该数组。
int main()
{
int arr[3] = { 1,2,3 };
printf("arr[0]=%p\n", &arr[0]);
printf("arr= %p", arr);
return 0;
}
显然我们默认打印数组的地址的时候,打印出的是数组首元素的地址,但当我们用sizeof打印arr的大小时,却出现了意外:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int ret = sizeof(arr);
printf("%d\n", ret);
return 0;
}
4、数组的地址首元素地址的差别
我们之前说到,整个数组的地址与首元素的地址的有区别的,那么区别在于哪里呢?
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", arr + 1);
printf("%p\n", &arr[0]);
printf("%p\n", &arr[0] + 1);
printf("%p\n", &arr);
printf("%p\n", &arr + 1);
return 0;
}
ji
5、指针访问数组
结合数组,指针与地址的关系,我们就可以很轻松的使用指针来访问数组了
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;//让p指向数组arr
for (int i = 0; i < 10; i++)
{
printf("%d ", arr[0] + i);
printf("%d\n", *(p + i));
}
return 0;
}
我们发现,arr[0]+i与*(p+i)是等效的,这就是因为其实两者的本职都是找到地址,然后在进行改变。
我们可以通过arr[i]找到地址,那也可以通过p(i)找到地址。
两者传参的本质都是差不多,都是传递的地址,所以⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。
二、指针数组
1、二级指针与多级指针
我们学会了用指针变量指向一个变量的地址,那么我们如何指向一个指针的地址呢?
这里就要用到二级指针了:
int main()
{
int a = 10;
int* p = &a;
int** pa = &pa;
return 0;
}
这里的*pa就是一个指向指针p地址的二级指针,**pa 先通过 *p找到 a ,然后对 a 进⾏解引⽤操作: *pa ,那找到的是 a 。
依次类推,多级指针也是这样。
2、指针数组与数组指针
指针数组是数组,存放指针的数组。
我们通常这样定义:
int * a[5];
char* b[3];
int*代表数组的存放的元素类型是int*指针,【5】代表元素个数是5,a为数组名,同理,char*是b数组的元素类型元素个数为3.
而数组指针有所不同,定义方式是:
int (*a)[5];
char (*b)[3];
*a代表这是一个指针,指向了一个有5个元素,并且全是int类型的的数组。
对于指针数组来说,在某种情况下可以代替二维数组来使用,例如:
int main()
{
int arr1[5] = { 1,2,3,4,5 };
int arr2[5] = { 3,4,5,6,7 };
int arr3[5] = { 6,7,8,9,10 };
int* str[3] = { arr1,arr2,arr3 };
//数组名是数组⾸元素的地址,类型是int*的,就可以存放在parr数组中
//用指针数组中的指针接受上面三个数组的地址
for (int i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{
printf("%d ", str[i][j]);
}
printf("\n");
}
return 0;
}
在这串代码中,我们创建的指针数组就模拟了二维数组的功能,但要记住,这实际上并非完全是⼆维数组,因为每⼀行并非是连续的。
而数组指针是用来存储数组地址的,我们需要用&数组名取出该数组的地址,并让指针指向该地址。
3、二维数组传参的本质
当我们调用函数,实参包含一个二维数组时,我们应该怎么传递该实参呢?
4、函数指针变量与函数指针数组
根据前面的一系列数组指针,整形指针,那我们不难推出,存储函数地址的指针,就是函数指针。
大家可能会好奇,函数也会有地址吗?
我们做个测试:
//函数指针地址的证明
int Add(int x, int y)
{
return x + y;
}
int main()
{
printf("%p\n", Add);
printf("%p\n", &Add);
return 0;
}
运行代码后,确实发现打印出来了地址,说明函数也是有地址的。
void test()
{
printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)() = test;
int Add(int x, int y)
{
return x + y;
}
int(*pf3)(int, int) = Add;//Add也代表地址
int(*pf3)(int x, int y) = &Add;//x和y写上或者省略都是可以的
以上我们创建了test与Add两个不同类型的函数,创建函数指针变量的方式自然也不同,以*pf3为例:
int (*pf3) (int x, int y)
| | ------------
| | |
| | pf3指向函数的参数类型和个数的交代
| 函数指针变量名
pf3指向函数的返回类型
int (*) (int x, int y) //pf3函数指针变量的类型
函数指针变量是一个指向函数的指针,它可以存储函数的地址。通过函数指针变量,我们可以调用相应的函数。下面是一个简单的例子:
int Add(int x, int y)
{
return x + y;
}
int(*pf3)(int, int) = Add;//Add也代表地址
/*int(*pf3)(int x, int y) = &Add;*///x和y写上或者省略都是可以的
int main()
{
printf("%d\n", pf3(2, 4));
printf("%d\n", (*pf3)(2, 4));
return 0;
}
最后都会打印出来6,第二个要使用括号将指针名与解引用操作符括起来,因为解引用优先级较低。
了解了函数指针类型,那么函数指针数组自然就是存放一系列函数指针的数组,但这个数组的命名方式有所不同,并不是把数组名放在后面,如:
//函数指针数组的声明:
int(*arr[5])();
arr[ 5 ]表示是一个数组,int(*)()表示储存的元素类型都是函数指针。
函数指针变量和函数指针数组是C语言中强大而有用的工具。它们允许我们动态地选择要执行的函数,并将函数作为参数传递给其他函数。通过理解和灵活运用函数指针变量和函数指针数组,我们可以写出更加灵活、可扩展的代码。希望本文能够帮助你理解并使用这些概念。