跳到主要内容
C++ 类和对象进阶:默认成员函数与运算符重载 | 极客日志
C++
C++ 类和对象进阶:默认成员函数与运算符重载 C++ 类默认成员函数涵盖构造函数、析构函数、拷贝构造函数及赋值运算符等核心概念。文章详细解析各函数的定义、生成机制与调用场景,区分浅拷贝与深拷贝的差异。重点阐述 const 成员函数与 mutable 关键字的使用规范,以及运算符重载的规则与实现细节。结合 Date 类完整示例,演示日期校验、比较运算及流操作符重载,深入理解类对象生命周期管理与资源控制。
极客工坊 发布于 2026/3/30 更新于 2026/4/23 1 浏览1、类的默认成员函数
类的默认成员函数是编译器在没有显式定义相应函数时自动生成的函数。这些函数通常是为了处理类对象的生命周期管理,包括对象的创建、复制、赋值和销毁等操作。确保即使开发者没有显式提供某些操作,编译器也能提供默认实现,以保证程序的基本功能。
通常包括以下几个函数:
默认构造函数
拷贝构造函数
拷贝赋值重载
析构函数
取地址操作符重载
这些默认成员函数由编译器在没有显式定义时自动生成,它们通常执行逐成员的浅拷贝或简单的资源释放。对于包含动态内存或复杂资源管理的类,通常需要显式实现这些函数,确保资源的正确管理。
C++11 之后还引入了两个成员函数:
移动构造函数(C++11 引入)
移动赋值重载(C++11 引入)
本文我们重点讲解前五个默认成员函数。
2、构造函数
构造函数的特点:
函数名称与类名相同。
没有返回类型(甚至没有 void)。
自动调用:每当创建对象时,构造函数会被自动调用。
可以有多个构造函数(构造函数重载)。
不能被显式调用:构造函数只能在对象创建时由编译器调用。
构造函数的类型:
默认构造函数(Default Constructor)
定义:没有参数或者所有参数都有默认值的构造函数。作用:初始化对象时如果没有传递参数,默认构造函数会被调用。
编译器自动生成:如果没有定义任何构造函数,编译器会生成一个默认构造函数;如果定义了其他构造函数(如带参数的构造函数),则编译器不会自动生成默认构造函数。
无参构造函数、全缺省构造函数、我们不显示定义时编译器默认生成的构造函数,都叫做默认构造函数。
但是这三个函数有且只有一个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认生成那个叫默认构造,实际上无参构造函数、全缺省构造函数也是默认构造,总结一下就是不传实参就可以调用的构造就叫默认构造函数。
编译器默认成的构造函数,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,取决于编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量没有默认构造函数,那么就会编译出错,我们要初始化这个成员变量,需要用初始化列表!(初始化列表后面再讲)
以 Date 类为例:如果我们不显示定义构造函数,编译器会自动生成默认默认构造函数
class Date {
public :
int _year;
int _month;
int _day;
};
无参构造函数:
Date () {
_year = 2025 ;
_month = 1 ;
_day = 15 ;
}
全缺省构造函数:
Date (int year=2025 , int month=1 , int day=15 ) {
_year = year;
_month = month;
_day = day;
}
以上两种构造函数以及编译器自动生成的构造函数都属于默认构造函数
带参数构造函数(Parameterized Constructor)
定义:包含一个或多个参数的构造函数,允许在创建对象时为对象成员传递初始值。
作用:通过传递不同的参数,创建对象时可以初始化不同的值。
Date (int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
3、析构函数
定义:析构函数在对象生命周期结束时调用,用于释放对象占用的资源。
自动生成:如果你没有定义析构函数,编译器会自动生成一个默认析构函数。它会销毁对象并释放资源。但如果类中有动态内存分配,或者存在资源申请,则需要显式定义析构函数。
析构函数的名称必须与类名相同,但在前面加上波浪号 ~。例如,类 Date 的析构函数名称应该是 ~Date()。
析构函数没有返回类型,(包括 void)。
析构函数不能带有参数,因此无法重载。
自动调用:析构函数由编译器在对象生命周期结束时自动调用。当一个对象超出其作用域时,或显式调用 delete 删除一个动态分配的对象时,析构函数会被调用。
调用顺序:对于局部对象,析构函数在对象超出作用域时被自动调用。对于动态分配的对象,析构函数在 delete 语句执行时被调用。
跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,对于自定义类型成员会调用它的析构函数。
class Date {
public :
~Date () {
}
};
析构函数的作用是清理对象在其生命周期内所占用的资源。常见的资源包括:
动态内存:通过 new 动态分配的内存需要在析构函数中通过 delete 或 delete[] 释放。
文件句柄、数据库连接、网络资源:如果对象打开了文件或建立了网络连接,析构函数应负责关闭它们。
其他资源:任何外部资源(例如锁、线程、内存映射等)都应在析构函数中清理。
对象生命周期结束时:当局部对象超出作用域时,析构函数会自动调用。例如,当 main() 函数结束时,局部对象的析构函数会被调用。
动态分配的对象:如果对象是通过 new 分配的,那么在调用 delete 时,析构函数会被自动调用。
一个局部域的多个对象,C++规定后定义的先析构。
#include <iostream>
using namespace std;
class MyClass {
public :
MyClass () {
ptr = new int [10 ];
cout << "Constructor: Memory allocated" << endl;
}
~MyClass () {
delete [] ptr;
cout << "Destructor: Memory deallocated" << endl;
}
private :
int * ptr;
};
int main () {
MyClass obj;
return 0 ;
}
4、拷贝构造函数 拷贝构造函数是一个特殊成员函数,用于通过另一个同类型的对象来初始化一个对象。如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数。它通常在以下几种情况下被调用:
对象作为函数参数传递时(按值传递)
从函数返回一个对象(返回值时)
初始化一个对象为另一个同类型对象的副本(拷贝初始化)
拷贝构造函数是构造函数的一个重载。
拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。拷贝构造函数也可以多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须有缺省值。
C++规定自定义类型对象进行拷贝必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝 (一个字节一个字的逐字节拷贝),对自定义类型成员变量会调用他的拷贝构造。
ClassName (const ClassName& other) {
}
ClassName 是类名。
const ClassName& other 是另一个同类型对象的引用,表示将要复制的数据。
关键点:
const:拷贝构造函数的参数必须是 const,这样可以保证传递的对象不被修改。
&:参数是引用类型,避免了对象的拷贝开销。
传递方式:拷贝构造函数通常使用按引用传递对象,而不是按值传递,防止出现不必要的递归调用。
按值传递参数时:当对象作为函数参数传递,并且该对象以值的方式传递时,会调用拷贝构造函数。
void function (MyClass obj) {
MyClass obj1;
function (obj1);
返回值时:当一个函数返回一个对象时,编译器会使用拷贝构造函数来复制返回值。
MyClass createObject () {
MyClass obj;
return obj;
}
拷贝初始化:当一个对象被另一个相同类型的对象初始化时,也会调用拷贝构造函数。
MyClass obj1;
MyClass obj2 (obj1) ;
如果没有显式定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数,通常执行浅拷贝。这意味着它会简单地复制成员变量的值,对于指针成员,它只会复制指针的值(即地址),不会分配新的内存。这样会导致多个对象共享相同的内存资源,可能会出现内存泄漏或意外的资源共享。
class MyClass {
public :
int * data;
MyClass (int val) { data = new int (val); }
~MyClass () { delete data; }
};
MyClass obj1 (10 ) ;
MyClass obj2 (obj1) ;
如果类的成员变量涉及动态内存分配,默认的浅拷贝可能导致问题。拷贝构造函数需要进行深拷贝,即为每个对象分配新的内存空间,以避免不同对象共享同一内存。
class MyClass {
public :
int * data;
MyClass (int val) { data = new int (val); }
MyClass (const MyClass& other) { data = new int (*other.data);
~MyClass () { delete data; }
};
MyClass obj1 (10 ) ;
MyClass obj2 = obj1;
如果没有指针成员,且不涉及动态内存分配,可以使用默认的浅拷贝。
如果类包含指针成员或动态分配的内存,必须自定义拷贝构造函数,并使用深拷贝。
注意自身赋值:在拷贝构造函数中,通常不需要处理自赋值问题(因为自身赋值问题通常出现在赋值运算符重载中),但实现赋值运算符重载时,需要特别注意自身赋值情况。
5、运算符重载 在 C++ 中,运算符重载(Operator Overloading)允许你为自定义类型定义或修改运算符的行为,使得可以使用运算符来操作类的对象。通过运算符重载,类的对象可以像内建类型一样使用运算符进行各种操作(如加法、减法等)。
运算符重载是具有特殊名字的函数,他的名字是由 operator 和后面要定义的运算符共同构成。和其他函数一样,它也具有其返回类型和参数列表以及函数体。
ReturnType operator 符号 (参数列表) {
}
ReturnType:返回类型,通常是操作后的结果类型。
operator 符号:运算符的标识符,例如 +、-、*、[]、= 等。
参数列表:指定操作数的类型和数量。
重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的 this 指针,因此运算符重载作为成员函数时,参数比运算对象少一个。
运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。
不能通过连接语法中没有的符号来创建新的操作符:比如 operator@。
.* :: (域作用解析符)sizeof ?: . 等运算符不能重载
重载 ++ 运算符时,有前置 ++ 和后置 ++,运算符重载函数名都是 operator++,无法很好的区分。
C++规定,后置 ++ 重载时,增加一个 int 形参,跟前置 ++ 构成函数重载,方便区分。
重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this 指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了对象<<cout,不符合使用习惯和可读性。重载为全局函数把 ostream/istream 放到第一个形参位置就可以了,第二个形参位置当类类型对象。
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; }
bool operator ==(const Date& d) {
return _year == d._year && _month == d._month && _day == d._day;
}
Date& operator ++() {
cout << "前置++" << endl;
return *this ;
}
Date operator ++(int ) {
Date tmp;
cout << "后置++" << endl;
return tmp;
}
private :
int _year;
int _month;
int _day;
};
5.1、赋值运算符重载 赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。
默认的赋值运算符执行浅拷贝,即直接复制对象的成员变量。如果类中有指针成员或者动态分配的内存,浅拷贝可能导致内存泄漏或多重释放等问题。因此,当类中涉及动态内存时,你必须手动实现赋值运算符,以确保深拷贝。
赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成 const 当前类类型引用,否则会传值传参会有拷贝
有返回值,且建议写成当前类类型引用。引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝 (一个字节一个字的拷贝),对自定义类型成员变量会调用它的赋值重载函数。
检查自赋值:如果左边对象和右边对象是同一个对象(即自赋值),则无需执行任何操作。
释放当前资源:在执行赋值之前,应该释放当前对象占用的资源,以避免内存泄漏。
执行深拷贝:将右边对象的数据复制到左边对象的成员变量中。
返回*this:以支持链式赋值操作。
class Date {
public :
Date (int year = 1 , int month = 1 , int day = 1 ) { _year = year; _month = month; _day = day; }
Date (const Date& d) { cout << " Date(const Date& d)" << endl; _year = d._year; _month = d._month; _day = d._day; }
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;
};
5.2、const 成员函数
const 成员函数是指那些不会修改对象状态的成员函数。它是通过在成员函数的声明和定义后加上 const 关键字来标识的。const 成员函数保证不会修改对象的成员变量,确保对象的状态在调用该函数时保持不变。
const 实际修饰该成员函数隐含的 this 指针。表明在该成员函数中不能对类的任何成员进行修改。
保证不修改对象状态:const 成员函数承诺不会修改对象的数据,这对于许多场合(如传递常量对象或在多线程中共享对象)非常重要。
编译时检查:通过将成员函数声明为 const,编译器可以帮助检查是否存在意外修改对象的情况。如果在 const 成员函数中尝试修改对象的成员变量,编译器会报错。
提高可读性和安全性:它明确表示该函数是只读操作,不会改变对象的状态,有助于代码的可读性和维护性。
例如 Date 类中的 Print 函数:修饰为 const 成员函数,它只读取对象的状态,不修改任何成员变量。
void Print () const {
cout << _year << "-" << _month << "-" << _day << endl;
}
不能修改成员变量:const 成员函数中不能修改对象的成员变量,除非这些成员变量被声明为 mutable。
不能调用非 const 成员函数:const 成员函数不能调用任何非 const 成员函数,因为后者可能会修改对象的状态。
5.3、mutable 关键字 在 C++ 中,mutable 关键字用于修饰类的成员变量,允许在 const 成员函数 中修改该成员变量。通常,const 成员函数表示该函数不会修改对象的状态,但有时我们希望某些成员变量在这些函数中仍然可以修改。使用 mutable 可以实现这一点。
class MyClass {
public :
MyClass (int counter = 0 ) { _counter = counter; }
void incrementCounter () const {
_counter++;
}
void displayCounter () const {
cout << "Counter: " << _counter << endl;
}
private :
mutable int _counter;
};
int main () {
MyClass obj;
obj.incrementCounter ();
obj.displayCounter ();
return 0 ;
}
缓存:有时我们希望在 const 成员函数中修改某些缓存数据,这时可以使用 mutable。
计数器:在一些只读操作中(如访问次数计数器),需要更新计数器而不影响对象的逻辑状态。
延迟计算:可以用于延迟计算或懒加载(lazy loading)等操作。
只修改 mutable 成员:mutable 关键字只影响被修饰的成员变量,其他成员变量依然受到 const 限制。
const 成员函数的约束:mutable 使得 const 成员函数能够修改成员变量,但不会改变对象的外部状态。它仍然保持对象的'逻辑不可变性',但允许修改内部的、仅对函数内部有意义的数据。
5.4、取地址运算符重载 取地址运算符(&)用于获取对象的内存地址。虽然我们可以直接使用 & 来获取对象的地址,但如果你需要在自定义类中对取地址运算符进行重载,则可以通过重载 operator& 来实现,通常情况下是返回当前对象的 this 指针。
取地址运算符重载分为普通取地址运算符重载和 const 取地址运算符重载
取地址运算符通常不常见于需要重载的运算符,因为它通常只是返回对象的地址。编译器默认生成的重载函数就可以满足我们的需求,不需要显式实现。
在一些特殊场景下:如(模拟智能指针、内存管理等)场景下发挥作用
或者当我们不想将当前类对象的地址被获取,可以通过重载取地址运算符的重载返回特定的地址!
class Date {
public :
Date *operator &() {
return this ;
}
const Date *operator &() const {
return this ;
}
private :
int _year;
int _month;
int _day;
};
6、完整的 Date 类实现
Date.h 头文件 class Date {
friend ostream& operator <<(ostream& out, const Date& d);
friend istream& operator >>(istream& in, Date& d);
public :
Date (int year = 2005 , int month = 5 , int day = 18 );
bool DateCheck () ;
void print () ;
int GetMonthDay (int year, int month) {
static int ArrMonth[13 ] = {-1 , 31 , 28 , 31 , 30 , 31 , 30 , 31 , 31 , 30 , 31 , 30 , 31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0 ) || (year % 400 == 0 ))) {
return 29 ;
}
return ArrMonth[month];
}
bool operator <(const Date& d) const ;
bool operator <=(const Date& d) const ;
bool operator >(const Date& d) const ;
bool operator >=(const Date& d) const ;
bool operator ==(const Date& d) const ;
bool operator !=(const Date& d) const ;
Date& operator +=(int day);
Date operator +(int day);
Date& operator -=(int day);
Date operator -(int day);
int operator -(const Date& d) const ;
Date& operator ++();
Date operator ++(int );
Date& operator --();
Date operator --(int );
private :
int _year;
int _month;
int _day;
};
Date.cpp 源文件 #include "Date.h"
bool Date::DateCheck () {
if (_month < 1 || _month > 12 || _day < 1 || _day > GetMonthDay (_year, _month)) {
return false ;
}
return true ;
}
Date::Date (int year, int month, int day) {
_year = year; _month = month; _day = day;
if (!DateCheck ()) {
cout << "日期非法" << endl;
print ();
}
}
void Date::print () {
cout << _year << "/" << _month << "/" << _day << endl;
}
Date& Date::operator +=(int day) {
if (day < 0 ) {
return *this -= (-day);
}
_day += day;
while (_day > GetMonthDay (_year, _month)) {
_day -= GetMonthDay (_year, _month);
++_month;
if (_month == 13 ) {
++_year; _month = 1 ;
}
}
return *this ;
}
Date Date::operator +(int day) {
Date tmp = *this ;
tmp._day += day;
while (tmp._day > GetMonthDay (tmp._year, tmp._month)) {
tmp._day -= GetMonthDay (tmp._year, tmp._month);
++tmp._month;
if (tmp._month == 13 ) {
++tmp._year; tmp._month = 1 ;
}
}
return tmp;
}
bool Date::operator <(const Date& d) const {
if (_year < d._year) {
return true ;
} else if (_year == d._year) {
if (_month < d._month) {
return true ;
} else if (_month == d._month) {
return _day < d._day;
}
}
return false ;
}
bool Date::operator <=(const Date& d) const {
return *this < d || *this == d;
}
bool Date::operator >(const Date& d) const {
return !(*this <= d);
}
bool Date::operator >=(const Date& d) const {
return !(*this < d);
}
bool Date::operator ==(const Date& d) const {
return _year == d._year && _month == d._month && _day == d._day;
}
bool Date::operator !=(const Date& d) const {
return !(*this == d);
}
int Date::operator -(const Date& d) const {
int flag = 1 ;
Date max = *this ;
Date min = d;
if (*this < d) {
max = d;
min = *this ;
flag = -1 ;
}
int count = 0 ;
while (min != max) {
++min;
++count;
}
if (1 == flag) {
return count;
} else {
return -count;
}
}
Date& Date::operator ++() {
*this += 1 ;
return *this ;
}
Date Date::operator ++(int ) {
Date tmp = *this ;
*this += 1 ;
return tmp;
}
Date& Date::operator --() {
*this -= 1 ;
return *this ;
}
Date Date::operator --(int ) {
Date tmp = *this ;
*this -= 1 ;
return tmp;
}
Date& Date::operator -=(int day) {
if (day < 0 ) {
return *this += (-day);
}
_day -= day;
while (_day <= 0 ) {
--_month;
if (_month == 0 ) {
_month = 12 ;
--_year;
}
_day += GetMonthDay (_year, _month);
}
return *this ;
}
Date Date::operator -(int day) {
Date tmp = *this ;
tmp -= day;
return tmp;
}
ostream& operator <<(ostream& out, const Date& d) {
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator >>(istream& in, Date& d) {
while (1 ) {
cout << "请依次输入年月日:>" ;
in >> d._year >> d._month >> d._day;
if (!d.DateCheck ()) {
cout << "输入日期非法:" ;
d.print ();
cout << "请重新输入!!!" << endl;
} else {
break ;
}
}
return in;
}
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online