C语言指针与数组深度解析:原理、运算差异及实战陷阱

Jasper
Jasper
发布时间:2026-10-13 | 3人浏览过
原创

C语言中指针与数组紧密相关但本质不同。数组是连续内存块,而指针仅存储地址。本文深入分析两者在内存分配、指针运算规则(加减法与比较)、函数参数退化及&a与a的区别等方面的原理,并通过经典代码案例展示如何正确使用指针遍历数组及避免常见的内存越界和段错误问题。

数组的本质与指针的基础运算

数组在内存中表现为一段连续的存储空间,其大小由元素类型和数量决定:sizeof(array_type) * array_size。虽然数组名常被视为指向首元素的常量指针,但需明确区分其与独立指针变量的差异。

第1步:理解指针算术运算规则

指针并非普通整数,其运算会自动考虑所指对象的字节大小。

  • 指针加法/减法:p + n 实际上等于 (unsigned int)p + n * sizeof(*p)。当指向数组元素时,p+1 指向下一元素,p-1 指向上一个元素。
  • 指针减法:仅支持相同类型的指针相减,结果为两指针之间的元素个数差:((unsigned int)p1 - (unsigned int)p2) / sizeof(type)。
  • 其他运算:指针之间不支持加法、乘法或除法。

以下代码验证了指针运算的合法性及结果:

#include <stdio.h>
 
int main()
{
    int a[5] = {0};
    int* p = NULL;
    
    printf("a = 0x%X\n", (unsigned int)(a));
    printf("a + 1 = 0x%X\n", (unsigned int)(a + 1));
    
    printf("p = 0x%X\n", (unsigned int)(p));
    printf("p + 1 = 0x%X\n", (unsigned int)(p + 1));
    
    return 0;
}

从输出可见,a + 1 的地址偏移量是一个整型的大小(通常为4字节),而非1字节。

第2步:执行指针间的减法与比较

指针减法要求操作数必须指向同一个数组内的元素,否则结果未定义。同时,只有同类型指针才能进行减法。此外,指针还支持关系运算(如 <, >),前提是它们指向同一数组,用于判断相对位置;任何两个指针均可使用 == 或 != 进行比较。

演示代码如下:

#include <stdio.h>
 
int main()
{
    char s1[] = {'H', 'e', 'l', 'l', 'o'};
    int i = 0;
    char s2[] = {'W', 'o', 'r', 'l', 'd'};
    char* p0 = s1;
    char* p1 = &s1[3];
    char* p2 = s2;
    int* p = &i;
	
    printf("%d\n", p0 - p1);
    //printf("%d\n", p0 + p2);  //ERROR
    printf("%d\n", p0 - p2);
    //printf("%d\n", p0 - p);   //ERROR
    //printf("%d\n", p0 * p2);  //ERROR
    //printf("%d\n", p0 / p2);  //ERROR
	
    return 0;
}

注意:p0 - p2 虽然在语法上合法(同为 char*),但由于两者不属于同一数组,计算出的差值没有实际逻辑意义。

第3步:利用指针遍历与边界技巧

在编写通用循环时,可以通过维护首尾指针来简化代码。这里介绍一个常用的边界处理技巧:让结束指针指向最后一个元素之后的下一个位置。

#include <stdio.h>
 
#define DIM(a) (sizeof(a) / sizeof(*a))
 
int main()
{
    char s[] = {'H', 'e', 'l', 'l', 'o'};
    char* pBegin = s;
    char* pEnd = s + DIM(s); // Key point
    char* p = NULL;
    
    printf("pBegin = %p\n", pBegin);
    printf("pEnd = %p\n", pEnd);
    
    printf("Size: %d\n", pEnd - pBegin);
	
    for(p=pBegin; p<pEnd; p++)
    {
        printf("%c", *p);
    }
    
    printf("\n");
   
    return 0;
}
  • #define DIM(a) (sizeof(a) / sizeof(*a)) 是获取数组元素个数的宏。
  • char* pEnd = s + DIM(s) 使指针指向 'o' 后面的地址。这在 C 语言和 C++ STL 中是合法的半开区间表示法,可用于安全地进行比较和减法运算。

小结

  • 数组声明时编译器自动分配连续内存,指针仅分配容纳地址的空间(通常4字节)。
  • 指针可与整数运算,结果为新指针。
  • 同类型且同数组的指针支持减法(得元素差)和关系比较。

数组访问方式及深层差异

数组元素可通过下标或指针形式访问。尽管现代编译器能优化下标形式的效率使其与指针相当,但从底层机制和特定场景看,二者仍有显著不同。

第1步:对比下标与指针访问

两种形式在语义上是等效的,遵循以下等价转换:

a[n] <--> *(a + n) <--> *(n + a) <--> n[a]

观察图片中的实现细节:

在实际编码中,若需频繁按固定增量移动指针,直接使用指针形式可能略优(尤其在硬件支持直接增量寻址时)。但在日常开发中,下标形式可读性更好。例如,i[a] 这种看似奇怪的写法,实则完全等同于标准的 a[i]。

第2步:解析跨文件数组声明与指针误用

为了厘清数组名与指针的本质,我们通过 extern 关键字在另一个文件中引用数组。

文件 ext.c:

int a[] = {1, 2, 3, 4, 5};

文件 test.c:

#include <stdio.h>
 
int main()
{
    extern int a[];
    
    printf("&a = %p\n", &a);
    printf("a = %p\n", a);
    printf("*a = %d\n", *a);
 
    return 0;
}

输出显示 &a 和 a 的值相同,说明数组名的地址就是数组起始地址:

但如果将声明改为指针,会发生严重错误:

#include <stdio.h>
 
int main()
{
    extern int* a;
    
    printf("&a = %p\n", &a);
    printf("a = %p\n", a);
    printf("*a = %d\n", *a);
 
    return 0;
}

此时编译器认为 a 是一个地址值(即 ext.c 中数组首地址 0x804a014)。因此 *a 试图去读取地址为 0x00000001 的内容(这是数组的第一个元素值 1 被当作地址了),这会导致非法内存访问并产生段错误:

第3步:剖析 a 与 &a 的根本区别

  • a 是数组首元素的地址(类型为 int*)。
  • &a 是整个数组的地址(类型为 int(*)[5])。

两者的数值可能相同,但指针算术运算的步长完全不同:

a + 1 增加 sizeof(int),而 &a + 1 增加整个数组的大小 5 * sizeof(int)。

第4步:挑战指针强转与越界读取的经典陷阱

下面这段代码展示了不同类型指针强转后的行为:

#include <stdio.h>
 
int main()
{
    int a[5] = {1, 2, 3, 4, 5};
    int* p1 = (int*)(&a + 1); 
    int* p2 = (int*)((int)a + 1);
    int* p3 = (int*)(a + 1);
    
    printf("%d, %d, %d\n", p1[-1], p2[0], p3[1]);
    
    return 0;
}

逻辑解析:

  • p1[-1]:p1 指向 a[5](越界后第一个位置)。p1[-1] 即回退一个整型宽度,正好指向最后一个元素 5。
  • p2[0]:(int)a + 1 强制将地址转为整数后加1(小端模式下地址低位加1,如 0x804a014 变为 0x804a015)。将其强转为 int* 后解引用,会截断读取内存数据,得到非预期的巨大数值(如示例中的 33554432)。
  • p3[1]:p3 指向 a[1],所以 p3[1] 即为 a[2],值为 3。

第5步:掌握数组参数作为函数实参时的退化

当数组作为函数参数传递时,编译器会自动将其视为指向首元素的指针。这意味着在函数内部无法通过 sizeof(数组名) 获取原数组大小。

#include <stdio.h>
 
void func1(char a[5])
{
    printf("In func1: sizeof(a) = %d\n", sizeof(a));
    
    *a = 'a';
    
    a = NULL;
}
 
void func2(char b[])
{
    printf("In func2: sizeof(b) = %d\n", sizeof(b));
    
    *b = 'b';
    
    b = NULL;
}
 
int main()
{
    char array[10] = {0};
    
    func1(array);
    
    printf("array[0] = %c\n", array[0]);
    
    func2(array);
    
    printf("array[0] = %c\n", array[0]);
    
    return 0;
}

输出中 sizeof(a) 为 4(指针大小),证实了“数组退化”现象:无论写 char a[5] 还是 char b[],在函数内部都等同于 char* a 和 char* b。由于是值传递,修改函数内部的形参指针指向不会影响外部数组。

总结

数组名与指针的使用方式相似,但内核截然不同:数组名代表首元素地址但不具备指针变量可修改的特性,其整体地址运算步长为整个数组长度。而在函数传参时,务必记住数组已退化为指针,建议额外传入长度参数以避免越界操作。

以上就是C语言指针与数组深度解析的详细内容,更多关于C语言 指针和数组的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表