Kodewick
第 19 课,共 30 课目录

指针

第 19 课,共 30 课 · 第 5 部分 · 内存和 lambda · 约 15 分钟

第 10 课讲了引用:变量的另一个名字。这一课讲 C++ 里另一种「指着别人」的东西:指针。指针本身也是一个变量,它存的不是分数、价格,而是另一个变量在内存里的地址。

main.cpp
1#include <iostream>2 3int main() {4    int score = 80;5    int* p = &score;6    std::cout << *p << '\n';7    *p = 95;8    std::cout << score << '\n';9    return 0;10}
输出
80
95

int* p 读作「p 是指向 int 的指针」。&score 取出 score 的地址,存进 p。*p 是「顺着 p 存的地址找到的那个变量」,也就是 score,所以 *p = 95 改的就是 score。点单步:走过第 5 行,画面上 p 那里出现一根箭头,指着 score。

地址

内存像一排很长很长的格子,每个格子有一个编号,这个编号就是地址。每个变量都住在某几个格子里,&变量 就是问「你住在几号」。

地址也能直接显示,std::cout << &score 会显示一串像 0x7ffd3a2c 的十六进制数。可这个数每次运行都不一样(系统每次给程序的位置不同),对写程序也没什么用,所以这门课不显示它,只看箭头。真正有用的是:两个指针存的地址一样,它们就指着同一个变量。

main.cpp
1#include <iostream>2 3int main() {4    int apples = 5;5    int pears = 5;6    int* a = &apples;7    int* b = &apples;8    int* c = &pears;9    std::cout << (a == b) << '\n';10    std::cout << (a == c) << '\n';11    std::cout << (*a == *c) << '\n';12    return 0;13}
输出
1
0
1

a == b 比的是地址:都是 apples 的地址,是 1。a == c 是 0:apples 和 pears 住在不同的地方,哪怕里面的数都是 5。要比里面的数,写 *a == *c。

注意: 同一个符号,放的地方不同,意思不同。声明里 int* p 的 * 表示「p 是指针」;用的时候 *p 的 * 表示「顺着地址找过去」。& 也一样:int& r 是引用,&score 是取地址。

通过指针改

和引用一样,把指针传给函数,函数就能改到外面的变量。不同的是,调用时要写 &,函数里要写 *:

main.cpp
1#include <iostream>2 3void add_stock(int* count, int amount) {4    *count += amount;5}6 7int main() {8    int stock = 12;9    add_stock(&stock, 8);10    add_stock(&stock, 5);11    std::cout << "库存:" << stock << '\n';12    return 0;13}
输出
库存:25

点单步进入 add_stock:count 的箭头从这一层伸出来,指着 main 里的 stock。*count += amount 顺着箭头去加。

调用的地方写了 &stock,一眼就能看出「这个函数可能会改 stock」。这是指针参数的一个好处;不过现代 C++ 里,要改外面的变量,一般还是用引用,写起来更干净。

nullptr:不指着谁

有时候指针暂时不该指着任何变量,这时给它 nullptr(空指针)。用之前先问一句 p != nullptr:

main.cpp
1#include <iostream>2#include <vector>3 4int main() {5    int best = 0;6    int* found = nullptr;7    std::vector<int> scores{72, 58, 91, 66};8    for (std::size_t i = 0; i < scores.size(); i++) {9        if (scores[i] > 90) {10            found = &scores[i];11        }12    }13    if (found != nullptr) {14        best = *found;15    }16    std::cout << "超过 90 分的:" << best << '\n';17    return 0;18}
输出
超过 90 分的:91

found 一开始是 nullptr,意思是「还没找到」。找到了,它就指着那个元素。顺着空指针找过去是不行的,那里什么也没有:

main.cpp
1#include <iostream>2 3int main() {4    int* found = nullptr;5    std::cout << *found << '\n';6    return 0;7}
输出
load of null pointer

发生了什么: 停在第 5 行:这个指针是 nullptr(空指针),不能通过它读写。

真的程序在这里多半会崩溃(Linux 上叫 Segmentation fault),也可能出更怪的事,这是未定义行为。我们的引擎在第 5 行停下,点「看看是怎么出错的」。

小技巧: 以前的代码里常见 NULL 或 0 当空指针。C++11 起用 nullptr,它只能当指针用,不会和数字 0 弄混。

没有赋值的指针

声明了指针却不给它值,它里面是一个不知道哪里来的地址,指着一个谁也说不清的地方。顺着它去写,比空指针还糟:

main.cpp
1#include <iostream>2 3int main() {4    int count = 3;5    int* p;6    *p = count;7    std::cout << *p << '\n';8    return 0;9}
输出
use of uninitialized value

发生了什么: 停在第 6 行:「p」还没有赋值就被用了。真的 C++ 里这会是一个乱数(每次运行都可能不一样)。

真的程序可能崩溃,也可能悄悄改坏了别的变量,过很久才在别处出问题。引擎在第 6 行停下。所以指针声明时就给它一个值:指着一个变量,或者先写 = nullptr。

换一个指着

引用绑定以后就不能换了;指针可以,给它一个新地址就行:

先猜一猜

这段程序显示什么?

1#include <iostream>2 3int main() {4    int a = 3;5    int b = 8;6    int* p = &a;7    p = &b;8    *p = 100;9    std::cout << a << " " << b << '\n';10    return 0;11}

指向对象:->

指针也能指着一个对象。要用它的成员,可以写 (*p).name,括号不能少;更常见的是一个箭头 p->name,意思一样:

main.cpp
1#include <iostream>2#include <string>3 4struct Product {5    std::string name;6    double price;7};8 9void discount(Product* item, double rate) {10    item->price = item->price * rate;11}12 13int main() {14    Product milk{"牛奶", 6.5};15    Product* p = &milk;16    std::cout << (*p).name << '\n';17    discount(p, 0.8);18    std::cout << p->name << ":" << milk.price << " 元\n";19    return 0;20}
输出
牛奶
牛奶:5.2 元

点单步:p 和函数里的 item 两根箭头都指着同一个 milk,item->price 改的就是 milk.price。下两课的 new 和智能指针,给的都是指针,-> 会天天用到。

还有一根你一直在用、却没看见的箭头:在成员函数里,有一个看不见的指针 this,指着调用这个函数的那个对象。*this 就是这个对象自己;成员函数里写的 balance_,其实就是 this->balance_:

main.cpp
1#include <iostream>2 3class Account {4public:5    Account(int balance) : balance_(balance) {}6 7    void deposit(int amount) {8        this->balance_ += amount;9        balance_ += amount;10    }11 12    int balance() const {13        return balance_;14    }15 16private:17    int balance_;18};19 20int main() {21    Account acc{100};22    acc.deposit(30);23    std::cout << acc.balance() << '\n';24    return 0;25}
输出
160

acc.deposit(30) 里,this 指着 acc。第 9 行和第 10 行是同一件事,写了两遍,所以存进去两个 30,显示 160。平时只写 balance_;第 15 课和第 17 课里的 this 和 *this 就是它。

指针和引用

引用 int& r指针 int* p
声明时必须绑定可以先给 nullptr
能不能换不能能:p = &b
可以是空的吗不能能:nullptr
怎么用直接用 r*p、p->name

一句话:能用引用就用引用;要「可能没有」或者「中途换一个」的时候,才用指针。

main.cpp
1#include <iostream>2#include <string>3 4void show_owner(const std::string* name) {5    if (name == nullptr) {6        std::cout << "没有主人\n";7    } else {8        std::cout << "主人:" << *name << '\n';9    }10}11 12int main() {13    std::string owner = "小林";14    show_owner(&owner);15    show_owner(nullptr);16    return 0;17}
输出
主人:小林
没有主人

const std::string* 是「指向一个不能改的 std::string」,和 const 引用一样只看不改。这里用指针,是因为「没有主人」也是一种可能,引用表示不了「没有」。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

顺着箭头加一

不要直接写 visits,只通过指针 p 把 visits 加 1。程序要显示 11。

要显示
11
exercise1.cpp
1#include <iostream>2 3int main() {4    int visits = 10;5    int* p = &visits;6    // 在这里通过 p 加 17    std::cout << visits << '\n';8    return 0;9}
输出
点「运行」或按 Ctrl+Enter
练习 2

指着最便宜的

让 cheapest 指着 prices 里最小的元素,再通过 cheapest 把它改成 0。程序要显示 12 0 9。

要显示
12 0 9
exercise2.cpp
1#include <iostream>2#include <vector>3 4int main() {5    std::vector<int> prices{12, 5, 9};6    int* cheapest = &prices[0];7    for (std::size_t i = 1; i < prices.size(); i++) {8        // 比现在指着的还小,就改去指它9    }10    *cheapest = 0;11    std::cout << prices[0] << " " << prices[1] << " " << prices[2] << '\n';12    return 0;13}
输出
点「运行」或按 Ctrl+Enter
练习 3

修好它:没有优惠券

没有优惠券时,coupon 是 nullptr,程序却顺着它去找。改一改 pay:有优惠券就减去它的值,没有就不减。程序要显示 80 和 100 两行。

要显示
80
100
exercise3.cpp
1#include <iostream>2 3int pay(int price, const int* coupon) {4    return price - *coupon;5}6 7int main() {8    int twenty = 20;9    std::cout << pay(100, &twenty) << '\n';10    std::cout << pay(100, nullptr) << '\n';11    return 0;12}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 指针是存地址的变量:int* p = &score;,& 取地址,*p 顺着地址找到那个变量。
  • 地址每次运行都不一样,一般不显示;p == q 比地址,*p == *q 比里面的值。
  • 指针参数让函数改到外面的变量:调用写 &stock,函数里写 *count。
  • nullptr 表示不指着谁,用之前先判断;顺着空指针或没赋值的指针去找是未定义行为(引擎会停下)。
  • 指针可以换指向(p = &b),引用不行。
  • 指向对象时用 p->name,等于 (*p).name。成员函数里的 this 指着对象自己,balance_ 就是 this->balance_。
  • 能用引用就用引用;「可能没有」或「要换」时才用指针。