在 C++ 面向对象编程中,类的默认成员函数是构建健壮代码的基石。当我们未显式实现这些函数时,编译器会自动生成,但其行为是否符合需求、何时需要手动重写,是初学者容易混淆的关键点。本文将围绕类的 6 个默认成员函数,结合实例深入解析构造函数、析构函数、拷贝构造函数、赋值运算符重载的核心逻辑,同时简要介绍取地址运算符重载,帮助你彻底掌握这一重要知识点。
一、默认成员函数概述:编译器的 "自动生成" 规则
默认成员函数是指用户未显式实现时,编译器会自动生成的成员函数。一个空类在编译器处理后,会隐式包含6 个默认成员函数,其中前 4 个是核心,后 2 个(取地址重载)极少需要手动实现。
| 函数类型 | 核心作用 | 是否需要频繁手动实现 |
|---|---|---|
| 构造函数 | 初始化对象 | 是(内置类型需手动初始化) |
| 析构函数 | 清理对象资源 | 是(有动态资源申请时必须实现) |
| 拷贝构造函数 | 用同类对象初始化新对象 | 是(有动态资源时需深拷贝) |
| 赋值运算符重载 | 两个已存在对象的拷贝 | 是(有动态资源时需深拷贝) |
| 普通取地址重载 | 获取对象地址 | 否(编译器生成足够用) |
| const 取地址重载 | 获取 const 对象地址 | 否(编译器生成足够用) |
学习默认成员函数的两个核心问题:
- 编译器自动生成的函数行为是什么?是否满足需求?
- 当自动生成的函数不满足需求时,如何正确手动实现?
二、构造函数:对象的 "初始化管家"
构造函数的核心作用是初始化对象,而非开辟内存空间(局部对象的内存在栈帧创建时已分配)。它替代了 C 语言中手动调用Init函数的繁琐操作,在对象实例化时自动执行。
2.1 构造函数的 7 个关键特性
- 函数名与类名相同:如
Date类的构造函数名为Date,无返回值(无需写void)。 - 自动调用:对象实例化时编译器自动调用,无需手动触发。
- 支持重载:可定义无参、带参、全缺省等多种构造函数,满足不同初始化场景。
- 默认构造函数的唯一性:无参构造、全缺省构造、编译器自动生成的构造,统称为 "默认构造函数",但三者不能同时存在(调用时会产生歧义)。
- 例:若同时定义
Date()和Date(int year=1, int month=1, int day=1),Date d1;会编译报错。
- 例:若同时定义
- 编译器自动生成的构造行为:
- 对内置类型成员(
int、指针等):不做初始化,值为随机值(依赖编译器)。 - 对自定义类型成员(如
Stack对象):自动调用该成员的默认构造函数。
- 对内置类型成员(
- 无参构造的调用注意:通过无参构造创建对象时,对象后不能加括号,否则编译器会误认为是函数声明。
- 错误:
Date d3();(编译器认为是声明一个返回Date的无参函数)。 - 正确:
Date d3;。
- 错误:
2.2 构造函数实例:Date 类与 Stack 类的实现
示例 1:Date 类的构造函数重载
#include<iostream>
using namespace std;
class Date {
public:
// 1. 无参构造函数
Date() {
_year = 1;
_month = 1;
_day = 1;
}
// 2. 带参构造函数
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
// 3. 全缺省构造函数(与无参构造不能同时存在)
/*Date(int year = 1, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}*/
void Print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1; // 调用无参构造
Date d2(2025, 1, 1); // 调用带参构造
d1.Print(); // 输出:1/1/1
d2.Print(); // 输出:2025/1/1
return 0;
}
示例 2:自定义类型成员的构造调用
当类的成员是自定义类型时,编译器生成的构造会自动调用该成员的默认构造:
class Stack {
public:
// Stack的全缺省构造
Stack(int n = 4) {
_a = (int*)malloc(sizeof(int) * n);
if (_a == nullptr) {
perror("malloc失败");
return;
}
_capacity = n;
_top = 0;
}
private:
int* _a;
size_t _capacity;
size_t _top;
};
class MyQueue {
private:
Stack pushst; // 自定义类型成员
Stack popst; // 自定义类型成员
};
int main() {
MyQueue mq; // 编译器生成MyQueue的构造,自动调用pushst和popst的默认构造
return 0;
}
三、析构函数:对象的 "资源清理工"
析构函数的核心作用是清理对象的资源(如动态内存、文件句柄),而非销毁对象本身(局部对象的内存会随栈帧销毁释放)。它替代了 C 语言中手动调用Destroy函数的操作,在对象生命周期结束时自动执行。
3.1 析构函数的 8 个关键特性
- 函数名格式:类名前加
~,如~Date(),无参数、无返回值。 - 唯一性:一个类只能有一个析构函数,不支持重载。
- 自动调用:对象生命周期结束时(如局部对象出作用域),编译器自动调用。
- 编译器自动生成的析构行为:
- 对内置类型成员:不做处理(无需清理)。
- 对自定义类型成员:自动调用该成员的析构函数。
- 手动实现的场景:当类中存在动态资源申请(如
malloc、new)时,必须手动实现析构函数释放资源,否则会导致内存泄漏。- 例:
Stack类的_a是动态内存,需在析构中free(_a);Date类无动态资源,可直接用编译器生成的析构。
- 例:
- 析构顺序:局部域中多个对象,后定义的先析构(与栈的 "后进先出" 一致)。
3.2 析构函数实例:Stack 类的资源释放
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack {
public:
// 构造:申请动态内存
Stack(int n = 4) {
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (_a == nullptr) {
perror("malloc失败");
return;
}
_capacity = n;
_top = 0;
}
// 析构:释放动态内存(必须手动实现)
~Stack() {
cout << "~Stack()" << endl;
free(_a); // 释放内存
_a = nullptr; // 避免野指针
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
int main() {
Stack st; // 构造调用:申请内存
// 函数结束时,st生命周期结束,自动调用析构:释放内存
return 0;
}
对比 C 与 C++ 的资源管理:C 语言实现Stack时,需手动调用STInit和STDestroy,若遗漏STDestroy会导致内存泄漏;C++ 通过构造和析构的自动调用,彻底避免了这一问题。
四、拷贝构造函数:用已有对象 "复制" 新对象
拷贝构造函数是一种特殊的构造函数,用于用同类已存在的对象初始化新对象(如Date d2(d1);)。它的核心是解决 "如何拷贝对象成员" 的问题,尤其是动态资源的拷贝。
4.1 拷贝构造函数的 6 个关键特性
- 特殊的构造重载:第一个参数必须是自身类类型的引用(
const Date& d),若用传值会引发无穷递归(传值传参需拷贝,拷贝又需传值,循环往复)。 - 参数要求:第一个参数为引用,后续参数可带默认值(极少用)。
- 自动调用场景:
- 用对象初始化新对象(
Date d2(d1);或Date d2 = d1;)。 - 自定义类型对象传值传参(如
void Func(Date d))。 - 自定义类型对象传值返回(如
Date Func())。
- 用对象初始化新对象(
- 编译器自动生成的拷贝行为:
- 对内置类型成员:值拷贝(浅拷贝,逐字节复制)。
- 对自定义类型成员:调用该成员的拷贝构造。
- 手动实现的场景:当类中存在动态资源时,浅拷贝会导致多个对象指向同一块内存,析构时重复释放(程序崩溃),需手动实现深拷贝。
- 例:
Stack类的_a是动态内存,浅拷贝会让st1._a和st2._a指向同一块内存,析构时free两次,导致崩溃。
- 例:
- 传值返回与引用返回:
- 传值返回:生成临时对象,调用拷贝构造(效率低)。
- 引用返回:返回对象别名,无拷贝(效率高),但需确保返回对象生命周期长于函数(如静态对象、成员对象),否则会产生野引用。
4.2 拷贝构造实例:深拷贝 vs 浅拷贝
问题场景:浅拷贝导致的崩溃
class Stack {
public:
Stack(int n = 4) { /* 构造:申请内存 */ }
~Stack() { free(_a); } // 析构:释放内存
// 未手动实现拷贝构造,编译器生成浅拷贝
private:
int* _a;
size_t _capacity;
size_t _top;
};
int main() {
Stack st1;
Stack st2 = st1; // 浅拷贝:st2._a = st1._a
// 析构时:先析构st2,free(st2._a);再析构st1,free(st1._a)(重复释放,崩溃)
return 0;
}
解决:手动实现深拷贝
class Stack {
public:
// 构造
Stack(int n = 4) {
_a = (int*)malloc(sizeof(int) * n);
if (_a == nullptr) { perror("malloc失败"); return; }
_capacity = n;
_top = 0;
}
// 深拷贝构造:为新对象申请独立内存
Stack(const Stack& st) {
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a == nullptr) { perror("malloc失败"); return; }
memcpy(_a, st._a, sizeof(int) * st._top); // 拷贝数据
_top = st._top;
_capacity = st._capacity;
}
// 析构
~Stack() {
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
size_t _capacity;
size_t _top;
};
int main() {
Stack st1;
Stack st2 = st1; // 深拷贝:st2._a指向新内存,析构无重复释放
return 0;
}
五、赋值运算符重载:两个已存在对象的 "拷贝"
赋值运算符重载用于两个已存在对象之间的拷贝(如d1 = d2;),需与拷贝构造区分:拷贝构造是 "用旧对象初始化新对象",赋值重载是 "两个对象都已存在,将一个的值赋给另一个"。
5.1 运算符重载基础
C++ 允许通过operator+运算符的形式,自定义运算符对类对象的行为。需注意:
运算符重载函数的参数个数 = 运算符的运算对象个数(一元运算符 1 个参数,二元运算符 2 个参数)。
若为成员函数,第一个参数默认是this指针(隐含),因此参数个数比运算对象少 1。
不可重载的运算符:.*、::、sizeof、?:、.(5 个,选择题高频考点)。
重载运算符至少有一个类类型参数(不能改变内置类型行为,如int operator+(int a, int b)是非法的)。
5.2 赋值运算符重载的 5 个关键特性
- 必须为成员函数:C++ 规定赋值运算符重载只能是类的成员函数,不能是全局函数(否则编译器会生成默认版本,导致冲突)。
- 参数与返回值:
- 参数:建议用
const 类类型&(避免传值拷贝,const确保不修改源对象)。 - 返回值:建议用
类类型&(引用返回,支持连续赋值如d1 = d2 = d3;,且避免拷贝)。
- 参数:建议用
- 自我赋值检查:需判断
this != &d(若d1 = d1;,直接赋值会导致资源泄漏,尤其是深拷贝场景)。 - 编译器自动生成的赋值行为:与拷贝构造一致,内置类型浅拷贝,自定义类型调用其赋值重载。
- 手动实现的场景:与拷贝构造相同,类中有动态资源时,需手动实现深拷贝,避免重复释放。
5.3 赋值运算符重载实例
class Date {
public:
Date(int year = 1, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
// 赋值运算符重载:深拷贝(Date无动态资源,此处为演示)
Date& operator=(const Date& d) {
// 自我赋值检查
if (this != &d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this; // 返回当前对象,支持连续赋值
}
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2024, 7, 5);
Date d2(2024, 7, 6);
d1 = d2; // 调用赋值重载:d1的值变为d2的值
d1.Print(); // 输出:2024-7-6
Date d3;
d3 = d1 = d2; // 连续赋值:d1 = d2,d3 = d1
d3.Print(); // 输出:2024-7-6
return 0;
}
六、取地址运算符重载:编译器生成即够用
取地址运算符重载分为两种:
- 普通取地址重载:
Date* operator&(),返回普通对象的地址。 - const 取地址重载:
const Date* operator&() const,返回 const 对象的地址。
核心结论:编译器自动生成的取地址重载已能满足绝大多数需求(返回this指针),无需手动实现。仅在特殊场景(如禁止外部获取对象地址)下,可手动重写并返回nullptr或其他地址。
class Date {
public:
// 普通取地址重载(编译器自动生成版本)
Date* operator&() {
return this;
// 禁止获取地址:return nullptr;
}
// const取地址重载(编译器自动生成版本)
const Date* operator&() const {
return this;
// 禁止获取地址:return nullptr;
}
private:
int _year;
int _month;
int _day;
};
七、总结:默认成员函数的核心决策指南
学习默认成员函数的关键,在于判断 "何时用编译器生成,何时手动实现"。以下是核心决策指南:
| 函数类型 | 编译器生成行为 | 手动实现场景 |
|---|---|---|
| 构造函数 | 内置类型随机值,自定义类型调用其默认构造 | 需初始化内置类型(如Date的年月日) |
| 析构函数 | 内置类型不处理,自定义类型调用其析构 | 类中有动态资源(malloc/new) |
| 拷贝构造 | 内置类型浅拷贝,自定义类型调用其拷贝构造 | 类中有动态资源(需深拷贝) |
| 赋值重载 | 内置类型浅拷贝,自定义类型调用其赋值重载 | 类中有动态资源(需深拷贝) |
记住一个小技巧:若类手动实现了析构函数(释放动态资源),则必须手动实现拷贝构造和赋值重载,否则会因浅拷贝导致资源泄漏或程序崩溃。
掌握默认成员函数,是 C++ 面向对象编程的第一步,也是写出高效、健壮代码的基础。建议结合本文实例动手实践,深入理解浅拷贝与深拷贝的差异,以及构造 / 析构的调用时机,为后续学习继承、多态打下坚实基础。
1658

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



