写在前面,由于学过C语言,导致想要跳跃式地翻阅《C和指针》,后来发现这实为错误,对于这本经典著作,要做的是从头到尾保持体系的完整性。
《C和指针》配套代码请移步网站:Pointers on C
作者Kenneth A. Reek的个人网站

文章目录

  • 6.1 内存和地址
    • 地址和内容
  • 6.2 值和类型
  • 6.3 指针变量的内容
  • 6.4 间接访问操作符
  • 6.5 未初始化和非法的指针
  • 6.6 NULL指针
  • 6.7 指针、间接访问和左值
  • 6.8 指针、间接访问和变量
  • 6.9 指针常量
  • 6.10指针的指针
  • 6.11指针表达式
  • 6.12 实例
  • 6.13 指针运算
    • 6.13.1 算术运算
    • 6.13.2 关系运算
  • 6.14 警告的总结
  • 6.15 编程提示的总结

6.1 内存和地址

我们可以把计算机的内存看作是一条长街上的一排房屋。每座房子都可以容纳数据,并通过一个房号来标识。

这个比喻颇为有用,但也存在局限性。计算机的内存由数以亿万计的位(bit)组成,每个位可以容纳0或1.由于一个位所能表示的值的范围太有限,所以单独的位用处不大,通常许多位合成一组作为一个单位,这样就可以存储范围较大的值。这里有一幅图,展示了现实机器中的一些内存位置。

这些位置的每一个都被称为字节(byte) ,每个字节都包含了存储一个字符所需要的位数。在许多现代机器上,每个字节包含8个位,可以存储无符号值0~255,或者有符号值-128 ~127.上面这张图并没有显示这些位置的内容,但内存中的每个位置总是包含一些值。每个字节通过地址来标识,如上图方框上面的数字所示。

为了存储更大的值,我们把两个或更多个字节合在一起作为一个更大的内存单位。例如,许多机器以字为单位存储整数,每个字一般由2个或4个字节组成。下面这张图所示的内存位置与上面这张图相同,但这次它以4个字节的字表示。

由于它们包含了更多的位,每个字可以容纳的无符号整数的范围是从0至4294967295(232−12^{32}-1232−1),可以容纳的有符号整数的范围是从-2147483648(−231-2^{31}−231)至2147483647(231−12^{31}-1231−1).

注意,尽管一个字包含了4个字节,它仍然只有一个地址。至于它的地址是从它最左边那个字节的位置还是最右边那个字节的位置,不同的机器有不同的规定。另一个需要注意的硬件事项是边界对齐(boundary alignment)。在要求边界对齐的机器上,整型值存储的起始位置只能是某些特定的字节,通常是2或4的倍数。但这些问题是硬件设计者的事情,它们很少影响C程序员。我们只对两件事情感兴趣:

  1. 内存中的每个位置由一个独一无二的地址标识。
  2. 内存中的每个位置都包含一个值。

地址和内容

这里有另外一个例子,这次它显示了内存中5个字的内容。

这里显示了5个整数,每个都位于自己的字中。如果你记住了一个值的存储地址,以后可以根据这个地址取得这个值。

但是,要记住所有这些地址太笨拙了,所以高级语言所提供的特性之一就是通过名字而不是地址来访问内存的位置。下面这张图与上图相同,但这次使用名字来代替地址。

当然,这些名字就是我们所称的变量。有一点非常重要,你必须记住,名字与内存位置之间的关联并不是硬件所提供的,它是由编译器为我们实现的。所有这些变量给了我们一种更方便的方法记住地址-------硬件仍然通过地址访问内存位置。

6.2 值和类型

现在让我们来看一下存储于这些位置的值。头两个位置所存储的是整数。第三个位置所存储的是一个非常大的整数,第4、5个位置所存储的也是整数。下面是这些变量的声明

int a=112,b=-1;
float c=3.14;
int *d=&a;
float  *e=&c;

在这些声明中,变量a和b确实用于存储整型值。但是,它声明的所存储的是浮点值。可是,在上图中c的值却是一个整数。那么到底它是哪个呢? 整数还是浮点数?

答案是该变量包含了一序列内容为0或1的位。它们可以被解释为整数,也可以被解释为浮点数,这取决于它们被使用的方式。如果使用的是整型算术指令,这个值就被解释为整型,如果使用的是浮点型指令,它就是个浮点型。

这个事实引出了一个重要结论:不能简单地通过检查一个值的位来判断它的类型。 为了判断值的类型(以及它的值),必须观察程序中这个值的使用方式。考虑下面这个二进制形式表示的32位值:

01100111011011000110111101100010

下面是这些位可能被解释的许多结果中的几种。这些值都是从一个基于Motorola 68000 的处理器上得到的。如果换个系统,使用不同的数据格式和指令,对这些位的解释又将有所不同。

这里,一个单一的值可以被解释为5个不同的类型。显然,值的类型并非值本身所固有的一种特性,而是取决于它的使用方式。

6.3 指针变量的内容

让我们把话题返回到指针,看看变量d和e 的声明。它们都被声明为指针,并用其他变量的地址予以初始化。指针的初始化是用&操作符完成的,它用于产生操作数的内存地址。


d和e的内容是地址而不是整型或浮点数数值。事实上,从图中可以容易地看到,d 的内容与a 的存储地址一致,而e 的内容和c的存储地址一致,这也正是我们对这两个指针进行初始化时所期望的结果。 区分变量d的地址(112)和它的内容(100)是非常重要的,同时也必须意识到100这个数值用于标识其他位置(是……的地址)。

在我们转到下一步之前,先看一些涉及这些变量的表达式。

int a=112,b=-1;
float c=3.14;
int *d=&a;
float  *e=&c;

下面这些表达式的值分别是什么呢?

a
b
c
d
e

前三个非常容易,a=112,b=-1,c=3.14.指针变量其实也很容易,d的值是100,e的值是108.

6.4 间接访问操作符

通过一个指针访问它所指向的地址的过程称为间接访问(indirection) 或解引用指针(dereferencing the pointer) 。这个用于执行间接访问的操作符是单目运算符*。

下面的声明和前面相同

d的值是100.当我们对d使用间接访问操作符时,它表示访问内存位置100并察看那里的值。因此,*d的右值是112-----位置100的内容,它的左值是位置100本身。

注意上面列表中各个表达式的类型:d是一个指向整型的指针,对它进行解引用操作将产生一个整型值。类似地,对float* 进行间接访问将产生一个float型值。

正常情况下,我们并不知道编译器为每个变量所选择的存储位置,所以我们事先无法预测它们的地址。这样,当我们绘制内存中的指针图时,用实际数值表示地址是不方便的。所以,绝大部分书改用箭头来代替,如下所示:

但是,这种记法可能会引起误解,因为箭头可能会使你误以为执行了间接访问操作,但事实上,它并不一定会执行这个操作。例如,根据上图,你会推断表达式d的值是什么?

如果你的答案是112,那么你就被这个箭头误导了。正确的答案是a 的地址,而不是它的内容。但是,这个箭头似乎会把你的注意力吸引到a上。要使你的思维不受箭头的影响是不容易的,这也是问题所在:除非存在间接访问操作符,否则不要被箭头所误导。

下面这个修正后的箭头记法试图消除这个问题。

这种记法的意图是既显示指针的值,但又不给你强烈的视觉线索,以为这个箭头是我们必须遵从的路径。事实上,如果不对指针变量进行间接访问操作,它的值只是简单的一些位的集合。当执行间接访问操作时,这种记法才使用实线箭头表示实际发生的内存访问。

注意箭头起始位置在方框内部,因为它表示存储于该变量的值。同样,箭头指向一个位置,而不是存储于该位置的值。这种记法提示跟随箭头执行间接访问操作的结果将是一个左值。

尽管这种箭头记法很有用,但为了正确使用它,你必须记住指针变量的值就是一个数字。箭头显示了这个数字的值,但箭头记法并未改变它本身就是个数字的事实。指针并不存在内建的间接访问属性,所以除非表达式中存在间接访问操作符,否则你不能按箭头所示实际访问它所指向的位置。

6.5 未初始化和非法的指针

下面这个代码段说明了一个极为常见的错误:


int *a;
*a=12;

这个声明创建了一个名为a的指针变量,后面那条赋值语句把12存储在a所指向的内存位置。

警告:

但是究竟a指向哪里呢?我们声明了这个变量,但从未对它进行初始化,所以我们没有办法预测12这个值将存储于什么地方。从这一点看,指针变量和其他变量并无区别。如果变量是静态的,它会被初始化为0;但如果变量是自动的,它根本不会被初始化。无论是哪种情况,声明一个指向整型的指针都不会“创建”用于存储整形值的内存空间。

所以,如果程序执行这个赋值操作,会发生什么情况呢? 如果你运气好,a的初始值会是个非法地址,这样赋值语句将会出错,从而终止程序。在UNIX系统上,这个错误被称为“段违例(segmentation violation)”或“内存错误(memory fault)”。它提示程序试图访问一个并未分配给程序的内存地址。在一台运行Windows的PC上,对未初始化或非法指针进行间接的访问操作是一般保护性异常(General Protection Exception)的根源之一。

对于那些要求整数必须存储于特定边界的机器而言,如果这种类型的数据在内存中的存储地址处于错误的边界上,那么对这个地址进行访问时将会产生一个错误。这种错误在UNIX系统中被称为“总线错误(bus error)”。

一个更为严重的情况是:这个指针偶尔可能包含了一个合法的地址。接下来的事情很简单:位于那个位置的值被修改,虽然你并无意去修改它。像这种类型的错误非常难以捕捉,因为引发错误的代码可能与原先用于操作那个值的代码完全不相干。所以,在你对指针进行间接访问之前,必须非常小心,确保它们已被初始化!!!

6.6 NULL指针

标准定义了NULL指针,它作为一个特殊的指针变量,表示不指向任何东西。要使一个指针变量为NULL,你可以给它赋一个零值。为了测试一个指针变量是否为NULL,你可以将它与零值进行比较。之所以选择零这个值是因为一种源代码约定。就机器内部而言,NULL指针的实际值可能与此不同。在这种情况下,编译器将负责零值和内部值之间的翻译转换。

NULL指针的概念是非常有用的,因为它给了你一种方法,表示某个特定的指针目前并未指向任何东西。例如,一个用于在某个数组中查找某个特定值的函数可能返回一个指向查找到的数组元素的指针。如果该数组不包含指定条件的值,函数就返回一个NULL指针。这个技巧允许返回值传达两个不同片段的信息。首先,有没有找到元素?其次,如果找到,它是哪个元素?

提示:
尽管这个技巧在C程序中极为常用,但它违背了软件工程的原则。用一个单一的值表示两种不同的意思是件危险的事情,因为将来很容易无法弄清哪个才是它真正的用意。在大型的程序中,这个问题更为严重,因为你不可能在头脑中对整个设计一览无余。一种更为安全的策略是让函数返回两个独立的值:首先是个状态值,用于提示查找是否成功;其次是个指针,当状态值提示查找成功时,它所指向的就是查找到的元素。

对指针进行解引用操作可以获得它所指向的值。但从定义上来看,NULL指针并未指向任何东西。因此,对一个NULL指针进行解引用操作是非法的。在对指针进行解引用操作之前,你首先必须确保它并非NULL指针。

警告:
如果对一个NULL指针进行间接访问操作会发生什么情况呢?\color{red}{如果对一个NULL指针进行间接访问操作会发生什么情况呢?}如果对一个NULL指针进行间接访问操作会发生什么情况呢?它的结果因编译器而异。在有些机器上,它会访问内存位置零。编译器能偶确保内存位置零没有存储任何变量,但机器并未妨碍你访问或修改这个位置。这种行为是非常不幸的,因为程序包含了一个错误,但机器却隐藏了它的症状,这样就使这个错误更加难以寻找。

在其他机器上,对NULL指针进行间接访问将引发一个错误,并终止程序。宣布这个错误比隐藏这个错误要好得多,因为程序员能够更容易修正它。

提示
如果所有的指针变量能够被自动初始化为NULL,那实在是一件幸事,但事实并非如此。不论你的机器对解引用NULL指针这种行为作何反应,对所有的指针变量进行显示的初始化是种好的做法。如果你已经知道指针将被初始化为什么地址,就把它初始化为该地址,否则就把它初始化为NULL。风格良好的程序会在指针解引用之前对它进行检查,这种初始化策略可以节省大量的调试时间。

6.7 指针、间接访问和左值

涉及指针的表达式能不能作为左值?如果能,又是哪些呢?对表5.1优先级表格进行快速查阅后可以发现,间接访问操作符所需要的操作数是个右值,但这个操作符所产生的结果是个左值。


让我们回到早些时候的例子。给定下面这些声明

int a;
int *d=&a;

考虑下面的表达式:

指针变量可以作为左值,并不是因为它们是指针,而是因为它们是变量。对指针变量进行间接访问表示我们应该访问指针所指向的位置。 间接访问指定了一个特定的内存位置,这样我们可以把间接访问表达式的结果作为左值使用。在下面这两条语句中

*d=10-*d; //OK
d=10-*d; //ERROR

第一条语句包含了两个间接访问操作。右边的间接访问作为右值使用,所以它的值是d所指向的位置所存储的值(a的值)。左边的间接访问作为左值使用,所以d所指向的位置(a)把赋值符右侧的表达式的计算结果作为它的新值。

第二条语句是非法的,因为它表示把一个整型数量(10-*d)存储于一个指针变量中。当我们实际使用的变量类型和应该使用的变量类型不一致时,编译器会发出抱怨,帮组我们判断这种情况。这些警告和错误信息是我们的朋友,编译器通过产生这些信息向我们提供帮助。d=10-*d; 在devc++编译器中返回错误

[Error] invalid conversion from 'int' to 'int*' [-fpermissive]

可运行代码如下:

#include<bits/stdc++.h>
using namespace std;int main(){int a=12;  //int 占用4字节 int *d =&a; //指针d指向变量a // *d 就是 a *d= 10 - *d;cout<<a<<endl; //&a 是a的地址,对地址间接访问*&a 就是a *&a=25;     // 即  a =25; cout<<a<<endl; }

程序输出结果:

-2
25

6.8 指针、间接访问和变量

如果你自以为精通了指针,不妨看一下这个表达式,看看你是否明白它的意思。

*&a=25;

如果你的答案是把25赋值给变量a,那么恭喜你,你答对了。让我们来分析这个表达式,首先,&操作符产生a的地址,它是一个指针常量,接着,*操作符访问其操作数所表示的地址。在这个表达式中,操作数是a的地址,所以值25就存储于a中。

这条语句和简单地使用a=25;有什么区别吗?从功能上来说,它们是相同的。

6.9 指针常量

让我们分析一个表达式。假定变量a存储于位置100,下面这条语句的作用是什么?

*100=25;

它看上去好像是把25赋值给a,因为a是位置100所存储的变量。但是,这是错的!这句语句实际上是非法的,因为字面值100的类型是整型,而间接访问只能作用于指针类型表达式。如果想要把25存储于位置100,必须使用强制类型转换。

*(int * )100=25;

强制类型转换把值100从整型变成指向整型的指针,这样对它进行间接访问就是合法的。如果a存储在位置100,那么这条语句的作用就是把值25存储于a。但是,需要使用这种技巧的机会是绝无仅有的!为什么?因为通常无法预测编译器会把某个特定的变量放在内存中的什么位置,所以无法预先知道它的地址。

这个技巧的唯一用处是偶尔需要通过地址访问内存中某个特定的位置,它并不是用于访问某个变量,而是访问硬件本身。

6.10指针的指针

看下面的例子

int a=12;
int *b=&a;
int **c=&b;

看一下内存分配


问题是,c是什么类型?显然它是一个指针,但它指向的是什么?变量b是一个“指向整型的指针”,所以任何指向b的类型必须是指向“指向整型的指针”的指针,更通俗地说,是一个指针的指针。

它合法吗?是的!指针变量和其他变量一样,占据内存中某个特定的位置,所以用&操作符取得它的地址是合法的。

指针的指针如何声明?

int ** c;

表示表达式**c 的类型是int

对表达式int **c=&b;进行分析:

int a=12;
int *b=&a;
int **c=&b;

分析:*操作符具有从右向左的结合性,所以这个表达式相当于*(*c),必须从里向外逐层求值。*c访问c所指向的位置,我们知道这是变量b。第二个间接访问操作符访问这个位置所指向的地址,也就是变量a。
上面的表达式的值各是多少呢?a的值是12,b的值是变量a的地址,c的值是变量b的地址。
*b的值是什么呢? *b作为右值,表示b所指向的地址里面的内容,也就是a,所以 *b=12;

*c的值是什么呢?*c作为右值,表示c所指向的地址里面的内容,也就是b,所以 *c=&a;

**c的值是什么呢?**c作为右值,表示(*c)所指向的地址里面的内容,即 **c= *&b,表示b这个地址里面的内容,也就是a,即 **c=12;

总结如下表

表达式 相当的表达式
a 12
b &a
*b a ,12
c &b
*c b ,&a
**c *b, a, 12

测试代码

#include<iostream>
using namespace std;int main(){int a=12;int *b=&a;int **c=&b;cout<<"a的值是: "<<a<<endl;cout<<"*b的值是:"<<*b<<endl;cout<<endl;cout<<"b的值是: "<<b<<endl;cout<<"*c的值是:"<<*c<<endl;cout<<endl;cout<<"a的地址是:"<<&a<<endl;cout<<"b的值是:  "<<b<<endl;cout<<endl;cout<<"b的地址是:"<<&b<<endl;cout<<"c的值是:  "<<c<<endl;cout<<endl;cout<<"*c的值=b的值:"<<*c<<endl;cout<<"**c的值=a的值:"<<**c<<endl;
}

6.11指针表达式

首先看一些声明

char ch='a';
char *cp=&ch;

现在我们有了两个变量,它们初始化如下

图中还显示了ch后面的那个内存位置,因为我们所求值的有些表达式将访问它(尽管在错误的情况下才会对它进行访问)。由于我们不知道它的初始值,所以用一个问号来代替。

首先来个简单的作为开始,如下面这个表达式

ch

当它作为右值使用时,表达式的值为’a’,如下图所示

这个粗椭圆提示变量ch的值就是表达式的值。但是,当这个表达式作为左值使用时,它是这个内存的地址而不是该地址所包含的值,所以它的图示方式有所不同

此时该位置用粗方框标记,提示这个位置就是表达式的结果。另外,它的值并没有显示,因为它并不重要。事实上,这个值将被某个新值代替。接下来的表达式将以表格的形式出现。每个表的后面是表达式求值过程的描述。

作为右值,这个表达式的值是变量ch的地址。注意这个值同变量cp中所存储的值一样。但这个表达式并未提到cp,所以这个结果值并不是因为它而产生的。 第二个问题是,为什么这个表达式不是一个合法的左值? 优先级表格显示&操作符的结果是个右值,它不能当作左值使用。但是为什么呢? 答案很简单,当表达式&ch进行求值时,它的结果应该存储于计算机的什么地方呢?它肯定会位于某个地方,但你无法知道它位于何处。这个表达式并未标识任何机器内存的特定位置,所以它不是一个合法的左值。

这个表达式前面见到过。它的右值就是cp的值。它的左值就是cp所处的内存位置。由于这个表达式并不进行间接访问操作,所以不必依箭头所示方向进行间接访问。

这个例子与&ch类似,不过这次我们所取的是指针变量的地址。这个结果的类型是指向字符的指针的指针。同样,这个值的存储位置并未清晰定义,所以这个表达式不是一个合法的左值。

现在我们加入了间接访问操作,所以它的结果应该不会令人惊奇。*cp作为右值表示 cp的内容,即‘a’;*cp作为左值表示cp的内容,也就是cp所存的地址。

需要记住的是

前提:cp是一个指针变量
cp作为左值,表示指针变量cp在内存中的位置
*cp作为左值,表示cp的内容,也就是存的地址

下面几个表达式就比较有意思。

这个图涉及的东西更多,所以让我们一步一步研究它。这里有两个操作符。*操作符的优先级高于+,所以首先执行的是间接访问操作(如图中cp到ch的实线箭头所示),我们可以得到它的值(如虚线椭圆所示)。我们取得这个值的一份拷贝并把它与1相加,表达式的最终结果是字符’b’. 图中虚线表示表达式求值时数据的移动过程。这个表达式的最终结果的存储位置并未清晰定义,所以它不是一个合法的左值。优先级表格证实+的结果不能作为左值。

在这个例子中,我们在前面那个表达式中增加了一个括号。这个括号使得表达式先执行加法运算,就是把1和cp中所存储的地址相加。此时的结果值是图中虚线椭圆所示的指针。接下来的间接访问操作随着箭头访问紧随ch之后的内存位置。这样,这个表达式的右值就是这个位置的值,而它的左值就是这个位置本身。

在这里我们需要学习的很重要的一点。注意指针加法运算的结果是个右值,因为它的存储位置并未清晰定义。如果没有间接访问操作,这个表达式将不是一个合法的左值。 然而,间接访问跟随指针访问一个特定的位置。这样*(cp+1)就可以作为左值使用,尽管cp+1本身并不是左值。间接访问操作符是少数几个其结果为左值的操作符之一。

但是,这个表达式所访问的是ch后面的那个内存位置,我们如何知道原先存储于那个地方的是什么东西?一般而言,我们无法得知,所以像这样的表达式是非法的。

++和- -操作符在指针变量中使用的相当频繁,所致在这总上下文环境中理解它们是非常重要的。在这个表达式中,我们增加了指针变量cp的值。(为了让图更清楚,我们省略了加法)。表达式的结果是增值后的指针的一份拷贝,因为前缀++先增加它的操作数的值再返回这个结果。这份拷贝的存储位置并未清晰定义,所以它不是一个合法的左值。

后缀++操作符同样增加cp的值,但它先返回cp值的一份拷贝然后再增加cp的值。这样,这个表达式的值就是cp原来的值的一份拷贝。

前面两个表达式的值都不是合法的左值。但如果我们在表达式中增加了间接访问操作符,它们就可以成为合法的左值,如下图的两个表达式所示。

这里,间接访问操作符作用域增值后的指针的拷贝上,所以的它的右值是ch后面那个内存地址的值,而它的左值就是那个位置本身。

下面这个例子很重要。

⭐*cp++

使用后缀++操作符所产生的结果不同: 它的右值和左值分别是ch的值和ch的内存位置,也就是cp原先所指\color{red}{它的右值和左值分别是ch的值和ch的内存位置,也就是cp原先所指}它的右值和左值分别是ch的值和ch的内存位置,也就是cp原先所指。同样,后缀++操作符在周围的表达式中使用其原先操作数的值。间接访问操作符和后缀++操作符的组合常常令人误解。 优先级表格显示后缀++操作符的优先级高于*操作符,但表达式的结果看上去像是先执行间接访问操作,实际上不是\color{red}{但表达式的结果看上去像是先执行间接访问操作,实际上不是}但表达式的结果看上去像是先执行间接访问操作,实际上不是。事实上,这里涉及三个步骤:
(1)++操作符产生cp的一份拷贝
(2)然后++操作符增加cp的值
(3)最后在cp的拷贝上执行间接访问操作。

后缀表达式cp++:先返回cp的值的一份拷贝,然后再增加cp的值。这样cp++的值就是cp原来的值的一份拷贝。

这个表达式常常在循环中出现,首先用一个数组的地址初始化指针,然后使用这种表达式就可以依次访问该数组的内容。


在这个表达式中,由于这两个操作符的结合性都是自右向左,所以首先执行的是间接访问操作。然后,cp所指向的位置的值加1(由‘a’变成‘b’),表达式的结果是这个增值后的值的一份拷贝。

和前面一些表达式相比,最后3个表达式在实际应用中使用的较少。但是,对它们有一个透彻的理解有助于提高你的技能。

使用后缀++操作符,我们必须加上括号,使它首先执行间接访问操作。这个表达式的执行结果和前一个表达式相似,但它的结果值是ch增值前的原先值。

这个表达式看上去相当诡异,但事实上并不复杂。这个表达式共有3个操作符,这些操作符的结合性都是从右向左的,所以首先执行的是++cp。cp下面的虚椭圆表示第一个中间结果。接着,我们对这个拷贝值进行间接访问,它使我们访问ch后面那个内存位置。第二个中间结果用虚线方框表示,因为下一个操作符把它当作一个左值使用。最后,我们在这个位置执行++操作,也就是增加它的值。我们之所以把结果值显示为?+1是因为我们并不知道这个位置原先的值。


这个表达式和前一个表达式的区别在于这次第一个++操作符是后缀形式而不是前缀形式。由于它的优先级较高,所以先执行它。 间接访问操作所访问的是cp所指向的位置,而不是cp所指向那个位置后面那个位置。

解释: 对于后面的*cp++,先执行++操作符产生cp的一份拷贝,然后++操作符增加cp的值(cp现在指向下一个位置),最后,在cp的拷贝上(原位置)执行间接访问操作,所以 ,作为右值得到的是ch里面的值‘a’。然后’a’执行前缀++,得到的是‘b’。 只不过此时cp已经指向下一个位置。

6.12 实例

用法举例:字符串长度函数strlen

#include<stdlib.h>
#include<iostream>
using namespace std;size_t
strlen(char *string){int length=0;while(*string++ != '\0'){ //(此处作为右值)取值,并++//再复习一下*string++的执行过程// 1.++操作符生成string 的一个拷贝//2.++操作符增加string的值(新值)//3在string的拷贝(原先值)上执行间接访问length+=1;}return length;
} int main(){char a[10]={1,2,4};cout <<strlen(a)<<endl;
}

在指针到达字符串末尾的NUL字节之前,while语句中*string++表达式的值一直为真。它同时增加指针的值,用于下一次测试。这个表达式甚至可以正确地处理空字符串。

警告:
如果这个函数调用时传递给它的是一个NULL指针,那么while语句中的间接访问将会失败。函数是不是应该在解引用指针前检查这个条件? 从绝对安全的角度来看,应该如此。但是,这个函数并不负责创建字符串。如果它发现参数为NULL,它肯定发现了一个出现在程序其他地方的错误。当指针创建时检查它有效是符合逻辑的,移位这样只需要检查一次。这个函数采用的就是这种方法。如果函数失败是因为粗心大意的调用者懒得检查参数的有效性而引起的,那是他活该如此。

程序6.2和6.3增加了一层间接访问。它们在一些字符串中搜索某个特定的字符值,但我们使用指针数组来表示这些字符串,如图6.1所示。


函数的参数是strings和value,strings是一个指向指针数组的指针,value是我们所查找的字符值。注意指针数组以一个NULL指针结束。函数将检查这个值来判断循环何时结束。下面这行表达式

while( (string = *strings++ )  != NULL) {

完成3项任务:
(1)它把strings当前所指向的指针复制到变量string中
(2)它增加strings的值,使它指向下一个值
(3)它测试string是否是NULL。当string指向当前字符串中作为终止标志的NUL字节时,内层的while循环就终止。

程序6.2:在一组字符串中查找:版本1

// 给定一个指向以NULL结尾的指针列表的指针,在列表中的字符串中查找一个特定的字符#include<stdio.h>#define TRUE 1
#define FALSE 0int
find_char(char **strings , char value){char *string; //我们当前正在查找的字符串//对于列表中的每个字符串while( ( string = *strings++ )  != NULL){//观察字符串中的每个字符,看看它是不是我们需要查找的那个while( *string != '\0'){if( *string ++ == value)return TRUE;}}return FALSE;}

如果string尚未到达其结尾的NUL字节,就执行下面这条语句

if(  *string ++ == value)

它测试当前的字符是否与需要查找的字符匹配,然后增加指针的值,使它指向下一个字符。

程序6.3实现相同的功能,但它不需要对指向每个字符串的指针做一份拷贝。但是,由于存在副作用,这个程序将破坏这个指针数组。这个副作用使得该函数不如前面那个版本有用,因为它只适用于字符串只需要查找一次的情况。

程序6.3 在一组字符串中查找:版本2

#include<stdio.h>
#include<assert.h>#define TRUE 1
#define FALSE 0int
find_char  (char ** strings ,int value){assert( strings!=NULL);//对列表中的每个字符串while(  *strings !=NULL){while(  ** strings !='\0'){if(  *( *strings )++ ==value)  return TRUE;}strings++;}return FALSE;
}

但是,在程序6.3中有两个有趣的表达式。第一个是 **strings。第1个间接访问操作访问指针数组中的当前指针,第2个间接访问操作随该指针访问字符串中的当前字符。内层的while语句测试这个字符的值并观察是否到达了字符串的末尾。

第二个有趣的表达式是*(*strings)++.括号是需要的,这样才能使表达式以正确的顺序进行求值。 第一个间接访问操作访问列表中的当前指针。增值操作把该指针所指向的那个位置的值加1,但第二个间接访问操作作用于原先那个值的拷贝上。这个表达式的直接作用是对当前字符串中的当前字符进行测试,看看是否到达了字符串的末尾。作为副作用,指向当前字符串字符的指针值将增加1.

6.13 指针运算

指针加上一个整数的结果是另一个指针。问题是,它指向哪里?如果你将一个字符指针+1,运算结果产生的指针指向内存中的下一个字符。float占据的内存空间不止1个字节,如果你将一个指向float的指针加1,将会发生什么呢? 它会不会指向该float值内部的某个字节呢?

幸运的是,答案是否定的。当一个指针和一个整数量执行算术运算时,整数在执行加法运算前始终会根据合适的大小进行调整。这个“合适的大小”就是指针所指向类型的大小,“调整”就是把整数值和“合适的大小”相乘。 为了更好地说明,试想在某台机器上,float占据4个字节。在计算float型指针加3的表达式时,这个3根据float类型的大小(此例中为4)进行调整(相乘)。这样,实际加到指针上的整型值为12.

把3与指针相加使指针的值增加3个float的大小,而不是3个字节。 这个行为较之获得一个指向一个float值内部某个位置的指针更为合理。下图中有一些加法运算的例子。调整的美感在于指针算法并不依赖于指针的类型。换句话说,如果p是一个指向char的指针,那么表达式p+1就指向下一个char。如果p是个指向float的指针,那么p+1就指向下一个float,其他类型也是如此。

6.13.1 算术运算

C的指针运算只限于两种形式。

第一种形式是 指针 ± 整数

标准定义这种形式只能用于指向数组中某个元素的指针,如下图所示。

并且这类表达式的结果类型也是指针。这种形式也适用于使用malloc函数动态分配获得的内存。

对指针执行加法或减法运算之后如果结果指针所指的位置在数组的第1个元素的前面或在数组最后一个元素的后面,那么其效果就是未定义的。 让指针指向数组最后一个元素后面的那个位置是合法的,但对这个指针执行间接访问可能会失败。

是该举个例子的时候了。 这里有个循环,把数组中所有的元素都初始化为0.

#define N_VALUES  5
float values[N_VALUES];
float *vp;for(vp=&values[0];vp<&values[N_VALUES];)*vp++=0;

for语句的初始部分把vp指向数组的第一个元素。

这个例子中的指针运算是用++操作符完成的。 增加值1与float长度相乘,其结果加到指针vp上。经过第1次循环之后,指针在内存中的位置如下:

经过5次循环之后,vp就指向数组最后一个元素后面的那个内存位置

此时循环终止。由于下标从零开始,所以具有5个元素的数组的最后一个元素的下标值为4.这样,&values[N_VALUES] 表示数组最后一个元素后面那个内存位置的地址。当vp到达这个值时,我们就知道到达了数组的末尾,故循环终止。

第2种类型的指针运算具有如下形式:指针-指针

只有当两个指针都指向同一个数组中的元素时,才允许从一个指针减去另一个指针,如下所示


两个指针相减的结果类型是ptrdiff_t,它是一种有符号整数类型。减法运算的值是两个指针在内存中的距离(以数组元素的长度为单位,而不是以字节为单位),因为减法运算的结果将除以数组元素类型的长度。 例如,如果p1指向array[i] 而 p2指向 array[j], 那么p2-p1的值就是j-i的值。

让我们看一下它是如何作用于某个特定类型的。假定前图中数据元素 的类型为float,每个元素占据4个字节的内存空间。如果数组的起始位置为1000,p1的值是1004,p2的值是1024,但表达式p2-p1的值将是5,因为两个指针的差值(20)将除以每个元素的长度(4)。

同样,这种对差值的调整使指针的运算结果与数据的类型无关。不论数组包含的元素类型如何,这个指针减法运算的值总是5.

那么,表达式p1-p2是否合法呢? 是的,如果两个指针都指向同一个数组中的元素,这个表达式就是合法的。在前一个例子中,这个值将是-5.

如果两个指针所指向的不是同一个数组中的元素,那么它们之间相减的结果是未定义的。

警告:
实际上,绝大多数编译器都不会检查指针表达式的结果是否位于合法的边界之内。因此,程序员应该负起责任,确保这一点。越界指针和指向未知值的指针是两个常见的错误根源。

6.13.2 关系运算

对指针执行关系运算也是有限制的。用下列关系操作符对两个指针值进行比较是可能的:

< <=  > >=

不过前提是它们都是指向同一个数组中的元素。 根据你所使用的操作符,比较表达式将告诉你哪个指针指向数组中更前或更后的元素。 标准并未定义如果两个任意的指针进行比较会产生什么结果。

然而,你可以在两个任意的指针间执行相等或不相等的测试,因为这类比较的结果和编译器闲则在何处存储数据并无关系—指针要不指向同一个地址,要不指向不同的地址。

让我们再观察一个循环。它用于清楚一个数组中所有的元素。

#define N_VALUES  5
float values[N_VALUES];
float *vp;for(vp=&values[0];vp<&values[N_VALUES];)*vp++=0;

for语句使用了一个关系测试来决定是否结束循环。这个测试是合法的,因为vp和指针变量都指向同意数组中的元素。

现在考虑下面这个循环

for( vp =&values[N_VALUES];vp>&values[0];)*--vp=0;

它和前面那个循环所执行的任务相同,但数组元素将以相反的顺序清除。我们让vp指向数组元素最后那个元素后面的内存位置,但对它进行间接访问之前先执行自减操作。当vp指向数组第一个元素时,循环便告结束,不过这发生在第一个元素被清除之后。

有些人可能反对像*–vp这样的表达式,觉得它的可读性差。但是,如果对其简化,看看这个循环会发生什么:

for( vp = &values[N_VALUES-1];vp>= &values[0];vp--)*vp=0;

现在vp指向数组最后一个元素,它的自减操作放在for语句的调整部分进行。这个循环存在一个问题,你能发现它吗?

警告:
在数组第一个元素被清除之后,vp的值还将减去1,而接下去的一次比较运算是用于结束循环的。但这就是问题所在:比较表达式 vp >= &values[0] 的值是未定义的,因为vp移到了数组的边界之外。标准允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针进行比较,但不允许与指向数组第一个元素之前的那个内存位置的指针进行比较。

实际上,在绝大多数C编译器中,这个循环将顺利完成任务。然而,你还是应该避免使用它,因为标准并不保证它可行。你迟早可能遇到一台这个循环将失败的机器。对于负责可以指代码的程序员而言,这类问题简直是噩梦。

6.14 警告的总结

1 错误地对一个未初始化的指针变量进行解引用。
2 错误地对一个NULL指针进行解引用
3 向函数错误地传递NULL指针。

4 未检测到指针表达式的错误,从而导致不可预料的错误

5 对一个指针进行加减运算,使它非法指向了数组第一个元素的前面的内存位置。

6.15 编程提示的总结

1 一个值应该只具有一个意思.
2 如果指针并不指向任何有意义的东西,就把它设置为NULL.

《C和指针》读书笔记-第六章指针相关推荐

  1. 《Python从入门到实践》读书笔记——第六章 字典

    <Python从入门到实践>读书笔记--第六章 字典 1. 一个简单的字典 alien_0 = {'color': 'green', 'points': 5}print(alien_0[' ...

  2. 深入理解 C 指针阅读笔记 -- 第六章

    Chapter6.h #ifndef __CHAPTER_6_ #define __CHAPTER_6_/*<深入理解C指针>学习笔记 -- 第六章*/typedef struct __p ...

  3. 《C++ Primer》读书笔记—第六章 函数

    声明: 文中内容收集整理自<C++ Primer 中文版 (第5版)>,版权归原书所有. 学习一门程序设计语言最好的方法就是练习编程 一.函数基础 1.一个典型的函数定义包括以下内容:返回 ...

  4. 《Microsoft Sql server 2008 Internals》读书笔记--第六章Indexes:Internals and Management(3)

    <Microsoft Sql server 2008 Internals>读书笔记订阅地址: http://www.cnblogs.com/downmoon/category/230397 ...

  5. Entity Framework 4 in Action读书笔记——第六章:理解实体的生命周期(三)

    objectstatemanager更改跟踪管理 ObjectStateManager组件(从现在开始称之为 state manager)负责与上下中对象追踪有关的一切: 1.当添加,附加到上下文或者 ...

  6. 财务自由之路 读书笔记 第六章 债务

    第六章 债务 25 绝不要用短期方法解决长期问题 ​ -丹尼尔·s<交易与收益> "赢家一生只做头等舱". 第一节 坏债是如何产生的 好债和坏债  个人认为,使用消费贷 ...

  7. 强化学习(RLAI)读书笔记第六章差分学习(TD-learning)

    第六章:Temporal-Difference Learning TD-learning算法是强化学习中一个独具特色而又核心的想法,结合了蒙特卡洛算法和动态规划的想法.和MC一样不需要环境模型直接从s ...

  8. [swift 进阶]读书笔记-第六章:函数 C6P1函数的灵活性(The flexibility of function)...

    第六章:函数(function) 6.1 函数的灵活性(The flexibility of function) 注:本节前部分主要通过一个排序的demo来介绍了函数的灵活性 话不多说,直接上代码 v ...

  9. c语言指针读书笔记,《C与指针》读书笔记一

    我平时不太看书.倒不是我没有读书的习惯.而是如今的社会知识传播的方式太多.书已经不是唯一知识的载体.至于"书是人类知识的阶梯"这句名言的时代已经过去了.每天各种微信公众号推介的文章 ...

最新文章

  1. html固定表的属性是什么,css如何固定表头
  2. 研究生导师一般希望招什么样的研究生?
  3. iRobot 公司招聘,机器人、SLAM、视觉感知、路径规划方向
  4. 图片旋转,拖拽,缩放,删除一体
  5. nginx源码编译安装及使用
  6. 直流稳压稳流电源基本功能,电源使用注意事项
  7. 解决xshell中数字小键盘不能使用的问题
  8. 一篇了解TrustZone
  9. 原创 | 从土地财政到数据财政
  10. 状压DP例题(种花小游戏+广场铺砖)
  11. 新浪微博PC端登陆js分析及Python实现微博post登陆
  12. python高逼格动态图_微信编辑哪里找高逼格 GIF 动图?
  13. TCP/UDP/IP/Socket的定义
  14. 使用etop工具监测Erlang运行环境
  15. 基于C++实现两个分数的加减法
  16. frida-ios-dump实现iOS应用砸壳
  17. 阿里:为了不死 只能求生 淘点点肩负重任
  18. stm32 arduino 驱动jlx液晶屏
  19. Win10系统自带的输入法,输入显示拼音按空格键才出中文
  20. 博德之门2增强版存档_《博德之门2:增强版》存档位置及修改方法

热门文章

  1. itools下载链接被360警告:虚假招聘网站
  2. 测试集的构成比例对网络分类性能的影响cp
  3. scipy是python下的什么_python – cholesky在numpy和scipy之间有什么区别?
  4. sharp扫地机器人讲话_扫地机机器人,智能扫地机器人推荐
  5. STM32 进阶教程 2 - micropython 使用
  6. STM32 基础系列教程 12 – ADC 中断
  7. Android Log等级的介绍
  8. 【PC工具】网站服务器端口检测工具,网络端口扫描工具,win10telnet安装方法
  9. 通过MATLAB读取mnist数据库
  10. Gradle 使用技巧(一)