声明:该读书笔记摘抄自《C和指针》——Kenneth A.Reek (著) 徐波(译)。为了克服自己走马观花,提高阅读和学习效率,决定将自己在读书过程中遇到的一些知识点加以摘抄和总结备忘,在此感谢原书作者和翻译。
一、有助于理解C语言指针的几个基础概念
1、左值:赋值号“=”左边的操作数; 右值:赋值号“=”右边的操作数。
2、变量的值:分配给该变量的内存位置存储的值,对指针变量来说它的值就是某个内存地址;
3、变量的地址:编译器为该变量分配的一个内存位置,用于存储变量本身,注意变量的地址和变量的值的区别,我想说的是对于普通变量很好理解,难点在于多级指针。
4、间接访问(也叫解引用):一个指针访问它所指向的地址,间接访问操作符 “ * ” 。注意:间接访问仅仅是指定了一个特定的内存地址而已。
5、当变量作为左值使用时,它表示一个内存地址; 当变量作为右值使用时它表示该变量里面存储的内容。真正地理解了这一点有助于理解指针的复杂应用和原理。
举个例子:
int a = 1, b = 2, c = 8,*p = &b;
*p = *p +1;
printf("b = %d\n", b);
p = p + 1;
printf("*p = %d\n", *p);
第一个printf结果很好分析,*p作为右值的时候,最终的结果是取出了p所指向的地址的内容,但是这里面包含了两个过程,第一步:*p表示间接访问,这一步确定了变量b,至于是访问b的地址还是它存储的内容,暂时还不清楚;第二步:*p在表达式中是个右值,因此编译器会确定访问它的内容。注意:左值和右值得区分是由编译器自动完成的。因此,第一个打印结果是 3 。
现在来看第二个,这整个例子是我自己写的,在linux-gcc下测试。 其实在我写的这个例子中p = p + 1;这个语句其实是非法的,因为指针的算术运算仅仅在内存地址连续分配的数组中才有意义,这里我仅仅是为了举例子演示而已。 现在来分析一下p = p + 1 这个表达式:当p作为右值时,编译器会访问指针变量p存储的内容,也就是b的地址,然后加1,这里又涉及到了指针的算术运算,要考虑步长的问题,p = p + 1 其实是 p = p + 1*(p所指向的对象的类型占用的字节数);p指向的对象是b,b声明为int类型,因此这里的加1实际上在内存中在变量b的基础上连续移动了4个字节。 那么它是向“前”移动还是向“后”移动呢?这里又会涉及到另一个知识点,就是关于栈的生长方向的问题。这里还要说明一点,就是在程序的编译过程中,编译器会根据程序中声明变量的顺序对其地址连续分配。例如在上例中,编译器会先给a变量分配一个地址,然后紧接着a的地址再给变量b分配,再给变量c分配,最后给指针变量p分配,至于他们的地址是不断增加还是减少,这就要看栈的生长方向了。在X86 linux下gcc编译测试,X86栈生长方向是向下的,即编译器在确定变量的地址时,从高地址向低地址分配使用内存栈空间,这样在上例中,a的地址最大,假设为0x2000001C,那么变量b的地址减小4,为0x20000018,变量c的地址为0x20000014,变量p的地址为0x20000010。 有了这些概念之后,我们就可以得出答案了,在p = p + 1表达式中,p + 1 就是变量p的值加4,变量p的值就是变量b的地址,因此p + 1的结果就是0x20000018 + 4 == 0x2000001c,可以看出,这是变量a的地址。然后在表达式p = p + 1中,当p作为左值使用时,编译器会将其识别为变量p自己的地址,执行完p = p + 1 之后, 变量p 的值已经变成变量a的地址,即指针变量p已经指向了变量a,在printf中再对指针变量p进行间接访问,这时将会访问指针变量p所指向的地址的内容,即变量a的值,结果为1。
注:在上例中,我之所以在变量b前面定义变量a是为了保证让p + 1合法,也是为了验证栈的生长方向;在b的后面定义变量c仅仅是为了验证栈的生长方向。
二、指针表达式
这部分内容在笔试题目里面出现的次数很多,很容易混淆,究其原因主要是没有弄清楚指针运算的本质过程,一条简单的C语句有可能是好几条汇编指令的集合过程,如果有条件可以从汇编的角度去理解。
这里定义几个实验变量:char a = '1',b = '2',c = '3', *p = &b;后面就用它们来举例说明几个指针表达式的执行过程:
1、 p++
第一步:该表达式首先返回指针变量p的一份拷贝,该拷贝将会被存放到内存的某一个地方以便后面使用,注意,这里说“某一个”是因为我们并不能确定具体的存储位置,这是一个表达式内部计算的中间过程,存放位置也是一个临时位置,这一点解释了为什么该指针表达式不能用作左值——因为拷贝值的存储位置不确定。因此p++只能充当右值。第二步:表达式返回一份拷贝后,将指针变量p自身的值加1。可以看出,该表达式实际上是执行了两个过程,在第一步执行完了以后,如果该指针的拷贝会被用到,例如 char *q = p++;那么该拷贝将会赋值给指针变量q。如果该拷贝不会被用到,例如 只有 一句p++;语句,那么该拷贝值将会被简单丢弃。
2、++p
第一步:该表达式首先将指针变量p的值加1,;第二步:产生一份加1之后的拷贝,产生的这份拷贝也将会被存储与内存的某一个位置留待后用或者不用被简单丢弃,这一点也解释了改表达式同样不能作为左值使用。
3、*++p
++优先级高于间接访问符,先执行++p,++p的执行过程已经解释过了,它的结果是增值之后的指针的拷贝,然后间接访问操作符再作用于该指针拷贝上,这样得到的结果将是对增值后的指针的间接访问,它可以作为左值也可以作为右值,具体说明见第一部分第5条。
4、*p++
这个表达式会引起误解:++优先级高于间接访问符,因此先执行p++,然后间接访问符作用于增值后的指针变量。这是错误的。 ++优先级高于间接访问符这句话没错,先执行p++也没错,错就错在了间接访问符的作用对象上,它其实是间接访问增值前的指针。表达式执行过程分解:第一步:++操作符产生指针变量p的一份拷贝先暂时存放到某个位置;第二步:++操作符增加指针变量p的值;第三步:在指针变量p的拷贝上执行间接访问操作,注意是在拷贝上执行间接访问操作,即间接访问的是增值前的指针。它可以作为左值也可以作为右值,具体说明见第一部分第5条。
5、++*p
++和间接访问符的结合性从右到左,首先执行对p的间接访问,然后将间接访问的内容增1,最后表达式返回一份内容增1后的拷贝。只能做右值,不能做左值。理由同p++。
6、++*++p
三个操作符结合性都是从右到左。*++p已经分析过,它返回对增值后的指针的间接访问。然后对*++p间接访问的内容进行增1操作,最后返回内容增1后的值的拷贝。只能做右值,不能做左值。理由同p++。
7、++*p++
*p++已经分析过了,它的结果是间接访问增值前的指针。然后对间接访问的内容进行增1操作,返回内容增1后的值的拷贝。只能做右值,不能做左值。理由同p++。
三、指针的算术运算和关系运算
1、指针的算术运算有两种形式,标准定义这两种定义只能用在连续存储的同一数组中。第一:指针加上(或者减去)一个整数,此时需要考虑步长;第二:指针减去指针,这里特别注意,两个指针相减时得到的结果是相距的距离,即两指针相减后还要除以步长,它是指针加减法的逆运算。
2、关系运算
> >= < <= !=
前面4个只能用在同一个数组中比较, !=可以用于任意两指针比较。
花了三个多小时来理清指针的思路,尽量表达清楚和全面,都已经深夜了,睡了。
以上是第六章的摘抄和总结,未完待续。。。。。