关于 C++ 默认构造函数 的几个误区

本文澄清了C++新手常有的误解,并详细解释了C++中默认构造函数的行为,包括何时及如何由编译器生成,默认构造函数的作用以及何时需要程序员自定义构造函数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

C++新手一般有两个常见的误解:

1.任何class如果没有定义default constructor,就会被合成一个来。

2.编译器合成出来的default constructor会明确设定“class 内每个data member的默认值”。

如你所见,没有一个是真的!

C++的几条原则

1、每个类必须至少有一个构造函数,否则没法创建对象;

2、若programer没有提供任何构造函数,则C++提供一个默认的构造函数,该默认构造函数是无参构造函数,它仅负责创建对象,不做任何初始化的工作;

3、只要programer定义了一个构造函数(不管是无参还是有参构造),C++就不再提供默认的默认构造函数。即如果为类定义了一个带参的构造函数,还想要无参构造函数,就必须自己定义;

4、与变量定义类似,在用默认构造函数创建对象时,如果创建的是全局对象或静态对象,则对象的位模式全为0,否则,对象值是随机的。

注:2所述,C++在某些情况下会提供默认构造函数,但在某些情况下并不会自动调用。实事上,当用户没有提供自定义的构造函数的时候,声明该类的对象,以及定义包含该对象的数组,都不会调用默认构造函数;但定义包含该对象的容器时,会自动调用默认的构造函数。

举例如下:

classStudent

{

public:

//Student() : val(6){} // 1

//Student(int val) : val(6){} // 2

int val;

};

 

voidmain()

{

Students;

cout<< s.val<<"first"<<endl;; // 编译通过,但运行时出错:the variable 's' is being used withoutbeing defined.

// 说明没有调用默认构造函数

Studentarr[4];

 arr[2]; // 编译通过,但运行出错:the variable 'arr' is being used without beingdefined.

// 说明没有调用默认构造函数

vector<Student>vec(5);

cout<< vec[3].val <<"second"<< endl; //运行无误,但输出结果未定义(取决于编译器,可能是随机值或0

// 说明调用了默认构造函数

Student*ptr = new Student[4];

cout<< ptr[1].val <<"third"<<endl; //运行无误,但输出结果未定义(取决于编译器,可能是随机值或0

// 说明调用了默认构造函数

Studentss = Student(); // 显示调用

cout<< ss.val<<"forth"<<endl; // 运行无误,但输出结果未定义(取决于编译器,可能是随机值或0

// 说明调用了默认构造函数

getchar();

}

若将Student定义体中的注释1去掉,即自定义无参构造函数,则main函数中的几种形式的对象定义都会调用该自定义的无参构造函数。

但只将注释2去掉,即自定义带参构造,而不提供无参构造,则main函数的几种形式的对象定义都将因为没有可用的默认构造函数而编译出错。


关于两种默认构造函数

在《C++ Annotated Reference Manual(ARM)[ELLIS90]》中的Section 12.1告诉我们:"Defaultconstructors...在需要的时候被编译器产生出来"。

其实默认构造函数也是分为两类的:有用的、无用的。

所谓有用的标准也是就默认构造函数会为我们的类做一些初始化操作。那么无用的就不会做任何工作,从而对我们的类也就没有任何意义。所以,我们通常所说的默认构造函数是指有用的默认构造函数,其英文名字叫nontrivial default constructor。

那么到底什么时候编译器会为我们产生nontrivial default constructor呢?有下面四中情况:

如果一个类里面某个成员对象有nontrivial default constructor,编译器就会为我们的类产生nontrivialdefault constructor。

那么编译器这样做的理由是什么?

答案是因为类成员对象有nontrivial default constructor,那么编译器就需要显式的来调用这个类成员对象的nontrivial default constructor。而编译器想显式的调用类成员对象的nontrivial default constructor,就需要自己来合成一些代码来调用。但是记住,编译器合成的nontrivial default constructor仅仅调用类成员对象的默认构造函数,而不对我们类里面的其它变量做任何初始化操作。

也就是说,如果你想初始化类成员变量以外的变量例如一个int、一个String,那么必须自己定义默认构造函数来完成这些变量的初始化。而编译器会对你定义的默认构造函数做相应的扩展,从而调用类成员对象的nontrivial default constructor。

如果一个派生类的基类有nontrivial default constructor,那么编译器会为派生类合成一个nontrivial default constructor。

编译器这样的理由是:因为派生类被合成时需要显式调用基类的默认构造函数。

如何一个类里面隐式的含有任何virtual function table(或vtbl)、pointer member(或vptr)。

编译器这样做的理由很简单:因为这些vtbl或vptr需要编译器隐式(implicit)的合成出来,那么编译器就把合成动作放到了默认构造函数里面。所以编译器必须自己产生一个默认构造函数来完成这些操作。

所以如果你的类里带有任何virtual function,那么编译器会为你合成一个默认构造函数。

如果一个类虚继承于其它类。

编译器这样做的理由和③类似:因为虚继承需要维护一个类似指针一样,可以动态的决定内存地址的东西(不同编译器对虚继承的实现不仅相同)。

那么除了以上四种情况,编译器并不会为我们的类产生默认构造函数。

所以编程中切忌想当然,要明白哪些事情是编译器做的,哪些事情需要程序员来完成的。就像堆所占用的资源需要程序员自己来释放,而栈空间是编译器管理的一样。

只有如此,才能编写出质量更高的代码。


<think>好的,我现在需要回答用户关于C++中移动构造函数和拷贝构造函数调用情况的问题。用户的问题是:“有移动构造函数,还会调用拷贝构造函数吗?” 我需要详细思考这个问题,确保回答准确并覆盖所有可能的情况。 首先,我需要回顾C++构造函数的基本概念。拷贝构造函数用于创建一个对象的副本,而移动构造函数则是将资源从一个临时对象转移到新对象,避免不必要的拷贝。当存在移动构造函数时,编译器在某些情况下会优先选择移动而非拷贝,但具体是否调用拷贝构造函数,还取决于实际情况。 接下来,我应该考虑不同的场景,比如当对象是左值还是右值,或者当移动构造函数不可用时的行为。例如,如果有一个对象是左值,那么即使存在移动构造函数,拷贝构造函数可能仍会被调用,因为移动构造函数通常绑定到右值。但如果对象是右值,编译器应该优先使用移动构造函数。 另外,需要考虑编译器自动生成构造函数的情况。如果用户显式声明了移动构造函数,编译器可能不会自动生成拷贝构造函数,除非用户显式要求。这时候如果尝试拷贝,而拷贝构造函数被删除,会导致编译错误。 还需要想到移动语义的触发条件,比如使用std::move将左值转换为右值,这时候会调用移动构造函数。如果没有使用std::move,即使有移动构造函数,也可能调用拷贝构造函数,特别是当对象是左值的时候。 然后,我应该举一些具体的例子来展示不同情况下的行为。例如,当返回一个局部对象时,编译器可能会进行返回值优化(RVO),从而避免调用任何构造函数。但如果没有RVO,且存在移动构造函数,则会调用移动构造函数而不是拷贝构造函数。 另外,当对象作为参数传递时,如果是临时对象(右值),会调用移动构造函数;如果是左值,则调用拷贝构造函数,除非使用std::move强制转换。 还需要注意,如果移动构造函数被标记为delete,或者不可访问(比如private权限),那么即使存在移动构造函数,拷贝构造函数也可能被调用,或者导致编译错误,具体情况取决于上下文。 此外,用户可能存在的误区是认为只要有移动构造函数,拷贝构造函数就不会被调用。但实际情况是,拷贝构造函数是否被调用取决于对象的类型(左值或右值)以及是否显式启用了移动语义。因此,需要明确说明在哪些情况下会调用哪一个构造函数。 总结起来,回答应该包括以下几点: 1. 存在移动构造函数时,当对象是右值(如临时对象、std::move转换后的对象)时,优先调用移动构造函数。 2. 当对象是左值时,仍然会调用拷贝构造函数,除非使用std::move将其转换为右值。 3. 如果拷贝构造函数不可用(如被删除或私有),尝试拷贝左值对象会导致编译错误。 4. 编译器在某些情况下会优化,如返回值优化(RVO),可能直接构造对象而不调用任何拷贝或移动构造函数。 5. 显式声明移动构造函数可能导致编译器不生成默认的拷贝构造函数,需要用户自行定义。 在组织回答时,需要结构清晰,分点说明,并给出代码示例帮助理解。同时,要注意术语的准确性,比如区分左值、右值、移动语义、完美转发等概念,但在这个问题中主要集中在移动和拷贝构造函数的调用条件上。 可能还需要提醒用户一些常见的错误,比如误以为所有情况都会自动调用移动构造函数,或者忽略移动构造函数后的对象状态管理问题。例如,移动后的对象应处于有效但未定义的状态,以确保程序正确性。 最后,确保回答语言简洁明了,用例子和对比来加强理解,避免过于技术化的术语堆砌,同时覆盖用户可能的疑问点,如编译器优化、左右值的影响、显式与隐式构造函数生成等。</think>在C++中,**如果类定义了移动构造函数,是否会调用拷贝构造函数,取决于具体的使用场景和对象的类型(左值或右值)**。以下是关键分析: --- ### **一、基本原则** 1. **右值优先匹配移动语义** 当操作对象是右值(如临时对象、`std::move`转换后的对象)时,优先调用移动构造函数。 2. **左值仍触发拷贝语义** 若操作对象是左值(具名对象、可获取地址),即使定义了移动构造函数,仍会调用拷贝构造函数(除非显式使用`std::move`)。 --- ### **二、典型场景分析** #### **1. 直接初始化右值对象** - **示例代码**: ```cpp class MyClass { public: MyClass() = default; MyClass(const MyClass&) { std::cout << "Copy Constructor\n"; } MyClass(MyClass&&) { std::cout << "Move Constructor\n"; } }; int main() { MyClass a; // 默认构造 MyClass b = a; // 调用拷贝构造函数(a是左值) MyClass c = std::move(a); // 调用移动构造函数(std::move(a)转为右值) MyClass d = MyClass(); // 调用移动构造函数(临时对象是右值) } ``` - **输出**: ``` Copy Constructor Move Constructor Move Constructor ``` #### **2. 函数返回值优化(RVO/NRVO)** - **编译器优化**: 若函数返回局部对象,C++17强制省略拷贝/移动构造(直接构造到调用方内存): ```cpp MyClass create() { MyClass obj; return obj; // C++17后直接构造,不调用任何构造函数 } ``` - **未优化时**: 若未优化且存在移动构造函数,返回局部对象会触发移动构造(而非拷贝构造)。 #### **3. 容器操作(如`std::vector`插入)** - **插入右值时触发移动**: ```cpp std::vector<MyClass> vec; vec.push_back(MyClass()); // 插入右值,调用移动构造函数 ``` - **插入左值时触发拷贝**: ```cpp MyClass a; vec.push_back(a); // 插入左值,调用拷贝构造函数 ``` --- ### **三、特殊情况** #### **1. 拷贝构造函数被隐式删除** - **规则**:若显式定义了移动构造函数/移动赋值运算符,编译器会删除默认的拷贝构造函数。 - **代码示例**: ```cpp class MyClass { public: MyClass(MyClass&&) {} // 显式定义移动构造函数 // 拷贝构造函数被隐式删除 }; int main() { MyClass a; MyClass b = a; // 错误:拷贝构造函数不可用 } ``` - **解决方案**:需显式定义拷贝构造函数: ```cpp MyClass(const MyClass&) = default; ``` #### **2. 移动语义不可用时的回退** - **若移动构造函数不可访问或已删除**: 即使操作对象是右值,仍会尝试调用拷贝构造函数: ```cpp class MyClass { public: MyClass(const MyClass&) {} // 拷贝构造函数 MyClass(MyClass&&) = delete; // 移动构造函数被删除 }; int main() { MyClass a; MyClass b = std::move(a); // 错误:尝试调用已删除的移动构造函数 } ``` --- ### **四、关键对比表** | 场景 | 对象类型 | 是否调用拷贝构造函数 | 是否调用移动构造函数 | |-----------------------|----------|----------------------|----------------------| | 初始化左值对象 | 左值 | ✔️ | ❌ | | 初始化右值对象 | 右值 | ❌ | ✔️ | | 函数返回局部对象 | 右值 | ❌(优化后) | ❌(优化后) | | 容器插入左值 | 左值 | ✔️ | ❌ | | 容器插入右值 | 右值 | ❌ | ✔️ | --- ### **五、实践建议** 1. **优先使用移动语义处理右值**: 通过`std::move`明确资源转移意图。 2. **注意拷贝构造的隐式删除**: 显式定义移动构造函数时,需检查是否需要手动定义拷贝构造函数。 3. **利用编译器优化**: 依赖RVO/NRVO减少不必要的拷贝/移动操作。 4. **移动后的对象状态管理**: 确保移动后的对象处于有效但可析构的状态(如指针置`nullptr`)。 --- ### **总结** - **有移动构造函数时,拷贝构造函数是否被调用,取决于操作对象是左值还是右值**。 - **右值优先触发移动语义,左值仍会调用拷贝构造函数**。 - 显式定义移动构造函数可能导致拷贝构造函数被隐式删除,需特别注意兼容性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值