C语言 指针进阶

本文深入探讨了C语言中数组与指针的关系,介绍了指针算术运算,如指针加整数、指针相减等。还阐述了数组名在不同情形下的含义,以及char型、void型、const型指针和二级指针的特点。此外,分析了二维数组与指针的联系,最后讲解了零长数组的用途和优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.数组与指针

  • 数组下标,其实际上是编译系统的一种简写,其等价形式是:
a[i] = 100;  等价于  *(a+i) = 100; 
  • 根据加法交换律,以下的所有的语句均是等价的:
#include <stdio.h>

int main()
{
    int arr[] = {99, 15, 100, 888, 252};
    int len = sizeof(arr) / sizeof(int); // 求数组长度
    int i;
    for (i = 0; i < len; i++)
    {
        printf("%d  ", *(arr + i)); // *(arr+i)等价于arr[i]
    }
    printf("\n");

    return 0;
}
  • C语言中指针和数组的关系是非常紧密的,理解指针和数组之间的关系对于熟练掌握C语言非常关键,需要知道的是,用指针处理数组的主要原因是效率,但是这里 的效率提升已经不再像当初那么重要了,这主要归功于编译器的改进。

  • 有上述可知,指针可以指向数组元素。例如,假设已经声明 a 和 p 如下:
int a[10], *p;
  • 通过下列写法可以使 p 指向 a[0] :
p = &a[0];

 

  • 现在可以通过 p 访问 a[0] 。例如,可以通过下列写法把值 5 存入 a[0] 中:
*p = 5;

 

  • C语言支持3种(而且只有3种)格式的指针算术运算:
    • 指针加上整数
    • 指针减去整数
    • 两个指针相减

  • 下面的所有例子都假设有如下声明
int a[10], *p, *q, i;

1.1 指针加上整数

  • 指针p 加上整数 j ,实际就是指针p 指向的地址移动了 j 个单位。如果 p 指向数组元素 a[i] ,那么 p+j 指向 a[ i + j ] (当然,前提是a[ i + j ] 必须 存在)
p = &a[2];

q = p + 3;

 

p += 6;

 

  • 指针减去整数和指针加上整数同理

1.2 两个指针相减

当两个指针相减时,结果为指针之间的距离(用数组元素的个数来度量)。因此,如果p 指向 a[ i ] 且q 指向 a[ j ] ,那么p - q 就等于 i - j

p = &a[5];
q = &a[1];

i = p - q; // 4
i = q - p; //-4

 例:

#include <stdio.h>

int my_strlen(char *str)
{
    char *start = str;
    while (*str != '\0')
    {
        str++;
    }
    return (str - start);
}

int main()
{
    int len = my_strlen("abcdef"); // 字符串作为参数传递时,传递的是首地址
    printf("%d\n", len); // 6
    
    return 0;
}

2.数组名含义

  • 数组名在不同情形会表现为不同的含义:
    • 第一含义是整个数组
    • 第二含义是首元素地址
  • 触发第一含义的情形:
  1. 在数组定义中 // 这里的数组名表示整个数组,定义的是整个数组
  2. 在 sizeof 运算表达式中 // sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小
  3. 在取址符&中 // &数组名,这里的数组名表示整个数组,取出的是整个数组的地址
  • 除了以上情况以外,其他所有情况都是第二含义。

  • 我们发现,数组名的第二个含义似乎跟指针有关,没错,指针的算术运算是数组和指针之间相互关联的一种方法,但这不是两 者之间唯一的联系。另外一种联系是:可以用数组的名字作为指向数组第一个元素的指针 。这种关系简化了指针的算术运算, 而且使数组和指针更加通用。
// 假设用如下形式声明a
int a[10];
// 用a 作为指向数组第一个元素的指针,可以修改a[0] :
*a = 7;
// 可以通过指针a + 1 来修改a[1]
*(a + 1) = 12;

注意:通常情况下,a + i 等同于&a[i] (两者都表示指向数组a 中元素 i 的指针),并且*(a+i) 等价于a[i] (两者都表示元素 i 本 身)。换句话说,可以把数组的取下标操作看成是指针算术运算的一 种形式。 

  • 下面的例子说明了数组名代表首元素地址和整个数组的区别:
#include <stdio.h>

int main()
{
    /*
    1.在数组定义中
    int a[10];

    2.在 sizeof 运算表达式中
    printf("%ld\n",sizeof(a));

    3.在取址符&中
    printf("%p\n",&a);
    */

    int arr[] = {10, 11, 12, 13, 14}; // 5*4=20

    printf("arr:%p\n", arr);         // arr:0x7ffe1b248030  表示的是数组的首元素地址
    printf("&arr:%p\n", &arr);       // &arr:0x7ffe1b248030 表示的是整个数组的首地址
    printf("&arr[0]:%p\n", &arr[0]); // &arr[0]:0x7ffe1b248030 对数组首元素进行取址

    // 虚拟地址有12位,16进制每一位在二进制中占4位
    // 12*4=48/8=6字节(地址占六个字节)    2^48=2^18G(物理空间没有那么大,为虚拟地址)

    // arr+1:arr+1:0x7ffe1b248034 对首元素地址+1,地址值往后偏移一个int单位(4字节)
    printf("arr+1:%p\n", arr + 1);

    // &arr+1:0x7ffe1b248044 是整个数组地址加一个单位(一整个数组20字节)
    printf("&arr+1:%p\n", &arr + 1);

    // &arr[0]+1:0x7ffe1b248034 是对数组首元素进行取址后再+1,地址往后偏移一个int单位(4字节)
    printf("&arr[0]+1:%p\n", &arr[0] + 1);

    printf("*arr:%d\n", *arr); // *arr:10 对数组首元素地址进行解引用

    printf("sizeof(arr):%ld\n", sizeof(arr)); // sizeof(arr):20 // 求整个数组所占内存大小

    printf("sizeof(arr+0):%ld\n", sizeof(arr + 0));
    // sizeof(arr+0):8 arr先与0结合,表示数组首元素偏移0个单位,得到的结果仍然为一个地址
    // sizeof()对地址求值相当于对指针变量求值,所以结果为8(地址占6个字节,变为8字节以提高存取效率)
    // 指针在不同位数机器下的长度:16位:2个字节,32位:4个字节,64位:8个字节

    int a = 100;
    int *p = &a;
    printf("%ld\n", sizeof(p)); // 8

    return 0;
}

例:

#include <stdio.h>
int main(void)
{
    int a[5] = {1, 2, 3, 4, 5};
    int *ptr = (int *)(&a + 1); // 强制转换
    printf("%d,%d\n", *(a + 1), *(ptr - 1)); // 2,5
    return 0;
}

3.char型指针

  • char型指针实质上跟别的类型的指针并无本质区别,但由于C语言中的字符串以字符数组的方式存储,而数组在大多数场合又会表现为指针,因此字符串在绝大多数场合就表现为char型指针。但是字符数组和字符指针在存储空间上有本质的区别(后面会说到)
#include <stdio.h>

int main()
{
    char a[10] = "abcd";
    char *p = "abcd";
    // 是把"abcd"这个字符串放入指针变量p?
    // 不是,是把"abcd"这个字符串首元素的空间内存地址给p
    printf("%c\n", *(p + 2)); // c
    printf("%c\n", p[2]);     // c

    return 0;
}

4.void型指针

  1. 空指针NULL是没有指向的指针,将暂时用不到的指针定义成空指针,能防止误用。
  2. 而void * 类型的指针是有指向的指针,但它的指向的数据的类型暂时不确定,后期一般要强制转换
  • 无法明确指针所指向的数据类型时,可以将指针定义为 void 型指针
    • void 型指针无法直接索引目标,必须将其转换为一种具体类型的指针方可索引目标
    • void 型指针无法进行加减法运算
  • void关键字的三个作用:
    • 修饰指针,表示指针指向一个类型未知的数据
    • 修饰函数参数列表,表示函数不接收任何参数
    • 修饰函数返回类型,表示函数不会返回任何数据
#include <stdio.h>
#include <stdlib.h>

int main()
{
    // 指针 p 指向一块 4 字节的内存,且这4字节数据类型未确定
    void *p = malloc(4);

    // 1,将这 4 字节内存用来存储 int 型数据
    *(int *)p = 100;
    printf("%d\n", *(int *)p); // 100

    // 2,将这 4 字节内存用来存储 float 型数据
    *(float *)p = 3.14;
    printf("%f\n", *(float *)p); // 3.140000

    return 0;
}
  • 注意:以上示例仅仅是为了讲解 void 的语法,实际应用中一般不需要定义 void 型指针,而是根据具体的需要,直接定义具体类型的指针

5.const型指针

  • const是一个C语言的关键字,它限定一个变量不允许被改变,产生静态作用,也就是说经过const 修饰的变量成为只读的变量之后,那么这个变量就只能作为右值(只能赋值给别人),绝对不能成为左值(不能接收别人的赋值)
// 经过 const 修饰的变量,在定义的时候,就要进行初始化
const int a = 10;  // 正确
const int a;  // 错误
  • const型指针有两种形式:①常量指针 ②指针常量

1.常量指针:const修饰指针本身,表示指针变量本身无法修改。

2.指针常量:const修饰指针的目标,表示无法通过该指针修改其目标。

  • 常量指针在实际应用中不常见。
  • 常目标指针在实际应用中广泛可见,用来限制指针的读写权限 
int a = 100;
int b = 200;

// 第1种形式,const修饰p1本身,导致p1本身无法修改
int * const p1 = &a; 

// 第2种形式,const修饰p2的目标,导致无法通过p2修改a
int const *p2 = &a;
//const int *p2 = &a;

6.二级指针

  • 不管是二级指针还是多级指针,考虑它们和一级指针都是一样的,一级指针保存的是普通变量的地址,而二级(多级)指针保存的是指针变量的地址,即地址的地址。
#include <stdio.h>

int main()
{
    int data = 100;
    int *p = &data;

    printf("data的地址是: %p\n", &data);
    printf("p保存data的地址是: %p,内容是: %d\n", p, *p);

    int **p2;
    p2 = &p;
    printf("p2保存的是p的地址: %p\n", p2);
    printf("*p2是: %p\n", *p2);
    printf("**p2来访问data: %d\n", **p2);

    return 0;
}
/*
data的地址是: 0x7ffe9729f1b4
p保存data的地址是: 0x7ffe9729f1b4,内容是: 100
p2保存的是p的地址: 0x7ffe9729f1b8
*p2是: 0x7ffe9729f1b4
**p2来访问data: 100
*/

7.二维数组与指针

  • 某些时候在描述二维数组时称之为二维,有行和列,但在内存中所有的数组元素都是连续排列的,它们之间没有“缝隙”。
int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };

 在内存中,a 的分布是一维线性的,整个数组占用一块连续的内存:

C语言允许把一个二维数组分解成多个一维数组来处理。对于数组 a,它可以分解成三个一维数组,即 a[0]、a[1]、a[2]。每一个一维数组又包含了 4 个元素,例如 a[0] 包含 a[0][0]、a[0][1]、a[0][2]、a[0][3]。

假设数组 a 中第 0 个元素的地址为 1000,那么每个一维数组的首地址如下图所示:

 

  • 定义一个指向 a 的指针变量 p:
int (*p)[4] = a;

括号中的*表明 p 是一个指针,它指向一个数组,数组的类型为int [4],这正是 a 所包含的每个一维数组的类型。[ ] 的优先级高于 *,( )是必须要加的,如果赤裸裸地写作 int *p[4],那么应该理解为 int *(p[4]),p 就成了一个指针数组,而不是二维数组指针

p 指向的数据类型是int [4],那么p+1就前进 4×4 = 16 个字节,p-1就后退 16 个字节,这正好是数组 a 所包含的每个一维数组的长度。也就是说,p+1会使得指针指向二维数组的下一行,p-1会使得指针指向数组的上一行。

问下列表示什么意思?

p+1 :p+1 是对指针变量p前进一个单位,结果还是指向一个地址;在这里表示指向下一行数组的地址。

*(p+1) :*(p+1)表示第二行所有数据,在数值上和第二行首元素地址值相等。

*(p+1)+1 : 表示第二行第二个数据元素的地址 ,a[1][1]的地址

*(*(p+1)+1) :表示第二行第二个数据元素的值,a[1][1]的值

  • 使用指针遍历二维数组:
#include <stdio.h>
int main()
{
    int arr[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
    int(*p)[4];
    int i, j;
    p = arr;
    for (i = 0; i < 3; i++)
    {
        for (j = 0; j < 4; j++)
            printf("%d  ", *(*(p + i) + j));
        printf("\n"); // 每一行一维数组输出完之后换行
    }
    return 0;
}
/*
0  1  2  3  
4  5  6  7  
8  9  10  11 
*/

8.什么是零长数组/柔性数组?为什么使用零长数组?

GNU/GCC 在标准的 C/C++ 基础上做了有实用性的扩展, 零长度数组(Arrays of Length Zero) 就是其中一个知名的扩展

struct Packet
{
    int state;
    int len;
    char arr[0]; // 这里的0长结构体就为变长结构体提供了非常好的支持
};
  • 用途 : 长度为0的数组的主要用途是为了满足需要变长度的结构体
  • 用法 : 在一个结构体的最后, 申明一个长度为0的数组, 就可以使得这个结构体是可变长的. 对于编译器来说, 此时长度为0的数组并不占用空间, 因为数组名本身不占空间, 它只是一个偏移量, 数组名这个符号本身代表了一个不可修改的地址常量
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct buffer
{
    int len;
    char a[0];
};

int main(void)
{
    struct buffer *buf;
    buf = (struct buffer *)malloc(sizeof(struct buffer) + 20);
    buf->len = 20;
    strcpy(buf->a, "hello");
    puts(buf->a); // hello
    // puts 函数输出字符串自动换行输出
    free(buf);
    buf = NULL;

    return 0;
}

在这个程序中,我们使用 malloc 申请一片内存,大小为 sizeof(buffer) + 20,即24个字节大小。其中4个字节用来存储结构体指针 buf 指向的结构体类型变量,另外20个字节空间,才是我们真正使用的内存空间。我们可以通过结构体成员 a,直接访问这片内存。

通过这种灵活的动态内存申请方式,这个 buffer 结构体表示的一片内存缓冲区,就可以随时调整,可大可小。这个特性,在一些场合非常有用。

比如,现在很多在线视频网站,都支持多种格式的视频播放:标清、高清、超清、蓝光甚至4K。如果我们本地程序需要在内存中申请一个 buffer 用来缓存解码后的视频数据,那么,不同的播放格式,需要的 buffer 大小是不一样的。 如果我们按照 4K 的标准去申请内存,那么当播放标清视频时,就用不了这么大的缓冲区,白白浪费内存。 而使用变长结构体,我们就可以根据用户的播放格式设置,灵活地申请不同大小的 buffer,大大节省了内存空间。

  • 为什么不使用指针来代替零长度数组?

数组名在作为函数参数传递时,确实传递的是一个地址,但数组名绝不是指针,两者不是同一个东西。数组名用来表征一块连续内存存储空间的地址,而指针是一个变量,编译器要给它单独再分配一个内存空间,用来存放它指向的变量的地址。

#include <stdio.h>

struct buffer1
{
    int len;
    int a[0];
};
struct buffer2
{
    int len;
    int *a;
} __attribute__((packed)); // 将结构体压实

int main(void)
{
    printf("buffer1: %ld\n", sizeof(struct buffer1)); // buffer1: 4
    printf("buffer2: %ld\n", sizeof(struct buffer2)); // buffer2: 12

    return 0;
}
  • 对于一个指针变量,编译器要为这个指针变量单独分配一个存储空间,然后在这个存储空间上存放另一个变量的地址,我们就说这个指针指向这个变量
  • 而数组名,编译器不会再给其分配一个存储空间的,它仅仅是一个符号,跟函数名一样,用来表示一个地址

至于为什么不使用指针来代替零长度数组呢?

例如:

使用零长度数组来写一个班级结构体的话可以这样:

#include <stdio.h>
#include <stdlib.h>

typedef struct student
{
    int num;       // 学号
    char name[20]; // 姓名
    char sex;      // 性别
} student;

typedef struct class
{
    char *teacher;
    int class_id;
    student students[];
} class;

int main()
{
    class *classA;
    classA = (class *)malloc(sizeof(class) + sizeof(student) * 5);

    // 遍历学生赋值
    for (int i = 0; i < 5; i++)
    {
        classA->students[i].num = 230580 + i;
        printf("请输入学号为%d的姓名:\n", classA->students[i].num);
        scanf("%s", classA->students[i].name);
    }
    // 遍历学生输出
    for (int i = 0; i < 5; i++)
    {
        printf("学号%d的姓名是%s:\n", classA->students[i].num, classA->students[i].name);
    }
    // 释放资源
    free(classA);
    classA = NULL; // 置空

    return 0;
}

 使用指针来写的话则会变成:

#include <stdio.h>
#include <stdlib.h>

typedef struct student
{
    int num;       // 学号
    char name[20]; // 姓名
    char sex;      // 性别
} student;

typedef struct class
{
    char *teacher;
    int class_id;
    student *students;
} class;

int main()
{
    class *classA;
    classA = (class *)malloc(sizeof(class));
    classA->students = (student *)malloc(sizeof(student) * 5);

    // 遍历学生赋值
    for (int i = 0; i < 5; i++)
    {
        classA->students[i].num = 230580 + i;
        printf("请输入学号为%d的姓名:\n", classA->students[i].num);
        scanf("%s", classA->students[i].name);
    }
    // 遍历学生输出
    for (int i = 0; i < 5; i++)
    {
        printf("学号%d的姓名是%s:\n", classA->students[i].num, classA->students[i].name);
    }
    // 释放资源
    free(classA->students);
    classA->students = NULL;
    free(classA);
    classA = NULL;

    return 0;
}

总结:

  • 零长数组一般和结构体搭配使用,其比起在结构体中声明一个指针变量、再进行动态分配的效率要高。因为在访问数组内容时,不需要间接访问,避免了两次访存。
  • 同时因为零长数组表示数据内容时,其数据空间是动态分配的,所以比静态分配内存要灵活。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cam_______

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值