C++ 类的 6 个默认成员函数与运算符重载详解
本文详细讲解了 C++ 中类的 6 个默认成员函数(构造函数、析构函数、拷贝构造函数、赋值运算符重载等)及运算符重载机制。内容涵盖构造函数的定义与特性、默认构造函数的生成条件、析构函数的资源清理职责、拷贝构造与赋值运算符的区别(浅拷贝与深拷贝)、以及运算符重载的规则与实现方式(如前置后置自增自减)。文章通过代码示例说明了对象生命周期管理的关键细节,帮助读者掌握 C++ 面向对象编程的核心基础。

本文详细讲解了 C++ 中类的 6 个默认成员函数(构造函数、析构函数、拷贝构造函数、赋值运算符重载等)及运算符重载机制。内容涵盖构造函数的定义与特性、默认构造函数的生成条件、析构函数的资源清理职责、拷贝构造与赋值运算符的区别(浅拷贝与深拷贝)、以及运算符重载的规则与实现方式(如前置后置自增自减)。文章通过代码示例说明了对象生命周期管理的关键细节,帮助读者掌握 C++ 面向对象编程的核心基础。

在 C++ 中,类的 6 个默认成员函数是编译器自动生成的核心操作,支撑着对象的创建、初始化、销毁、拷贝等基础行为。从构造函数初始化对象,到析构函数清理资源,再到拷贝构造、赋值重载等操作,这些函数虽常被隐式调用,却决定了类的基本交互逻辑。而运算符重载则让自定义类型能像内置类型一样使用运算符,简化了代码表达。
类中没有成员函数和成员变量。
在 C++ 里,当你定义一个类时,即便你没有明确编写某些成员函数,编译器也会自动为这个类生成 6 个默认的成员函数。用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
#include <iostream>
using namespace std;
typedef int DataType;
class Stack {
public:
// 构造函数等价于 InitStack(int capacity = 4)
Stack(int capacity = 4) {
cout << "Stack(int capacity = 4)" << endl;
_a = (DataType*)malloc(sizeof(DataType) * 4);
if (nullptr == _a) {
perror("malloc fail");
return;
}
_capacity = capacity;
_top = 0;
}
void Init() {
_a = (DataType*)malloc(sizeof(DataType) * 4);
if (nullptr == _a) {
perror("malloc fail");
return;
}
_capacity = 4;
_top = 0;
}
void Push(int x) {
_a[_top++] = x;
}
void Destroy() {
free(_a);
_a = nullptr;
_top = _capacity;
}
int Top() {
return _a[_top - 1];
}
private:
int* _a;
int _top;
int _capacity;
};
int main() {
Stack s; // 构造函数等价于 Init
// s.Init();
s.Push(1);
s.Push(2);
s.Push(3);
s.Push(4);
s.Destroy();
return 0;
}
上面的代码可以成功运行,相比于之前的代码,构造函数 Stack 通过构造函数整合初始化逻辑,实例化对象时编译器自动调用构造函数完成初始化,无需手动调用 Init,简化流程且让对象初始化更自然高效。
如果类中没有显式定义构造函数,则 C++ 编译器 会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。C++ 标准没有规定要初始化为 0,但有的编译器规定初始化为 0。 默认构造函数是指编译器生成的、全缺省的、无参的。 C++ 主要有内置类型 / 基本类型、语言本身定义的基础类型 int/char/double/指针等等和自定义、用 struct / class 等等定义的类型。 如果不写构造函数,编译器默认生成构造函数,内置类型可能不做处理,自定义类型会去调用它的默认构造函数。
#include <iostream>
using namespace std;
typedef int DataType;
class Stack {
public:
// 构造函数支持重载
Stack(DataType* a, int n) {
cout << "Stack(DataType* a, int n)" << endl;
_a = (DataType*)malloc(sizeof(DataType) * 4);
if (nullptr == _a) {
perror("malloc fail");
return;
}
memcpy(_a, a, sizeof(DataType) * n);
_capacity = n;
_top = 0;
}
// 构造函数等价于 Init
Stack(int capacity = 4) {
cout << "Stack(int capacity = 4)" << endl;
_a = (DataType*)malloc(sizeof(DataType) * 4);
if (nullptr == _a) {
perror("malloc fail");
return;
}
_capacity = capacity;
_top = 0;
}
void Init() {
_a = (DataType*)malloc(sizeof(DataType) * 4);
if (nullptr == _a) {
perror("malloc fail");
return;
}
_capacity = 4;
_top = 0;
}
void Push(int x) {
_a[_top++] = x;
}
// 析构函数在对象生命周期结束后自动调用
~Stack() {
cout << "~Stack()" << endl;
if (_a) {
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
}
}
void Destroy() {
free(_a);
_a = nullptr;
_top = _capacity;
}
int Top() {
return _a[_top - 1];
}
private:
int* _a;
int _top;
int _capacity;
};
class Date {
public:
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// 内置类型
int _year;
int _month;
int _day;
// 自定义类型
Stack _st;
};
int main() {
Date d1;
Date d2;
d1.Print();
return 0;
}
有自定义类型 内置类型会被初始化为 0,编译器会自动调用自定义类型的默认构造函数。
没有自定义类型 内置类型不会被初始化为 0。
总结
class Date {
public:
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// 这里不是初始化,因为这里只是声明
// 这里是缺省值,给编译器生成的默认构造函数用
int _year = 1;
int _month = 1;
int _day = 1;
Stack _st;
};
无参构造函数
#include <iostream>
using namespace std;
class Date {
public:
// 无参构造函数
Date() {
cout << "Date()" << endl;
_year = 1;
_month = 2;
_day = 1;
}
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// 内置类型
// 这里不是初始化,因为这里只是声明
// 这里是缺省值,给编译器生成的默认构造函数用
// 无参构造函数这里可以添加缺省值
int _year = 1;
int _month = 1;
int _day = 1;
};
int main() {
Date d1;
d1.Print(); // 对象。函数()
return 0;
}
无参构造函数可以在内置类型的声明后面添加缺省值,缺省值可以给编译器默认生成的构造函数使用,在 main 函数里面,d1 是对象,和普通函数不一样的是,这里不能加括号,如果添加括号会报错。如果声明和无参构造函数后面同时添加值,则使用无参数构造函数的值。
有参构造函数
#include <iostream>
using namespace std;
class Date {
public:
// 有参构造函数
Date(int year, int month, int day) {
cout << "Date(int year, int month, int day)" << endl;
_year = year;
_month = month;
_day = day;
}
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// 有参构造函数这里不能添加缺省值
int _year;
int _month;
int _day;
};
int main() {
Date d2(2023, 1, 2);
d2.Print();
return 0;
}
与无参构造函数不一样的是有参构造函数不可以再内置类型的声明后面添加缺省值,在 main 函数里面,d2 是对象,对象后面添加括号,对内置函数进行初始化。
#include <iostream>
using namespace std;
class Date {
public:
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// 内置类型
// 这里不是初始化,因为这里只是声明
// 这里是缺省值,给编译器生成的默认构造函数用
int _year = 1;
int _month = 1;
int _day = 1;
};
int main() {
Date d1;
d1.Print();
return 0;
}
如果不写构造函数,C++ 编译器默认生成无参构造函数,即不能在对象后面添加括号进行传参,否则会报错。
如果在内置类型的声明后面不写缺省值,则内置类型是随机值。
注意
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
#include <iostream>
using namespace std;
typedef int DataType;
class Stack {
public:
// 构造函数等价于 Init
Stack(int capacity = 4) {
cout << "Stack(int capacity = 4)" << endl;
_a = (DataType*)malloc(sizeof(DataType) * 4);
if (nullptr == _a) {
perror("malloc fail");
return;
}
_capacity = capacity;
_top = 0;
}
void Init() {
_a = (DataType*)malloc(sizeof(DataType) * 4);
if (nullptr == _a) {
perror("malloc fail");
return;
}
_capacity = 4;
_top = 0;
}
void Push(int x) {
_a[_top++] = x;
}
// 析构函数等价于 Destroy
~Stack() {
cout << "~Stack()" << endl;
if (_a) {
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
}
}
void Destroy() {
free(_a);
_a = nullptr;
_top = _capacity;
}
int Top() {
return _a[_top - 1];
}
private:
int* _a;
int _top;
int _capacity;
};
int main() {
Stack s;
// 构造函数等价于 Init
// s.Init();
s.Push(1);
s.Push(2);
s.Push(3);
s.Push(4);
// s.Destroy();
return 0;
}
有了构造函数和析构函数,就不害怕没写初始化和清理函数了,也简化了代码。
只有单个形参,该形参是本类类型对象的引用(一般常用 const 修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。拷贝构造使用 1 个对象去初始化另一个对象,是已经存在的两个对象之间的拷贝。
一般建议在引用前加上 const,防止代码出现错误。
错误代码
正确代码
如果加上 const 则代码会报错,有利于检查代码
拷贝构造函数的参数只有一个且必须是类类型对象的引用或指针,使用传值方式编译器直接报错,因为会引发无穷递归调用。
引用
指针
注意
如果使用指针传参时不写取地址符,则函数传参不会进入拷贝构造函数,代码可能会报错。
#include <iostream>
using namespace std;
class Date {
public:
Date(int year = 1900, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数
Date(Date& d) // 类对象的引用
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
void Func(Date d) {}
void Func(int d) {}
int main() {
Date d1(2023, 4, 25);
// Date d2(d1); // 规定了自定义类型必须要用拷贝构造去完成
Func(d1); // 内置类型直接拷贝
Func(10);
return 0;
}
C++ 规定,自定义类型必须使用拷贝构造函数才能完成,内置类型可以直接拷贝。如果传参传的是构造函数,则函数传参后先进入拷贝构造函数,然后进入函数体。 例如在上面的代码中,Func(d1) 传参之后先进入拷贝构造函数,再进入 Func 函数。
Date d2(d1) 在这句代码,d1 就是 d 的别名,this 指针指向的是 d2。
在上面代码中,_year、_month 和 _day 是私有的,只能在类里面访问,在外面不能访问,拷贝构造函数中,等号左右两边的变量不是声明中的 _year、_month 和 _day,等号左边的 _year、_month 和 _day 是 this 指针的,等号右边的 _year、_month 和 _day 是 d 的。可以将拷贝构造函数改写成这样。
没有开辟空间
#include <iostream>
using namespace std;
class Date {
public:
Date(int year = 1900, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2023, 4, 25);
Date d2(d1);
return 0;
}
由结果可知,如果没有开辟空间,默认的拷贝构造函数可以使用,不需要写拷贝构造函数。
开辟空间
#include <iostream>
using namespace std;
class Stack {
public:
Stack() {
cout << "Stack()" << endl;
_a = (int*)malloc(sizeof(int) * 4);
if (nullptr == _a) {
perror("malloc fail");
return;
}
_top = 0;
_capacity = 4;
}
Stack(int capacity) {
cout << "Stack(int capacity)" << endl;
_a = (int*)malloc(sizeof(int) * capacity);
if (nullptr == _a) {
perror("malloc fail");
return;
}
_top = 0;
_capacity = capacity;
}
~Stack() {
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main() {
Stack st1;
Stack st2(st1);
return 0;
}
上面的程序会崩溃,原因是拷贝构造函数在拷贝空间时,会将 st1 和 st2 指向同一块空间,析构函数会先释放 st2,此时空间销毁,再去释放 st1 的空间程序会崩溃。如果 1 个修改,因为指向同一个空间另一个也会被修改。
释放空间的顺序为先释放 st2 再释放 st1,拷贝构造函数是将 st1 拷贝给 st2,先有 st1 再有 st2,符合先进后出原则。
深拷贝
Stack(const Stack& st) {
_a = (int*)malloc(sizeof(int) * st._capacity);
if (nullptr == _a) {
perror("malloc fail");
return;
}
memcpy(_a, st._a, sizeof(int) * st._top);
_top = st._top;
_capacity = st._capacity;
}
运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通函数类似。是否要重载运算符,要明确这个运算符对这个类是否有意义,以日期函数为例,日期 + 日期没有意义,日期 - 日期有意义。
关键字 operator 后面接需要重载的运算符符号。
返回值类型 operator 操作符 (参数列表)
返回类型 operator 运算符 (参数列表) {
// 函数体
}
运算符重载函数的定义方式有两种,既可以作为类的成员函数,也能作为全局函数。 这里以日期函数为例进行解释运算符重载。
全局函数
#include <iostream>
using namespace std;
class Date {
public:
Date(int year = 1900, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
bool operator<(const Date& x1, const Date& x2) {
if (x1._year < x2._year) {
return true;
} else if (x1._year == x2._year && x1._month < x2._month) {
return true;
} else if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day) {
return true;
}
return false;
}
int main() {
Date d1(2023, 4, 25);
Date d2(2023, 5, 25);
cout << (d1 < d2) << endl;
cout << (operator<(d1, d2)) << endl;
return 0;
}
内置类型可以直接比较,自定义类型不能直接比较,<< 是流插入运算符,它的优先级高于比较运算符,所以流插入运算符和比较运算符同时存在时应给比较运算符添加括号。
在上面的代码中,d1 < d2; 和 operator<(d1, d2); 是完全等价的,因为运算符重载本质上是函数调用,d1 < d2 是隐式调用,operator<(d1, d2); 是显示函数调用,编译器会将 d1 < d2 解释为对 operator< 函数的调用,就像调用普通函数一样。
如果要在全局函数使用类中的变量,权限必须是 public,即权限是公有的,如果权限是私有的,则不能在类外面访问变量。
成员函数
#include <iostream>
using namespace std;
class Date {
public:
// 构造函数
Date(int year = 1, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
// 作为类成员函数重载时,其形参看起来比操作数数目少 1,因为成员函数的第一个参数为隐藏的 this
bool operator<(const Date& x) {
if (_year < x._year) // 这里等价于 if (this->_year < x._year)
return true;
else if (_year == x._year && _month < x._month)
return true;
else if (_year == x._year && _month == x._month && _day < x._day)
return true;
return false;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2018, 9, 26);
Date d2(2018, 10, 27);
// 日期 - 日期有意义
// 日期 + 日期没意义
// 是否要重载运算符,这个运算符对这个类是否有意义
// 全局函数
// d1 < d2;
// 转换成 operator<(d1, d2);
// 成员函数
d1 < d2;
// 转换成 d1.operator<(d2);
d1.operator<(d2);
if (d1 < d2) {}
return 0;
}
相比于全局函数,如果是成员函数,函数调用应该写成 d1.operator<(d2); 和全局函数类似,d1.operator<(d2); 和 d1 < d2 是等价的,如下图,在汇编语言中这两段代码的地址一样,编译器会将 d1 < d2 转换成 d1.operator<(d2); 这句代码。
赋值运算符是一种拷贝,拷贝构造是使用 1 个对象去初始化另一个对象,赋值运算符重载是已经存在的两个对象之间的拷贝。
#include <iostream>
using namespace std;
class Date {
public:
// 构造函数
Date(int year = 1, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
// 作为类成员函数重载时,其形参看起来比操作数数目少 1,因为成员函数的第一个参数为隐藏的 this
bool operator<(const Date& x) {
if (this->_year < x._year) // 这里等价于 if (this->_year < x._year)
return true;
else if (this->_year == x._year && this->_month < x._month)
return true;
else if (this->_year == x._year && this->_month == x._month && this->_day < x._day)
return true;
return false;
}
void operator=(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2018, 9, 26);
Date d2(2018, 10, 27);
d1 = d2;
d1.operator=(d2);
return 0;
}
这里是浅拷贝,也就是值拷贝没有开辟空间。和运算符重载类似 d1 = d2; 等价于 d1.operator=(d2);
如果要进行连续赋值则需要返回 this 指针,返回不能是 void。在连续赋值中,运算是从右向左执行的,即先执行 k = 0,返回 k 的值,以此类推,j = k 返回 j 的值,最后 i = j 返回 i 的值。
int i, j, k;
i = j = k = 0;
如果是连续赋值,d2 = d3 返回的是 d2,在这里 this 是 d2 的地址,即返回 this 指针。this 指针不能在形参和实参的位置显示,但在函数内部可以使用。类似的 d1 = d2 返回 d1,this 此时是 d1 的地址。
Date d1(2018, 9, 26);
Date d2(2018, 10, 27);
Date d3(2018, 11, 28);
d1 = d2 = d3;
Date& operator=(const Date& d) {
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
return *this;
}
在这里如果添加拷贝构造函数,在连续赋值中,会调用两次拷贝构造函数,可以使用引用返回提高代码效率。在这里 this 指针是全局变量,出了函数作用域不会被销毁。
拷贝构造函数
Date(const Date& d) {
cout << "Date(const Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
原来的
Date operator=(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
改进后
Date& operator=(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
函数返回时会生成临时副本返回,连续赋值会生成临时副本,调用两次拷贝构造函数,而使用引用返回不会调用拷贝构造函数,而是直接返回对象本身的别名。
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。默认生成的赋值运算符重载函数支持连续赋值。
注意
内置类型成员变量是直接赋值的,也就是值拷贝或者浅拷贝,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。 如果开辟空间需要写赋值运算符重载函数,不开辟空间不需要写。
没有写赋值运算符重载函数
前置 ++ 是先 ++ 再使用,返回的是 +1 之后的结果,可以直接使用 this 指针加 1。 后置 ++ 是先使用后 ++,返回的是 +1 之前的结果,因此要使用临时变量,再 +1。 为了让前置 ++ 和后置 ++ 两个函数构成重载,在后置 ++ 的参数位置添加整型 int 用来占位,来使两个函数构成函数重载。
#include <iostream>
using namespace std;
class Date {
public:
// 构造函数
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
// 前置 ++
Date& operator++() {
*this += 1;
return *this;
}
// 后置 ++
// int 是为了占位,不是为了接受具体的值,方便区分,为了跟前置 ++ 构成重载
Date operator++(int) {
Date tmp = *this;
*this += 1;
return tmp;
}
private:
int _year;
int _month;
int _day;
};
void TestDate() {
// 前置 ++
Date d3(2025, 7, 13);
++d3;
d3.Print();
// 后置 ++
Date d4(2025, 7, 13);
d4++;
d4.Print();
}
int main() {
TestDate();
return 0;
}
注意
在前置 ++ 中使用传引用返回,在后置 ++ 中不能使用传引用返回,是因为 tmp 是局部变量,局部变量在函数出了作用域后会销毁,无法使用传引用返回。
前置 -- 是先 -- 再使用,返回的是 -1 之后的结果,可以直接使用 this 指针减 1。 后置 -- 是先使用后 --,返回的是 -1 之前的结果,因此要使用临时变量,再 -1。 和 ++ 类似,为了让前置 -- 和后置 -- 两个函数构成重载,在后置 -- 的参数位置添加整型 int 用来占位,来使两个函数构成函数重载。
#include <iostream>
using namespace std;
class Date {
public:
// 构造函数
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
// 前置 --
Date& operator--() {
*this -= 1;
return *this;
}
// 后置 --
// int 是为了占位,不是为了接受具体的值,方便区分,为了跟前置 -- 构成重载
Date operator--(int) {
Date tmp = *this;
*this -= 1;
return tmp;
}
private:
int _year;
int _month;
int _day;
};
void TestDate() {
// 前置 --
Date d3(2025, 7, 13);
--d3;
d3.Print();
// 后置 --
Date d4(2025, 7, 13);
d4--;
d4.Print();
}
int main() {
TestDate();
return 0;
}
类的默认成员函数与运算符重载,是 C++ 管理对象生命周期和简化操作的核心工具。它们规范了对象从创建到销毁、从拷贝到运算的全过程,尤其在资源处理时,深拷贝、析构函数等的合理实现直接影响程序稳定性。理解这些机制,能帮助我们写出更规范、高效的面向对象代码,充分发挥 C++ 的封装与抽象优势。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online