在C++11及以后的标准中,一个空类(未显式定义任何成员函数)会被编译器自动生成6个默认成员函数;C++11新增了2个(移动构造、移动赋值),C++03及更早版本只有4个。
一、完整默认函数列表(C++11+)
按功能和常用度排序,每个函数的核心信息如下:
| 函数类型 | 函数原型(以class T为例) | 生成条件 | 核心作用 |
|---|---|---|---|
| 1. 默认构造函数 | T::T() | 未显式定义任何构造函数 | 创建对象时初始化(空类的默认构造函数啥也不做,仅保证对象能被创建) |
| 2. 析构函数 | T::~T() | 未显式定义析构函数 | 对象销毁时清理资源(如释放动态内存、关闭文件) |
| 3. 拷贝构造函数 | T::T(const T& other) | 未显式定义拷贝构造函数 | 用已有对象创建新对象(如T a = b),默认是浅拷贝 |
| 4. 拷贝赋值运算符重载 | T& T::operator=(const T& other) | 未显式定义拷贝赋值运算符 | 已有对象间赋值(如a = b),默认是浅拷贝 |
| 5. 移动构造函数(C++11) | T::T(T&& other) | 未显式定义移动构造/拷贝构造/拷贝赋值/析构 | 用临时对象(右值)创建新对象,“窃取”临时对象的资源(避免深拷贝,提升效率) |
| 6. 移动赋值运算符(C++11) | T& T::operator=(T&& other) | 未显式定义移动赋值/拷贝构造/拷贝赋值/析构 | 临时对象赋值给已有对象,同样“窃取”资源 |
二、关键函数详解(新手重点掌握前4个)
1. 默认构造函数
- 生成条件:只要你显式定义了任何构造函数(比如带参构造),编译器就不会生成默认构造函数;
- 示例:
class Goods { public: // 显式定义带参构造,编译器不再生成默认构造 Goods(const char* name) {} }; Goods g1; // 编译错误!没有默认构造函数 Goods g2("苹果"); // 正确,调用带参构造 - 修复:如需默认构造,可手动定义或用
=default(C++11):Goods() = default; // 显式要求编译器生成默认构造
2. 析构函数
- 默认行为:空类的默认析构函数啥也不做;如果类有动态内存(如
char* mName),必须手动定义析构函数释放内存,否则会内存泄漏; - 注意:析构函数不能重载,一个类只有一个析构函数。
3. 拷贝构造函数 vs 拷贝赋值运算符
- 核心区别:
- 拷贝构造:创建新对象(
T a = b),对象生命周期从无到有; - 拷贝赋值:已有对象赋值(
a = b),对象已存在;
- 拷贝构造:创建新对象(
- 默认浅拷贝的坑:如果类有指针成员(如
char* mName),浅拷贝会让两个对象的指针指向同一块内存,析构时会重复释放,导致程序崩溃。
4. 移动构造/移动赋值(C++11进阶)
- 核心优势:针对临时对象(如
Goods("香蕉")),移动构造直接“拿走”临时对象的内存指针,而非深拷贝,效率极高; - 触发场景:仅对右值(临时对象、
std::move转换的对象)生效:Goods g1("苹果"); Goods g2 = std::move(g1); // 调用移动构造,g1的资源被转移,之后g1不应再使用
三、如何禁用默认函数
如果不想让编译器生成某个默认函数(比如禁止拷贝),可显式删除(C++11):
class NonCopyable {
public:
// 禁用拷贝构造和拷贝赋值
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
// 保留默认构造
NonCopyable() = default;
};
四、新手易混点
- 空类不是真的“空”:即使你只写
class A {};,编译器也会自动生成6个默认函数(C++11); - 显式定义一个,影响一批:比如显式定义析构函数后,编译器不再生成移动构造/移动赋值;
- 浅拷贝仅对POD类型安全:POD类型(如
int、固定数组)浅拷贝没问题,指针/动态内存必须手动实现深拷贝。
总结
- 核心数量:C++11+有6个默认函数(默认构造、析构、拷贝构造、拷贝赋值、移动构造、移动赋值),C++03有4个(无移动相关);
- 生成规则:显式定义某类函数后,编译器不再生成对应的默认版本;
- 关键坑点:默认拷贝/赋值是浅拷贝,动态内存成员必须手动实现深拷贝,否则会内存泄漏/重复释放;
- 进阶优化:移动构造/赋值可提升临时对象的处理效率,是现代C++的优化手段。
。
完整实现代码(包含所有6个默认函数)
#include <iostream>
#include <cstring>
#include <utility> // 用于std::swap
using namespace std;
class Cgoods {
private:
char* mName; // 动态分配内存
float m_price;
public:
// 1. 默认构造函数(无参构造)
// 用=default显式声明,强调这是编译器生成的默认行为(也可手动实现,效果一致)
Cgoods() : mName(nullptr), m_price(0.0f) {
cout << "无参构造函数调用" << endl;
}
// 2. 带参构造函数(自定义,编译器不会生成)
Cgoods(const char* name, float price) : m_price(price) {
if (name == nullptr) {
mName = new char[1];
mName[0] = '\0';
} else {
mName = new char[strlen(name) + 1];
strcpy(mName, name);
}
cout << "带参构造函数调用:" << mName << endl;
}
// 3. 拷贝构造函数(深拷贝)
Cgoods(const Cgoods& other) {
// 分配新内存,避免浅拷贝
mName = new char[strlen(other.mName) + 1];
strcpy(mName, other.mName);
m_price = other.m_price;
cout << "拷贝构造函数调用:" << mName << endl;
}
// 4. 拷贝赋值运算符重载(深拷贝,防泄漏)
Cgoods& operator=(const Cgoods& other) {
// 防止自赋值
if (this == &other) {
return *this;
}
// 释放旧内存 → 分配新内存 → 拷贝数据
delete[] mName;
mName = new char[strlen(other.mName) + 1];
strcpy(mName, other.mName);
m_price = other.m_price;
cout << "拷贝赋值运算符重载调用:" << mName << endl;
return *this;
}
// 5. 移动构造函数(C++11新增,“窃取”临时对象资源)
Cgoods(Cgoods&& other) noexcept { // noexcept:声明不抛异常,提升容器兼容性
// 直接接管临时对象的资源,无需重新分配内存
mName = other.mName;
m_price = other.m_price;
// 将源对象的指针置空,避免析构时重复释放
other.mName = nullptr;
other.m_price = 0.0f;
cout << "移动构造函数调用:" << mName << endl;
}
// 6. 移动赋值运算符重载(C++11新增,赋值时窃取资源)
Cgoods& operator=(Cgoods&& other) noexcept {
// 防止自赋值(移动自赋值无意义,但仍需防护)
if (this == &other) {
return *this;
}
// 释放当前对象的旧内存
delete[] mName;
// 接管临时对象的资源
mName = other.mName;
m_price = other.m_price;
// 源对象指针置空
other.mName = nullptr;
other.m_price = 0.0f;
cout << "移动赋值运算符重载调用:" << mName << endl;
return *this;
}
// 7. 析构函数(释放动态内存)
~Cgoods() {
if (mName != nullptr) {
cout << "析构函数调用:释放 " << mName << " 的内存" << endl;
delete[] mName;
mName = nullptr;
}
}
// 辅助函数:打印商品信息
void showInfo() const {
cout << "商品名称:" << (mName ? mName : "空") << ",价格:" << m_price << endl;
}
};
// 测试代码(覆盖所有6个默认函数的调用场景)
int main() {
cout << "===== 1. 测试默认构造 + 带参构造 =====" << endl;
Cgoods goods1; // 默认构造
Cgoods goods2("苹果", 9.9f); // 带参构造
goods2.showInfo();
cout << "\n===== 2. 测试拷贝构造 =====" << endl;
Cgoods goods3 = goods2; // 拷贝构造(新对象创建)
goods3.showInfo();
cout << "\n===== 3. 测试拷贝赋值 =====" << endl;
goods1 = goods3; // 拷贝赋值(已有对象赋值)
goods1.showInfo();
cout << "\n===== 4. 测试移动构造 =====" << endl;
Cgoods goods4 = Cgoods("香蕉", 5.9f); // 移动构造(临时对象初始化新对象)
goods4.showInfo();
cout << "\n===== 5. 测试移动赋值 =====" << endl;
goods1 = std::move(goods4); // 移动赋值(将goods4的资源转移给goods1)
goods1.showInfo();
goods4.showInfo(); // goods4已失去资源,变为空
return 0;
}
关键新增函数解析
1. 移动构造函数(Cgoods(Cgoods&& other) noexcept)
- 核心作用:针对临时对象(右值) 初始化新对象时,直接“窃取”临时对象的资源(如
mName指向的内存),无需重新分配内存,大幅提升效率。 - 关键细节:
- 参数是
Cgoods&&(右值引用),仅匹配临时对象或std::move转换的对象; noexcept:声明函数不会抛出异常,这是标准要求(容器如vector会优先使用带noexcept的移动构造);- 接管资源后,必须将源对象的
mName置空,否则源对象析构时会释放已转移的内存,导致崩溃。
- 参数是
2. 移动赋值运算符重载(operator=(Cgoods&& other) noexcept)
- 核心作用:已有对象赋值时,接管临时对象/
std::move对象的资源,逻辑和移动构造一致:- 释放当前对象的旧内存;
- 接管源对象的资源;
- 源对象指针置空;
- 返回自身引用,支持链式赋值。
运行结果(验证所有函数调用)
===== 1. 测试默认构造 + 带参构造 =====
无参构造函数调用
带参构造函数调用:苹果
商品名称:苹果,价格:9.9
===== 2. 测试拷贝构造 =====
拷贝构造函数调用:苹果
商品名称:苹果,价格:9.9
===== 3. 测试拷贝赋值 =====
拷贝赋值运算符重载调用:苹果
商品名称:苹果,价格:9.9
===== 4. 测试移动构造 =====
带参构造函数调用:香蕉
移动构造函数调用:香蕉
商品名称:香蕉,价格:5.9
===== 5. 测试移动赋值 =====
移动赋值运算符重载调用:香蕉
商品名称:香蕉,价格:5.9
商品名称:空,价格:0
析构函数调用:释放 香蕉 的内存
析构函数调用:释放 苹果 的内存
析构函数调用:释放 香蕉 的内存
析构函数调用:释放 苹果 的内存
核心知识点补充
1. 默认函数的生成规则
- 如果你手动实现了拷贝构造/拷贝赋值/析构函数,编译器不会自动生成移动构造/移动赋值;
- 如果你手动实现了移动构造/移动赋值,编译器不会自动生成拷贝构造/拷贝赋值;
- 代码中我们手动实现了所有6个函数,覆盖了所有场景。
2. 移动语义的触发场景
- 移动构造:用临时对象初始化新对象(如
Cgoods g = Cgoods("香蕉", 5.9f)); - 移动赋值:用
std::move将左值转为右值(如g1 = std::move(g4)),或赋值临时对象(如g1 = Cgoods("香蕉", 5.9f))。
3. 移动后的对象状态
- 移动后,源对象(如
goods4)会被置为空(mName=nullptr),此时对源对象的唯一安全操作是:赋值新值或析构,不要访问其原有资源。
总结
- 完整默认函数:代码包含C++11标准下的6个默认成员函数(默认构造、带参构造(自定义)、拷贝构造、拷贝赋值、移动构造、移动赋值、析构);
- 移动语义核心:移动构造/赋值通过“窃取”资源提升效率,避免深拷贝,是现代C++的优化核心;
- 安全要点:移动后必须将源对象指针置空,析构函数要判断
nullptr,防止重复释放/野指针; - 使用场景:拷贝构造/赋值用于已有对象的复用,移动构造/赋值用于临时对象的高效转移。
2919



被折叠的 条评论
为什么被折叠?



