2018-2019-1 20189221 《从问题到程序》第 6 周学习总结
第七章 指针
7.1 地址与指针
许多高级语言把程序对象(如变量)的地址作为一种可处理数据,称为地址值或指针值,以地址为值的变量称为指针变量,简称指针(pointer)。我们知道,机器语言层对各种对象的操作都要通过地址。指针变量里保存程序对象的地址,通过它们就可以访问和处理有关对象。高级语言里的指针是访问程序对象的手段,以便能更灵活方便地实施操作。
对指针变量的操作包括:
-
指针赋值
将程序对象的地址(如变量地址,还有其他情况。为简单起见,下面以变量为例)存入指针变量。
当一个指针变量保存了某个变量的地址时,也说该指针指向了那个变量。 -
通过指针访问被指对象(变量),称为间接访问。
7.2 指针变量的定义和使用
指向整型变量的指针也简称为整型指针。
取变量地址的操作用一元运算符&;间接访问操作用一元运算符*,也称间接操作。这两个运算符与其他一元运算符的优先级相同,自右向左结合。
利用指针解决问题的方案包括三方面:函数定义时用指针参数;函数里通过指针参数间接访问被指变量;函数调用时把变量地址传给函数。
函数调用时形参与实参的关系:
空指针
空指针是个特殊指针值,也是唯一对任何指针类型都合法的指针值。一个指针变量具有空指针值,表示它当时没指向有意义的东西,处于闲置状态。空指针值用 0 表示,这个值绝不会是任何程序对象的地址。给一个指针赋值 0 就表示要它不指向任何有意义的东西。为了提高程序的可读性,标准库定义了一个与 0 等价的符号常量NULL。
7.3 指针与数组
int *p1, *p2, *p3, *p4;
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
p1 = &a[0]; p2 = p1; p3 = &a[5]; p4 = &a[10];
指针运算:间接访问
由指针值出发进行的运算称为指针运算。
可以通过下面各种方式遍历数组a,打印出a中各个元素:
for (p1 = a, p2 = a+10; p1 < p2; ++p1)
printf("%d
", *p1);
for (p1 = a; p1 < a+10; ++p1)
printf("%d
", *p1);
for (p1 = p2 = a; p1 - p2 < 10; ++p1)
printf("%d
", *p1);
for (p1 = a; p1 - a < 10; ++p1)
printf("%d
", *p1);
用指针方式实现计算字符串长度的函数。第一种实现方式是:
int strLength (const char *s) {
int n = 0;
while (*s != ' ') {
++s;
++n;
}
return n;
}
int strLength (const char *s) {
char *p = s;
while (*p != ' ') ++p;
return p - s;
}
例4,考虑用指针参数的方式写出上一章的数组元素划分函数。
为函数定义一对界定数组范围(或数组中一段)的指针参数和一个划分值;
让函数返回划分后的分界位置,这里用指向数组元素的指针值,令它指向大于等于划分值的第一个元素。
如果不存在这种元素,返回指向(数组)序列后面一个位置的指针值。
第一个指针参数指向序列的第一个元素,第二个指针
参数指向序列最后元素之后一个位置。
可以给出下面的定义:
double* partition(double *begin, double *end, double cut) { while (begin < end) {
while (begin < end && *begin < cut) ++begin; while (begin < end && *(--end) >= cut) ; if (begin < end) { double x = *begin;
*begin = *end;
*end = x;
++begin;
} }
return begin;
}
这里的大循环内部还是用了两个while循环,其中的一个更新向右移的指针,另一个更新向左移的指针。在条件语句内部定义了一个用于交换元素的临时变量。第二个内部的while 语句值得注意。该循环以空语句为体,循环条件中还包括对变量end 的更新。当然也可以用更普通的形式写这个循环。但目前写法很紧凑,熟悉 C 语言的人们经常采用这类简洁写法,因此,在学习 C 语言和程序设计中也应逐渐习惯这类写法。
图 7.6 展示了上述函数对一个数组的处理过程,
假定函数调用时以 5 作为数据的分界值。
- (1) 函数开始时,两个参数指针分别指向序列两端(半闭半开);
- (2) 两个内部循环第一次执行之后,两个指针分别指向一对需要交换的元素;
- (3) 交换元素并更新指针之后的现场;
- (4) 两个内部循环执行之后,begin指针没有动,end指针向左移了一个位置;
- (5) 交换并移动指针之后,循环条件失败,循环终止。返回的指针值指向划分之后大于等于划分值的那一段的第一个元素。
7.4 指针数组
指针也是数据,自然就可以定义指针的数组。指针数组在复杂的程序里使用广泛。
7.5 多维数组作为参数的通用函数
按语言规定,当函数参数是两维或更多维数组时,参数说明必须给出除第一维外其他各维的大小。
面讨论可以写出下面函数定义,函数由两个整型参数得到数组两个维的大小:
void prtMatrix (int m, int n, int *mp) {
int i, j;
for (i = 0; i < m; ++i) {
for (j = 0; j < n; ++j)
printf("%d ", *(mp + i * n + j));
putchar('
');
}
}
如果要打印上面定义的数组a,正确的函数调用形式是:
prtMatrix(10, 8, &a[0][0]);
这里&a[0][0]在类型上是指向整型的指针,这正是函数参数mp所要求的类型。
7.6 动态存储管理
程序中需要用变量(各种简单类型变量、数组变量等)保存被处理数据和各种状态信息,变量在使用前必须安排好存储:放在哪里、占据多少存储单元等等,这个工作被称作存储分配。用机器语言写程序时,所有存储分配问题都需要人处理,这个工作琐碎繁杂、容易出错。在用高级语言写程序时,人通常不需要考虑存储分配细节,主要工作由编译程序在加工程序时自动完成。这也是用高级语言工作效率较高的一个重要原因。
C 程序里的变量分为几种。外部变量、局部静态变量的存储问题在编译时确定,其存储空间的实际分配在程序开始执行前完成。程序执行中访问这些变量,就是直接访问它们的固定存储位置。对于局部自动变量,在执行进入变量定义所在的复合语句时为它们分配存储。应该看到,这种变量的大小也是静态确定的。例如,局部自动数组的元素个数必须用静态可求值的表达式描述。这样,一个函数在调用时所需的存储量(用于安放其中的所有自动变量)在编译时就完全确定了。函数定义里描述了所需要的自动变量和参数,定义了数组的规模,这些就决定了该函数在执行时实际需要的存储空间大小。
使用动态存储管理
1) 注意检查分配的成功与否。人们常用的写法是:
if ((p = (... )malloc(...)) == NULL) {
.. ... / 对分配未成功情况的处理 */
2) 系统对所分配存储块的使用完全不进行检查。写程序的人需要保证这种使用的正确性,切不可超出实际存储块的范围进行访问。
3) 动态分配存储块的存在期不依赖于分配该块的位置。如果在某函数里分配了一个块,这个块的存在期与该函数的执行期无关。只有通过free释放这个块,才使其存在期结束。注意,变量存在期的结束时刻就是它所占存储被收回的时刻。
4) 如果在某函数里分配了动态存储,并通过局部指针访问这种存储块,那么在函数退出前就必须考虑如何处理这些存储块:或是将它们释放;或是将它们的地址赋给存在期更长的指针变量(如全局变量)。否则,在函数退出时局部指针变量撤销,它们所指的尚未释放的存储块就再也找不到了(流失了)。
5) 其他情况也可能造成存储块丢失,例如一个指向动态块的指针赋了其他值,如果原被指存储块没有其他访问路径,那么就再也无法找到它了。如果存储块丢失,在本程序随后的运行中将永远不能再用这个存储块所占的存储。
6) 请注意计算机系统里存储管理的关系。一个程序运行时将从操作系统取得一部分存储空间,用于保存其代码和数据。用于数据存储的空间里包括一部分动态存储区,由程序里的动态存储管理系统管理。在这个程序的运行期间,所有动态存储申请都由这块空间里分配。程序代码中释放存储,就是将不用的存储块交还程序的动态存储管理系统。一旦该程序结束,操作系统将收回这个程序所获得的所有存储区域。所以,我们所说“存储流失”是程序内部的问题,而不是整个系统的问题。