C语言梦开始的地方10:指针进阶

文章目录

C语言梦开始的地方10:指针进阶

上一章我们了解什么是指针,指针类型的意义,指针整数运算,野指针,assert断言和指针的使用和传址调用。本章将继续了解指针的其他使用方法以及上一章的留下的坑。

数组名的理解

上一章我们写了这样的代码

c 复制代码
    int arr[5] = {1,2,3,4,5};

    int *p = arr; 
    printf("*(p+1):%d\n",*(p + 1));

那么这里为什么是 int *p = arr; 而不是 int *p = &arr; 呢?

因为类型不匹配,&arr 是指向整个数组的指针,如果要存它,必须写成 int (*p)5 = &arr;。

但是,我们平时遍历数组只需要元素指针,所以通常不用后者。

不过,我们平时遍历数组只需要指向元素的指针,所以通常用 arr 就够了。为了直观地验证"数组名 arr 在数值上等于首元素的地址",我们打印一下看看:

c 复制代码
#include <stdio.h>

int main()
{
    int arr[5] = {1,2,3,4,5}; 
    
    printf("&arr[0] = %p\n", &arr[0]);
    printf("arr     = %p\n", arr);
    return 0;
}

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

但是数组名有两个例外:

  1. sizeof(数组名) ------ 此时的数组名代表整个数组,返回整个数组占用的字节数。
  2. &数组名 ------ 取出的是整个数组的地址(类型为 int (*)N),虽然数值与首元素地址相同,但步长不同。

为什么说是两个例外呢?

因为数组arr我们知道数组名就是数组首元素的地址,地址使用sizeof计算时我们在数组计算大小的一章说过是按数组长度 * 数组类型大小 但是按照我们上一章的了解应该是按32/64位平台4/8字节 但是我们知道实际上不是这样的是按数组长度 * 数组类型大小

&数组名这个代码就能说明

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

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

但是&arr 和 &arr+1相差40个字节,因为&arr是获取整个数组的地址 + 1当然会跳过整个数组。

那么为什么&arr打印的是首元素地址?

&arr 和 arr 虽然数值相同,但类型不同。可以把 arr 看作"一楼101室的门牌号",而 &arr 看作"整栋一楼的地址"。它们指向同一个物理位置,但 arr+1 走到102室(跳过4字节),&arr+1 则直接走到二楼(跳过整个数组40字节)。

使用指针访问数组

我们在上一章了解过 *(p + 1) 等价于 p1,那么我们结合刚才的了解此时在看指针访问数组就很简单就了解了。

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;
}

一维数组传参的本质

在我们了解函数那一章节时说到过使用数组当参数时函数内部不能使用sizeof来计算数组大小,因为无法计算正常大小。

c 复制代码
#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) 计算的是一个地址的大小(单位字节)而不是数组的大小(单位字节)。正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。

冒泡排序

冒泡排序的核心思想就是:两两相邻的元素进行比较。

c 复制代码
void bubble_sort(int arr[], int n) {
    for(int i = 0; i < n-1; i++) {
        int swapped = 0;
        // j < n-1-i 是因为每轮排序后,末尾已排好序的元素个数增加1
       // 所以内层循环的比较次数逐轮递减
        for(int j = 0; j < n-1-i; j++) {
            if(arr[j] > arr[j+1]) {
                int temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
                swapped = 1;
            }
        }
        if(!swapped) 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);
    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

二级指针

我们知道指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?

答案:二级指针。

对于二级指针的运算有:

  • *ppa 通过对ppa中的地址进行解引用,这样找到的是pa , *ppa 其实访问的就是pa .
c 复制代码
int a = 10;
int *pa = &a;    // 一级指针
int **ppa = &pa; // 二级指针 -》等价于 *pa = &a;
  • **ppa 先通过*ppa 找到pa ,然后对pa 进行解引用操作: *pa ,那找到的是a .
c 复制代码
**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;

指针数组

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

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

那指针数组呢?是存放指针的数组。

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

如下图:

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

指针数组模拟二维数组

我们在数组章节了解过二维数组本质就是多个一维数组组成的,那么只有我们让指针数组存放存放多个一维数组就可以用指针数组模拟二维数组,如下图:

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*的,就可以存放在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;
}

实际上并非完全是二维数组,因为真正的二维数组在内存中所有行是连续紧挨的,而这里 arr1、arr2、arr3 三块内存不一定在物理上连续。

相关推荐
小新讲网安9 小时前
WiFi安全攻防实战:WPA3新协议与传统破解技术全解析
开发语言·网络·安全·php·漏洞·nmap·漏洞检测
必须会一定会15 小时前
Agent Plugins 1.0实战:plugin.json、skills、mcp.json目录结构与迁移
开发语言·人工智能·ai编程
St_rive15 小时前
Page Object设计模式
java·开发语言·设计模式
wp123_115 小时前
硬件元器件笔记|IPX8 防水 Type‑C 母座安费诺 124018802112A 与 TONEVEE TY48086‑24A 分析
c语言·开发语言·笔记
wuyk55515 小时前
4.树:一对多的层次数据结构
开发语言·数据结构·stm32·单片机
luj_176816 小时前
桥牌思维启示:系统设计的模块化架构
c语言·开发语言·c++·经验分享·算法
caimouse18 小时前
ReactOS 图形系统分析(16):字符串对象 — STROBJ(string.c)
c语言·开发语言
Aphelios38018 小时前
一次锁内网络IO引发的Tomcat线程池“饿死”事故
java·开发语言·spring boot·elasticsearch·tomcat·网络io阻塞·线程池耗尽
不可求~18 小时前
C++ std::string_view 不是字符串:从悬空引用到安全用法
java·开发语言·c++