C语言从入门到进阶——第13讲:深入理解指针(3)

文章目录

  • [1. 数组名的理解](#1. 数组名的理解)
  • [2. 使用指针访问数组](#2. 使用指针访问数组)
  • [3. 一维数组传参的本质](#3. 一维数组传参的本质)
  • [4. 冒泡排序](#4. 冒泡排序)
  • [5. 二级指针](#5. 二级指针)
    • [5.1 二级指针的定义](#5.1 二级指针的定义)
    • [5.2 二级指针的运算](#5.2 二级指针的运算)
  • [6. 指针数组](#6. 指针数组)
  • [7. 指针数组模拟二维数组](#7. 指针数组模拟二维数组)

1. 数组名的理解

在使用指针访问数组内容时,会通过&arr[0]获取数组第一个元素的地址,而实际上数组名arr本身就是数组首元素的地址,通过以下代码可验证:

c 复制代码
//测试环境:X86
#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);
return 0;
}

输出结果:

复制代码
&arr[0] = 004FF9CC
arr = 004FF9CC

输出结果中&arr[0]arr的打印地址完全一致,证明数组名是数组首元素(第一个元素)的地址。

存在一个疑问:若数组名是首元素地址,sizeof(arr)的结果却并非地址的大小(4/8字节),示例代码如下:

c 复制代码
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", sizeof(arr));
return 0;
}

该代码输出结果为40,这是因为数组名表示首元素地址存在两个例外情况

  1. sizeof(数组名):sizeof中单独放数组名时,数组名表示整个数组,计算的是整个数组的大小,单位是字节
  2. &数组名:这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址存在区别)

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

进一步测试&arr[0]arr&arr的地址,代码如下:

c 复制代码
//测试环境:X86
#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;
}

三者打印结果完全相同,为区分差异,通过地址+1的操作验证,代码如下:

c 复制代码
//测试环境:X86
#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);
printf( "arr = %p\n", arr);
printf( "arr+1 = %p\n", arr+1);
printf( "&arr = %p\n", &arr);
printf( "&arr+1 = %p\n", &arr+1);
return 0;
}

输出结果:

复制代码
&arr[0 ] = 0077F820
&arr[0]+1 = 0077F824
arr = 0077F820
arr+1 = 0077F824
&arr = 0077F820
&arr+1 = 0077F848

从结果可得出结论:

  • &arr[0]arr都是首元素的地址,+1操作会跳过一个int类型元素,因此地址相差4个字节
  • &arr是整个数组的地址,+1操作会跳过整个数组,因此和原地址相差40个字节(10个int元素,每个4字节)

综上,数组名的核心意义为:数组名是数组首元素的地址,存在上述两个例外情况

2. 使用指针访问数组

基于数组名是首元素地址的特性,可通过指针便捷访问数组,实现数组的输入和输出,代码如下:

c 复制代码
#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);
//scanf("%d", arr+i);//也可以这样写
}
//输出
for(i = 0; i < sz; i++)
{
printf("%d ", *(p+i));
}
return 0;
}

由于数组名arr和指针变量p均指向数组首元素,二者在此处等价,因此可尝试用数组下标方式访问指针指向的内容,修改输出部分代码后仍可正常运行:

c 复制代码
#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);
//scanf("%d", arr+i);//也可以这样写
}
//输出
for(i = 0; i < sz; i++)
{
printf("%d ", p[i]);
}
return 0;
}

由此得出核心等价关系:

  • p[i] 等价于 *(p+i)
  • 同理,数组元素访问的本质是arr[i] 等价于 *(arr+i)

编译器处理数组下标访问时,会将其转换为首元素地址+偏移量求出元素地址,再通过解引用操作访问元素。

3. 一维数组传参的本质

先通过一个问题引入:将数组传递给函数后,能否在函数内部正确计算数组的元素个数?测试代码如下:

c 复制代码
#include <stdio.h>
//测试环境是x86
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]);
printf("sz1 = %d\n", sz1);
test(arr);
return 0;
}

输出结果为:

复制代码
sz1 = 10
sz2 = 1

函数内部无法正确获取数组元素个数,其原因就是一维数组传参的本质:数组传参时传递的是数组名,而数组名是首元素的地址,因此数组传参本质上传递的是数组首元素的地址。

基于此,函数形参理论上应使用指针变量接收首元素的地址,在函数内部执行sizeof(arr)时,计算的是指针变量的大小 (x86环境下4字节,x64环境下8字节),而非整个数组的大小,因此无法通过sizeof(arr)/sizeof(arr[0])计算元素个数。

以下两种函数形参写法本质完全相同:

c 复制代码
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;
}

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

4. 冒泡排序

冒泡排序的核心思想:两两相邻的元素进行比较,若顺序不符合要求则交换,经过多轮比较后,将最大/最小的元素逐步"冒泡"到数组末端/前端。

冒泡排序的实现代码(对整型数组进行升序排序):

c 复制代码
void bubble_sort(int arr[], int sz)//参数接收数组元素个数
{
int i = 0;
//外层循环:控制排序的轮数,共sz-1轮
for(i = 0; i < sz-1; i++)
{
int j = 0;
//内层循环:控制每轮的比较次数,每轮减少i次(末尾已排好序)
for(j = 0; j < sz-i-1; j++)
{
//相邻元素比较,前大后小则交换
if(arr[j] > arr[j+1])
{
int tmp = arr[j];
arr[j]= arr[j+1];
arr[j+1]=tmp;
}
}
}
}
int main()
{
int arr[] = {3,1,7,5,8,9,0,2,4,6};
int sz = sizeof(arr)/sizeof(arr[0]);
bubble_sort(arr, sz);
int i = 0;
//打印排序后的数组
for(i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}

5. 二级指针

5.1 二级指针的定义

指针变量也是变量,变量都有对应的内存地址,二级指针就是用来存放指针变量地址的变量。

示例代码:

c 复制代码
#include <stdio.h>
int main()
{
int a = 10;
int *pa = &a; //一级指针:存放整型变量a的地址
int** ppa = &pa;//二级指针:存放一级指针pa的地址
return 0;
}

变量内存地址关系示意:

  • a的值为10,内存地址为0x0012ff50
  • pa的值为0x0012ff50(a的地址),自身内存地址为0x0012ff42
  • ppa的值为0x0012ff42(pa的地址),自身内存地址为0x0012ff30

5.2 二级指针的运算

二级指针的核心运算为解引用,有两层解引用逻辑:

  1. 第一次解引用(*ppa) :对ppa中存放的地址解引用,找到的是一级指针pa,因此*ppa等价于pa。
    示例:

    c 复制代码
    int b = 20;
    *ppa = &b;//等价于 pa = &b;
  2. **第二次解引用(ppa) :先通过*ppa找到pa,再对pa解引用(pa),最终找到的是整型变量a,因此**ppa等价于 pa,也等价于a。
    示例:

    c 复制代码
    **ppa = 30;
    //等价于*pa = 30;
    //等价于a = 30;

6. 指针数组

对于指针数组的定义,可通过类比整型数组、字符数组理解:

  • 整型数组:存放整型数据的数组
  • 字符数组:存放字符数据的数组
  • 指针数组:存放指针(地址)的数组

指针数组的声明示例:int* parr[3]

该声明表示:parr是一个数组,数组包含3个元素,每个元素的类型为int*(整型指针),即每个元素都用于存放整型变量的地址。

整型数组、字符数组与指针数组的对比示意:

指针数组的特性:每个元素都是地址,因此每个元素都可以指向一块对应的内存区域。

7. 指针数组模拟二维数组

利用指针数组的特性,可通过多个一维数组结合指针数组模拟出二维数组的访问效果,实现代码如下:

c 复制代码
#include <stdio.h>
int main()
{
int arr1[] = {1,2,3,4,5};
int arr2[] = {2,3,4,5,6};
int arr3[] = {3,4,5,6,7};
//数组名是数组首元素的地址,类型是int*,可存放在int*类型的指针数组parr中
int* parr[3] = {arr1, arr2, arr3};
int i = 0;
int j = 0;
//外层循环:遍历指针数组的元素(对应二维数组的行)
for(i = 0; i < 3; i++)
{
//内层循环:遍历每个一维数组的元素(对应二维数组的列)
for(j = 0; j < 5; j++)
{
printf("%d ", parr[i][j]);
}
//每行打印完成后换行
printf("\n");
}
return 0;
}

访问原理
parr[i]:访问指针数组parr的第i个元素,该元素是一个int*类型的指针,指向对应的一维数组(arr1/arr2/arr3);
parr[i][j]:等价于*(parr[i]+j),先通过parr[i]找到对应的一维数组首地址,加上偏移量j后解引用,访问一维数组的第j个元素,实现类似二维数组的行列访问。

本质区别

指针数组模拟的二维数组并非真正的二维数组,真正的二维数组在内存中是连续存放的,而指针数组中每个元素指向的一维数组,其内存地址并非连续的,只是通过下标访问方式模拟了二维数组的效果。

指针数组模拟二维数组的内存示意:

相关推荐
white-persist1 小时前
【CTF线下赛 AWD】AWD 比赛全维度实战解析:从加固防御到攻击拿旗
网络·数据结构·windows·python·算法·安全·web安全
冰糖雪梨dd1 小时前
【JavaScript】 substring()方法详解
开发语言·前端·javascript
AsDuang1 小时前
Python 3.12 MagicMethods - 45 - __rpow__
开发语言·python
liuyao_xianhui1 小时前
动态规划_简单多dp问题_打家劫舍_打家劫舍2_C++
java·开发语言·c++·算法·动态规划
王伟19821 小时前
圆周率的历史发展与国际圆周率日
算法·圆周率
程序员夏末1 小时前
【LeetCode | 第五篇】算法笔记
笔记·学习·算法·leetcode
老鱼说AI1 小时前
祖师爷KR的C语言讲解:第6期-输入与输出
c语言·开发语言
小鸡脚来咯2 小时前
SQL表连接
java·开发语言·数据库
大鹏说大话2 小时前
消息队列 Kafka/RabbitMQ/RocketMQ 怎么选?业务场景对比指南
开发语言