智能程序设计(C语言)


指针基础 (上)

计算机学院    杨已彪

yangyibiao@nju.edu.cn

提纲

  • 指针变量: 什么是地址

  • 声明指针变量

  • & 和 * 两个运算符

  • 指针赋值

  • 任务: 指针追踪

  • 课堂小测


指针变量

理解指针的第一步是在机器级别观察它们表示的内容. 内存按字节划分, 每个字节存 8 位:

每个字节都有唯一的地址。

内存共 n 个字节, 地址就是从 \(0\) 到 \(n-1\) 的编号:


指针变量: 为什么需要地址

程序中的每个变量都占用一个或多个字节的内存. 变量占的第一个字节的地址, 就是变量的地址。 下图中变量 i 的地址是 2000:

地址可以存储在特殊的指针变量中. 把变量 i 的地址存进指针变量 p, 就说 p指向i。 图形表示:


声明指针变量

int* p;

声明指针变量时, 名字前面必须有星号: p 是一个指针变量, 能够指向 int 类型的对象 (用"对象"而非"变量", 因 p 可能指向不属于变量的内存区域)

int i, j, a[10], b[20], *p, *q;

指针变量可以与其他变量一起声明

int* p;    /* 只指向整数 */
double *q; /* 只指向双精度 */
char *r;   /* 只指向字符 */

C 要求每个指针变量只能指向特定类型的对象; void * 较特殊: 可存放任意类型对象的地址


取地址运算符 &

int *p;  /* 没有指向特定对象 */
int i, *p;
…
p = &i;
int i;
int *p = &i;
int i, *p = &i;

声明指针只给它留了空间, 还没指向任何对象:

在使用p之前需要初始化它.

初始化指针的一种方法: 把变量的地址赋给它:

把 i 的地址赋给 p, 让 p 指向 i:

也可以在声明时就初始化:

i的声明甚至可以与p的声明合并:


间接寻址运算符 *

代码
printf("%d\n", *p);
j = *&i; /* 等同于 j = i; */
说明

一旦指针变量指向一个对象, 就可以使用*(间接寻址)运算符来访问对象中存储的内容.

如果p指向i, 我们可以打印i的值如下:

  • 对变量用 & 得到它的地址 (一个指针值); 把该值赋给指针变量后, 指针指向该变量

  • 对指针用 * 则解引用: 顺着地址找到原变量, 读写它

只要p指向i, *p就是i的别名. *p与i具有相同的值. 更改*p的值会更改i的值. 下一张幻灯片上的示例说明了*p和i的等价性.

p 的盒子里存"i 的地址"; 顺着地址就能找到 i p int * 2000 存的是地址 i int 1 地址恰好是 2000 p 指向 i &i 得到 2000 (i 的地址, 是一个值) 赋给 p *p 就是 i (同一块内存): 读 *p 得 1, 写 *p = 2 就把 i 改成 2
间接寻址运算符 * (上)

printf("%d\n", i); /* 打印 1 */
printf("%d\n", *p); /* 打印 1 */
*p = 2;
printf("%d\n", i); /* 打印 2 */
printf("%d\n", *p); /* 打印 2 */

p = &i;

i = 1;


间接寻址运算符 * (下)

int* p;
printf("%d", *p); /*** 错误的 ***/
int* p;
*p = 1; /*** 错误的 ***/

将间接寻址运算符应用于未初始化的指针变量会导致未定义的行为.

给*p赋值是特别危险的:

p ??? 未初始化的指针 指向哪里? ? 未知内存 垃圾值,不是有效地址 解引用未初始化的指针:写到未知内存,未定义行为
指针赋值 (上)

int i, j, *p, *q;
p = &i;
q = p;
*p = 1;
*q = 2;

C 允许使用赋值运算符来复制相同类型的指针. 假设有以下声明:

q现在指向与p相同的位置:

如果p和q都指向i, 就可以通过为*p或*q赋一个新值来更改i:

任意数量的指针变量都可以指向同一个对象.


指针赋值 (下): 不要混淆 q=p 与 *q=*p

注意不要混淆

q = p;

和

*q = *p;

第一条语句是指针赋值, 但第二条不是. 下面的示例显示了第二个语句的效果.

p = &i;
q = &j;
i = 1;
*q = *p;

任务: 指针追踪

任务: 令 int a = 3, *p = &a;, 打印 a、&a、p、*p; 再执行 *p = 7, 重新打印 a 和 *p

样例 (地址每次运行都不同):

a = 3, &a = 0x7ffd1a2b, p = 0x7ffd1a2b, *p = 3
改写 *p 之后: a = 7, *p = 7

提示:

  • 打印地址用 %p, 打印整数用 %d——占位符要和参数类型对应

  • 观察 &a 与 p 是否相等; 改写 *p 之后 a 是否跟着变

⚡ 在 VS Code 中打开 task-pointer-trace.c (骨架)

任务: 参考代码与要点

#include <stdio.h>

int main(void)
{
  int a = 3;
  int *p = &a;                 /* p 指向 a */

  printf("a = %d, &a = %p, p = %p, *p = %d\n",
         a, (void *)&a, (void *)p, *p);

  *p = 7;                      /* 通过 p 改写 a */
  printf("改写 *p 之后: a = %d, *p = %d\n", a, *p);
  return 0;
}
  • p = &a: p 里存的是 a 的地址; 所以 &a 与 p 打印结果相同

  • *p 是 a 的别名: 读 *p 就是读 a, 写 *p = 7 就是写 a

  • %p 打印指针 (地址); 每次运行的地址不同, 不要硬编码比较

  • 指针类型要匹配: int *p 只能指向 int 对象


课堂小测

  1. 概念: 指针变量里存的是什么?int *p; 中 p 的类型是什么?
  2. 概念: &x 和 *p 分别做什么?它们互为逆运算吗?
  3. 代码阅读: int i = 1, *p = &i; *p = 2; printf("%d", i); 输出什么?
  4. 纠错: int *p; *p = 1; 错在哪里?



未完待续

🔒 本课件尚未发布