深入剖析 C++ 默认函数:拷贝构造与赋值运算符重载
目录
1. 简单认识C++ 类的默认函数
1.1 默认构造函数
1.2 析构函数
1.3 拷贝构造函数
2. 拷贝构造函数的深入理解
拷贝构造的特点:
实际运用
3. 赋值运算符重载的深入理解
3.1.运算符重载
3.2样例
1.比较运算符重载
2.算术运算符重载
3.自增和自减运算符重载
4.输入输出运算符重载
3.3.赋值运算符重载
总结
在 C++ 编程中,类的默认函数拷贝构造函数以及赋值运算符重载是非常重要的概念。它们不仅可以让我们更方便地操作对象,还能增强代码的可读性和可维护性。本文将结合提供的日期类(Date
)代码,深入剖析这些概念。
1. 简单认识C++ 类的默认函数
在 C++ 中,当我们定义一个类时,编译器会自动为我们生成一些默认的成员函数,包括默认构造函数、析构函数、拷贝构造函数和赋值运算符重载函数。不过,当我们手动定义了其中某些函数时,编译器就不会再生成对应的默认函数了。
1.1 默认构造函数
默认构造函数是在创建对象时没有提供任何参数时调用的函数。在Date
类中,我们手动定义了一个带参数的构造函数:
Date(int year = 1, int month = 1, int day = 1)
{_year = year;_month = month;_day = day;
}
这个构造函数有默认参数,因此它既可以作为带参数的构造函数使用,也可以作为默认构造函数使用。当我们创建对象时不提供参数,就会使用默认值year = 1, month = 1, day = 1
。
1.2 析构函数
析构函数在对象生命周期结束时自动调用,用于释放对象占用的资源。在Date
类中,析构函数如下:
~Date()//可不写
{_year = 0;_month = 0;_day = 0;
}
1.3 拷贝构造函数
拷贝构造函数用于创建一个新对象,该对象是另一个已存在对象的副本。在Date
类中,注释掉的拷贝构造函数如下:
// Date(const Date& d1)//可不写
// {
// _year = d1._year;
// _month = d1._month;
// _day = d1._day;
// }
2. 拷贝构造函数的深入理解
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数。
拷贝构造的特点:
1.拷贝构造函数是构造函数的一个重载。
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。
3.C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
4.若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。
5.像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显示实现拷贝构造。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的拷贝构造会调用Stack的拷贝构造,也不需要我们显示实现MyQueue的拷贝构造。这里还有一个小技巧,如果一个类显示实现了析构并释放资源,那么他就需要显示写拷贝构造,否则就不需要。
实际运用
拷贝构造函数的原型通常为ClassName(const ClassName& other)
。当我们使用以下方式创建对象时,会调用拷贝构造函数:
Date d1(2025, 6, 12);
Date d2(d1); // 调用拷贝构造函数
在Date
类中,由于成员变量都是基本数据类型,浅拷贝就足够了。但如果类中包含动态分配的资源(如指针),浅拷贝可能会导致多个对象指向同一块内存,从而在析构时引发重复释放的问题,这时就需要手动定义拷贝构造函数进行深拷贝。
3. 赋值运算符重载的深入理解
3.1.运算符重载
1.当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。
2.运算符重载是具有特殊名字的函数,他的名字是由operator和后面要定义的运算符共同构成。和其他函数一样,它也具有其返回类型和参数列表以及函数体。
3.重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
4.如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个。
5.运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。不能通过连接语法中没有的符号来创建新的操作符:比如operator@。
6.注意以下5个运算符不能重载
7.重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如:int
operator+(int x,int y)
8.一个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如Date类重载operator-就有意义,但是重载operator+就没有意义。
3.2样例
1.比较运算符重载
比较运算符(==
, !=
, >
, >=
, <
, <=
)用于比较两个对象的大小关系。以==
运算符重载为例:
bool Date::operator==(const Date& d) const
{return _year == d._year&& _month == d._month&& _day == d._day;
}
这个函数比较两个Date
对象的年、月、日是否相等,如果相等则返回true
,否则返回false
。其他比较运算符的重载也类似,通过比较成员变量的值来确定对象的大小关系。
2.算术运算符重载
算术运算符(+
, -
, +=
, -=
)用于对日期进行加减操作。以+=
运算符重载为例
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) const
{Date tmp = *this;tmp += day;return tmp;
}
3.自增和自减运算符重载
自增和自减运算符(++
, --
)分为前置和后置两种形式。前置运算符返回引用,后置运算符返回临时对象。以前置++
运算符重载为例:
Date& Date::operator++()
{*this += 1;return *this;
}
后置++
运算符重载通过一个int
参数来区分前置和后置:
Date Date::operator++(int)
{Date tmp = *this;*this += 1;return tmp;
}
4.输入输出运算符重载
输入输出运算符(<<
, >>
)用于将对象输出到流中或从流中读取对象。
ostream& operator<<(ostream& out, const Date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日" << endl;return out;
}
这个函数将Date
对象的年、月、日输出到输出流中。>>
运算符重载则用于从输入流中读取年、月、日,并检查输入的日期是否合法:
istream& operator>>(istream& in, Date& d)
{while(1){cout << "请输入年月日:>";in >> d._year >> d._month >> d._day;if(!d.CheckDate()){cout << "输入的日期非法!";d.Print();cout << "请重新输入年月日!!" << endl;}else{break;}}return in;
}
3.3.赋值运算符重载
赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。
赋值运算符重载的特点:
1.赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const 当前类类型引用,否则会传值传参会有拷贝
2.有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续值场景。
3.没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。
4.像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的赋值运算符重载会调用Stack的赋值运算符重载,也不需要我们显示实现MyQueue的赋值运算符重载。这里还有一个小技巧,如果一个类显示实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要。
总结
通过对Date
类的分析,我们深入了解了 C++ 类的默认函数、拷贝构造函数和赋值运算符重载的概念和用法。默认函数为我们提供了基本的对象创建和销毁机制,拷贝构造函数用于创建对象的副本,运算符重载则让我们可以像使用内置类型一样使用自定义类型进行运算。在实际编程中,我们需要根据类的具体情况来决定是否需要手动定义这些函数,以确保代码的正确性和性能。