《C与指针》读书笔记
1.内存与地址
在计算机中,最小的存储单位是一个字节,一个字节由八个bit组成,可以存储无符号值0到255。在计算机内存中每一个字节都用一个地址来标记它的位置。如下图所示标记一段内存,从100到107八个字节。
图1
为了存储更大的值,我们把两个或更多的字节合在一起作为一个更大的内存单位。下图的一个存储单位大小为4个字节。可以容纳无符号整数的范围是从0到2^32-1,可以容纳的有符号整数的范围是从-2^31至2^31-1。
注意,尽管一个字包含4个字节,它仍然只有一个地址。至于他它的地址是它的最左边那个字节的位置还是最右边那个字节的位置,不同机器有不同的规定。另外,大部分硬件都有边界对齐。在要求边界对齐的机器上,整形值的存储位置的起始位置只能是某些特定的字节。通常是2或者4的倍数。
图2
地址与内容
下图是内存中五个字以及每个字的内容。
图3
通常我们不是通过地址来访问某个位置,而是通过名字来访问。图4使用名字来代替图3中的地址。名字与内存位置之间的关联是由编译器为我们实现的。
图4
2.值与类型
关于内存中存储的值,取决于这块内存如何别解释。
3.指针变量的内容
先看看下面这段代码
int a = 112,b = -1;
float c = 3.14;
int *d = &a;
float *e = &c;
其中d和e都被声明为指针,并用其他变量的地址予以初始化。d和e的内容是地址而不是整形或者浮点型。d存储了a的地址,e存储了c的地址。
图5
4.间接访问操作符
通过一个指针访问它所指向的地址的过程称为间接访问或解引用指针。这个用于执行间接访问的操作符是单目操作符*。
d的值是100.当我们对d使用间接访问操作符时,它表示访问内存位置100并且查看那里的值,即是112。因此,*d的右值是112。它的左值是还是d本身。
图6
5.未初始化和非法的指针
下面这段代码说明一个极为常见的错误:
int *a;
*a = 12;
这个声明创建了一个名叫a的指针变量,后面那条赋值语句把12存储在a所指向的内存位置。
但是由于没有对a进行初始化,a指向哪里并未定义。所以我们没有办法预测12这个值将存储在什么地方。如果变量a是静态的,它会被初始化为0;如果是自动的,它根本不会被初始化。当程序执行到这个赋值操作的时候,a的初始值可能会是个非法地址,程序终止。或者是个合法地址,位于此地址的值会被修改。者可能比程序终止更严重,因为像这样的错误更难以捕捉。
6.NULL指针
标准定义了NULL指针,它作为一个特殊的指针变量,表示不指向任何东西。
7.指针、间接访问和左值
给出下列声明:
int a;
int *d = &a;
考虑下面的表达式:
表达式 | 左值 | 指定位置 |
a | 是 | a |
d | 是 | d |
*d | 是 | a |
指针变量可以作为左值,并不是因为它们是指针,而是因为它们是变量。对指针进行间接访问表示访问指针所指向的位置。
8.指针的指针
考虑下面声明:
int a = 12;
int *b = &a;
它们的内存分配如下图:
图7
假定我们有第3个变量,名叫c,并用下面这条语句对它进行初始化
c = &b;
它们的内存分配大概如下图:
图8
c是指针的指针,它指向一个“指向整形的指针”。它的类型是int**,它像下面这样声明。
int **c;
像下面这样初始化
int **c = &b;
9.指针表达式
这部分将给出一些指针表达式,并对它们作为左值和右值时进行求值。
首先,我们来看一些声明。
char ch = 'a';
char *cp = &ch;
它们在内存中的表示如下:
图9
分析下面的表达式
ch
当它作为右值使用时,表达式的值是'a'。如下图。粗椭圆表示ch的值。
图10
当作为左值时,表示一个位置,使用粗矩形标识。
图11
下面使用表格对各个表达式进行左值和右值求值。
作为右值,这个表达式的值是变量ch的地址,它跟变量cp中存储的值是一样的,由于这个表达式没有提及cp,所以把它们分开来。当&ch作为左值时,我们并不知道&ch存在什么地方,它并不能标识机器内存的特定位置。所以它不是一个合法的左值。
cp作为右值,它的值就是变量ch的地址。cp作为左值就是cp自身。
&cp作为右值,它的值是变量cp的地址。作为左值是非法的。
*cp作为右值,对cp进行间接访问操作,它的值是变量ch的值。*cp作为左值,它的值就是变量ch所处的位置。
*cp + 1作为右值,先对*cp求值,得到ch的值,然后再+1得到b。作为左值非法。
*(cp + 1)作为右值,先对cp + 1求值,得到变量ch的下一个位置,再进行间接访问操作,得到变量ch下一个位置的值。作为左值,它标识了变量ch的下一个位置。
++cp作为右值,cp本来指向ch,经过前缀++操作之后,它指向ch的下一个位置。作为左值非法。
cp++作为右值,cp本来指向ch,经过后缀++操作之后,先返回cp的拷贝,再将值+1。左值非法。
*cp++作为右值,cp本来指向ch,经过前缀++操作之后指向ch的下一个位置,进行间接访问将得到ch下一位置的存储的值。作为左值,标识了ch的下一个位置。
*cp++作为右值,cp本来指向ch,经过后缀++操作之后,先返回cp的拷贝,这时对cp进行间接访问,得到的依然是ch的值。之后cp指向ch的下一个位置。作为左值,它标识了ch的位置。在这之后,它指向ch的下一个位置。
++*cp作为右值,*cp得到ch的的值'a',对值进行前缀++操作,得到'b'。但是ch的值也被修改为b。
由于++*cp 相当于 *cp = *cp + 1; 很明显cp指向的位置ch的值已经被修改。
(*cp)++的右值,*cp得到ch的值'a',先返回该值,在对值进行+1,得到b。此时ch的值被修改为'b'。
由于这些操作的结合性都是从右到左的,所以首先执行++cp。cp下面的虚线椭圆表示第一个中间结果。接着我们对这个拷贝值进行间接访问,它使我们访问到ch后面的那个内存位置。第二个中间结果用虚线方框表示。因为下一个操作符把它当作一个左值使用。最后我们在这个位置执行++操作。也就是增加它的值。
这个表达式和前一个表达式的区别在与这次第一个操作是后缀++。间接访问位置是cp指向的位置而不是cp所指向位置的下一个位置。
10.指针运算
10.1 算术运算
(1)指针 +- 整数
标准定义这种形式只能用于指向数组中的某个元素的指针。并且这类表达式的结果类型也是指针。
对指针执行加法或减法运算之后如果结果指针指向位置再数组第一个元素的前面,或者在数组最后一个元素的后面,那么其效果就是未定义的。让指针指向数组最后一个元素的后面那个位置是合法的,但是对这个指针进行间接访问可能会失败。
(2)指针 - 指针
只有当两个指针都指向同一个数组中的元素时,才允许从一个指针减去另一个指针。结果的类型是ptrdiff_t,它是一种有符号整数类型,表示两个指针在内存中的距离。
10.2 关系运算
当两个指针指向同一数组元素时,可以对指针执行关系运算:< 、<= 、 > 、 >=。