跳转至

10. 指针

10.1 基本概念

指针变量里存的是地址& 取得变量地址,* 再按这个地址找到变量:

int x = 3;
int *p = &x;
*p = 5;             /* x 变为 5 */

int *p, q; 中只有 p 是指针,q 仍然是普通 int。野指针不能解引用;暂时不指向对象时可设为 NULL,但 NULL 只是明确表示“没有指向”,照样不能写 *p

void * 可以保存任意对象指针,转换回原类型后再解引用。指针类型不仅说明所指对象类型,也决定解引用方式和指针算术的步长。

10.2 指针运算与数组

p 指向数组元素,p + 1 就指向下一个元素。实际地址增加多少字节由 p 的类型决定:增量是 sizeof *p,并不是固定加 1 个字节。

在多数表达式中,数组名会转换为首元素指针:

int a[10];
int *p = a;          /* 与 &a[0] 相同 */
a[i] == *(a + i);    /* 两种访问等价 */

不过 sizeof a&a 是两个例外。sizeof a 算整个数组大小;&a 取得的是整个数组的地址,类型为 int (*)[10],不是普通的 int *

10.3 指针数组、数组指针和二级指针

声明应从变量名向外读:

int *p[10];          /* p 是数组,有 10 个 int * 元素:指针数组 */
int (*q)[10];        /* q 是指针,指向含 10 个 int 的数组:数组指针 */
int **r;             /* r 指向 int *:二级指针 */

二维数组和 int ** 不是一回事。二维数组的各行连续,传参时要用列数匹配的数组指针;int ** 指向的是一个 int *,两者的内存结构并不一样。

10.4 动态内存

动态数组开在堆上,用完要自己释放。相关函数都在 <stdlib.h>

  • malloc(bytes):申请指定字节数,内容未初始化;
  • calloc(count, size):申请元素数组,并把所有字节置 0;
  • realloc(ptr, new_size):调整内存块大小,地址可能改变;
  • free(ptr):释放动态内存,free(NULL) 合法。
int *a = (int *)malloc(n * sizeof *a);
if (a == NULL) {
    /* 申请失败 */
}

/* 使用 a */

free(a);
a = NULL;

课件中的写法会把 malloc 返回的 void * 强制转换成目标指针类型。申请大小写成 n * sizeof *a,以后就算改了 a 的类型,这一行也不用跟着手改。

realloc 有个麻烦:扩容后地址可能改变,但失败时原来的内存又还在。因此先用临时指针接:

int *tmp = (int *)realloc(a, new_n * sizeof *a);
if (tmp != NULL) {
    a = tmp;
}

动态内存的错来来回回就这几种:申请后忘记释放、同一块内存释放两次、释放后还在用、访问越界、拿不是动态申请的地址去 free。写代码时顺着每个 malloc 找一次对应的 free,比较不容易漏。

10.5 字符串指针

char a[] = "abc";          /* 数组内容可修改 */
const char *p = "abc";     /* 指针可改指向,字面量不可修改 */

数组名不能改成指向别处,指针变量可以。两者都能访问字符串,但不是同一种东西。函数只读字符串时,参数写成 const char *

10.6 函数指针和返回指针的函数

double add(double x, double y) { return x + y; }

double (*operation)(double, double) = add;
double result = operation(2.0, 3.0);

函数指针最直观的用法是“把要调用的函数也当成参数传进去”。按条件选择不同运算、写回调函数时都会见到它。函数指针数组写作 double (*ops[4])(double, double)

课件最后的变步长梯形求积就是函数指针的一个用法:积分程序只负责不断减小步长并比较两次结果,被积函数通过函数指针传进来,于是同一套积分程序可以计算不同函数。

返回指针的函数写作 int *find(...)。判断一个地址能不能返回,关键看函数结束后它指向的对象还在不在:局部自动变量已经没了,数组元素、动态内存、静态对象或调用者传入的对象仍可能有效。

10.7 main 的命令行参数

int main(int argc, char *argv[])
{
    /* argc 是参数个数,argv[0] 通常是程序名 */
    return 0;
}

argv 是字符串指针数组,每个 argv[k] 指向一个参数字符串,argv[argc]NULL。处理命令行参数时通常从 argv[1] 开始,因为 argv[0] 留给程序名。

评论