new 和 delete
到现在为止,变量都是在函数里声明的,函数结束它就没了。这些变量放在栈上,生和死都由编译器管。C++ 还有另一块地方叫堆:用 new 向它要一块,用完自己用 delete 还回去。
1#include <iostream>2 3int main() {4 int* stock = new int(40);5 std::cout << *stock << '\n';6 *stock -= 15;7 std::cout << *stock << '\n';8 delete stock;9 return 0;10}
40 25
new int(40) 在堆上建一个 int,值是 40,给你它的地址。这个 int 没有名字,只能通过指针 stock 找到它。delete stock; 把它还回去。点单步:走过第 4 行,堆上出现一个块,stock 的箭头指着它;走过第 8 行,块变灰,已经还了。
new 一个对象
new 也能建对象。new 的时候构造函数运行,delete 的时候析构函数运行:
1#include <iostream>2#include <string>3 4class Order {5public:6 Order(const std::string& item) : item_(item) {7 std::cout << "创建订单:" << item_ << '\n';8 }9 ~Order() {10 std::cout << "删除订单:" << item_ << '\n';11 }12 void show() const {13 std::cout << "订单:" << item_ << '\n';14 }15 16private:17 std::string item_;18};19 20int main() {21 std::cout << "开始\n";22 Order* order = new Order{"咖啡"};23 order->show();24 delete order;25 std::cout << "结束\n";26 return 0;27}
开始 创建订单:咖啡 订单:咖啡 删除订单:咖啡 结束
点单步,看三行字和堆上的块:第 22 行出现块、显示「创建订单」,第 24 行显示「删除订单」、块变灰。指针拿到的是对象的地址,所以用 -> 调成员函数(上一课讲过)。
活过函数
栈上的变量,函数一结束就没了。堆上的东西不一样:只要没人 delete,它就一直在。所以一个函数可以在堆上建好东西,把地址交出去:
1#include <iostream>2#include <string>3 4class Order {5public:6 Order(const std::string& item) : item_(item) {7 std::cout << "创建订单:" << item_ << '\n';8 }9 ~Order() {10 std::cout << "删除订单:" << item_ << '\n';11 }12 void show() const {13 std::cout << "订单:" << item_ << '\n';14 }15 16private:17 std::string item_;18};19 20Order* make_order(const std::string& item) {21 Order* order = new Order{item};22 return order;23}24 25int main() {26 Order* tea = make_order("奶茶");27 tea->show();28 delete tea;29 return 0;30}
创建订单:奶茶 订单:奶茶 删除订单:奶茶
点单步:make_order 那一层结束、从画面上消失了,堆上的订单还在,main 里的 tea 接着指着它。代价是:谁拿到了这个地址,谁就要记得 delete。
new[] 和 delete[]
new 类型[个数] 一次要一排,个数可以是变量。用 p[i] 取第 i 个,和 vector 一样。还的时候要写 delete[]:
1#include <iostream>2 3int main() {4 int days = 5;5 double* temps = new double[days];6 for (int i = 0; i < days; i++) {7 temps[i] = 20 + i * 1.5;8 }9 double total = 0;10 for (int i = 0; i < days; i++) {11 total += temps[i];12 }13 std::cout << "平均气温:" << total / days << '\n';14 delete[] temps;15 return 0;16}
平均气温:23
点单步,看堆上那一排格子一个个填上。new 配 delete,new[] 配 delete[],不能混着用。
注意: new double[days] 要来的格子里没有确定的值,要自己一个个填,填之前别去读。想一开始就有值,用 vector 更方便。
忘了 delete:内存泄漏
要了不还,叫内存泄漏。程序不会报错,输出也看不出来:
1#include <iostream>2#include <string>3 4class Order {5public:6 Order(const std::string& item) : item_(item) {7 std::cout << "创建订单:" << item_ << '\n';8 }9 ~Order() {10 std::cout << "删除订单:" << item_ << '\n';11 }12 13private:14 std::string item_;15};16 17int main() {18 Order* order = new Order{"蛋糕"};19 order = new Order{"面包"};20 delete order;21 return 0;22}
创建订单:蛋糕 创建订单:面包 删除订单:面包
只有一行「删除订单」。点单步:走过第 19 行,order 的箭头从「蛋糕」转到了「面包」,「蛋糕」那个块再也没有箭头指着,没人能用它,也没人能还它。走到最后,它还留在堆上,标着「内存泄漏」。
跑一下就结束的程序,结束时系统会把内存都收回去,看不出害处。可是一直开着的程序(服务器、游戏)每次漏一点,占的内存就越来越多。
这段程序显示什么?
1#include <iostream>2 3class Box {4public:5 Box() {6 std::cout << "+";7 }8 ~Box() {9 std::cout << "-";10 }11};12 13int main() {14 for (int i = 0; i < 3; i++) {15 Box* b = new Box{};16 if (i == 1) {17 delete b;18 }19 }20 std::cout << '\n';21 return 0;22}
每一圈 new 一个,显示 +;只有 i == 1 那一圈 delete,显示 -。另外两个 Box 漏掉了,它们的析构函数从来没有运行。b 是循环里的变量,每圈结束它就没了,可它指着的堆上的块不会跟着没。
delete 以后还在用
反过来的错更危险:已经 delete 了,指针还指着那里,接着用:
1#include <iostream>2 3int main() {4 int* balance = new int(500);5 delete balance;6 *balance += 100;7 std::cout << *balance << '\n';8 return 0;9}
heap-use-after-free发生了什么: 停在第 6 行:这块内存已经还回去了(delete 过了;vector、string 变大搬家时,旧的位置也会还回去),不能再用。
delete 以后,balance 里的地址没变,可那块内存已经不是你的了,随时可能分给别人。真的程序可能照样显示 600,可能显示别的,也可能过很久才在别处出事,这是未定义行为。我们的引擎在第 6 行停下,点「看看是怎么出错的」。
同一块 delete 两次也不行。两个指针指着同一块时最容易出这种错:
1#include <iostream>2 3int main() {4 int* points = new int(30);5 int* copy = points;6 *copy += 5;7 delete points;8 delete copy;9 return 0;10}
attempting double-free发生了什么: 停在第 8 行:这块内存已经 delete 过了,不能再 delete 一次。
copy = points 复制的只是地址,两根箭头指着同一个块。第 7 行已经还了,第 8 行又还一次,引擎停下。
小技巧: delete p; 以后马上写 p = nullptr;。之后不小心再用,至少是一个看得出来的空指针;delete nullptr 什么也不做,也不算错。
new[] 配了 delete
new[] 要来的一排,只写 delete 还,也是未定义行为:
1#include <iostream>2 3int main() {4 int* scores = new int[3];5 scores[0] = 90;6 scores[1] = 75;7 scores[2] = 82;8 std::cout << scores[0] + scores[1] + scores[2] << '\n';9 delete scores;10 return 0;11}
247
alloc-dealloc-mismatch发生了什么: 停在第 9 行:这块内存是 new[] 来的(一组),要用 delete[],不是 delete。
加上 -Wall 时 g++ 会给一个警告(mismatched-new-delete);我们的引擎在第 9 行停下。改成 delete[] scores; 就好了。
为什么少写 new
上面这些错,全靠人记着:每个 new 有且只有一个对应的 delete,时机要对,类型要配。程序一大,函数中途 return,就很容易漏。所以现代 C++ 里很少直接写 new 和 delete:
- 要一排东西,用
std::vector:它自己在堆上要格子,自己还。 - 要堆上的一个对象,用下一课的智能指针:没人用了,它自己
delete。
把上面算平均气温的例子换成 vector,没有一个 delete,也不会漏:
1#include <iostream>2#include <vector>3 4int main() {5 int days = 5;6 std::vector<double> temps(days);7 for (int i = 0; i < days; i++) {8 temps[i] = 20 + i * 1.5;9 }10 double total = 0;11 for (double t : temps) {12 total += t;13 }14 std::cout << "平均气温:" << total / days << '\n';15 return 0;16}
平均气温:23
std::vector<double> temps(days) 是圆括号:建 days 个格子,都是 0。(写成花括号 {days} 就成了「一个元素,值是 5」。)
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。这一课的练习,做完都点单步走到最后,看看堆上还剩什么。
还回去
✓程序 new 了一个 Ticket,却没有还。加一行,让它的析构函数运行,程序显示下面三行。
出票:12 号 看电影 作废:12 号
1#include <iostream>2 3class Ticket {4public:5 Ticket(int seat) : seat_(seat) {6 std::cout << "出票:" << seat_ << " 号\n";7 }8 ~Ticket() {9 std::cout << "作废:" << seat_ << " 号\n";10 }11 12private:13 int seat_;14};15 16int main() {17 Ticket* t = new Ticket{12};18 std::cout << "看电影\n";19 return 0;20}
堆上的平方
✓用 new int[n] 要 n 个格子,第 i 个存 (i + 1) * (i + 1),再把它们加起来显示,最后用 delete[] 还回去。n 是 5,程序要显示 55。
55
1#include <iostream>2 3int main() {4 int n = 5;5 int* squares = new int[n];6 // 填好每个格子7 int total = 0;8 // 把格子里的数加进 total9 std::cout << total << '\n';10 // 还回去11 return 0;12}
修好它:还早了
✓程序在 delete 以后还在用 price。调换顺序(不要删掉 delete),让程序显示 打折后:48。
打折后:48
1#include <iostream>2 3int main() {4 int* price = new int(60);5 *price = *price * 4 / 5;6 delete price;7 std::cout << "打折后:" << *price << '\n';8 return 0;9}
这一课学会了
new在堆上建一个东西,给你它的地址;delete还回去。new对象时构造函数运行,delete时析构函数运行。- 堆上的东西不会随函数结束消失,可以交给别的函数,但总得有人
delete。 new 类型[n]配delete[];new配delete,不能混。- 忘了
delete是内存泄漏:不报错,输出也看不出来,点单步看堆上剩下的块。 delete以后再用、delete两次、new[]配delete,都是未定义行为(引擎会停下)。- 现代 C++ 少写
new/delete:一排东西用vector,一个对象用下一课的智能指针。