首先必须非常明确:指针完整的名字应该叫指针变量,简称为指针。指针的实质就是个变量,从内存和数据的角度来说,它跟普通变量没有任何本质区别。
来看下面一段代码:
#include
int main(int argc,char**argv)
{
int a=5;
int* p=&a;
printf("a = %d,p = %p.\n",a,p);
return 0;
}
运行结果:
a = 5,p = 0x7ffe97237f6c.
以64位机器为例,来分析一下对于上面的代码编译器会做哪些工作:
int a=5;
,编译器会在内存中找一个4个字节的内存空间,然后把这个空间和符号a
绑定起来,之后操作a
就等于操作这个内存空间,再将数值5以二进制补码的形式存放到这片空间当中去。int* p=&a;
,编译器会在内存中找一个8个字节的空间,把这段空间和符号p
绑定起来,再将变量a
的地址存放到这片空间中去。通过上面的分析可以知道,其实不管是int a
还是int* p
,编译器做的事情实际上是没有区别的:都是在内存中分配一段空间,将这个空间和一个符号绑定,然后往该空间内赋上指定的数据。
指针变量和其他非指针普通变量的区别是:指针变量内部存储的数据应该是另一个普通变量(包括指针变量)的地址。就好像我们买了2个桶a
和p
,a
用来放水,p
用来放盐,本质上a
和p
是没有区别的,它们都是桶,都可以用来存放东西,区别就是各自应当放入的东西是不一样的。
指针的出现是为了实现间接访问。在汇编语言中也有间接访问,这其实就是在计算机组成原理课程中CPU寻址方式中的间接寻址。CPU的间接寻址是CPU设计时决定的,这也决定了汇编语言必须能够实现间接寻址,从而又决定了汇编之上的C语言也必须实现间接寻址。
尽管高级语言如Java
、C#
等没有指针,但是语言本身封装实现了间接访问。
指针使用的三部曲:
举个栗子:
#include
int main(int argc,char**argv)
{
int a=5;
//1.定义指针变量
int* p;
//2.关联指针变量,将a的地址赋值给p,让p指向a
p=&a;
//3.对指针变量解引用
printf("*p = %d.\n",*p);
return 0;
}
单纯的使用int * p;
定义一个指针变量p
而不通过赋值符号初始化时,因为p
是局部变量,所以也遵循C
语言局部变量的一般规律:定义局部变量并且未初始化则值是随机的,所以此时p
变量中存储的是一个随机的数字。此时如果解引用p
,则相当于我们访问了这个随机数字为地址的内存空间。那这个空间到底能不能访问不知道,也许行也许不行,所以如果直接定义指针变量未绑定有效地址就去解引用几乎一定会导致错误。
因此定义一个指针变量,不经绑定有效地址就去解引用,就好像拿一个上了镗的枪随意转了几圈然后开了一枪。指针绑定的意义就在于:让指针指向一个可以访问并且应该访问的地方,就好象拿着枪瞄准目标的过程一样;指针的解引用是为了间接访问目标变量,就好象开枪是为了打中目标一样。
在C
语言中*
可以表示乘号,也可以表示与指针相关的操作符。这两个用法是毫无关联的,只是恰好用了同一个符号而已。
星号在用于指针相关功能的时候有2种用法:
*
结合前面的类型(如int
,char
)用于表明要定义的指针的类型。p
解引用时,*p
表示p
指向的变量本身。取地址符使用时直接加在一个变量的前面,然后取地址符和变量加起来构成一个新的符号,这个符号表示这个变量的地址,可以作为右值赋给指针变量。例如&a
得到的就是变量a
的地址。
指针变量定义时可以初始化,指针的初始化其实就是给指针变量初值,跟普通变量的初始化没有任何本质区别。
指针变量定义同时初始化的语法是:
int a = 32;
int *p = &a;
指针变量定义时不初始化之后再赋值的语法是:
int a = 32;
int *p;
p = &a; //正确
*p = &a;//错误,*p表示p指向的变量
放在赋值运算符=
左边的就叫左值,右边的就叫右值。所以赋值操作其实就是:
左值 = 右值;
当一个变量做左值时,编译器认为这个变量符号的真实含义是这个变量所对应的那个内存空间。当一个变量做右值时,编译器认为这个变量符号的真实含义是这个变量的值,也就是这个变量所对应的内存空间中存储的那个数。
左值与右值的区别,就好象现实生活中"家"这个字的含义。比如说"我回家了",这里面的家指的是你家的房子,类似于左值;但是说"家比事业重要",这时候的家指的是家人,就是住在家所对应的那个房子里面的人,类似于右值。
野指针就是指针指向的位置是不可知的、随机的、不正确的、没有明确限制的。野指针很可能触发运行时段错误(Sgmentation fault
),因为指针变量在定义时如果未初始化,它的值是随机的。指针变量的值其实就是别的变量(指针所指向的那个变量)的地址,野指针意味着这个指针指向了一个地址是不确定的变量,这时候去解引用就是去访问这个地址不确定的变量,所以结果是不可知的,很可能会导致错误。
野指针因为指向地址是不可预知的,所以有3种情况:
a
,那么野指针的解引用就会刚好修改这个变量x
的值,导致这个变量莫名其妙的被改变,程序出现离奇的错误,一般最终都会导致程序崩溃,或者数据被损害,这种危害是最大的。指针变量如果是局部变量,则分配在栈上,本身遵从栈的规律:反复使用,使用完不擦除,所以是脏的,本次在栈上分配到的变量的默认值是上次这个栈空间被使用时余留下来的值,就决定了栈的使用多少会影响这个默认值。因此野指针的值是有一定规律不是完全随机,但是这个值的规律对我们没意义,因为不管落在上面野指针3种情况的哪一种,都不是我们想看到的。
野指针的错误来源就是指针定义了以后没有初始化,也没有赋值,总之就是指针没有明确的指向一个可用的内存空间就去解引用。
知道了野指针产生的原因,避免方法就出来了:在指针的解引用之前,一定确保指针指向一个绝对可用的空间。常规的4点做法是:
NULL
。NULL
。NULL
。野指针的防治方案4点绝对可行,但是略显麻烦,很多人懒得这么做。在中小型程序中,自己水平可以把握的情况下,不必严格参照这个标准。但是在大型程序,或者自己水平感觉不好把握时,建议严格参照这个方案。
NULL
在C/C++
中定义为:
#ifdef _cplusplus // 定义这个符号就表示当前是C++环境
#define NULL 0 // 在C++中NULL就是0
#else
#define NULL (void *)0 // 在C中NULL是强制类型转换为void *的0
#endif
在C
语言中,int *p;
之后可以p = (int *)0;
但是不可以p = 0;
因为类型不相同。
所以NULL
的实质其实就是0,只不过这个0代表的是地址,而不是一个整数,然后我们给指针赋初值为NULL
,其实就是让指针指向0地址处。指向0地址处有2个原因:
C
语言程序员不按规矩(不检查是否等于NULL
就去解引用)写代码直接去解引用就会触发段错误,程序员就可以发现问题然后去解决,这样的结果已经是最好的结果了。一般在判断指针是否野指针时,都写成
if (NULL != p)
而不是写成
if (p != NULL)
原因是:如果NULL
写在后面,当中间是==
号的时候,有时候容易写错写成了=
,这时候其实程序已经错误,但是编译器不会报错。这个错误(对新手)很难检查出来。但是如果习惯了把NULL
写在前面,当错误的把==
写成了=
时,编译器会报错,程序员就可以发现这个错误。
const
关键字在C
语言中用来修饰变量,表示这个变量是常量。指针变量也是变量,所以const
关键字自然也可以用来修饰指针。
const
修饰指针常见的有4种形式,区分清楚这4种即可全部理解const
和指针。以int
类型指针为例:
const int *p;
,表示p
本身不是const
,*p
是const
。int const *p;
,表示p
本身不是const
,*p
是const
。int * const p;
,表示p
本身是const
,*p
不是const
。const int * const p;
,表示p
本身是const
,*p
也是const
。关于指针变量的理解,主要涉及到2个变量:第一个是指针变量p
本身,第二个是p
指向的那个变量,也就是*p
。一个const
关键字只能修饰一个变量,所以弄清楚这4个表达式的关键就是搞清楚const
放在某个位置是修饰谁的。
在gcc
环境下,const
修饰的变量其实是可以改的。在某些单片机环境下,const
修饰的变量是不可以改的。const
修饰的变量到底能不能真的被修改,取决于具体的环境,C
语言本身并没有完全严格一致的要求。
在gcc
中,const
是通过编译器在编译的时候执行检查来确保实现的(也就是说const
类型的变量不能改是编译错误,不是运行时错误。)所以我们只要想办法骗过编译器,就可以修改const
定义的常量,而运行时不会报错。更深入一层的原因,是因为gcc
把const
类型的常量也放在了data
段,其实和普通的全局变量放在data
段是一样实现的,只是通过编译器认定这个变量是const
的,运行时并没有标记const
标志,所以只要骗过编译器就可以修改了。
const
是在编译器中实现的,在代码编译时检查,因此运行时并非不能骗过。所以在C
语言中使用const
就好象是一种道德约束而非法律约束,所以使用const
时更多是传递一种信息,就是告诉编译器,也告诉读程序的人,这个变量是不应该也不必被修改的。