• C++指針淺析(2)


    一.指针和数组的定义

        指针是指针,指针变量存储的是一个地址,用来间接访问数据,在32位系统下,一个指针变量(包括void指针)始终占4个字节的空间。指针可以指向任何内存空间,但不是任何内存空间都可以通过指针去访问。

        数组是数组,定义一个数组之后,编译器便根据该数组元素的类型和个数在内存开辟一段连续的空间来存放数据,从而直接访问数据。

        下面看一个例子

        在file1.c中有如下代码:

    char p[100]="abcdef";

        在file2.c中有如下代码:

    #include<stdio.h>

    extern char *p;

    int main(void)
    {
    printf("%c\n",p[1]);
    return 0;
    }

        发现能够编译通过,但是能正确执行么?调试发现:出现下图这个错误,无法计算得到p[1]的值。原因稍后作解释。

    从这里就可以看出,指针和数组并不是等同的,数组的定义并不等同于指针的外部声明(注意声明和定义的区别,定义是为一个变量或者对象分配内存空间,而声明只是描述类型)。

    二.指针和数组访问时的区别

         对数组下标的引用:


         对指针的引用:

    从上面的图中可以看出,指针和数组根本就是两个完全不一样的东西。对于数组,由于编译器在编译的时候就已经知道每个符号的地址,因此如果需要一个地址来执行某种操作,可以直接进行操作,并不需要增加指令首先取得具体地址,对于数组就是如此;而对于指针,必须在运行时首先取得它当前的具体值然后才能进行引用。从这点就可以解释为什么上面的程序无法正确执行,因为在file1.c中定义的p是一个数组,而在file2.c中却声明的是一个指针。因此在file2.c中引用时默认p是一个指针变量,并且会把指针变量中的任何数据当做地址来处理,因此首先取原数组的前4个字节的内容:0x61 0x62 0x63 0x64构成一个地址(暂不考虑大小端的问题)0x61626364,然后按照char型读取0x61626364这个地址中的内容,但是这个地址可能并不是有效地地址,即使是有效地,也不是我们想要的。大家可以想一下如果在file1.c中将p定义为指针类型,而在file2.c中将p声明为数组类型,会是什么情况?

    解决上述问题的办法就是在任何时候保持定义和声明一致。

    测试程序:

    file2.c

    #include<stdio.h>

    extern char p[];
    extern void print();

    int main(void)
    {
    printf("%x\n",p[0]);
    printf("%x\n",p[1]);
    printf("%08x\n",p); //注意此时p的值是存储原指针p(file1.c中的p)的内存单元的首地址
    print();
    return 0;
    }

    file1.c

    #include<stdio.h>
    char *p="abcdef";

    void print()
    {
    printf("%08x\n",p);
    printf("%08x\n",&p);
    }

    执行结果为:

    28
    20
    00424a30
    00424a30
    00422028
    00424a30
    Press any key to continue

    三.一些应该注意的地方

       1.sizeof计算所占空间时的区别。

          对于数组,sizeof计算的是整个数组所占的空间,而在32位系统下,sizeof 指针的值始终为4.

       2.数组名作为左值时不能被修改,而指针作为左值时可以被赋值。

       3.指针可以进行自增(自减)运算(void指针除外,因为void指针无法知道步长),但是数组不能进行自增或者自减运算。
       4.理解char *p="abcde"和char str[]="abcde"的区别。

     前面已经讨论了指针和数组的一些区别,然而在某些情况下,指针和数组是等同的,下面讨论一下什么时候指针和数组是相同的。

    C语言标准对此作了说明:

    规则1:表达式中的数组名被编译器当做一个指向该数组第一个元素的指针;

              注:下面几种情况例外

              1)数组名作为sizeof的操作数

              2)使用&取数组的地址

    规则2:下标总是与指针的偏移量相同;

    规则3:在函数参数的声明中,数组名被编译器当做指向该数组第一个元素的指针。

           规则1和规则2结合在一起理解,就是对数组下标的引用总是可以写成“一个指向数组的起始地址的指针加上偏移量”。如a[i]总是被编译器解析为*(a+i)的形式。

    规则1:表达式中的数组名总被编译器解析为指针,因此如下语句int a[3];int *p=a;是可以正确编译执行的。在表达式中a被解析为指向数组第一个元素的指针,那么赋值符号两边的类型匹配,因此可以正确编译执行。

    规则2:下标总是和指针的偏移量相同。C语言中将数组的下标改写成指针偏移量的主要原因在于指针和偏移量是底层硬件所使用的基本类型。如a[i]中的i总被编译器解析为偏移量,所以a[i]总是被改写成*(a+i)的形式,a是指向数组第一个元素的指针,加上偏移量i,表示该指针向后移i个步长,然后取a+i所在单元的内容。由此就可以解释为什么C语言中数组的下标可以为负,而且在我看来,C语言中不检查数组的下标是否越界同样跟这个有关,如下面这段程序:

    #include<stdio.h>

    int main(void)
    {
    int a[3]={1,2,3};
    int *p=(a+3);
    printf("%d\n",p[-1]);
    return 0;
    }

    程序执行结果为3,虽然下标为-1,但是被编译器解析为偏移量,因此相当于*(p-1)。

    规则3:在函数参数的声明中,数组名被编译器当做指向该数组第一个元素的指针。在C语言中将形参的数组和指针等同起来是出于效率的考虑。假如不这么做,将整个数组的每个元素的值都拷贝一份进行传递,这样无论在时间上还是空间上的开销都可能是非常大的。但是又要能操作到数组中的元素,只需将数组第一个元素的地址传递给调用函数,然后通过指针去访问想要访问的空间,这样一来时空消耗将大大减少。因此在函数内部,编译器始终把参数中声明的数组名当做一个指向数组第一个元素的指针,这样一来,编译器可以产生正确代码,并不需要对数组和指针这两种情况作区分。因此void fun(int a[]);和void fun(int *a)两种形式的效果完全等同,在函数内部去引用a的话,始终都会被编译器认为是指针。因为void fun(int a[]);这种形式最终还是会被编译器解析为void fun(int *a);这种形式告诉我们调用时必须传递一个指向整型数据的指针。所以下面这段代码可以正确编译和执行:

    #include<stdio.h>

    void fun(int a[])
    {
    printf("%d\n",a[0]);
    }
    int main(void)
    {
    int a[3]={1,2,3};
    int *p1,*p2;
    int b=4;
    p1=a;
    p2=&b;
    fun(a);
    fun(&a[1]);
    fun(p1);
    fun(p2);
    fun(&b);
    return 0;
    }

    区分几个表达式的含义:

    &p,p,a,&a

    &p:表示取存储指针变量p的内存单元的地址;  sizeof(&p)=4;

    p:表示取指针变量p存储的地址;                     sizeof(p)=4;

    a:表示取数组第一个元素的地址;                    sizeof(a)=3*4=12;

    &a:表示取整个数组的首地址;                        sizeof(&a)=4(在VC++6.0中该值为12,我认为是错误的,因为其类型是数组指针)

    虽然a和&a的值相同,但是所表达的含义完全不同,a表示取数组第一个元素的地址,而&a表示取数组的首地址。它们所代表的类型也完全不同,a是一个int型指针,而&a是一个int (*p)[]型指针,即数组指针(在后续文章中会作解释)。所以a+1和&a+1得到的结果不同,a+1表示将指向该数组的第一个元素的指针向后移一个步长(这里的步长为数组元素类型所占的字节数);而&a+1表示将指向该数组的指针向后移动一个步长(而此处的步长为数组元素个数*元素类型所占的字节数)。

    #include<stdio.h>

    int main(void)
    {
    int a[3]={1,2,3};
    int *p=a;
    printf("%08x\n",&p);
    printf("%08x\n",p);
    printf("%08x\n",&p+1);
    printf("%08x\n",p+1);
    printf("%08x\n",a);
    printf("%08x\n",&a);
    printf("%08x\n",a+1);
    printf("%08x\n",&a+1); //注意输出结果
    return 0;
    }
  • 相关阅读:
    ES6 常用总结(前端开发js技术进阶提升总结)
    web前端之es6对象的扩展
    ES6数组及对象遍历的新增方法 entries(),keys() 和 values()
    关于日期
    最近遇到的几个小东西
    求模
    同步 异步请求的认识
    变量名和函数名声明提升
    机顶盒前端开发小结
    js节点使用 碎片节点
  • 原文地址:https://www.cnblogs.com/Mayvar/p/wanghonghua_201202060447.html
Copyright © 2020-2023  润新知