时间:2018.1.29 作者:Tom 工作:HWE 说明:如需转载,请注明出处。
说明:本文主要参考朱有鹏老师linux嵌入式C语言高级篇笔记,已注明转载。
1. 指针到底是什么?
1.1 指针变量和普通变量的区别
首先必须非常明确:指针的实质就是变量,它和普通变量没有任何本质区别。指针完整的名字应该叫指针变量,简称指针。
void main(void)
{
int a;
int *p;
a = 4;
//p = 4; //只报警告不报错,结果为0x4
//p = &a; //p = 0xbfa409e4
p =(int *)4; //没有错误没有警告,结果为0x4
printf("a = %d.\n", a);
printf("p = %p.\n", p);
printf("p = 0x%x.\n", p);
}
int a定义了一个int型变量,名字是a,a的实质是编译器中的符号,编译器中a和一个内存空间联系起来,这个内存空间就是a所代表的那个变量。int *p定义了一个指针变量,名字叫p,p指向一个int型变量。p也是符号,与这个指针针地址空间绑定起来。指针变量虽然实质上也是普通变量,但是它的用途和普通变量不同。指针变量存储的应该是另一个变量的地址,而不是随意存一些int类型的数。因此p = 4,是不行的。
但是p =(int *)4是可以的,我们知道其实就是数字4,但我强制类型转换成int *类型的4,相当于我告诉编译器,这个4其实就是个地址(而且还是int类型变量的地址),那么(int *)4就和p类型相匹配了,编译器就通过了。这里4就是个地址,在内存空间中是有0x4这个地址的。让p指向内存地址为4的那个变量。
1.2 为什么需要指针?
-
指针的出现是为了实现间接访问,在汇编中都有间接访问,其实就是CPU的寻址方式中的间接寻址;
-
高级语言如Java、C#等没有指针,那他们怎么实现间接访问?答案是语言本身帮我们封装了。
-
一定要记住:
1.3 指针的使用
指针使用就三步:定义指针变量、关联指针变量、解引用。
定义指针变量:int *p;
关联指针变量:p = &a;或者p = (int *)4;或者int *p = &a;
解引用:*p = 555;等
-
当我们int*p定义一个指针变量时,因为p是一个局部变量,所以也遵循C语言局部变量的一般规律(定义局部变量并且没有初始化,则值是随机的),所以此时p变量中存储的是一个随机数字地址;
-
此时我们解引用p,相当于我们随机访问了这个随机数字为地址的内存空间,那个这个空间到底能不能访问不知道(也许行也许不行),所以如果直接定义指针变量和未绑定有效地址就去解引用几乎是必死无疑。定义一个指针变量,不经绑定有效地址就去解引用,就好像拿了一个上膛的枪随意转了几圈然后开了一枪。
-
指针绑定的意义就在于:让指针指向一个可以访问、应该访问的地方,指针的解引用是为了间接访问目标变量。
1.4 指针符号
我们写的代码是给编译器看的,代码要想达到你想象的结果,就必须要编译器对你的代码的理解和你自己对代码的理解一样。编译器理解代码就是理解的符号,所以我们要正确理解C语言的符号,才能像编译器一样思考程序、理解代码。
- 星号*
C语言中*号可以表示乘号,也可以表示指针符号。但两个毫无关联,只是恰巧同一个符号而已。
星号在用于指针相关功能的时候有两种用法:
第一种指针定义时,*结合前面的类型(*虽然靠近p,但是和int结合的)用于表明要定义的指针的类型,如此来说定义int*类型的变量p有四种写法,但结果是一样的:
int*p;
int *p;
int* p;
int * p;
int *p1, *p2; //p1是指针变量,p2指针变量
int* p1, p2; //p1是指针变量,p2是int类型变量
int *p1, p2; //p1是指针变量,p2是int类型变量
第二种功能是指针解引用,解引用时*p表示p指向的变量本身。
int b = 23;
int *p = &b;
c = *p; //取值
*p = c; //赋值
- 取值地址符&
取地址符使用时直接加在一个变量的前面,然后取地址符和变量加起来构成一个新的符号,这个符号表示这个变量的地址。编译器给我们变量申请分配一个地址空间,但编译器知道这个地址,我们却不知道,因为我们没法直接把a的地址数字赋值给p,只有使用符号&来替代。
理解&a,*p这样的符号,关键在于要明白当&和*和后面的变量结合起来后,就共同构成了一个新的符号,这个新的符号具有一定的意义。但定义时int *p是两个方面意思(1.定义一个指针2.这个指针指向int类型的变量),解引用时*p是一个方面意思(指的就是变量)。
1.5 指针定义
-
指针定义时可以初始化,指针的初始化其实就是给指针变量赋初值(跟普通变量的初始化没有任何本质区别)。
-
指针变量定义同时初始化的格式是:int a = 32; int *p = &a;
-
不初始化时指针变量先定义再赋值:int a = 32; int *p; p = &a;是正确的,*p = &a;是错误的
int a =3, b = 5;
a = b; // 当a做左值时,我们关心的是a所对应的内存空间,而不是其中存储的3
b = a; // 当a做右值时,我们关心的是a所对应空间中存储的数,也就是5
3. 野指针
-
野指针,就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
-
野指针很可能触发运行时段错误(Sgmentation fault)
-
因为指针变量在定义时如果未初始化,值也是随机的。指针变量的值其实就是别的变量(指针所指向的那个变量)的地址,所以意味着这个指针指向了一个地址是不确定的变量,这时候去解引用就是去访问这个地址不确定的变量,所以结果是不可知的。
-
野指针因为指向地址是不可预知的,所以有3种情况:
-
第一种是指向不可访问(操作系统不允许访问的敏感地址,譬如内核空间)的地址,结果是触发段错误,这种算是最好的情况了;
-
第二种是指向一个可用的、而且没什么特别意义的空间(譬如我们曾经使用过但是已经不用的栈空间或堆空间),这时候程序运行不会出错,也不会对当前程序造成损害,这种情况下会掩盖你的程序错误,让你以为程序没问题,其实是有问题的;
-
第三种情况就是指向了一个可用的空间,而且这个空间其实在程序中正在被使用(譬如说是程序的一个变量x),那么野指针的解引用就会刚好修改这个变量x的值,导致这个变量莫名其妙的被改变,程序出现离奇的错误。一般最终都会导致程序崩溃,或者数据被损害。这种危害是最大的。
-
野指针的错误来源就是指针定义了以后没有初始化,也没有赋值(总之就是指针没有明确的指向一个可用的内存空间),然后去解引用。
-
知道了野指针产生的原因,避免方法就出来了:在指针的解引用之前,一定确保指针指向一个绝对可用的空间。
-
常规的做法是:
第一点:定义指针时,同时初始化为NULL
第二点:在指针解引用之前,先去判断这个指针是不是NULL
int a;
int *p = NULL;
p = &a;
if (NULL != p)
{
*p = 4;
}
P = NULL;
第三点:指针使用完之后,将其赋值为NULL
第四点:在指针使用之前,将其赋值绑定给一个可用地址空间
-
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,然后我们给指针赋初值为NULL,其实就是让指针指向0地址处。为什么指向0地址处?2个原因。第一层原因是0地址处作为一个特殊地址(我们认为指针指向这里就表示指针没有被初始化,就表示是野指针);第二层原因是这个地址0地址在一般的操作系统中都是不可被访问的,如果C语言程序员不按规矩(不检查是否等于NULL就去解引用)写代码直接去解引用就会触发段错误,这种已经是最好的结果了。
-
一般在判断指针是否野指针时,都写成
if (NULL != p)而不是写成 if (p != NULL)
原因是:如果NULL写在后面,当中间是==号的时候,有时候容易忘记写成了=,这时候其实程序已经错误,但是编译器不会报错。这个错误(对新手)很难检查出来;如果习惯了把NULL写在前面,当错误的把==写成了=时,编译器会报错,程序员会发现这个错误。
5. CONST关键字与指针
5.1 CONST修饰指针的4种形式
-
CONST关键字,在C语言中用来修饰变量,表示这个变量是常量。
-
CONST修饰指针有4种形式,区分清楚这4种即可全部理解const和指针。
·第一种: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的
-
关于指针变量的理解,主要涉及到两个变量:第一个指变量p本身,第二个是p指向的那个变量(*p)一个const关键字只能修饰一个变量,所以弄清楚这4个表达式的关键就是搞清楚const放在某个位置是修饰谁的。
5.2 const修饰的变量真的不能改吗?
-
课堂练习说明:const修饰的变量其实是可以改的(前提是gcc环境下)。答案是用指针可以修改。
-
在某些单片机环境下,const修饰的变量是不可以改的。const修饰的变量到底能不能真的被修改,取决于具体的环境,C语言本身并没有完全严格一致的要求。
-
在gcc中,const是通过编译器在编译的时候执行检查来确保实现的(也就是说const类型的变量不能改是编译错误,不是运行时错误。)所以我们只要想办法骗过编译器,就可以修改const定义的常量,而运行时不会报错。
-
更深入一层的原因,是因为gcc把const类型的常量也放在了data段,其实和普通的全局变量放在data段是一样实现的,只是通过编译器认定这个变量是const的,运行时并没有标记const标志,所以只要骗过编译器就可以修改了。
const是在编译器中实现的,编译时检查,并非不能骗过。所以在C语言中使用const,就好象是一种道德约束而非法律约束,所以大家使用const时更多是传递一种信息,就是告诉编译器、也告诉读程序的人,这个变量是不应该也不必被修改的。