第09讲:指针

《计算机程序设计》

苏醒

计算机学院计算机研究所编译系统研究室

课前准备

  • 在WSL Ubuntu中建立一个专门的目录用于课上练习,如~/course
$ mkdir ~/course
  • 打开VSCode并连接到WSL,打开此目录并新建文件,如~/course/main.cpp
  • 编辑main.cpp,随堂编程练习的代码请直接在此文件中编辑
main.cpp
#include <iostream>
#include <iomanip>
using namespace std;

int main() {
  // ============= begin =============

  // =============  end  =============
  return 0;
}

温故

考考你

阅读下列程序,判断程序输出是什么?

#include <iostream>
#include <cstdlib>

double range(double numbers[], int n) {
  double ub = numbers[0], lb = numbers[0];
  for (int i = 1; i < n; i++) {
    if (numbers[i] > ub) ub = numbers[i];
    if (numbers[i] < lb) lb = numbers[i];
  }
  return ub - lb;
}

int main() {
  int n;
  std::cin >> n;
  double numbers[n];
  for (int i = 0; i < n; i++)
    numbers[i] = (double)std::rand() / RAND_MAX; // 生成随机数
  std::cout << range(numbers, n) << std::endl;
  return 0;
}

在C++中实现“动态类型”

编程求解

C++是一种静态强类型语言,可否在C++中实现一个“动态类型”,即支持在同一内存空间中存储不同类型的数据

dynamictype.cpp
#include <iostream>

void writeI(void *buffer, int i) { /* store i to buffer */ }
void writeD(void *buffer, double d) { /* store d to buffer */ }
void writeC(void *buffer, char c) { /* store c to buffer */ }

int main() {
  int type;
  char buffer[sizeof(double)];
  int i; double d;
  std::cin >> type;
  // read in a value and write the value to buffer according to type
  switch (type) { // 0: int, 1: double
    case 0: std::cin >> i; writeI(buffer, i); break;
    case 1: std::cin >> d; writeD(buffer, d); break;
    case 2: std::cin >> c; writeC(buffer, c); break;
    default: std::cerr << "Invalid type" << std::endl; return 1;
  }
  return 0;
}

学习目标

  • 学习内容
    • 指针的内涵
    • 使用指针
    • 指针应用

学习目标

  • 理解指针的本质(深刻地
  • 掌握使用指针操作内存的方法(随心所欲地
  • 应用指针实现动态类型(小心翼翼地

指针是C/C++语言的精华!

一、指针的内涵

变量的地址属性(复习)

  • 变量的地址:变量在内存中的位置
  • 变量的地址是一个常量,不可改变

思考

地址的本质就是一个整数,那么能否把变量的地址存储在另一个变量中?

int a = 10;
??? addr;  // 声明一个变量addr用于保存地址
addr = ??? // 将a的地址存入变量addr中

---
config:
  look: handDrawn
  themeVariables:
    fontSize: 28px
---

mindmap
Root(变量)
  变量名:标识符
  类型:数据类型
  值:存储的数据
  地址:内存中的位置
  作用域:变量的可见范围
  生存周期:变量的有效时间

变量的六个属性

  • 可以

两个问题

  • 如何获取一个变量的地址?
  • 使用什么类型的变量来存储地址?

1.1 取地址运算符&

  • 取地址运算符&可以获取变量的地址
    • &a是一个表达式,其值即为变量a的地址

考考你

地址本质上就是一个整数,那么&a可否赋值给一个整型变量?

intaddr.cpp
int main() {
  int a = 10;
  int addr = &a;
  return 0;
}
$ g++ -o code/intaddr code/intaddr.cpp
code/intaddr.cpp: In function ‘int main()’:
code/intaddr.cpp:3:14: error: invalid conversion from ‘int*’ to ‘int’ [-fpermissive]
    3 |   int addr = &a;
      |              ^~
      |              |
      |              int*

1.2 指针类型

明察秋毫

&a的类型为int *,这就是指针类型!指针,是带有类型地址

  • 指针类型即专门用于存储地址的类型
指针示例
int a = 10;
int *p = &a;    // p是一个指针变量,其类型为'int *'
double *q = &a; // 错误:类型不匹配

考考你

使用指针来存储地址,与使用整型变量有什么区别?

明察秋毫

关键的区别在于指针带有类型!通过指针,可以准确地刻画一块内存区域的性质:首地址+长度+对齐

1.3 指针的定义与初始化

  • 定义指针变量的语法与一般变量类似
指针定义
int *p0;                 // 定义一个int *指针p0,不做初始化
double *p1 = nullptr;    // 定义一个double *指针p1,初始化为空指针(nullptr),即0地址
int a = 10;
int *p2 = &a;            // 定义一个int *指针p2,并初始化为&a
char *p3, *p4 = nullptr; // 在一个语句中定义多个指针变量,注意每个变量名前都要加*

考考你

若上述代码第5行的p4前忘记加*,会发生什么?

1.4 指针变量的属性

  • 指针变量q(第6行)的属性
    • 变量名:q
    • 类型:int *
    • 值:&b
    • 地址:指针变量也存储在内存中吗?
    • 作用域:块作用域
    • 生存周期:块代码的开始执行到执行结束

---
config:
  look: handDrawn
  themeVariables:
    fontSize: 28px
---

mindmap
Root(变量)
  变量名:标识符
  类型:数据类型
  值:存储的数据
  地址:内存中的位置
  作用域:变量的可见范围
  生存周期:变量的有效时间

变量的六个属性

指针变量的属性
extern char *name;
int a = 10;
static int *p = &a;
{
  int b = 20;
  int *q = &b;
}
&p; // 指针p的地址是什么?

1.5 指针的存储

考考你

指针本身的大小与对齐与什么有关?与它指向的类型有关吗?

ptrstorage.cpp
#include <iostream>
#include <iomanip>
using namespace std;
int main() {
  int i = 0;
  int *pi = &i;
  double *pd = nullptr;
  cout << left << setw(16) << "pointer" << setw(16) << "address" << setw(8) << "size" << setw(8) << "align" << endl;
  cout << setw(16) << pi << setw(16) << &pi << setw(8) << sizeof(int *) << setw(8) << alignof(int *) << endl;
  cout << setw(16) << pd << setw(16) << &pd << setw(8) << sizeof(double *) << setw(8) << alignof(double *) << endl;
  return 0;
}
$ g++ -o code/ptrstorage code/ptrstorage.cpp
$ ./code/ptrstorage
pointer         address         size    align   
0x7ffe256c55d4  0x7ffe256c55d8  8       8       
0               0x7ffe256c55e0  8       8       

明察秋毫

指针变量本身也需要内存空间,其大小与对齐与指向的类型无关!在32/64位系统中,指针变量大小固定为4/8字节

指针的内涵小结

  • 指针是带有类型地址
  • 指针变量的定义与初始化
    • char *c;
    • int *p = &a;
    • double *q = nullptr;
  • 指针变量具有一般变量的六个属性
    • 需要存储空间,有大小与对齐要求

---
config:
  look: handDrawn
  themeVariables:
    fontSize: 28px
---
mindmap
指针的内涵
  取地址运算符
  指针是带有*类型*的*地址*
  指针变量的定义与初始化
  指针的存储

指针的内涵

测一测

表达式&(a+b)中取地址运算符的使用是否恰当?

二、使用指针

2.1 解引用运算符*

  • 当指针p保存了变量a的地址,称指针p指向变量a
  • 通过解引用运算符*,指针p可以访问其指向的变量,称为间接访问
indaccess.cpp
#include <iostream>
int main() {
  int a = 10;
  int *p = &a;                  // p指向a
  std::cout << *p << std::endl; // 通过指针p读变量a
  *p = 20;                      // 通过指针p写变量a
  std::cout << a << std::endl;
  return 0;
}
$ g++ -o code/indaccess code/indaccess.cpp
$ ./code/indaccess
10
20

考考你

间接访问的“间接”体现在哪里?

间接访问

明察秋毫

  • 直接访问:通过变量名访问变量,访问一次内存
  • 间接访问:通过指针访问变量,访问两次内存

2.2 解引用与取地址

  • 解引用运算符*与取地址运算符&互为逆运算
    • *(&a)等价于a
    • &(*p)等价于p
int a = 10;
int *p = &a;
int *q = &*p;
int b = *&a;

明察秋毫

*&优先级相同,故*(&a)中的括号可以省略

2.3 野指针与空指针

考考你

空指针是值为nullptr(0地址)的指针,野指针是指向未知内存的指针,使用它们会发生什么?

invalidptr.cpp
#include <iostream>
int main() {
  int a = 10;
  int *p;           // 我是一个野指针!
  int *q = nullptr; // 你是一个空指针!
  std::cout << p << ' ' << q << std::endl;
  *p = 10;          // 咱们都是危险分子
  *q = 10;          // 令你程序莫名崩溃
  return 0;
}
$ g++ -o code/invalidptr code/invalidptr.cpp
$ ./code/invalidptr
0x7f63f14a6934 0
/bin/bash: line 1: 21011 Segmentation fault      (core dumped) ./code/invalidptr 2>&1

避坑

C/C++中最常见也是最致命的软件BUG之一:通过空指针野指针访问内存!

2.4 规避野指针

  • 指针变量赋初值(指向变量或nullptr
    • 使用前检查是否为nullptr
  • 避免出现指针变量的生命周期超过其指向变量的生命周期
  • 在指向的内存失效(超出生命周期或内存被释放)后,将指针重置为nullptr
int *p = nullptr;
{
  int a = 10;
  p = &a;
}
*p = 20;     // 错误:a的生命周期结束,p指向的内存已失效
p = nullptr; // 重置指针p
if (p) {     // nullptr为假,非nullptr为真(将地址视为一个整数,相当于0为假、非0为真)
  // 使用指针p
}

2.5 指针类型转换

考考你

能否使用一个double *指针指向一个int变量?

ptrcast.cpp
#include <iostream>
int main() {
  int a = 10;
  double *pd = &a;
  return 0;
}
$ g++ -o code/ptrcast code/ptrcast.cpp
code/ptrcast.cpp: In function ‘int main()’:
code/ptrcast.cpp:4:16: error: cannot convert ‘int*’ to ‘double*’ in initialization
    4 |   double *pd = &a;
      |                ^~
      |                |
      |                int*

明察秋毫

  • int *double *是两种不同的指针类型,彼此不能隐式转换
  • 如果希望转换,必须显式采用强制类型转换

2.5 指针类型转换

考考你

下列程序可以通过编译,请预测输出内容

explicitptrcast.cpp
#include <iostream>
int main() {
  int a = 10;
  double *pd = (double *)&a;
  *pd = 3.14;
  std::cout << *pd << std::endl;
  return 0;
}
$ g++ -o code/explicitptrcast code/explicitptrcast.cpp
$ ./code/explicitptrcast
/bin/bash: line 1: 21022 Segmentation fault      (core dumped) ./code/explicitptrcast 2>&1

考考你

程序崩溃了!为什么?

明察秋毫

尝试向pd指向的内存区域写入8字节!但实际这里只分配了4字节!

2.6 指针算术运算

  • 指针支持算术运算,其运算的对象是指针指向的地址,本质上是地址的整数运算
    • 指针与整数的加减法:移动指针(以指向的类型为单位)
    • 指针与指针的减法:计算两个指针之间的距离(以指向的类型为单位)
ptrarith.cpp
#include <iostream>
int main() {
  int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
  int *p1 = &arr[0]; // p1指向arr的低一个元素
  int *p3 = p1 + 5;  // p3由p1向高地址移动5个int得到
  int *p2 = p3 - 4;  // p2由p3向低地址移动4个int得到
  std::cout << p1 << ' ' << p2 << ' ' << p3 << std::endl;
  std::cout << *p1 << ' ' << *p2 << ' ' << *p3 << std::endl;
  std::cout << p2 - p1 << ' ' << p1 - p2 << std::endl; // p2和p1之间的距离,以int为单位
  return 0;
}
$ g++ -o code/ptrarith code/ptrarith.cpp
$ ./code/ptrarith
0x7ffe62d7f770 0x7ffe62d7f774 0x7ffe62d7f784
1 2 6
1 -1

2.6 指针算术运算

  • 指针算术运算以指向的类型为单位,非以字节为单位
  • 指针与指针相减(求距离)必须是同类型指针
指针算术运算(其中p/p1/p2为同类型指针,n为整数)
表达式 类型 含义
p + n 指针 指向p之后n个元素的指针
p - n 指针 指向p之前n个元素的指针
p1 - p2 整数 p1p2之间的元素个数
p++/p-- 指针 改变p的值使其指向后一个元素,返回更新前的指针
++p/--p 指针 改变p的值使其指向后一个元素,返回更新后的指针
p+=n/p-=n 指针 改变p的值使其指向p之后/之前n个元素,返回更新后的指针

2.7 指针关系运算

  • 指针支持关系运算,其运算的对象是指针指向的地址,本质上是地址的整数运算
    • 支持==!=><>=<=等关系运算
ptrrel.cpp
#include <iostream>
#include <iomanip>
int main() {
  int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
  int *p1 = &arr[0]; // p1指向arr的低一个元素
  int *p2 = p1 + 5;  // p3由p1向高地址移动5个int得到
  std::cout << std::boolalpha << (p1 < p2) << ' ' << ( p1 > p2) << std::endl;
  std::cout << (p2 == &arr[5]) << ' ' << (p2 != p1) << std::endl;
  return 0;
}
$ g++ -o code/ptrrel code/ptrrel.cpp
$ ./code/ptrrel
true false
true true

2.8 指针参数

  • 函数参数可以是指针类型
ptrparam.cpp
#include <iostream>

void update(int *p) { *p += 1; }

int main() {
  int a = 0, *p = &a;
  update(p);
  std::cout << a << std::endl;
  return 0;
}
$ g++ -o code/ptrparam code/ptrparam.cpp
$ ./code/ptrparam
1

测一测

指针参数的传递方式是传值还是传引用?类比数组参数进行思考,也可以通过打印实参/形参地址进行比较!

明察秋毫

数组参数与指针参数本质上都是以传值方式传递地址!因此能够实现类似于通过引用参数修改实参的效果

使用指针小结

  • 解引用运算符*:间接访问
    • 与取地址运算符&互逆
  • 野指针与空指针
  • 指针运算
    • 算术运算
    • 比较运算
  • 指针参数

---
config:
  look: handDrawn
  themeVariables:
    fontSize: 20px
---
mindmap
使用指针
  解引用运算符
  空指针与野指针
  指针运算
    算术
    关系
  指针参数

指针练习

测一测

判断下列程序的输出

ptrquiz.cpp
#include <iostream>
int *trickme(int *p) {
  ++*(p++);
  return p;
}
int main() {
  int arr[5] = {1, 2, 3, 4, 5};
  int *p = arr, sum = 0;
  trickme(trickme(p));
  for (int i = 0; i < 5; ++i)
    sum += arr[i];
  std::cout << sum << std::endl;
  return 0;
}
$ g++ -o code/ptrquiz code/ptrquiz.cpp
$ ./code/ptrquiz
17

三、指针应用:动态类型

动态类型

编程求解

C++是一种静态强类型语言,可否在C++中实现一个“动态类型”,即支持在同一内存空间中存储不同类型的数据

dtypemain.cpp
#include <iostream>
#include "dtype.h"
int main() {
  int type;
  char buffer[sizeof(double)];
  int i; double d;
  std::cin >> type;
  // read in a value and write to buffer according to type
  switch (type) { // 0: int, 1: double
    case 0: std::cin >> i; writeI(buffer, i); break;
    case 1: std::cin >> d; writeD(buffer, d); break;
  }
  // read out the value from buffer according to type
  switch (type) {
    case 0: std::cout << readI(buffer) << std::endl; break;
    case 1: std::cout << readD(buffer) << std::endl; break;
  }
  return 0;
}
dtype.h
void writeI(void *buffer, int i);
void writeD(void *buffer, double d);
int readI(void *buffer);
double readD(void *buffer);
dtype.cpp
void writeI(void *buffer, int i) {
  /* store i to buffer */
}
void writeD(void *buffer, double d) {
  /* store d to buffer */
}
int readI(void *buffer) {
  /* read i from buffer */
}
double readD(void *buffer) {
  /* read d from buffer */
}
$ g++ -o code/dtype code/dtypemain.cpp code/dtype.cpp
$ ./code/dtype

总结

本节内容

---
config:
  look: handDrawn
  themeVariables:
    fontSize: 20px
---
mindmap
  指针
    指针的内涵
      取地址运算符
      指针的类型      
      指针的存储
      指针的六个属性
    使用指针
      解引用运算符
      空指针与野指针
      指针运算
        算术
        关系
      指针参数
    指针应用
      动态类型

学习目标

  • 理解指针的本质(深刻地
  • 掌握使用指针操作内存的方法(随心所欲地
  • 应用指针实现动态类型(小心翼翼地

课后作业

  • 预习
    • 教材6.3:字符串
    • 教材7.3:指针与数组
    • 教材7.4:字符指针与字符数组

计算机程序设计