@@ -421,7 +421,7 @@ <h2 id="index-_1">前言</h2>
421
421
<blockquote>
422
422
<p><img src="../img/bulb.png" height="30px" width="auto" style="margin: 0; border: none"/> 本书还在持续更新中……要追番的话,可以在 <a href="https://github.com/parallel101/cppguidebook">GitHub</a> 点一下右上角的 “Watch” 按钮,每当小彭老师提交新 commit,GitHub 会向你发送一封电子邮件,提醒你小彭老师更新了。</p>
423
423
</blockquote>
424
- <p>更新时间:2024年09月09日 17:24:48 (UTC+08:00)</p>
424
+ <p>更新时间:2024年09月13日 12:37:56 (UTC+08:00)</p>
425
425
<p><a href="https://parallel101.github.io/cppguidebook">在 GitHub Pages 浏览本书</a> | <a href="https://142857.red/book">在小彭老师自己维护的镜像上浏览本书</a></p>
426
426
<h2 id="index-_2">格式约定</h2>
427
427
<blockquote>
@@ -6098,7 +6098,7 @@ <h2 id="no_more_new-raii-delete">RAII 比起手动 delete 的优势</h2>
6098
6098
<blockquote>
6099
6099
<p><img src="../img/question.png" height="30px" width="auto" style="margin: 0; border: none"/> 在<a href="#error_code">错误处理专题</a>中有进一步的详解。</p>
6100
6100
</blockquote>
6101
- <p>然而这有时我们会忘记在提前返回的分支中 delete 之前分配过的所有智能指针 。</p>
6101
+ <p>然而这有时我们会忘记在提前返回的分支中 delete 之前分配过的所有指向动态内存的指针 。</p>
6102
6102
<pre><code class="language-cpp">int func() {
6103
6103
Foo *foo = new Foo();
6104
6104
...
@@ -6111,6 +6111,74 @@ <h2 id="no_more_new-raii-delete">RAII 比起手动 delete 的优势</h2>
6111
6111
return 0;
6112
6112
}
6113
6113
</code></pre>
6114
+ <p>过去,人们使用 <code>goto</code> 大法拙劣地在提前返回时 <code>delete</code> 动态内存:</p>
6115
+ <pre><code class="language-cpp">int main() {
6116
+ Foo *foo1, *foo2;
6117
+ int ret = 0;
6118
+ foo1 = new Foo();
6119
+ ...
6120
+ if (出错) {
6121
+ ret = -1;
6122
+ goto out_foo1;
6123
+ }
6124
+ ...
6125
+ Foo *foo2 = new Foo();
6126
+ ...
6127
+ if (出错) {
6128
+ ret = -2;
6129
+ goto out_foo2;
6130
+ }
6131
+ ...
6132
+ out_foo2: delete foo2;
6133
+ out_foo1: delete foo1;
6134
+ return ret;
6135
+ }
6136
+ </code></pre>
6137
+ <p>这对于“写”程序的人,其实还不算什么,无非就是注意匹配,反正都是一次性写完就得了。</p>
6138
+ <p>真正遭罪的是“改”程序的人,如果他要删掉foo1,那么他需要在两个地方来回跳转,如果foo1变成 <code>new[]</code> 了,那么他需要跳到下面把 <code>delete foo1</code> 也改成 <code>delete[] foo1</code>。如果foo1要改名,那么还需要跳到下面 <code>out_foo1:</code> 标签也改了。如果要新增一个foo3指针,那还需要跳到上面加个 <code>Foo *foo3;</code>,下面加个标签和 delete。</p>
6139
+ <blockquote>
6140
+ <p><img src="../img/awesomeface.png" height="30px" width="auto" style="margin: 0; border: none"/> BUG漫漫其修远兮,吾将上下而求索。</p>
6141
+ </blockquote>
6142
+ <p>你是否遇到过写程序梭哈,事后“上下求索”的情况?同一个变量 foo 的生命周期被极限撕扯,分居两地,极大的妨碍了我们改程序的效率。</p>
6143
+ <p>而统计表明,程序员10%的时间用于写代码,90%的时间花在改代码上。节约改代码的时间,就是节约程序员90%的生命。改代码不容易出错,可以省去软件中90%的BUG。</p>
6144
+ <p>所以,除非你是一次性交差项目不打算更新了,或者确认了改代码的人不会是你,否则必然要用包括智能指针、设计模式在内的各种手段竭力避免代码分散化。</p>
6145
+ <p>在一个庞大的代码系统中看到原始指针是最头疼的:</p>
6146
+ <pre><code class="language-cpp">Student *getstu(const char *name);
6147
+ </code></pre>
6148
+ <p>没有任何信息告诉我:</p>
6149
+ <ol>
6150
+ <li>这个指针指向对象生命周期如何?</li>
6151
+ <li>要我负责释放内存吗?</li>
6152
+ <li>如何释放,<code>delete</code>、<code>delete[]</code>、<code>free</code>、还是 <code>fclose</code>?</li>
6153
+ <li>可以是空指针吗?</li>
6154
+ <li>数组还是单个对象?</li>
6155
+ <li>如果是数组,那么长度多少?是C风格0结尾字符串吗?</li>
6156
+ <li>是否移交所有权?</li>
6157
+ <li>共享或独占?</li>
6158
+ <li>该资源可以拷贝吗?</li>
6159
+ <li>如果可以,如何拷贝?</li>
6160
+ </ol>
6161
+ <p>而你只能通过查阅文档,才能确认这些信息,拖慢了开发效率。</p>
6162
+ <p>而使用 RAII 容器(不仅是只能指针)作为类型,就能让人一眼就看出以上10个信息。</p>
6163
+ <pre><code class="language-cpp">gsl::not_null<std::unique_ptr<Student>> getstu1(std::string_view name);
6164
+ Student &getstu2(std::string_view name);
6165
+ </code></pre>
6166
+ <p>以上代码中,一看就明白,getstu1会移交所有权,且不可能为空;getstu2不转移所有权,仅仅只是提供给调用者访问,且不可能为空。</p>
6167
+ <p>传统指针因为语义不明确,功能多样化,有用错的可能,例如用户可能偷懒不看文档,就擅自瞎写:</p>
6168
+ <pre><code class="language-cpp">char name;
6169
+ Student *stu = getstu(&name);
6170
+ if (stu == NULL) exit(1);
6171
+ free(stu);
6172
+ </code></pre>
6173
+ <p>实际上 <code>getstu</code> 的参数 <code>name</code> 需要是一个 C 风格 0 结尾字符串,用户却不小心当作单个 <code>char</code> 的指针写了,编译器没有报错。</p>
6174
+ <p>而 <code>stu</code> 实际上是 <code>getstu</code> 返回给调用者的临时引用,并不移交所有权,而用户却想当然的释放了。</p>
6175
+ <p>并且实际上 <code>getstu</code> 从不返回空指针,用户根本不用提心吊胆地检查。</p>
6176
+ <p>一个优质的函数接口,就不应该给用户这种犯错的机会。</p>
6177
+ <p>我知道,你会说,<code>std::string</code> 不也能从 <code>&name</code> 构造,放任编译通过,不也会犯错吗?</p>
6178
+ <p>是这样的,你甚至可以从 <code>0</code> 构造 <code>std::string</code>,编译一样通过,程序会直接崩溃:</p>
6179
+ <pre><code class="language-cpp">std::string s = 0; // 0 被当作 NULL,从而调用构造函数 std::string(const char *)
6180
+ </code></pre>
6181
+ <p>标准库里不符合小彭老师设计模式的多了去了,标准库的垃圾是历史遗留问题,不是小彭老师的问题。</p>
6114
6182
<p>而智能指针,不论是提前返回还是最终的返回,只要是函数结束了,都能自动释放。智能指针使得程序员写出“提前返回式”毫无精神压力,再也不用惦记着哪些需要释放。</p>
6115
6183
<pre><code class="language-cpp">int func() {
6116
6184
shared_ptr<Foo> foo = make_shared<Foo>();
0 commit comments