类的六个默认成员函数
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
一.构造函数
构造函数(Constructor)
构造函数是 C++ 中用于初始化对象的特殊成员函数,它在对象创建时自动调用。以下是构造函数的全面解析:
1. 基本概念
- 作用:初始化对象成员变量
- 特点:
- 函数名与类名相同
- 无返回值(连 void 都没有)
- 支持重载(一个类可以有多个构造函数)
- 调用时机:
- 对象定义时( Class obj; )
- 动态创建对象时( new Class() )
- 临时对象创建时( Class() )
2. 构造函数的类型
(1) 编译器生成的默认构造函数
- 触发条件:当类没有定义任何构造函数时,编译器自动生成
- 行为:
- 对基本类型( int / float 等)不初始化(值随机)
- 对类类型成员调用其默认构造函数
class DefaultExample
{
public:
// 编译器自动生成默认构造函数
int val; // 未初始化
};
void demo()
{
DefaultExample obj; // val的值随机
}
(2) 自定义默认构造函数(无参构造)
class MyClass
{
public:
// 显式定义默认构造函数
MyClass() : x(0), y(0) {}
private:
int x, y;
};
(3) 全缺省构造函数
- 所有参数都有默认值,可以替代默认构造函数
class AllDefault
{
public:
// 全缺省构造函数(也是默认构造函数)
AllDefault(int a = 0, int b = 0) : x(a), y(b) {}
private:
int x, y;
};
void demo()
{
AllDefault obj1; // 等效于 AllDefault(0, 0)
AllDefault obj2(5); // AllDefault(5, 0)
}
(4) 部分缺省构造函数
class PartialDefault
{
public:
// 部分缺省参数
PartialDefault(int a, int b = 10) : x(a), y(b) {}
private:
int x, y;
};
void demo()
{
PartialDefault obj(5); // PartialDefault(5, 10)
PartialDefault obj2(1, 2);
}
3. 必须自定义构造函数的场景
在 C++ 中,以下情况必须手动编写构造函数,不能依赖编译器生成的默认构造函数:
_类包含引用类型成员
- 原因:引用必须在初始化时绑定到一个对象,且之后不能更改绑定目标。
- 解决方案:必须通过构造函数的初始化列表显式初始化引用成员。
_类包含 const 常量成员
- 原因:const 成员必须在对象构造时初始化,且之后不可修改。
- 解决方案:必须在构造函数的初始化列表中初始化 const 成员。
_类需要管理动态资源
- 原因:默认构造函数不会自动分配堆内存、打开文件等资源。
- 典型场景:
- 成员包含指针并需要动态分配内存(如 int* data = new int[100] )
- 需要初始化文件句柄、数据库连接等外部资源
- 解决方案:在构造函数中显式申请资源,并在析构函数中释放。
_类成员需要特定初始值
- 原因:编译器生成的默认构造函数不会初始化基本类型成员(如 int / float 等)。
- 解决方案:手动定义构造函数,确保成员初始化为预期值(例如初始化为 0 或空字符串)。
_类继承体系中基类没有默认构造函数
- 原因:如果基类只有带参数的构造函数,派生类必须显式调用基类构造函数。
- 解决方案:在派生类构造函数的初始化列表中调用基类的构造函数。
_需要禁止某些对象的构造方式
- 场景:
- 希望强制使用特定参数构造对象(如必须传入 ID)
- 单例模式中需要私有化构造函数
- 解决方案:手动定义构造函数并删除默认版本(如 ClassName() = delete )
4. 初始化列表 vs 构造函数体内赋值
方式 特点
初始化列表 - 在对象构造时直接初始化成员 - 必须用于引用/const成员初始化
构造函数体内赋值 - 实际上是先默认构造再赋值 - 对非基本类型有额外性能开销
class InitDemo
{
public:
// 推荐:初始化列表
InitDemo(int a, std::string s) : num(a), str(s) {}
// 不推荐:构造函数内赋值
InitDemo(int a) {
num = a; // 先执行string的默认构造,再赋值
str = "default";
}
private:
int num;
std::string str; // 类类型成员
};
5. 特殊构造函数
(1) 委托构造函数(C++11)
class Delegating
{
public:
// 主构造函数
Delegating(int a, double b) : x(a), y(b) {}
// 委托给主构造函数
Delegating() : Delegating(0, 0.0) {}
private:
int x;
double y;
};
(2) explicit 构造函数
- 禁止隐式类型转换
class ExplicitDemo
{
public:
explicit ExplicitDemo(int x) : val(x) {} // 必须显式调用
};
void demo()
{
// ExplicitDemo obj = 5; // 错误!禁止隐式转换
ExplicitDemo obj(5); // 正确
}
6. 构造函数的调用顺序
1. 基类构造函数(如果存在继承)
2. 成员变量的构造函数(按声明顺序)
3. 当前类的构造函数体
class Member
{
public:
Member() { std::cout << "Member构造\n"; }
};
class Base
{
public:
Base() { std::cout << "Base构造\n"; }
};
class Demo : public Base
{
public:
Demo() : m(), Base() {
std::cout << "Demo构造\n";
}
private:
Member m;
};
// 输出顺序:
// Base构造 → Member构造 → Demo构造
7. 实际应用建议
_优先使用初始化列表:尤其对类类型成员
_对单参数构造函数加 explicit :避免意外类型转换
_资源管理类遵循三五法则:如果需要自定义析构函数,通常也需要自定义拷贝构造和赋值运算符
_避免在构造函数中调用虚函数:此时虚函数机制未完全建立
class ResourceOwner
{
public:
explicit ResourceOwner(size_t size)
: data(new int[size]), size(size) {}
~ResourceOwner() { delete[] data; }
// 三五法则:需要自定义拷贝控制成员
ResourceOwner(const ResourceOwner&) = delete;
ResourceOwner& operator=(const ResourceOwner&) = delete;
private:
int* data;
size_t size;
};
二.析构函数
析构函数是 C++ 中用于对象销毁时自动调用的特殊成员函数,负责资源清理工作
1. 基本概念
- 作用:释放对象占用的资源(内存/文件/锁等)
- 特点:
- 函数名为 ~类名
- 无参数无返回值
- 不可重载(每个类只能有一个析构函数)
- 调用时机:
- 对象离开作用域时(栈对象)
- delete 动态对象时(堆对象)
- 容器销毁时(如 vector 析构会调用元素析构)
class FileHandler
{
public:
FileHandler()
{
file = fopen("data.txt", "r");
}
~FileHandler()
{
if(file) fclose(file); // 必须手动关闭文件
std::cout << "资源已释放\n";
}
private:
FILE* file;
};
2. 必须自定义析构函数的场景
// 场景1:动态内存管理
class MemoryPool
{
public:
MemoryPool(size_t size) { data = new int[size]; }
~MemoryPool() { delete[] data; } // 必须自定义
private:
int* data;
};
// 场景2:文件资源管理
class DatabaseConn
{
public:
~DatabaseConn() { disconnect(); } // 确保连接关闭
};
// 场景3:多态基类
class Base
{
public:
virtual ~Base() = default; // 虚析构函数!!!
};
class Derived : public Base
{
~Derived() override { /* 清理派生类资源 */ }
};
3. 默认析构函数的问题
问题1:浅拷贝导致双重释放
class ShallowCopy
{
public:
ShallowCopy(int size) { data = new int[size]; }
~ShallowCopy() { delete[] data; } // 危险!
private:
int* data;
};
void demo()
{
ShallowCopy obj1(10);
ShallowCopy obj2 = obj1; // 浅拷贝:两个对象共享data指针
// 析构时会导致同一内存被释放两次 → 程序崩溃
}
解决方案:实现拷贝控制(拷贝构造+赋值运算符)或禁用拷贝(C++11)
class SafeCopy
{
public:
// 方法1:禁用拷贝
SafeCopy(const SafeCopy&) = delete;
SafeCopy& operator=(const SafeCopy&) = delete;
// 方法2:实现深拷贝
SafeCopy(const SafeCopy& other)
{
data = new int[other.size];
std::copy(other.data, other.data+other.size, data);
}
};
问题2:部分资源泄漏
class ResourceLeak
{
public:
ResourceLeak()
{
res1 = new int[100];
res2 = new int[200];
}
~ResourceLeak() { delete[] res1; } // res2泄漏!
private:
int *res1, *res2;
};
解决方案:使用 RAII 对象管理资源(如 std::unique_ptr )
class SafeResource
{
std::unique_ptr<int[]> res1;
std::unique_ptr<int[]> res2;
public:
SafeResource() :
res1(std::make_unique<int[]>(100)),
res2(std::make_unique<int[]>(200)) {}
// 无需显式析构函数!
};
4. 析构函数调用顺序
1. 执行析构函数体:当前类的清理代码
2. 调用成员变量的析构函数(按声明逆序)
3. 调用基类析构函数(若存在继承,从派生类向基类析构)
class Member
{
public:
~Member() { std::cout << "Member析构\n"; }
};
class Base
{
public:
virtual ~Base() { std::cout << "Base析构\n"; }
};
class Derived : public Base
{
public:
~Derived() override
{
std::cout << "Derived析构\n";
}
private:
Member m;
};
// 调用示例:
Base* obj = new Derived();
delete obj;
输出顺序:
Derived析构 → Member析构 → Base析构
5. 最佳实践
1. RAII原则:资源获取即初始化,通过对象生命周期管理资源
2. 三五法则:若自定义析构函数,通常需要同时处理拷贝/移动操作
3. 多态基类声明虚析构函数:防止通过基类指针删除派生类对象时资源泄漏
4. 优先使用智能指针:避免手动内存管理错误
// 现代C++推荐写法
class ModernExample
{
std::unique_ptr<int[]> data; // 自动管理内存
std::mutex mtx; // RAII管理锁
public:
ModernExample() : data(std::make_unique<int[]>(100)) {}
// 无需显式析构函数!
};
关键总结
- 必须自定义析构函数:当类管理动态分配的资源或需要确保资源释放时
- 危险陷阱:默认浅拷贝会导致双重释放,需通过深拷贝或禁用拷贝避免
- 多态必备:基类必须声明虚析构函数,否则通过基类指针删除派生类对象会导致派生类资源泄漏
- 现代替代方案:优先使用智能指针和STL容器替代裸指针/手动资源管理
三.拷贝构造函数
拷贝构造函数是C++中的一种特殊构造函数,用于创建一个新对象作为现有对象的副本。
基本语法
class MyClass
{
public:
// 拷贝构造函数
MyClass(const MyClass& other)
{
// 复制other的成员到当前对象
}
};
特点
1. 参数是对同类型对象的const引用
2. 通常不应修改源对象,因此参数通常是const
3. 如果没有显式定义,编译器会自动生成一个默认的拷贝构造函数
使用场景
拷贝构造函数在以下情况下被调用:
1. 用一个对象初始化另一个对象时:
MyClass obj1;
MyClass obj2 = obj1; // 调用拷贝构造函数
MyClass obj3(obj1); // 调用拷贝构造函数
2. 对象作为函数参数按值传递时:
void func(MyClass obj);
MyClass original;
func(original); // 调用拷贝构造函数
3. 函数返回对象时(可能被编译器优化):
MyClass createObject()
{
MyClass obj;
return obj; // 可能调用拷贝构造函数
}
深拷贝与浅拷贝
1. 浅拷贝:仅复制指针值,不复制指针指向的内容(默认拷贝构造函数的行为)
2. 深拷贝:复制指针指向的内容,需要自定义拷贝构造函数
class DeepCopyExample
{
int* data;
public:
// 深拷贝构造函数
DeepCopyExample(const DeepCopyExample& other)
{
data = new int(*other.data); // 分配新内存并复制值
}
};
禁用拷贝
可以通过将拷贝构造函数声明为delete来禁止拷贝:
class NonCopyable
{
public:
NonCopyable(const NonCopyable&) = delete;
};
最佳实践
1. 对于包含动态分配资源的类,应实现自定义拷贝构造函数
2. 对于不应被拷贝的类,应显式禁用拷贝构造函数
3. 考虑使用移动语义(C++11引入)来提高性能
四.赋值运算符重载 (Copy Assignment Operator)
赋值运算符重载是C++中允许类自定义对象间赋值行为的特性,通常与拷贝构造函数一起使用。
基本语法
class MyClass
{
public:
// 赋值运算符重载
MyClass& operator=(const MyClass& other) {
if (this != &other)
{ // 防止自赋值
// 复制other的成员到当前对象
}
return *this; // 支持链式赋值
}
};
特点
1. 必须是成员函数,不能是友元函数
2. 通常返回当前对象的引用以支持链式赋值
3. 参数是对同类型对象的const引用
4. 需要处理自赋值情况
使用场景
当对象被赋值时自动调用:
MyClass obj1, obj2;
obj1 = obj2; // 调用赋值运算符重载
拷贝构造函数与拷贝赋值运算符的区别
拷贝构造函数和拷贝赋值运算符虽然都用于对象拷贝,但在本质上有重要区别:
1. 根本目的不同
- 拷贝构造函数:用于在创建新对象时,用已有对象初始化它。这是对象的"出生"过程。
- 拷贝赋值运算符:用于两个已存在对象之间的赋值操作。这是对象的"改变"过程。
2. 调用时机不同
拷贝构造函数在以下情况调用:
MyClass obj1; // 默认构造
MyClass obj2(obj1); // 直接调用拷贝构造
MyClass obj3 = obj1; // 这也是调用拷贝构造
MyClass func(MyClass o); // 参数传递时可能调用拷贝构造
拷贝赋值运算符在以下情况调用:
MyClass obj1, obj2; // 默认构造
obj1 = obj2; // 调用赋值运算符
3. 对象生命周期不同
- 拷贝构造:目标对象尚未存在,需要在构造过程中初始化
- 拷贝赋值:目标对象已经存在,需要先清理原有资源再赋新值
4. 实现要点不同
拷贝构造的实现:
MyClass(const MyClass& other)
{
// 只需关心从other复制到新对象
data = new int(*other.data); // 深拷贝示例
}
拷贝赋值的实现:
MyClass& operator=(const MyClass& other)
{
if(this != &other)
{ // 1. 检查自赋值
delete data; // 2. 释放原有资源
data = new int(*other.data); // 3. 深拷贝
}
return *this; // 4. 返回引用
}
5. 资源管理差异
- 拷贝构造:只需分配新资源并复制
- 拷贝赋值:需要先释放旧资源,再分配新资源,还要处理自赋值情况
6. 返回值不同
- 拷贝构造:没有返回值
- 拷贝赋值:通常返回对象引用以支持链式赋值(a = b = c)
实际应用建议
1. 遵循"三大法则":如果实现了其中任何一个(拷贝构造、拷贝赋值或析构),通常需要实现全部三个
2. 对于资源管理类,必须同时正确实现两者
3. 现代C++中可考虑使用"复制交换惯用法"简化实现
4. 不需要拷贝时应显式禁用(=delete)
深拷贝实现示例
class DeepCopyExample
{
int* data;
public:
// 赋值运算符重载
DeepCopyExample& operator=(const DeepCopyExample& other)
{
if (this != &other)
{
delete data; // 释放原有资源
data = new int(*other.data); // 分配新内存并复制
}
return *this;
}
};
复制交换惯用法 (Copy-and-Swap Idiom)
更安全和高效的实现方式:
class MyClass
{
// ... 其他成员 ...
public:
friend void swap(MyClass& first, MyClass& second) noexcept
{
// 交换所有成员
using std::swap;
swap(first.data, second.data);
}
MyClass& operator=(MyClass other) noexcept
{
swap(*this, other); // 交换当前对象与参数
return *this; // other离开作用域时会自动清理旧资源
}
};
禁用赋值
可以通过声明为delete来禁止赋值:
class NonAssignable
{
public:
NonAssignable& operator=(const NonAssignable&) = delete;
};
五.const成员函数
const成员函数是C++中保证函数不会修改对象状态的重要机制,它在类设计中扮演着关键角色。
基本概念
定义方式
在成员函数声明和定义的参数列表后添加 const 关键字:
class MyClass
{
public:
// 声明const成员函数
void display() const;
int getValue() const
{
return value; // 直接定义的const成员函数
}
private:
int value;
};
// 类外定义const成员函数
void MyClass::display() const
{
// 函数实现
}
核心特性
1. 不可修改性:不能修改类的非mutable成员变量
2. 调用权限:可以被const和非const对象调用
3. 重载机制:可以与同名非const成员函数形成重载
使用场景
1. 访问器方法(Getters)
class Student
{
std::string name;
public:
// const成员函数作为getter
const std::string& getName() const
{
return name;
}
};
2. 不修改对象状态的工具函数
class Matrix
{
double data[10][10];
public:
// 计算行列式但不修改矩阵内容
double determinant() const;
};
3. const对象接口
const Student s("Alice");
s.getName(); // 只能调用const成员函数
重要规则
1. 修改限制
class Counter
{
int count;
mutable int accessCount; // mutable例外
public:
void increment() const
{
// count++; // 错误!不能修改非mutable成员
accessCount++; // 正确:mutable成员可修改
}
};
2. 重载决议
class TextBlock
{
std::string text;
public:
// const版本
const char& operator[](size_t pos) const
{
return text[pos];
}
// 非const版本
char& operator[](size_t pos)
{
return text[pos];
}
};
const TextBlock ctb("Hello");
TextBlock tb("World");
ctb[0]; // 调用const版本
tb[0]; // 调用非const版本
高级用法
1. 逻辑常量性
class CachedData
{
mutable std::string cachedValue;
bool cacheValid;
public:
std::string getValue() const
{
if (!cacheValid)
{
// 虽然修改了成员,但不影响逻辑常量性
cachedValue = computeValue();
cacheValid = true;
}
return cachedValue;
}
};
2. 返回类型优化
class BigData
{
std::vector<int> data;
public:
// 返回const引用避免拷贝
const std::vector<int>& getData() const
{
return data;
}
};
最佳实践
1. 80%原则:80%的成员函数应该声明为const
2. const一致:相关函数应提供const和非const版本
3. 避免cast:不要用const_cast去掉const性质
4. mutable慎用:只在真正不影响逻辑状态时使用
5. 文档说明:对mutable成员的使用添加注释说明
C++中的const成员
const成员是指被 const 关键字修饰的类成员,包括const成员变量和const成员函数。
一、const成员变量
1. 特点
- 必须在构造函数的初始化列表中初始化
- 初始化后值不可修改
- 每个对象的const成员可以有不同的值
2. 声明与初始化
cpp
class MyClass
{
public:
const int size; // const成员变量声明
// 必须在初始化列表中初始化
MyClass(int s) : size(s) {}
// 错误!不能在构造函数体内赋值
// MyClass(int s) { size = s; }
};
3. 使用场景
- 类中需要保持不变的配置参数
- 对象特有的常量值
- 防止意外修改的重要成员
二、const成员函数
1. 特点
- 在函数声明和定义后加 const 关键字
- 不能修改类的任何非mutable成员变量
- 可以访问但不能修改对象状态
- 可以被const和非const对象调用
2. 语法
cpp
class MyClass
{
int value;
public:
// const成员函数
int getValue() const
{
return value; // 可以读取但不能修改成员
}
// 错误!const成员函数不能修改成员
// void setValue(int v) const { value = v; }
};
3. 使用场景
- 不修改对象状态的getter方法
- 保证线程安全的成员函数
- 需要被const对象调用的方法
三、const对象
1. 特点
- 声明为const的对象
- 只能调用const成员函数
- 所有成员变量被视为const
cpp
const MyClass obj(10);
int x = obj.getValue(); // 正确
// obj.setValue(20); // 错误!const对象不能调用非const方法
四、mutable成员
1. 特点
- 用 mutable 关键字修饰的成员变量
- 可以在const成员函数中被修改
- 通常用于不影响对象逻辑状态的成员
2. 示例
cpp
class MyClass
{
mutable int cache; // mutable成员
int value;
public:
int getValue() const
{
cache++; // 可以修改mutable成员
return value;
}
};
五、最佳实践
1. 尽可能将不修改对象状态的成员函数声明为const
2. const成员变量应在初始化列表中初始化
3. 对于需要频繁修改的缓存或计数器使用mutable
4. 设计类时应考虑const正确性
5. const成员函数可以重载非const版本
六.取地址及const取地址操作符重载
在C++中,我们可以重载取地址运算符( operator& )来定制对象地址获取行为,这在某些特殊场景下非常有用。
基本语法
普通取地址运算符重载
class MyClass
{
public:
MyClass* operator&()
{
return this; // 默认行为,通常不需要重载
// 可以返回其他指针,但需谨慎
}
};
const版本取地址运算符重载
class MyClass
{
public:
const MyClass* operator&() const
{
return this; // const对象的取地址
}
};
使用场景
1. 禁止获取真实地址(封装实现)
class NoAddress
{
private:
static int dummy;
public:
int* operator&() { return &dummy; } // 返回假地址
const int* operator&() const
{
return &dummy;
}
};
int NoAddress::dummy = 0;
2. 智能指针模拟
template<typename T>
class PtrWrapper
{
T* ptr;
public:
PtrWrapper(T* p) : ptr(p) {}
T* operator&() { return ptr; }
const T* operator&() const { return ptr; }
};
3. 代理模式
class AddressProxy
{
int* realTarget;
public:
AddressProxy(int* target) : realTarget(target) {}
int** operator&() { return &realTarget; } // 返回指针的地址
const int* const* operator&() const
{ return &realTarget; }
};
重要注意事项
1. 谨慎重载:除非有充分理由,否则不要重载取地址运算符
2. 保持一致性:const和非const版本应保持逻辑一致
3. 避免混淆:重载行为应与常规预期相符,避免造成困惑
4. STL兼容性:某些STL实现可能依赖标准取地址行为
实际应用示例
安全指针包装器
class SafePointer
{
void* ptr;
public:
SafePointer(void* p) : ptr(p) {}
// 普通版本
void** operator&()
{
static int guard;
if(!ptr) return reinterpret_cast<void**>(&guard);
return &ptr;
}
// const版本
const void* const* operator&() const
{
return const_cast<const void* const*>(&ptr);
}
};
最佳实践
1. 仅在特殊需求时重载取地址运算符
2. 确保重载后的行为有明确文档说明
3. 考虑提供 addressof() 成员函数作为替代方案
4. 保持const和非const版本的逻辑一致性
5. 注意线程安全性问题