C之指针与数组组合

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C之指针与数组组合相关的知识,希望对你有一定的参考价值。

        我们在前面讲到数组的本质是一段连续的内存空间,那么它的大小为 sizeof(array_type) * array_size,同时数组名可看做指向数组第一个元素的常量指针。那么问题来了,数组 a + 1 的意义是什么呢?结果又是怎样呢?指针运算的意义又是什么?结果呢?下来我们看个示例代码,代码如下

#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 相当于一个常量指针,而它便指向的首元素的地址,a + 1 便是首元素的地址加 4,也就是数组第二个元素的地址。因为指针 p int 型,所以  p + 1 相当于加 4。

        指针是一种特殊的变量,它与整数的运算规则为 p + n <==> (unsigned int)p + n*sizeof(*p);那么便是当指针指向同一类型的数组的元素时:p + 1 将指向当前元素的下一个元素;p - 1 将指向当前元素的上一个元素。指针之间只支持减法运算,并且参与减法运算的指针类型必须相同。p1 - p2 <==> ((unsigned int)p1 - (unsigned int)p2)/sizeof(type);注意:a> 只有当两个指针指向同一个数组中的元素时,指针相减才有意义,其意义为指针所指元素的下标差;b> 当两个指针指向的元素不在同一个数组中时,结果为定义

        指针也可以进行关系运算(<, <=, >, >=),指针关系运算的前提是同时指向同一个数组中的元素;任意两个指针之间的比较运算(==,!=),参与比较运算的指针类型必须相同。

        下来我们来看个示例代码,代码如下

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

        我们在第3行定义的宏是求这个数组元素的个数,在第9行定义的指针 pEnd 为数组首元素的地址加上数组元素个数,那么它刚好指向数组最后一个元素的临界。这是 C 语言中的灰色地带,在 C 语言中是合法的。我们来看看编译结果

技术分享图片

        我们看到结果是如我们所想的那,因为是 char 类型的数组,所以 pEnd = pBegin + 5。

        数组名可以当做常量指针使用,那么指针是否也可以当做数组名来使用呢?我们往后接着说,在数组中的访问方式有两种:1、以下标的形式访问数组中的元素;2、以指针的形式访问数组中的元素。那么这两种方式有何区别呢?当指针以固定增量在数组中移动时,效率要高于下标形式。尤其是指针增量为 1 且硬件具有硬件增量模型时效率更高。下标形式与指针形式之间还会相互转换:a[n] <==> *(a + n) <==> *(n + a) <==> n[a]。这种表示法是不是很奇怪?但经过理论推导完全是成立的,下面我们就来看看是否支持这种写法

#include <stdio.h>

int main()
{
    int a[5] = {0};
    int* p = a;
    int i = 0;
    
    for(i=0; i<5; i++)
    {
        p[i] = i + 1;
    }
    
    for(i=0; i<5; i++)
    {
        printf("a[%d] = %d\n", i, *(a + i));
    }
    
    printf("\n");
    
    for(i=0; i<5; i++)
    {
        i[a] = i + 10;
    }
    
    for(i=0; i<5; i++)
    {
        printf("p[%d] = %d\n", i, p[i]);
    }
    
    return 0;
}

        我们看到在程序第6行定义指针 p 并将它指向数组 a,接下来就是我们之前说的到底指针是否也可以当做数组名来使用呢,如果可以第11行便不会报错。在第16行我们以指针的形式来打印数组 a 中的值,第23行则验证我们上面 i[a] 这种写法是否正确,第28行则通过下标形式来访问数组。我么来看看编译结果

技术分享图片

        我们看到程序没报错并且完美执行,这就回答了我们上面的问题和疑问。但是得注意:在现代编译器中,生成代码优化率已大大提高,在固定增量时,下标形式的效率已经和指针形式相当了;但从代码的可读性和维护的角度来看,下标形式更优秀,这就是为什么我们平时见到的代码中的数组都是以下标形式访问的啦。

        我们下来再做个实验,看看数组和指针的区别

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


ext.c 代码

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

        我们看到在 ext.c 中定义了一个数组,我们先以数组的方式在 test.c 中访问,看看打印结果

技术分享图片

        我们看到的结果和我们想的是一致的,&a 就代表数组的地址,a 就代表数组首元素的地址,两个是相同的。*a 的值便是数组中第一个元素的值啦。我们再来将 test.c 中的第5行改成 extern int* a; 这样呢,我们来看看编译结果

技术分享图片

        我们看到发生段错误了,这是什么情况呢?数组 a 的值为 1,*a 发生段错误了,在内存中,数组的值是这样存储的 0001 0002 ... 0005(大端机器)。那么 a 自然也就为 1了,计算机中的 1 地址处为内核态,用户态的程序想要访问内核态的地址,计算机当然会报错。

        那么 a 和 &a 有何区别呢? a 为数组首元素的地址,&a 为整个数组的地址。a 和 &a 的区别在于指针运算。a + 1 ==> (unsigned int)a + sizeof(*a);&a + 1 ==> (unsigned int)(&a) + sizeof(*&a) ==> (unsigned int)(&a) + sizeof(a);

        下来我们来看个经典的指针运算问题,同时也是一道笔试面试题

#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 +1 - 1] =>p1[&a],自然它的值也就为 5 了。p3[1] ==> (a + 1 +2) ==> (a + 2),自然也就为 3 啦。第二个的数感觉是随机数,但我们仔细分析下,它是首地址加 1,也就是往后移一位。这个数组在小端系统中,就是 1000 2000 ... 5000这样分布的,后移一位就变成了 0002,便是 0x02000000 转成十进制便是 33554432 啦。

        数组作为函数参数时,编译器将其编译成对应的指针。如:void f(int a[]) <==> void f(int* a);void f(int a[5]) <==> void f(int* a);在一般情况下,当定义的函数中有数组参数时,需要定义另一个参数来标定数组的大小。

        我们来看个示例代码

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

        我们在 func1 中打印它的参数大小,并且以指针方式进行赋值和指向 NULL,如果是数组的话便会报错。我们来看看编译结果

技术分享图片

        我们发现两函数的数组参数都被当成指针来处理了。 通过本节对指针和数组的学习,总结如下:1、数组声明时编译器自动分配一片连续的内存空间,指针声明时只分配了用于容纳地址值的4字节空间;2、指针和整数可以进行运算,其结果为指针。指针之间只支持减法运算,其结果为数组元素下标差;3、指针之间支持比较运算,其类型必须相同;4、数组名和指针仅使用方式相同,数组名的本质不是指针,指针的本质不是数组;5、数组名并不是数组的地址,而是数组首元素的地址;6、函数的数组参数化为指针。


        欢迎大家一起来学习 C 语言,可以加我QQ:243343083

以上是关于C之指针与数组组合的主要内容,如果未能解决你的问题,请参考以下文章

C语言之数组指针指针数组

C语言中数组与指针的异同之处!你不知道的编程奥秘~

C语言系列之 数组强化与三级指针-尹成-专题视频课程

C艺术篇 3-1 指针与一维数组

C语言review之指针

❥关于C++之数组与指针