【《C和指针》阅读笔记】Chapter6 Pointers
文章目录
-
- 值和类型
关于指针变量的细节及其相关操作中的一些关键点如下:间接访问操作符通常用于执行非直接的操作,在编程逻辑中扮演着重要角色。未赋值或非法的指向器可能导致运行时错误或不稳定的行为。无值指向器在内存管理和错误检测中具有特殊的意义。此外,在处理数据结构时需要特别注意双重指向器以及其潜在的影响。这些概念构成了编程中的核心知识点,并且在实际应用中经常被深入探讨或演示。
- 指针运算
-
- 算数运算
- 关系运算
值和类型
下面有一个例子,它显示了内存中的5个字的内容。


下面是这些变量的声明:
int a = 112, b = -1;
float c = 3.14;
int *d = &a;
float *e = &c;
- c声明的数据类型是浮点型;然而,在图表中c却被表示为整数值。这是因为该变量的一系列位要么是0要么是1;这些位既可以被解读为整数值;也可以被视为浮点数值;具体取决于它们如何被使用。
- 不要仅仅依靠检查一个值的二进制表示来确定其数据类型。
指针变量的内容
指针的赋值使用&操作符完成,在程序运行时它决定了操作数在内存中的具体位置。在上图所定义的五个字中,d和e的内容代表的是内存地址而非整型或浮点数值。实际上,在内存布局中,d的位置与a完全相同,同样地,e的位置则与c完全相同。
间接访问操作符
利用一个变量获取该变量所指向内存地址被称为 indirect addressing 或 dereferencing。这个用于实现 indirect addressing 的操作符是单目运算符 * 。
| 表达式 | 右值 | 类型 |
|---|---|---|
| a | 112 | int |
| b | -1 | int |
| c | 3.14 | float |
| d | 100 | int * |
| e | 108 | float * |
| *d | 112 | int |
| *e | 3.14 | float |
其中变量d所在的内存地址为数值常量一百。当我们通过间接访问操作符来读取变量d的位置时,它表示通过间接访问操作符读取内存地址一百处的数据。此时*d的右操作数指向的是内存地址一百一十二——即位置一百的内容。其左操作数指向的是内存地址一百本身。
未初始化和非法的指针
下面这段代码段说明了一个极为常见的错误:
int *a;
...
*a = 12;
- 该声明定义了一个名为a的指针变量。
- 这条赋值语句将数值12存放在由a指向的内存地址上。
第二段: - 那么我们具体了解a会指向哪里吗?
- 我们声明了该变量
- 但从未对它进行初始化操作
- 因此无法预判数值12将在何处被存入内存
- 声明一个指向整型的指针都不会‘创建’用于存储整型值的内存空间。
NULL指针
根据标准定义,在编程中...是一个特殊的指针类型,在这种情况下...通常被用作指示符...其作用域为空或无目标对象。在多数编程语言中...要使某个特定的数值(如整数)表示无目标状态,则可以通过赋予该数值一个零值来实现这一点。通过赋予特定的数值来表示无目标状态的方法已经被广泛接受,并且这种做法在现代编程语言中已经得到了统一的标准规范。为了判断某个特定的数值是否符合无目标状态时的状态,则可以通过将该数值与零值进行比较的方式来实现这一目的。判断某个特定的数值是否符合无目标状态时的状态这一过程已经被广泛接受,并且这种做法在现代编程语言中已经得到了统一的标准规范。
- 因为NULL指针没有被赋值到任何对象中, 这就意味着对一个NULL指针执行解引用操作是不允许的。
- 在执行解引用操作之前, 请确保该变量不是NULL指针。
指针、间接访问和左值
虽然一个指针变量能够充当左侧操作数,并非由于它具有"pointer"属性本身所导致的原因,则是因为它是一个普通的标识符而已。为了实现对一个'pointer'对象进行'indirect' access操作,则必须获取该'pointer'所指向的内存地址内容。一旦我们为一个特定的内存地址进行了标记(即执行了一次 indirect access操作),则返回值可被用作左侧操作数使用,在接下来几行代码中
int a;
int *d = &a;
*d = 10 - *d;
d = 10 - *d;
- 第一条涉及两种间接访问操作,在程序执行过程中会产生特定的效果。
在右侧的操作符中进行的是右值引用,在这种情况下其实际取到的是被d指向的对象存储区中的数据。
左侧的操作符则会执行左值引用,在这种情况下它会将右侧表达式的计算结果赋给当前d所指向的对象存储区。 - 第二个语句违反了程序设计语言的基本语法规范,
因为它试图将一个整型数值(如10减去*d的结果)赋给一个指向器变量,
这种行为在大多数编程语言中都是严格禁止的。
指针、间接访问和变量
*&a = 25; //把值25赋值给变量a
首先,在程序中使用按位取址运算符&将能够获取到变量a所占用的具体内存地址位置。这个返回值将是一个固定的指针常量(其具有固定指向的位置信息)。随后,在程序执行过程中:
- 首先会通过*运算符来访问该固定位置所对应的内存内容。
在本表达式的运算过程中: - 被取址的对象即为变量a自身所占据的具体内存位置。
因此,在该运算完成后: - 变量a的内容将会被赋值为25(即在该运算后变量a的内容被赋值为25)。
指针常量
*100 = 25;
由于字面值100属于整型数据类型,在进行间接访问操作时只能作用于带有指针类型的表达式。因此,在目标地址位置100无法直接存储数值25的情况下,则必须通过显式类型转换操作将数值转换为目标地址所在的存储区域。
(int *)100 = 25;
指针的指针
考虑下面这样的声明:
int a = 12;
int *b = &a;
c = &b;
- 变量b被定义为一个指向整型数据类型的指针变量。因此,在程序中定义任何新变量来指向变量b时,则这些新变量必须是双层指针类型——即它们本身也是一个指向整型指针类型的指针。这种双重结构在编程逻辑中是完全合法且等价的操作方式。与普通变量相同,这些双层针对应占用内存空间中的特定区域,并且通过使用&运算符可以获取其所在内存地址也是有效的操作。
宣布如下声明:
int **c;
- 表示表达式**c的类型是int。
实例
程序1
程序1计算一个字符串的长度。
/* ** 计算一个字符串的长度
*/
#include <stdlib.h>
size_t
strlen( char *string )
{
int length = 0;
/* ** 依次访问字符串的内容,计数字符数,知道遇见NUL终止符。
*/
while( *string++ != '\0')
length += 1;
return length;
}
当指针在遇到字符串末尾的NUL字节之前时,在while循环中使用*string++作为条件时,在每次循环迭代过程中都会对该条件进行验证。该循环也会随之将指针指向下一个字符的位置。即使输入字符串为空的情况下,“*string++”这一表达式仍然能够正常运行并返回正确的结果。
程序2
程序2及程序3增加了对数据的一层间接访问。它们用于在特定字符串中查找特定字符,并采用指向器数组来表示这些字符串(如图所示)。该函数接受两个参数:一个指向器数组strings和一个目标字符value。请注意,在此结构中每个元素都用NULL终止。该函数将通过检查指定的位置来确定循环终止条件。

while( ( string = *strings++ ) != NULL ){
- 上面这行表达式实现了以下三项功能:
- 将该行代码实现了以下三个主要功能:第一是将current_strings中的指针赋值给新的字符串变量;第二是通过自增操作使得current_strings指针移动至下一个字符位置;第三是对新字符串进行有效性检查。
- 当且仅当current_strings不为空并且其最后一个字符为NUL字节时,
内部循环就会停止执行。
/* ** 给定一个指向以NULL结尾的指针列表的指针,在列表中的字符串中查找一个特定的字符。
*/
#include <stdio.h>
#define TRUE 1
#define FALSE 0
int
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 )
It evaluates whether the current character matches the target character, then increments the pointer to move to the next character.
程序3
程序3采用了与程序2相同的功能模块,并且无需复制指向每个字符串的指针。然而,由于存在潜在的问题可能会导致该指针数组被破坏,在这种情况下该函数的实际应用效果将受到影响,并不如之前的版本那样高效可靠。这些潜在的问题使得该函数在适用性上不及之前的版本,并非适用于所有场景下的使用需求。
/* ** 给定一个指向以NULL结尾的指针列表的指针,在列表中的字符串中查找一个特定的字符。这个函数将破坏这些指针,所以它只适用于这组字符串只使用一次的情况。
*/
#include <stdio.h>
#include <assert.h>
#define TRUE 1
#define FALSE 0
int
find_char( char **strings, char value )
{
// 若string == NULL,则终止程序
assert( strings != NULL );
// 对于列表中的每个字符串...
while( *strings != NULL ){
// 观察字符串中每个字符,看看它是否是我们查找的那个。
while( **strings != '\0' ){
if( *(*strings)++ == value )
return TRUE;
}
strings++;
}
return FALSE;
}
- 程序3中存在两个有趣的表达式。第1个是
**strings。第1个间接访问操作访问指针数组中的当前指针,第2个间接访问操作随该指针访问字符串中的当前字符。内层的while语句测试这个字符的值并观察是否到达了字符串的末尾。- 第2个有趣的表达式是
*(*strings)++。括号是需要的,这样才能使表达式以正确的顺序进行求值。第1个间接访问操作访问列表中的当前指针。增值操作把该指针所指向的那个位置的值加1,但第2个间接访问操作作用于原先那个值的拷贝上。这个表达式的直接作用是对当前字符串中的当前字符进行测试,看看是否到达了字符串的末尾。作为副作用,指向当前字符串字符的指针值将增加1。
- 第2个有趣的表达式是
指针运算
当将一个整数与某变量地址进行算术运算时,
那么它的指向位置如何?
如果对字符指针执行加一操作,
其结果得到的新指针对应的位置会移动到下一个连续的位置。
对于浮点型变量来说,
由于它们占用更多的存储空间,
当对浮点型变量所对应的地址进行加一操作时,
这会产生什么后果呢?
值得庆幸的是,
在计算中涉及一个整数值与某个变量地址进行算术运算时,
系统会在处理这类算术操作时自动考虑变量类型所占存储空间的大小。
其中这个‘合适范围’或‘合适大小’指的是
该变量类型所能占用的实际存储空间。
而具体来说,
当你在一个结构体对象实例上引用成员函数并调用该成员函数的时候,
编译器会生成相应的机器指令序列。
算数运算
C的指针算数运算只限于两种形式。
第1种形式是:
指针 ± 整数
- 标准定义这种形式仅限于指向数组中某个特定元素的指针,并且这类表达式的返回类型同样是所指向的对象。
- 数组中的各个元素按照顺序排列在连续的内存空间中;每一个后续元素的内存地址都比前一个高。因此,在给一个指针变量加上数值1后使其指向当前元素之后的那个位置;相反地减去数值1则使其指向当前元素之前的那个位置。
- 对任意一个指针变量执行加法或减法运算后如果计算后的位置落在数字序列的第一个前趋位置之前或者超出该序列的最后一项之后,则该操作的效果将被视为未定义的行为。
#define N_VALUES 5
flaot values[N_VALUES];
float *vp;
for( vp = &value[0]; vp < &value[N_VALUES]; )
*vp++ = 0;
在这个例子中,最终指向的是数组末尾元素之后的那个内存区域.该指针可能会合法地获取到这个值,然而在对其进行间接访问时可能会意外地读取到原本位于该位置的变量.因此,在大多数情况下都不建议对指向该位置的指针进行间接操作.其形式表现为:
- 只有当两个指针指向同一个数组中的同一元素时
- 两个指针相减得到的结果是ptrdiff_t类型的一种有符号整数
- 减法运算的具体结果代表的是这两个指针在内存中所处的位置之间的距离
- 这种距离是以每个数组元素所占用字节为单位来衡量的
- 具体来说,在内存中这两个指针之间的距离是以每个数组元素所占用字节为单位计算出来的
- 例如:如果p1指向array[i]而p2指向array[j]的话
- 两个指针相减得到的结果是ptrdiff_t类型的一种有符号整数
关系运算
对指针执行关系运算同样有一定的限制。使用下列关系操作符来比较两个指针值是可行的:
< <= > >=
不过前提是它们都指向同一个数组中的元素。
- 通过所选的操作符进行比较运算后得出的结果会指示出哪一个指针指向数组中的前一个或后一个元素。
- 在任意两个指针之间都可以执行相等性测试或者不相等性测试——这类操作的结果与编译器在存储数据时所做的选择无关——换句话说,在这种情况下这两个指针要么指向同一个内存地址要么指向不同的内存地址。
