美文网首页C/C++知识点
解读 C 语言中的指针

解读 C 语言中的指针

作者: Python编程导师 | 来源:发表于2019-01-23 14:31 被阅读5次

    我想对很多学习C语言的新手来说,指针无疑是一个难点。这篇文章也是我发布的第三篇关于C语言指针问题的文章了。但是,我觉得指针也是C语言特别重要的一个特性。也许,你在除了C和C++以外的编程语言中,很少看到指针。而C++中,也多用引用,而非指针。指针,作为一种高效的工具,可谓是一把双刃剑——用得好,可以大大提高程序效率,但用的不好,就是很多bug的滋生地。

    这或许也是人们对指针褒贬不一的原因吧。就我个人而言,我还是很喜欢这个特性,因为我需要经常和硬件以及一些底层的软件打交道。这个时候,指针便体现出它独特的魅力。指针的知识很多,有一本经典的书叫《C和指针》,如果有兴趣可以读一读。这里,我主要总结一些如何去解读指针(说实话这个东西实在是很容易让人困惑)的方法,一方面给自己做查询用,另一方面,希望可以给别人一些帮助。本人从事在线教育多年,将自己的资料整合建了一个QQ群,有兴趣一起交流学习c/c++的小伙伴可以加群:941636044,里面有大神会给予解答,也会有许多的资源可以供大家学习分享,欢迎大家前来一起学习进步!

    一,基本概念

    关于指针的基本概念,我就不详细介绍了,因为有许多书都介绍的很详细。这里我只介绍一部分。指针指向一个地址,而指针本身在大多数系统上都是一个无符号整数(在32bit机上是4byte,在64bit机上是8byte)。下面用一个例子来说明其机制:

    在上面的例子中,先定义了一个指针p,它的类型是int,也就是说它只能指向一个int型的变量,而不能指向其他类型的变量。最后我们将a变量的地址赋给p。在这个过程中,涉及到两个内存块,一个是存放指针p的内存(用&p可得到内存地址),一个是存放a的值的内存块(用&a可以得到内存地址)。而第一个内存存的p的值经过赋值语句后也就是&a的值了。另外一个注意点是, *(星号)和变量类型以及变量名之间可以有任意个空格,也可以没有。比如下面三种方式都是一样的:

    int a = 10;

    int *p;    //声明一个指针,但未初始化,此时为野指针

    p = &a;     //将a变量的地址赋给指针p

    在上面的例子中,先定义了一个指针p,它的类型是int,也就是说它只能指向一个int型的变量,而不能指向其他类型的变量。最后我们将a变量的地址赋给p。在这个过程中,涉及到两个内存块,一个是存放指针p的内存(用&p可得到内存地址),一个是存放a的值的内存块(用&a可以得到内存地址)。而第一个内存存的p的值经过赋值语句后也就是&a的值了。另外一个注意点是, *(星号)和变量类型以及变量名之间可以有任意个空格,也可以没有。比如下面三种方式都是一样的:

    int* a;

    int * a;

    int *a;

    解读方法:

    首先从标示符开始阅读,然后往右读,每遇到圆括号的右半边就调转阅读方向。重复这个过程直到整个声明解析完毕。需要注意的是,已经读过的部分在后续作为一个整体来看。

    看下面一个例子:

    int *a[3];

    //首先a右边是[],说明a是一个具有3个元素的数组

    //右边读完,则读左边。a左边是int*,说明a的元素是int类型的指针

    int (*a)[3]

    //首先,a右边是圆括号的右半边,转向,左边是一个*,说明a是一个指针

    //遇到括号,再转向,是一个[],说明a是一个指向3个元素的数组的指针

    //左边是int,说明元素类型是int

    //所以,a是一个指向具有3个整型元素的数组的指针

    int (*func)(int p);

    //相同的方法,func首先是一个指针

    //然后右边是一个括号,说明(func)是个函数,而func是指向这个函数的指针

    //这个函数具有int类型的参数,返回值类型为int

    int (*func[3])(int p);

    //同理,func首先是一个具有3个元素的数组

    //其次,func左边是一个*,说明func数组的元素是指针。要注意修饰的是func[3],而不是func。因为已经读过的部分在后面都作为一个整体来对待

    //跳出第一个圆括号,右边又是一个圆括号,说明func数组的元素是函数类型的指针。这个函数具有int类型的参数和int型返回值

    二,数组首地址a,&a,&a[0]

    注:a,&a,&a[0]的含义虽然不同,但是他们三个的值是相等的!

    以int a[3]为例说明:

    a作为右值时,代表数组首元素的首地址,而非数组地址。 也就是a[0]的地址。int i = (a+1),这里a是右值,所以代表首元素的首地址,a+1代表下一个元素的首地址,即&a[1]。

    a是整个数组的名字。所以sizeof(a)的值为sizeof(int) * 3 = 40,代表整个数组的大小。

    &a即为取a的首地址,也即整个数组的首地址。所以sizeof(&a) = 4。 int p = (int)(&a+1)中的&a+1代表下一个数组的首地址,显然是越界的。

    &a[0]代表首元素的首地址。 所以sizeof(&a[0]) = 4。

    &a[3],很显然数组越界了,但它的sizeof是多少呢? 也是4,因为关键字sizeof求值是在编译的时候,虽然并不存在a[3]这个元素,但是这里并没有真正访问a[3],而是根据数组元素类型来确定其值的。所以sizeof(a[3])不会出错。

    a[-1]代表什么意思?首先要明白下标的形式被编译器解析成指针的形式,即a[1]被解析成(a+1)。那么,a[-1]被解析成*(a-1)。

    关于数组首元素的首地址和数组的首地址的区别:其实,数组首元素的首地址和数组首地址的值是相同的,即&a[0]和a(以及&a)是相等的,但是而这含义不一样。首元素的首地址加1后,是第二个元素的首地址(之所以一直说首地址,是因为有的类型存储时会占多个地址),但数组的首地址加1后是“下一个数组的地址”,这里的下一个数组只是为了说明加1时加了整个数组的大小,而不是一个元素的大小。

    有一点比较容易混淆:a虽然代表整个数组,但(a+1)却代表下一个元素的首地址,即和(&a[0]+1)一样,下一个数组的形式为:(&a+1)。 下面以一个程序来说明:

    #include<stdio.h>

    int main()

    {

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

        printf("%ld\n",sizeof(long unsigned int));

        printf("*(a+1)=%d\n",*(a+1));

        printf("sizeof(a)=%ld\n", sizeof(a));

        printf("sizeof(&a[3])=%ld\n", sizeof(&a[3]));

        printf("a[-1]=%d\t*(a-1)=%d\n",a[-1],*(a-1));

        printf("a=%p\t&a=%p\t&a[0]=%p\n",a, &a,&a[0]);

        printf("a=%p\t(a+1)=%p\t(&a+1)=%p\n",a,(a+1),(&a+1));

        return 0;

    }

    输出结果:

    *(a+1)=2

    sizeof(a)=12

    sizeof(&a[3])=8

    a[-1]=0 *(a-1)=0

    a=0x7fffcb4cb980        &a=0x7fffcb4cb980       &a[0]=0x7fffcb4cb980

    a=0x7fffcb4cb980        (a+1)=0x7fffcb4cb984    (&a+1)=0x7fffcb4cb98c

     说明(下面的行数只计算main函数内有代码的行)

    程序第1行定义了一个具有3个元素的整型数组。

    第2行打印了long型的大小。因为我是64bit的,所以一个long是8byte。

    第3行打印了*(a+1)的值,结果和a[1]的值相等。说明a虽然代表整个数组,但作为右值时,的确代表首元素的首地址。

    第4行输出值为12,是整个数组的大小。

    第5行打印了一个出界元素的大小,没有报错,验证了上面第5条。

    第6行打印了a[-1]和*(a-1),输出值相等。验证了上面第6条。

    第7行打印了a和&a[0],值相等。说明数组的首地址和首元素的首地址是相等的。

    第8行打印了a,(a+1),(&a+1),由结果就可以看出首元素的首地址加1是加了一个数组元素的大小,而数组首地址加1是加了一个数组的大小。

    三,指针数组和数组指针

    指针数组: 首先它是一个数组,数组的元素是指针,也成为“存储指针的数组”。

    数组指针: 首先它是一个指针,它指向一个数组,也可以理解为“数组的指针”。 也可以利用前面的“解读方法”去分析。

    四,函数指针和指针函数

    函数指针: 指向函数的指针变量。

    指针函数: 带指针的函数,也就是返回指针的函数。

    char * fun(char* a, char* b)  //定义为指针函数

    {

    ...

    ...

    }

    int main()

    {

        char* (*p)(char* p1, char* p2); //定义函数指针

        p = &fun; //把函数地址赋给它

        //p = fun; //这样写也行

        (*p)("aa", "bb"); //使用函数指针

      return 0;

    }

    五,指针常量和常量指针

    const char* p1;    //常量指针,指向常量的指针

    char const* p2;    //同上

    char* const p3;     //指针常量,指针是常量

    怎么记?

    1. 可以先把类型名去掉,然后看const离谁近,就修饰谁。

    2. 也可以const在*左边的为常量指针,const在*右边的为指针常量。

    三~五的万能钥匙

    其实,关于“指针数组与数组指针、函数指针与指针函数、指针常量与常量指针”的判断,有一个万能钥匙。那就是根据我们强大的中文语法:前边是修饰词,后边才是主语。比如“指针数组”,前面的指针只是修饰词,后面的数组才是主语,所以它是一个数组。

    在这里说一句:本人从事在线教育多年,将自己的资料整合建了一个QQ群,有兴趣一起交流学习c/c++的小伙伴可以加群:941636044,里面有大神会给予解答,也会有许多的资源可以供大家学习分享,欢迎大家前来一起学习进步!

    六,野指针

    野指针指没有确定指向的指针。造成野指针的情况有:

    1. 指针变量创建但没有初始化。

    2. 指针p被free或者delete之后,没有置为NULL。

    相关文章

      网友评论

        本文标题:解读 C 语言中的指针

        本文链接:https://www.haomeiwen.com/subject/yehjjqtx.html