Skip to content

Commit 03889fc

Browse files
authored
Apply suggestions from code review
1 parent 2977e0a commit 03889fc

File tree

1 file changed

+81
-15
lines changed

1 file changed

+81
-15
lines changed

docs/no_more_new.md

+81-15
Original file line numberDiff line numberDiff line change
@@ -222,31 +222,97 @@ int func() {
222222
过去,人们使用 `goto` 大法拙劣地在提前返回时 `delete` 动态内存:
223223

224224
```cpp
225-
int func() {
226-
Foo *foo1 = nullptr;
227-
Foo *foo2 = nullptr;
225+
int main() {
226+
Foo *foo1, *foo2;
227+
int ret = 0;
228228
foo1 = new Foo();
229229
...
230-
foo2 = new Foo();
231-
...
232230
if (出错) {
233-
goto EXIT;
231+
ret = -1;
232+
goto out_foo1;
234233
}
235234
...
236-
EXIT:
237-
if (nullptr != foo1) {
238-
delete foo1;
239-
}
240-
if (nullptr != foo2) {
241-
delete foo2;
235+
Foo *foo2 = new Foo();
236+
...
237+
if (出错) {
238+
ret = -2;
239+
goto out_foo2;
242240
}
243-
return 0;
241+
...
242+
out_foo2: delete foo2;
243+
out_foo1: delete foo1;
244+
return ret;
244245
}
245246
```
246247

247-
这就会让编写程序的人精神紧绷,更加仔细的检查 `new``delete` 是否配对,`goto EXIT` 会不会有其他负面影响。享受不到编程的快乐就算了,反而在遭罪。。。
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+
```
248314

249-
此外,`new``delete` 使用不配套时,也会导致严重的错误。由于 `new``delete` 是基于指针操作的,但是在很庞大的代码系统中我们拿到一个指针 `data *ptr` 后。这个 `ptr` 是空指针吗?还是已经被释放了吗?还是已经有内存了?还是交由我们申请内存?很容易忘记 `new` 而直接 `delete`,或者对同一个指针 `new` 了两次,或者没有 `delete` 等等。看到屎山代码后的坏心情会导致代码维护难度指数级增加
315+
标准库里不符合小彭老师设计模式的多了去了,标准库的垃圾是历史遗留问题,不是小彭老师的问题
250316

251317
而智能指针,不论是提前返回还是最终的返回,只要是函数结束了,都能自动释放。智能指针使得程序员写出“提前返回式”毫无精神压力,再也不用惦记着哪些需要释放。
252318

0 commit comments

Comments
 (0)