C++ 中的构造函数
<p></p><div class="toc"><div class="toc-container-header">目录</div><ul><li>核心逻辑</li><li>构造函数的执行流<ul><li>为什么首选初始化列表?</li></ul></li><li>构造函数的分类<ul><li>默认构造函数(Default Constructor)</li><li>参数化构造函数(Parameterized Constructor)</li><li>拷贝构造函数(Copy Constructor)</li><li>移动构造函数(Move Constructor)</li></ul></li><li>关键机制与陷阱<ul><li><code>explicit</code> 关键字:拒绝隐式转换</li><li>委托构造(Delegating Constructors)</li><li>构造与虚函数</li></ul></li><li>RAII 与构造函数</li><li>相关关键字<ul><li>控制编译器行为<ul><li><code>= default</code></li><li><code>= delete</code>(C++11)</li><li><code>using</code>(继承构造函数)</li></ul></li><li>性能优化<ul><li><code>noexcept</code></li><li><code>constexpr</code>(C++11/14)</li></ul></li><li>逻辑控制与异常处理<ul><li><code>explicit</code></li><li><code>try</code>(Function-try block)</li></ul></li></ul></li></ul></div><p></p><blockquote>
<p><strong>本文首发于我的个人博客:Better Mistakes</strong></p>
<p><strong>版权声明</strong>:本文为原创文章,转载请附上原文出处链接及本声明。<br>
由于技术迭代较快,文章内容可能随时更新(含勘误及补充)。为了确保您看到的是最新版本,并获得更好的代码阅读体验,请访问:</p>
<p>🍭 <strong>原文链接</strong>:https://bfmhno3.github.io/note/constructor-in-cpp/](https://bfmhno3.github.io/note/constructor-in-cpp/)</p>
</blockquote>
<hr>
<p>对于 C++ 对象而言,我们认为:对象 = 内存 + 语义(不变量)。</p>
<ul>
<li>内存:仅仅是电子与硅晶体中状态未知的比特位。</li>
<li><strong>语义</strong>:这段内存代表什么含义(是 <code>int</code>、是 <code>char</code> 还是 <code>float</code>),以及它必须满足的条件(“不变量”,Invariant)。</li>
</ul>
<p><strong>构造函数</strong>(<em>Constructor</em>)的本质就是将 “原始、混沌” 的内存强制转换为 “持有特定语义的、合法的对象” 的原子操作过程。</p>
<h2 id="核心逻辑">核心逻辑</h2>
<p>在 C 语言中,创建一个 <code>struct</code> 通常分为两步:</p>
<ol>
<li>分配内存(<code>malloc</code> 或栈上声明)</li>
<li>赋值(<code>init</code> 函数或手动赋值)</li>
</ol>
<p>问题在于:如果在第 1 步和第 2 步之间使用该对象,就会导致灾难(未定义行为)。或者,如果使用者忘记了第 2 步,系统就会处于 “非法状态”。</p>
<p>C++ 引入构造函数就是为了保证:</p>
<blockquote>
<p>如果一个对象存在,那么它一定是合法的。</p>
</blockquote>
<p>构造函数保证了<strong>初始化</strong>(<em>Initialization</em>)与<strong>定义</strong>(<em>Defination</em>)的不可分割性。</p>
<h2 id="构造函数的执行流">构造函数的执行流</h2>
<p>当你写下 <code>T object(args);</code> 时,编译器实际执行了以下步骤:</p>
<ol>
<li><strong>分配内存</strong>:在栈或堆上找到一块足够容纳 <code>sizeof(T)</code> 的空间。此时,内存里的数据是随机的(Garbage)。</li>
<li><strong>执行初始化列表</strong>(Initialization List):这是真正的初始化时刻。</li>
<li><strong>执行函数体</strong>(Function Body):这实际上是后续的计算或赋值操作,而非初始化。</li>
</ol>
<h3 id="为什么首选初始化列表">为什么首选初始化列表?</h3>
<p>因为 C++ 规定成员变量在进入构造函数体 <code>{}</code> 之前必须完成构建。</p>
<pre><code class="language-cpp">Class() : member(value) {} // 直接在内存位置上构造 member
</code></pre>
<p>使用初始化列表的成本仅为 1 次构造。</p>
<pre><code class="language-cpp">Class() { member = value; }
</code></pre>
<p>过程:</p>
<ol>
<li>调用 <code>member</code> 的默认构造函数(无参)。</li>
<li>调用 <code>member</code> 的赋值运算符 <code>operator=</code>。</li>
</ol>
<p>在这个过程中的成本为:1 次构造 + 1 次赋值(还可能设计旧内存释放和新内存申请)。</p>
<blockquote>
<p>初始化列表不仅是效率优化,对于 <code>const</code> 成员或 <code>reference</code>(引用)成员,它是<strong>唯一</strong>的初始化方式,因为它们创建后不可修改(不可赋值)。</p>
</blockquote>
<h2 id="构造函数的分类">构造函数的分类</h2>
<p>根据对象资源管理的不同需求,构造函数演化出了四种主要形态。我们将用<strong>资源所有权</strong>的视角来区分它们。</p>
<h3 id="默认构造函数default-constructor">默认构造函数(Default Constructor)</h3>
<ul>
<li>语义:无中生有</li>
<li>形式:<code>T()</code></li>
<li>视角:当对象被创建但外界未提供任何信息时,对象应处于什么状态?通常是 “空状态” 或 “零状态”</li>
<li>注意:如果类中包含原始指针,编译器生成的默认构造函数不会置空指针(由于 C 的遗留包袱),这会导致悬垂指针。因此现代 C++ 提倡显式定义或使用成员默认初始化(<code>int* p = nullptr;</code>)</li>
</ul>
<h3 id="参数化构造函数parameterized-constructor">参数化构造函数(Parameterized Constructor)</h3>
<ul>
<li>语义:根据蓝图定制</li>
<li>形式:<code>T(args...)</code></li>
<li>视角:将外部数据约束映射到内部不变量。例如,创建 “圆” 对象,参数是半径。构造函数必须检查 <code>radius > 0</code>,这就是维护 “不变量”</li>
</ul>
<h3 id="拷贝构造函数copy-constructor">拷贝构造函数(Copy Constructor)</h3>
<ul>
<li>语义:复制(细胞分裂、克隆)</li>
<li>形式:<code>T(const T& other)</code></li>
<li>视角:
<ul>
<li>如果对象时<strong>值语义</strong>(如整数、坐标),直接按位拷贝(Shallow Copy)</li>
<li>如果对象持有<strong>资源</strong>(如堆内存指针、文件句柄),必须进行<strong>深拷贝</strong>(<em>Deep Copy</em>)</li>
<li>本质矛盾:如果只复制指针,两个对象指向同一块内存,析构时会发生 “Double Free” 错误。因此拷贝构造函数必须重新分配资源。</li>
</ul>
</li>
</ul>
<h3 id="移动构造函数move-constructor">移动构造函数(Move Constructor)</h3>
<p>移动构造函数是 C++11 提出的革命性进步。</p>
<ul>
<li>语义:所有权转移(器官移植)</li>
<li>形式:<code>T(T&& other)</code></li>
<li>视角:
<ul>
<li>在 C++98 中,如果要将一个临时对象(即将销毁)放入容器,比如先复制再销毁。这极度浪费性能(如复制一个巨大的 <code>std::vector</code>)</li>
<li>移动构造函数利用<strong>右值引用</strong>(<code>&&</code>),识别出 <code>other</code> 是一个即将消亡的对象。</li>
<li>它<strong>偷走</strong> <code>other</code> 的资源(指针指向新主,旧指针置空),而非复制数据</li>
<li>代价:极低(仅是指针赋值)</li>
</ul>
</li>
</ul>
<h2 id="关键机制与陷阱">关键机制与陷阱</h2>
<h3 id="explicit-关键字拒绝隐式转换"><code>explicit</code> 关键字:拒绝隐式转换</h3>
<p>C++ 默认允许单参数构造函数进行隐式类型转换。</p>
<pre><code class="language-cpp">struct Buffer { Buffer(int size) { ... } };
void func(Buffer b);
func(42); // 编译器偷偷执行了 Buffer(42),可能并不是你想要的
</code></pre>
<p>从安全角度(Safety First)出发,隐式类型转换破坏了强类型系统。标记 <code>explicit</code> 禁止这种 “自作聪明” 的行为,强制显式调用。</p>
<h3 id="委托构造delegating-constructors">委托构造(Delegating Constructors)</h3>
<p>允许一个构造函数调用同类的另一个构造函数。这是为了准许 <strong>DRY</strong>(<em>Don't Repeat Yourself</em>)原则,防止初始化逻辑碎片化。</p>
<h3 id="构造与虚函数">构造与虚函数</h3>
<p><strong>永远不要在构造函数中调用虚函数</strong>。</p>
<ul>
<li>原理:在基类构造期间,派生类的部分尚未初始化。为了安全,C++ 此时将对象视为基类类型。虚函数表(<code>vtalbe</code>)指针指向基类表,多态失效。</li>
</ul>
<h2 id="raii-与构造函数">RAII 与构造函数</h2>
<p>将上述所有内容串联起来的概念就是 RAII(<em>Resource Acquisition Is Initialization</em>),这是 C++ 的灵魂。</p>
<ul>
<li><strong>资源获取即初始化</strong>:资源的生命周期严格绑定对象的生命周期</li>
<li><strong>构造函数</strong>:资源的获取点(锁住互斥量、打开文件、分配内存)</li>
<li><strong>析构函数</strong>:资源的释放点(解锁、关闭、释放)</li>
</ul>
<p>C++ 的构造函数不仅仅是用来 “赋值” 的函数,它是类型系统安全性的守门人,是资源管理自动化的起点。</p>
<p>掌握构造函数,不仅仅是记住语法,而是要时刻思考:</p>
<blockquote>
<p>这个对象诞生的一瞬间,我如何保证它拥有了所需的资源,且处于绝对合法的状态?</p>
</blockquote>
<h2 id="相关关键字">相关关键字</h2>
<h3 id="控制编译器行为">控制编译器行为</h3>
<p>C++ 编译器通常会 “自作聪明” 地为你生成默认构造、拷贝构造等。以下关键字则可以用于<strong>精确控制</strong>这种自动行为。</p>
<h4 id="-default"><code>= default</code></h4>
<ul>
<li>语义:出厂设置</li>
</ul>
<p>当你手写了一个参数化构造函数 <code>T(int a)</code> 后,编译器认为你是一个有主见的人,于是不再自动生成无参的默认构造函数 <code>T()</code>。如果此时你又想要那个 “空” 的默认构造函数,不需要再手写个空函数体 <code>{}</code>(这会导致它变成 “用户提供的”,从而失去某些 trivial/POD 特性),直接用 <code>= default</code> 让编译器恢复它的默认生成逻辑。</p>
<pre><code class="language-cpp">struct Example {
Example(int a); // 自定义构造
Example() = default; // 强制找回默认构造,且比手写 {} 更高效
};
</code></pre>
<h4 id="-deletec11"><code>= delete</code>(C++11)</h4>
<ul>
<li>语义:此路不通</li>
</ul>
<p>有些对象在语义上是<strong>独一无二</strong>的(例如:单例模式、硬件驱动句柄 <code>Mutex</code>、<code>FileStream</code>),它们<strong>绝不能</strong>被拷贝。</p>
<p>在 C++11 之前,我们通过把拷贝构造函数设为 <code>private</code> 来防止拷贝。C++11 之后,可以直接在<strong>语法层面</strong> “删除” 这个函数的存在。</p>
<pre><code class="language-cpp">struct Mutex {
// 任何尝试拷贝代码的操作,在编译期间就会报错
Mutex(const Mutex&) = delete;
Mutex& operator(const Mutex&) = delete;
};
</code></pre>
<h4 id="using继承构造函数"><code>using</code>(继承构造函数)</h4>
<ul>
<li>语义:拿来主义</li>
</ul>
<p>派生类通常不会继承基类的构造函数。如果基类有 10 种构造方式,派生类想支持同样的 10 种,以前得手动写 10 个转发函数。</p>
<p><code>using</code> 关键字告诉编译器:把基类的构造函数直接 “引入” 到当前作用域。</p>
<pre><code class="language-cpp">struct Base {
Base(int); Base(std::string); Base(float);
};
struct Derived: Base {
using Base::Base; // 一句话,拥有了上述三种构造方式
};
</code></pre>
<h3 id="性能优化">性能优化</h3>
<p>这部分关键字主要服务于嵌入式开发和高性能计算,通过向编译器提供更多信息来优化机器码。</p>
<h4 id="noexcept"><code>noexcept</code></h4>
<ul>
<li>语义:我保证不惹麻烦(不抛出异常)</li>
</ul>
<p>这是<strong>移动语义</strong>(Move Semantics)生效的关键。当 <code>std::vector</code> 扩容时,它需要把旧数据搬到新内存。如果你的移动构造函数没有标记 <code>noexcept</code>,<code>std::vector</code> 为了内存安全(怕搬到一半抛异常,导致旧数据没了,新数据也没好),会放弃移动,强行降级为拷贝。</p>
<p>这在大数据量或高性能要求场景下会带来极大的损耗。</p>
<pre><code class="language-cpp">class BigData {
public:
// 承诺:移动操作绝不会失败,编译器看到这个才会大胆优化
BigData(BigData&& other) noexcept { ... }
};
</code></pre>
<h4 id="constexprc1114"><code>constexpr</code>(C++11/14)</h4>
<ul>
<li>语义:在编译时就已经准备好了</li>
</ul>
<p>如果一个对象的构造参数在编译时就是确定的常量,那么为什么要等到程序运行(Runtime)才去分配内存、赋值呢?</p>
<p><code>constexpr</code> 构造函数允许编译器在<strong>编译阶段</strong>就计算出对象的内存布局,并直接烧录在二进制文件的只读数据端(<code>.rodata</code>)或直接作为立即数嵌入指令中。</p>
<p>这对于<strong>嵌入式系统</strong>(节省运行时开销、Flash/RAM 布局)至关重要。</p>
<pre><code class="language-cpp">struct Point {
int x, y;
constexpr Point(int _x, int _y) : x(_x), y(_y) {}
};
// 编译后,p 甚至可能不存在,直接被优化为立即数操作
constexpr Point p(10, 20);
</code></pre>
<h3 id="逻辑控制与异常处理">逻辑控制与异常处理</h3>
<h4 id="explicit"><code>explicit</code></h4>
<p>在前文已经讲到。同时,除了单参数构造函数,<strong>多参数构造函数</strong>(C++11 列表初始化)也需要注意。</p>
<pre><code class="language-cpp">struct Vector3 {
explicit Vector3(float x, float y, float z);
};
void func(Vector3 v);
func({1.0, 2.0, 3.0}); // 错误!因为 explicit 禁止了 {list} -> Object 的隐式类型转换
func(Vector3{1.0, 2.0, 3.0}); // 正确,显式调用
</code></pre>
<h4 id="tryfunction-try-block"><code>try</code>(Function-try block)</h4>
<ul>
<li>语义:在进入内部前就能捕获错误</li>
</ul>
<p>构造函数分两步:初始化列表 <span class="math inline">\(\rightarrow\)</span> 函数体。如果在<strong>初始化列表</strong>阶段(比如基类构造、成员对象构造)抛出了异常,普通的 <code>try-catch</code> 包裹函数体是<strong>抓不住</strong>的。必须把 <code>try</code> 写在函数体外,这就是<strong>函数 try 块</strong>。</p>
<pre><code class="language-cpp">ResourceManager() try : core_resource(new core) {
// ... 函数体
} catch (...) {
// 能够捕获 core_resource 初始化时抛出的异常
// 注意:构造函数里的 catch 必定会再次抛出异常,因为对象构造函数失败了,必须通知外界
}
</code></pre>
<hr>
<blockquote>
<p>📢 <strong>写在最后</strong></p>
<p>如果你觉得这篇文章对你有帮助,欢迎到我的个人博客 <strong>Better Mistakes</strong> 逛逛。</p>
<p>在那里我归档了更多高质量的技术文章,也欢迎通过 RSS 订阅我的最新动态!</p>
</blockquote><br><br>
来源:https://www.cnblogs.com/bfmhno3/p/19435248
頁:
[1]