指针与数组:深入C语言的内存操作艺术

数组名的理解

在上⼀个章节我们在使⽤指针访问数组的内容时,有这样的代码:

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

这⾥我们使⽤ &arr[0] 的⽅式拿到了数组第⼀个元素的地址,但是其实数组名本来就是地址,⽽且

是数组⾸元素的地址,我们来做个测试。

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

运行后你会发现,数组名和数组首元素的地址打印出来的结果是一模一样的呀,这就再次印证了数组名就是数组首元素(也就是第一个元素)的地址这一说法。

不过,这时候可能有的同学就会产生疑问啦。要是数组名是数组首元素的地址,那下面这段代码又该怎么理解呢?

cs 复制代码
#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 呢。按之前说的,如果 arr 仅仅是数组首元素的地址,那输出的应该是 4 或者 8 才对啊。
其实数组名就是数组⾸元素(第⼀个元素)的地址是对的,但是有两个例外:

  • sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩,单位是字节。
  • 当用 &数组名 时,这里的数组名同样表示的是整个数组,取出的是整个数组的地址(要知道整个数组的地址和数组首元素的地址可是有区别的哟)。

除了这两个例外情况之外,在其他任何地方使用数组名时,数组名所表示的就是首元素的地址啦。

这时候呀,可能又有好奇的同学会进一步尝试下面这段代码呢。

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

你看,这三个打印结果又是一模一样的,这下可能又让人纳闷了,那 arr&arr 到底有啥区别呀?咱们再来看下面这段代码以及它的输出结果哦

cs 复制代码
#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[0]+1 相差 4 个字节,arrarr+1 也相差 4 个字节,这是因为 &arr[0]arr 都是首元素的地址呀,当进行 +1 操作时,就是跳过一个元素哦。而 &arr&arr+1 相差 40 个字节呢,这就是因为 &arr 表示的是整个数组的地址,这里的 +1 操作意味着跳过整个数组哟。

讲到这儿,大家应该对数组名的意义比较清楚了吧。简单来说,数组名通常是数组首元素的地址,但有那两个特殊的例外情况哦。

使用指针访问数组

有了前面关于数组名相关知识的支撑,再结合数组自身的特点,我们就能很方便地使用指针来访问数组啦。来看看下面这段代码吧。

cs 复制代码
#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,在这里它们其实是等价的。我们知道可以用 arr[i] 来访问数组的元素,那 p[i] 是不是也可以访问数组呢?咱们再来看看下面这段代码哦。

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

你瞧,在代码的第 18 行那里,把 *(p+i) 换成 p[i] 也是能够正常打印输出的哦。所以呀,本质上 p[i] 是等价于 *(p+i) 的呢。同理,arr[i] 也应该等价于 *(arr+i) 哦。在编译器处理数组元素访问的时候,实际上就是将其转换成首元素的地址加上偏移量求出元素的地址,然后再通过解引用的方式来访问元素的哟。

一维数组传参的本质

咱们已经学习过数组了,也知道数组是可以传递给函数的呀。那在这个小节呢,咱们就来探讨一下数组传参的本质到底是什么。

咱们先从一个问题入手哈,之前我们都是在函数外部去计算数组的元素个数,那能不能把数组传给一个函数后,在函数内部去求数组的元素个数呢?咱们看看下面这段代码以及它的运行情况哦。

cs 复制代码
#include <stdio.h>
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;
}

我们会发现呀,在函数内部并没有正确地获得数组的元素个数呢。这就需要深入了解一下数组传参的本质啦。在上个小节咱们学习过,数组名其实就是数组首元素的地址哦。那么在数组传参的时候,传递的就是数组名呀,也就是说从本质上讲,数组传参传递的就是数组首元素的地址呢。

所以呀,函数形参的部分理论上应该使用指针变量来接收这个首元素的地址哦。这样一来,在函数内部写 sizeof(arr) 时,计算的其实是一个地址的大小(单位是字节),而不是数组的大小(单位字节)啦。正是因为函数的参数部分本质上是指针,所以在函数内部是没办法求得数组元素个数的哟。咱们再看看下面这两种函数定义形式哦。

cs 复制代码
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

cs 复制代码
void bubble_sort(int arr[], int sz)  //参数接收数组元素个数
{
    int i = 0;
    for(i=0; i<sz-1; i++)
    {
        int j = 0;
        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);   
    for(i=0; i<sz; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

方法 2 优化

cs 复制代码
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-i-1; j++)
        {
            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;
    }
}
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);
    for(i=0; i<sz; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

二级指针

要知道呀,指针变量它本身也是变量呢,既然是变量那就有地址呀。那指针变量的地址存放在哪儿呢?

对于二级指针的运算呀,有下面这些情况哦:

  • *ppa 呢,是通过对 ppa 中的地址进行解引用操作,这样就能找到 pa 啦,所以 *ppa 其实访问的就是 pa 哦。比如下面这样的代码:

    cs 复制代码
    int b = 20;
    *ppa = &b;  //等价于 pa = &b;
  • **ppa 呢,先是通过 *ppa 找到 pa,然后再对 pa 进行解引用操作,也就是 *pa,这样找到的就是 a 啦。像下面这样:

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

    指针数组

那指针数组到底是指针还是数组呀?咱们可以类比一下哦,整型数组呢,是用来存放整型数据的数组,字符数组是存放字符的数组。那指针数组呀,就是存放指针的数组哦。

指针数组模拟二维数组

咱们来看看下面这段代码哈。

cs 复制代码
#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*的,就可以存放在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 数组的元素哦,而 parr[i] 找到的数组元素指向了整型一维数组,那么 parr[i][j] 就是整型一维数组中的元素啦。不过要注意哦,上述代码虽然模拟出了二维数组的效果,但实际上它并非完全等同于二维数组呢,因为每一行的数据在内存中并非是连续存放的哟。

通过以上对数组名、指针访问数组、数组传参、冒泡排序以及二级指针、指针数组等多方面知识的详细讲解与探讨,相信大家对这些 C 语言中重要的知识点有了更深入且清晰的理解。希望大家在后续的编程学习与实践中,能够灵活运用这些知识,不断提升自己的编程能力哦。

相关推荐
玉红77712 分钟前
R语言的数据类型
开发语言·后端·golang
夜斗(dou)16 分钟前
node.js文件压缩包解析,反馈解析进度,解析后的文件字节正常
开发语言·javascript·node.js
觅远17 分钟前
python+PyMuPDF库:(一)创建pdf文件及内容读取和写入
开发语言·python·pdf
chenziang118 分钟前
leetcode hot 100 二叉搜索
数据结构·算法·leetcode
神雕杨1 小时前
node js 过滤空白行
开发语言·前端·javascript
lvbu_2024war011 小时前
MATLAB语言的网络编程
开发语言·后端·golang
single5942 小时前
【c++笔试强训】(第四十五篇)
java·开发语言·数据结构·c++·算法
游客5202 小时前
自动化办公-合并多个excel
开发语言·python·自动化·excel
Cshaosun2 小时前
js版本之ES6特性简述【Proxy、Reflect、Iterator、Generator】(五)
开发语言·javascript·es6
yuyanjingtao2 小时前
CCF-GESP 等级考试 2023年9月认证C++五级真题解析
c++·青少年编程·gesp·csp-j/s·编程等级考试