指针
第 10 课讲了引用:变量的另一个名字。这一课讲 C++ 里另一种「指着别人」的东西:指针。指针本身也是一个变量,它存的不是分数、价格,而是另一个变量在内存里的地址。
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 的十六进制数。可这个数每次运行都不一样(系统每次给程序的位置不同),对写程序也没什么用,所以这门课不显示它,只看箭头。真正有用的是:两个指针存的地址一样,它们就指着同一个变量。
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 是取地址。
通过指针改
和引用一样,把指针传给函数,函数就能改到外面的变量。不同的是,调用时要写 &,函数里要写 *:
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:
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,意思是「还没找到」。找到了,它就指着那个元素。顺着空指针找过去是不行的,那里什么也没有:
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 弄混。
没有赋值的指针
声明了指针却不给它值,它里面是一个不知道哪里来的地址,指着一个谁也说不清的地方。顺着它去写,比空指针还糟:
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 = &b 改的是 p 自己存的地址:从此它指着 b。*p = 100 顺着箭头改的就是 b,a 还是 3。(要是第 7 行写成 *p = b,那才是把 8 存进 a。)
指向对象:->
指针也能指着一个对象。要用它的成员,可以写 (*p).name,括号不能少;更常见的是一个箭头 p->name,意思一样:
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_:
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 |
一句话:能用引用就用引用;要「可能没有」或者「中途换一个」的时候,才用指针。
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 引用一样只看不改。这里用指针,是因为「没有主人」也是一种可能,引用表示不了「没有」。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
顺着箭头加一
✓不要直接写 visits,只通过指针 p 把 visits 加 1。程序要显示 11。
11
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}
指着最便宜的
✓让 cheapest 指着 prices 里最小的元素,再通过 cheapest 把它改成 0。程序要显示 12 0 9。
12 0 9
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}
修好它:没有优惠券
✓没有优惠券时,coupon 是 nullptr,程序却顺着它去找。改一改 pay:有优惠券就减去它的值,没有就不减。程序要显示 80 和 100 两行。
80 100
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}
这一课学会了
- 指针是存地址的变量:
int* p = &score;,&取地址,*p顺着地址找到那个变量。 - 地址每次运行都不一样,一般不显示;
p == q比地址,*p == *q比里面的值。 - 指针参数让函数改到外面的变量:调用写
&stock,函数里写*count。 nullptr表示不指着谁,用之前先判断;顺着空指针或没赋值的指针去找是未定义行为(引擎会停下)。- 指针可以换指向(
p = &b),引用不行。 - 指向对象时用
p->name,等于(*p).name。成员函数里的this指着对象自己,balance_就是this->balance_。 - 能用引用就用引用;「可能没有」或「要换」时才用指针。