@@ -203,7 +203,7 @@ some_c_function(foo.get());
203
203
204
204
> {{ icon.detail }} 在[错误处理专题](error_code.md)中有进一步的详解。
205
205
206
- 然而这有时我们会忘记在提前返回的分支中 delete 之前分配过的所有智能指针 。
206
+ 然而这有时我们会忘记在提前返回的分支中 delete 之前分配过的所有指向动态内存的指针 。
207
207
208
208
```cpp
209
209
int func() {
@@ -219,6 +219,101 @@ int func() {
219
219
}
220
220
```
221
221
222
+ 过去,人们使用 ` goto ` 大法拙劣地在提前返回时 ` delete ` 动态内存:
223
+
224
+ ``` cpp
225
+ int main () {
226
+ Foo *foo1, *foo2;
227
+ int ret = 0;
228
+ foo1 = new Foo();
229
+ ...
230
+ if (出错) {
231
+ ret = -1;
232
+ goto out_foo1;
233
+ }
234
+ ...
235
+ Foo *foo2 = new Foo();
236
+ ...
237
+ if (出错) {
238
+ ret = -2;
239
+ goto out_foo2;
240
+ }
241
+ ...
242
+ out_foo2: delete foo2;
243
+ out_foo1: delete foo1;
244
+ return ret;
245
+ }
246
+ ```
247
+
248
+ 这对于“写”程序的人,其实还不算什么,无非就是注意匹配,反正都是一次性写完就得了。
249
+
250
+ 真正遭罪的是“改”程序的人,如果他要删掉foo1,那么他需要在两个地方来回跳转,如果foo1变成 ` new[] ` 了,那么他需要跳到下面把 ` delete foo1 ` 也改成 ` delete[] foo1 ` 。如果foo1要改名,那么还需要跳到下面 ` out_foo1: ` 标签也改了。如果要新增一个foo3指针,那还需要跳到上面加个 ` Foo *foo3; ` ,下面加个标签和 delete。
251
+
252
+ > {{ icon.fun }} BUG漫漫其修远兮,吾将上下而求索。
253
+
254
+ 你是否遇到过写程序梭哈,事后“上下求索”的情况?同一个变量 foo 的生命周期被极限撕扯,分居两地,极大的妨碍了我们改程序的效率。
255
+
256
+ 而统计表明,程序员10%的时间用于写代码,90%的时间花在改代码上。节约改代码的时间,就是节约程序员90%的生命。改代码不容易出错,可以省去软件中90%的BUG。
257
+
258
+ 所以,除非你是一次性交差项目不打算更新了,或者确认了改代码的人不会是你,否则必然要用包括智能指针、设计模式在内的各种手段竭力避免代码分散化。
259
+
260
+ 在一个庞大的代码系统中看到原始指针是最头疼的:
261
+
262
+ ``` cpp
263
+ Student *getstu (const char * name);
264
+ ```
265
+
266
+ 没有任何信息告诉我:
267
+
268
+ 1. 这个指针指向对象生命周期如何?
269
+ 2. 要我负责释放内存吗?
270
+ 3. 如何释放,`delete`、`delete[]`、`free`、还是 `fclose`?
271
+ 4. 可以是空指针吗?
272
+ 5. 数组还是单个对象?
273
+ 6. 如果是数组,那么长度多少?是C风格0结尾字符串吗?
274
+ 7. 是否移交所有权?
275
+ 8. 共享或独占?
276
+ 9. 该资源可以拷贝吗?
277
+ 10. 如果可以,如何拷贝?
278
+
279
+ 而你只能通过查阅文档,才能确认这些信息,拖慢了开发效率。
280
+
281
+ 而使用 RAII 容器(不仅是只能指针)作为类型,就能让人一眼就看出以上10个信息。
282
+
283
+ ```cpp
284
+ gsl::not_null<std::unique_ptr<Student>> getstu1(std::string_view name);
285
+ Student &getstu2(std::string_view name);
286
+ ```
287
+
288
+ 以上代码中,一看就明白,getstu1会移交所有权,且不可能为空;getstu2不转移所有权,仅仅只是提供给调用者访问,且不可能为空。
289
+
290
+ 传统指针因为语义不明确,功能多样化,有用错的可能,例如用户可能偷懒不看文档,就擅自瞎写:
291
+
292
+ ``` cpp
293
+ char name;
294
+ Student *stu = getstu(&name);
295
+ if (stu == NULL ) exit(1 );
296
+ free (stu);
297
+ ```
298
+
299
+ 实际上 `getstu` 的参数 `name` 需要是一个 C 风格 0 结尾字符串,用户却不小心当作单个 `char` 的指针写了,编译器没有报错。
300
+
301
+ 而 `stu` 实际上是 `getstu` 返回给调用者的临时引用,并不移交所有权,而用户却想当然的释放了。
302
+
303
+ 并且实际上 `getstu` 从不返回空指针,用户根本不用提心吊胆地检查。
304
+
305
+ 一个优质的函数接口,就不应该给用户这种犯错的机会。
306
+
307
+ 我知道,你会说,`std::string` 不也能从 `&name` 构造,放任编译通过,不也会犯错吗?
308
+
309
+ 是这样的,你甚至可以从 `0` 构造 `std::string`,编译一样通过,程序会直接崩溃:
310
+
311
+ ```cpp
312
+ std::string s = 0; // 0 被当作 NULL,从而调用构造函数 std::string(const char *)
313
+ ```
314
+
315
+ 标准库里不符合小彭老师设计模式的多了去了,标准库的垃圾是历史遗留问题,不是小彭老师的问题。
316
+
222
317
而智能指针,不论是提前返回还是最终的返回,只要是函数结束了,都能自动释放。智能指针使得程序员写出“提前返回式”毫无精神压力,再也不用惦记着哪些需要释放。
223
318
224
319
``` cpp
0 commit comments