第12讲:深⼊理解指针-通过指针引用数组

目录

  1. 数组名的理解
  2. *使用指针访问数
  3. 一维数组传参的本质
  4. 冒泡排序
  5. 二级指针
  6. 指针数组
  7. 指针数组模拟二维数组

正文开始

数组名的理解(补充知识点)

使用指针访问数组的内容时,有这样的代码:

1
2
int arr[ 10 ] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 };
int *p = &arr[ 0 ];

这里我们使用&arr[0]的方式拿到了数组第一个元素的地址,但是其实数组名本来就是地址,而且是数组首元素的地址,我们来做个测试

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main()
{
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; //数组在内存中是连续存放的,所以只用找到数组首元素的地址即可找到整个数组
//int* p = &arr[0]; //数组名是数组首元素的地址
printf("&arr[0] = %p\n", &arr[0]); //取出首元素,得到数组首元素的地址
printf("arr = %p\n", arr); //数组名就是数组首元素的地址

return 0;
}

输出结果(一下是在x86(打印的地址较短)架构下编译的):

alt text

我们发现数组名和数组首元素的地址打印出的结果一模一样,数组名就是数组首元素(第一个元素)的地址

这时候有同学会有疑问?数组名如果是数组首元素的地址,那下面的代码怎么理解呢?

1
2
3
4
5
6
7
#include <stdio.h>
int main()
{
int arr[ 10 ] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 };
printf("%d\n", sizeof(arr)); //sizeof是就是计算数组大小的,如果数组名表示首元素地址,x86——>4,x64——>8,但是在x86打印出的是40,x64打印出的是80,这时候就有点迷糊了。
return 0 ;
}

alt text

输出的结果是: 40 ,如果arr是数组首元素的地址,那输出应该的应该是 4 / 8 才对

其实数组名就是数组首元素(第一个元素)的地址是对的,但是有两个例外:

  • sizeof(数组名) ,sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节,所以打印出来的结果是40

  • &数组名 ,这里的数组名表示整个数组,取出的是 整个数组的地址 (整个数组的地址和数组首元素的地址是有区别的)

除此之外,任何地方使用数组名,数组名都表示首元素的地址

这时有好奇的同学,再试一下这个代码:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main()
{
int arr[ 10 ] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 };
printf("&arr[0] = %p\n", &arr[ 0 ]); //打印数组首元素的地址
printf("arr = %p\n", arr); //数组名打印数组首元素的地址
printf("&arr = %p\n", &arr); //打印整个数组的地址
return 0 ;
}

alt text

三个打印结果一模一样,这时候又纳闷了,那arr和&arr有啥区别呢?

总结

指针类型决定了指针的差异


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
int main()
{
int arr[ 10 ] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 };
printf("&arr[0] = %p\n", &arr[ 0 ]); //打印数组首元素的地址
printf("&arr[0]+1 = %p\n", &arr[ 0 ]+ 1 ); //int*类型指针,地址加1,跳过第一个元素,4个字节

printf("arr = %p\n", arr); //数组名打印数组首元素的地址
printf("arr+1 = %p\n", arr+ 1 ); //int*类型指针,地址加1,跳过第一个元素,4个字节

printf("&arr = %p\n", &arr); //打印整个数组的地址
printf("&arr+1 = %p\n", &arr+ 1 ); //int*类型指针,地址加1,跳过整个数组,40个字节

return 0 ;
}

输出结果:

1
2
3
4
5
6
7
8
&arr[ 0 ] = 0077F 820
&arr[ 0 ]+ 1 = 0077F 824

arr = 0077F 820
arr+ 1 = 0077F 824

&arr = 0077F 820
&arr+ 1 = 0077F 848

这里我们发现&arr[ 0 ]和&arr[ 0 ]+ 1 相差 4 个字节,arr和arr+ 1 相差 4 个字节,是因为&arr[ 0 ] 和 arr都是首元素的地址,+ 1 就是跳过一个元素

但是&arr和&arr+ 1 相差 40 个字节,这就是因为&arr是数组的地址,+ 1 操作是跳过整个数组的

到这里大家应该搞清楚数组名的意义了吧

数组名是数组首元素的地址,但是有 2 个例外

使用指针访问数组

有了前面知识的支持,再结合数组的特点,我们就可以很方便的使用指针访问数组了


以下是使用数组下标访问数组元素的过程:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
int main()
{
int arr[ 10 ] = { 0 };
int sz = sizeof(arr)/sizeof(arr[ 0 ]); //整个数组大小/一个元素的大小=数组的元素个数

//输入
int i = 0;
for(i= 0 ; i<sz; i++)
{
scanf("%d", &arr[i]);
}
//输出
for(i= 0 ; i<sz; i++)
{
printf("%d ", arr[i]);
}
return 0 ;
}

输出结果:

alt text


以下是使用指针访问数组元素的过程:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
int main()
{
int arr[ 10 ] = { 0 };
int sz = sizeof(arr)/sizeof(arr[ 0 ]); //整个数组大小/一个元素的大小=数组的元素个数

//输入
int i = 0 ;
int* p = arr;

for(i= 0 ; i<sz; i++)
{
scanf("%d", p+i);//p+i是下标为i的元素的地址
}
//输出
for(i= 0 ; i<sz; i++)
{
printf("%d ", *(p+i)); //对(p+i)进行指针加法,解引用,得到元素的值
}
return 0 ;
}

输出结果:

alt text

数组名arr是数组首元素的地址,可以赋值给p,其实数组名arr和p在这里是等价的。那我们可以使用arr[i]可以访问数组的元素,那p[i]是否也可以访问数组呢?


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
int main()
{
int arr[ 10 ] = { 0 };
//输入
int i = 0 ;
int sz = sizeof(arr)/sizeof(arr[ 0 ]); //整个数组大小/一个元素的大小=数组的元素个数

//输入
int* p = arr;
for(i= 0 ; i<sz; i++)
{
//scanf("%d", p+i); //p+i是下标为i的元素的地址
scanf("%d", arr+i);//也可以这样写
}

//输出
for(i= 0 ; i<sz; i++)
{
//printf("%d ", *(p+i));
printf("%d ", *(arr+i)); //也可以这样写
}
return 0 ;
}

输出结果:

alt text

在第 18 行的地方,将(p+i)换成p[i]也是能够正常打印的,所以本质上p[i]是等价于(p+i)**。

同理arr[i]应该等价于(arr+i),数组元素的访问在编译器处理的时候,也是转换成首元素的地址+偏移量求出元素的地址,然后解引用来访问的*。

总结

alt text

内容总结

alt text

一维数组传参的本质

数组我们学过了,之前也讲了,数组是可以传递给函数的,这个小节我们讨论一下数组传参的本质

首先从一个问题开始,我们之前都是在函数外部计算数组的元素个数,那我们可以把数组传给一个函数后,函数内部求数组的元素个数吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

//数组传参的时候,形参可以写成指针形式,也可以写成数组形式
//写成数组形式,最简单,是为了方便理解,容易接受这种语法
//但是即使写成数组形式,本质上还是指针变量

//int * arr
void test(int arr[]) //参数写成数组形式,本质上还是指针
{
int sz2 = sizeof(arr)/sizeof(arr[ 0 ]); //计算一个指针变量的大小
printf("sz2 = %d\n", sz2);
}

int main()
{
int arr[ 10 ] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 };
int sz1 = sizeof(arr)/sizeof(arr[ 0 ]); //szl=10
printf("sz1 = %d\n", sz1); //10

test(arr); //arr是数组名,本质上是数组首元素的地址
//数组传参的本质,传递的是数组首元素的地址
//所以形参即使写成数组形式,本质上也是一个指针变量

return 0 ;
}

输出的结果:

alt text

我们发现在函数内部是没有正确获得数组的元素个数

这就要学习数组传参的本质了,上个小节我们学习了:

数组名是数组首元素的地址;
那么在数组传参的时候,传递的是数组名,也就是说 本质上数组传参传递的是数组首元素的地址 。

所以函数形参的部分理论上应该使用指针变量来接收首元素的地址。
那么在函数内部我们写sizeof(arr)计算的是一个地址的大小(单位字节)而不是数组的大小(单位字节)。
正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void test(int arr[])//参数写成数组形式,本质上还是指针
{
printf("%d\n", sizeof(arr));
}

void test(int* arr)//参数写成指针形式
{
printf("%d\n", sizeof(arr));//计算一个指针变量的大小
}

int main()
{
int arr[ 10 ] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 };
test(arr);
return 0 ;
}

总结: 一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。

冒泡排序

冒泡排序的核心思想就是:两两相邻的元素进行比较,不满足顺序就交换位置,满足顺序就找下一个数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
void input(int *arr, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
scanf("%d", arr + i);
}
}

int count = 0;
void bubble_sort(int *arr, int sz)
{
//确定趟数
int i = 0;
for (i = 0; i < sz - 1; i++)
{
int flag = 1;//假设已经满足顺序
//每一趟内部的比较
int j = 0;
for (j = 0; j < sz-1-i; j++)
{
count++;
if (arr[j] > arr[j + 1])
{
flag = 0;//还不是有序
//交换
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
if (flag == 1)
{
break;
}
}
}

void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}

int main()
{
int arr[10] = { 0 };
//输入一些值
//1 2 3 4 5 6 7 8 9 10
//9 8 7 6 5 4 3 2 1 0
//2 1 9 7 0 3 4 8 5 6
int sz = sizeof(arr) / sizeof(arr[0]);
input(arr, sz);

//排序 - 写一个函数完成数组的排序,排成升序
bubble_sort(arr, sz);//使用冒泡排序
print_arr(arr, sz);
printf("\ncount = %d\n", count);
return 0;
}

画图理解

alt text

二级指针

什么是二级指针?

1
2
3
4
char*
int*
double*
//一级指针

这就是二级指针

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main()
{
int a = 10 ;
int* pa = &a ;//pa一级指针变量
int** ppa = &pa ;//ppa是二级指针变量,指向一级指针

printf("%d\n", **ppa); //输出10
return 0 ;
}
1
**ppa == *pa == a ==10 //三者等价

alt text

alt text

对于二级指针的运算有:

  • ppa通过对ppa中的地址进行解引用,这样找到的是pa,ppa其实访问的就是pa.
1
2
int b = 20 ;
*ppa = &b;//等价于 pa = &b;
  • *ppa先通过ppa找到pa,然后对pa进行解引用操作:*pa,那找到的是a.
1
2
3
**ppa = 30 ;
//等价于*pa = 30;
//等价于a = 30;

总结

二级指针变量是用来存放一级指针变量的地址的。

指针数组

指针数组是指针还是数组?

我们类比一下,整型数组,是存放整型的数组,字符数组是存放字符的数组。

1
2
3
char arr[10]; //字符数组-存储字符的数组
int arr[5]; //整型数组-存储整型的数组

那指针数组呢?

指针数组-存放指针的数组,数组的每个元素其实是指针类型,都是用来存放地址的。

1
2
char* arr[5]; //存放字符指针的数组
int* arr[5]; //存放整型指针的数组

是存放指针的数组。

alt text

指针数组的每个元素都是用来存放地址(指针)的

如下图:

alt text

指针数组的每个元素是地址,又可以指向一块区域。

通过一个简单的例子来说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>

int main()
{
int a = 10;
int b = 20;
int c = 30;

//使用指针变量,这样太麻烦了,所以使用指针数组
//int* p1 = &a;
//int* p2 = &b;
//int* p3 = &c;

//使用指针数组
int* arr[3] = { &a, &b, &c }; //存放3个整型指针的数组
// 0 1 2 //通过下标进行指针解引用

int i = 0;
for(i = 0; i < 3; i++)
{
printf("%d ", *(arr[i])); //解引用,输出地址的值
}
return 0;
}

*输出结果:

1
10 20 30

说明:

  1. 指针数组的每个元素是地址,解引用可以得到地址的值。

  2. 指针数组的本质就是存放指针的数组,数组的每个元素都是指针类型,都是用来存放地址的。

  3. 指针数组的每个元素都是地址,又可以指向一块区域。

指针数组模拟二维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
int main()
{
int arr1[5] = { 1 , 2 , 3 , 4 , 5 }; //定义3个整型一维数组
int arr2[5] = { 2 , 3 , 4 , 5 , 6 };
int arr3[5] = { 3 , 4 , 5 , 6 , 7 };
//arr1 arr2 arr3 是三个数组的数组名,表示三个数组的首元素的地址

//数组名是数组首元素的地址,类型是int*的,就可以存放在arr数组中
int* arr[ 3 ] = {arr1, arr2, arr3};
int i = 0 ;

for(i= 0 ; i< 3 ; i++) //通过for循环,访问三个数组的数组名
{
int j = 0 ;
for(j= 0 ; j< 5 ; j++) //通过for循环,访问三个数组中某个数组的元素
{
printf("%d ", arr[i][j]); //*(*(arr+i)+j)
}
printf("\n");
}
return 0 ;
}

输出结果:

1
2
3
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7

画图理解:

alt text

下面是对上述第10~20行代码的解释:

arr1 arr2 arr3 是三个数组的数组名,表示三个数组的首元素的地址

arr[i]访问arr数组的元素arr[i]找到的数组元素指向了整型一维数组arr[i][j]就是整型一维数组中的元素

上述的代码模拟出二维数组的效果,实际上并非完全是二维数组,因为每一行并非是连续的。