Kodewick
第 20 课,共 30 课目录

new 和 delete

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

到现在为止,变量都是在函数里声明的,函数结束它就没了。这些变量放在栈上,生和死都由编译器管。C++ 还有另一块地方叫堆:用 new 向它要一块,用完自己用 delete 还回去。

main.cpp
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 的时候析构函数运行:

main.cpp
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,它就一直在。所以一个函数可以在堆上建好东西,把地址交出去:

main.cpp
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[]:

main.cpp
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:内存泄漏

要了不还,叫内存泄漏。程序不会报错,输出也看不出来:

main.cpp
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}

delete 以后还在用

反过来的错更危险:已经 delete 了,指针还指着那里,接着用:

main.cpp
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 两次也不行。两个指针指着同一块时最容易出这种错:

main.cpp
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 还,也是未定义行为:

main.cpp
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,也不会漏:

main.cpp
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」。)

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。这一课的练习,做完都点单步走到最后,看看堆上还剩什么。

练习 1

还回去

程序 new 了一个 Ticket,却没有还。加一行,让它的析构函数运行,程序显示下面三行。

要显示
出票:12 号
看电影
作废:12 号
exercise1.cpp
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}
输出
点「运行」或按 Ctrl+Enter
练习 2

堆上的平方

用 new int[n] 要 n 个格子,第 i 个存 (i + 1) * (i + 1),再把它们加起来显示,最后用 delete[] 还回去。n 是 5,程序要显示 55。

要显示
55
exercise2.cpp
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}
输出
点「运行」或按 Ctrl+Enter
练习 3

修好它:还早了

程序在 delete 以后还在用 price。调换顺序(不要删掉 delete),让程序显示 打折后:48。

要显示
打折后:48
exercise3.cpp
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}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • new 在堆上建一个东西,给你它的地址;delete 还回去。new 对象时构造函数运行,delete 时析构函数运行。
  • 堆上的东西不会随函数结束消失,可以交给别的函数,但总得有人 delete。
  • new 类型[n] 配 delete[];new 配 delete,不能混。
  • 忘了 delete 是内存泄漏:不报错,输出也看不出来,点单步看堆上剩下的块。
  • delete 以后再用、delete 两次、new[] 配 delete,都是未定义行为(引擎会停下)。
  • 现代 C++ 少写 new / delete:一排东西用 vector,一个对象用下一课的智能指针。