文章目录
- [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,这是因为数组名表示首元素地址存在两个例外情况:
sizeof(数组名):sizeof中单独放数组名时,数组名表示整个数组,计算的是整个数组的大小,单位是字节&数组名:这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址存在区别)
除此之外,任何地方使用数组名,数组名都表示首元素的地址。
进一步测试&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,内存地址为0x0012ff50pa的值为0x0012ff50(a的地址),自身内存地址为0x0012ff42ppa的值为0x0012ff42(pa的地址),自身内存地址为0x0012ff30
5.2 二级指针的运算
二级指针的核心运算为解引用,有两层解引用逻辑:
-
第一次解引用(*ppa) :对ppa中存放的地址解引用,找到的是一级指针pa,因此
*ppa等价于pa。
示例:cint b = 20; *ppa = &b;//等价于 pa = &b; -
**第二次解引用(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个元素,实现类似二维数组的行列访问。
本质区别 :
指针数组模拟的二维数组并非真正的二维数组,真正的二维数组在内存中是连续存放的,而指针数组中每个元素指向的一维数组,其内存地址并非连续的,只是通过下标访问方式模拟了二维数组的效果。
指针数组模拟二维数组的内存示意:
