前言
本篇博客主要介绍类的六个默认成员函数及运算符重载:构造函数、析构函数、拷贝构造函数、赋值运算符重载、取地址操作符重载
🎓作者:如何写出最优雅的代码
📑如有错误,敬请指正🌹🌹💬开发工具:VS2019
目录
补充几个知识点:
- 如果一个类中什么成员都没有,简称为空类。
- 但空类中并不是什么都没有,任何类在什么都不写的情况下,编译器会自动生成以下六个默认成员函数。
- 默认成员函数:用户没有显示实现,编译器会自动生成的成员函数称为默认成员函数。
1. 构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在该对象的整个生命周期内只调用一次。
构造函数是特殊的成员函数,虽然命名为构造函数,但构造函数的主要任务并不是开辟空间创建对象,而是初始化对象。
具有以下特征:
- 函数名与类名相同
- 无返回值,也不能写void
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数可以重载
下面编写一个简单的Date类:
class Date { public: // 1.无参构造函数 Date() {} // 2.带参构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; void TestDate() { Date d1; // 调用无参构造函数 Date d2(2015, 1, 1); // 调用带参的构造函数 // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明! // 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象 // warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?) Date d3(); } int main() { TestDate(); return 0; }
显示定义的无参构造器:
Date() {}
显示定义的带参构造器:
Date(int year, int month, int day) { _year = year; _month = month; _day = day; }
如果类中没有显式定义的构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义,则编译器将不再生成。
这里需注意:C++把类型分为内置类型(基本类型)和自定义类型,内置类型就是语言提供的数据类型,如int、char、double……,自定义类型就是使用class、struct、union……自己定义的类型。编译器生成的默认的构造函数对内置类型不做处理,但会去调用自定义类型的默认构造函数。
这里由于默认生成的无参构造器对内置类型不做处理是一个小缺陷,C++11中打了补丁,即:内置类型成员变量在类声明时可以给默认值(缺省值)
默认构造函数:无参的构造函数和全缺省的构造函数、以及我们没写构造函数时编译器默认生成的无参构造函数都可以认为是默认构造函数。
综合这里来看,一般推荐在类中只定义一个全缺省的构造函数
显示定义的全缺省构造函数:
Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; }
注意:定义了全缺省的构造函数后,不可以再定义无参的构造函数,因为编译器在创建无参对象时不知道应该调用哪一个构造函数!
2. 析构函数
析构函数:与构造函数功能相反,析构函数不是完成对象本身的销毁,局部对象的销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数是特殊的成员函数,具有以下特征:
- 析构函数名是在类名前加上字符~
- 无参数无返回值类型(不能写void)
- 一个类只能有一个析构函数。若未显示定义,系统会自动生成默认的析构函数。需注意析构函数不能重载
- 对象生命周期结束时,C++编译器(或者说系统)自动调用析构函数
- 析构函数主要功能是完成资源清理,比如释放malloc开辟的内存。但编译器自动生成的析构函数,对内置类型成员一样不做处理,对自定义类型成员会去调用它的析构函数。结合这点来看,如果一个类中没有申请资源时,析构函数可以不写,直接使用默认生成的析构函数;有资源申请时一定要写,否则会造成资源泄漏
3. 拷贝构造函数
对一个已经存在的对象,我们想要创建一个和已存在对象一模一样的对象,应该如何做?下面引入拷贝构造函数。
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时,由编译器自动调用
拷贝构造函数也是特殊的成员函数,具有以下特征:
- 拷贝构造函数是构造函数的一个重载形式,参数不同构成重载
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用(不使用引用传值,形参是实参的拷贝,创建形参时自动调用拷贝构造函数,就会产生无穷递归调用)
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } // Date(const Date d) // 错误写法:编译报错,会引发无穷递归 Date(const Date& d) // 正确写法 { _year = d._year; _month = d._month; _day = d._day; } private: int _year; int _month; int _day; }; int main() { Date d1;//调用默认构造函数 Date d2(d1);//用已存在的d1创建新对象d2,调用拷贝构造函数 return 0; }
- 如果显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数在创建对象时按内存存储中的字节序完成拷贝,即浅拷贝,或者称为值拷贝。同时,默认生成的拷贝构造函数,对于内置类型是按照字节方式直接拷贝,而自定义类型是调用其拷贝构造函数完成拷贝。
- 编译器生成的默认构造函数已经可以完成字节序的值拷贝了,但特殊情况下仍需自己显式定义,比如自定义的栈,里面存在一个指向malloc开辟的空间的指针,如果只是值拷贝,那么新创建的对象和原先对象中的指针会指向同一块空间,达不到拷贝的要求,这时要显式定义,利用深拷贝去解决!
- 拷贝构造函数的使用场景:使用已存在的对象创建新对象、函数传参类型为类类型对象(以实参创建形参的过程)、函数返回值类型为类类型对象(有tmp返回值的拷贝构造和接收变量的拷贝构造)
- 为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
4. 赋值运算符重载
4.1. 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:operator需要重载的运算符符号
函数原型:返回值类型 operator操作符(参数列表) {……}
注意:
- 不能通过连接其他非操作符的符号来创建新的操作符,比如operator@、operator#
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置类型的整数+,不能改变其含义。这里我们要深入理解运算符重载的意义:C++中引入了类与对象,而运算符重载是为了让这些自定义的对象也能像内置类型数据一样可以使用加减乘除、大小比较、赋值、输入输出等操作符
- 作为类成员函数重载时,其形参看起来比操作数目少1,因为成员函数的第一个参数为隐藏的this指针
- .* :: sizeof ? . 注意这五个运算符都不能重载
下面以日期类Date为例,完善运算符重载操作:
下面先给完整的源代码,再结合每一个运算符进行分析:
Date.h
#pragma once #include <iostream> #include <assert.h> using namespace std; class Date { //友元函数 -- 这个函数内部可以使用Date对象访问私有保护成员 friend ostream& operator<<(ostream& out, const Date& d); friend istream& operator>>(istream& in, Date& d); public: //获取某年某月天数 //被频繁调用,所以直接放在类里面定义作为inline int GetMonthDay(int year, int month) { static int days[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };//频繁调用,static就可以避免重复创建 int day = days[month]; if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) { day += 1; } return day; } bool CheckDate() { if (_year >= 1 && _month > 0 && _month < 13 && _day>0 && _day <= GetMonthDay(_year, _month)) { return true; } else { return false; } } //构造函数会被频繁调用,所以直接放在类里面定义作为inline Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; //if (!CheckDate()) //{ // Print(); // cout << "日期非法" << endl; //} assert(CheckDate()); } void Print()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; bool operator<=(const Date& d)const; Date& operator=(const Date& d) { if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; } Date operator+(int day)const; Date& operator+=(int day); //++d1; //d1++; //直接按特性重载,无法区分 //特殊处理,使用重载区分,后置++重载增加一个int参数跟前置++构成函数重载进行区分 Date& operator++();//前置++ Date operator++(int);//后置++ Date operator-(int day)const; Date& operator-=(int day); //--d1; //d1--; //直接按特性重载,无法区分 //特殊处理,使用重载区分,后置--重载增加一个int参数跟前置++构成函数重载进行区分 Date& operator--();//前置-- Date operator--(int);//后置-- //日期相减 int operator-(const Date& d)const; //void operator<<(ostream& out);//成员函数,左操作数只能是日期类 private: int _year; int _month; int _day; }; //流插入重载 inline ostream& operator<<(ostream& out, const Date& d) { //out就是cout的别名 out << d._year << "-" << d._month << "-" << d._day << endl;//无法访问私有成员,利用友元解决 return out; } //流提取重载 inline istream& operator>>(istream& in, Date& d) { in >> d._year >> d._month >> d._day; assert(d.CheckDate()); return in; }
Date.cpp
#include "Date.h" void Date::Print()const { cout << _year << "/" << _month << "/" << _day << endl; } //任何一个类,只需要实现 > 、== 或者 < 、== 重载即可,后面的比较运算符可以复用他们的重载 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);//复用 == 重载 } bool Date::operator>(const Date& d)const { if ((_year > d._year) || (_year == d._year && _month > d._month) || (_year == d._year && _month == d._month && _day > d._day)) { return true; } else { 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); } Date Date::operator+(int day)const { //8/24 - 02:56 //Date ret(*this)//拷贝构造 Date ret = *this;//虽然用 = ,但仍是拷贝构造 //两个已经存在的对象才是赋值!!!,正在创建的是拷贝构造 ret += day; return ret; } 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++()//前置++ { *this += 1; return *this; } Date Date::operator++(int)//后置++ { Date tmp(*this); *this += 1; return tmp; } Date Date::operator-(int day)const { Date ret = *this; ret -= day; return ret; } Date& Date::operator-=(int day) { _day -= day; while (_day <= 0) { --_month; if (_month == 0) { _year--; _month = 12; } _day += GetMonthDay(_year, _month); } return *this; } Date& Date::operator--()//前置-- { return *this -= 1; } Date Date::operator--(int)//后置-- { Date tmp(*this); *this -= 1; return tmp; } //d1 - d2 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 n = 0; while (min != max) { ++min; ++n; } return n * flag; }
比较运算符重载:
- 我们想让日期类可以像内置类型数据一样进行比较大小,并返回布尔值,在不写运算符重载的情况下显然是不能直接比较的,有了这样的需求,下面通过运算符重载进行操作即可。
- 先写其中两个吧,比如 == 和 > 运算符的重载
bool Date::operator== (const Date& d)const { return _year == d._year && _month == d._month && _day == d._day; } bool Date::operator>(const Date& d)const { if ((_year > d._year) || (_year == d._year && _month > d._month) || (_year == d._year && _month == d._month && _day > d._day)) { return true; } else { return false; } }
- 重载函数体里面操作的实际是该对象里的内置类型数据,这时的比较操作符的含义普通数据比较大小的含义一样,不能改变!相信看到这里,也比较容易理解为什么会有运算符重载的需要了,因为对于自定义类型,并不能简单地像内置类型一样进行比较大小的操作。
- 技巧:任何一个类,只需要实现 > 、== 或者 < 、== 重载即可,后面的比较运算符可以复用他们的重载
bool Date::operator!=(const Date& d)const { return !(*this == d);//复用 == 重载 } 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); }
加减运算符重载:日期相减,加减天数都是有意义的,所以可以进行运算符重载,而日期乘除就没有意义,所以可以不用写。有意义的操作符都可以进行重载
- +和+=天数,可以先写+=运算符重载,然后写+时再复用
Date Date::operator+(int day)const { //8/24 - 02:56 //Date ret(*this)//拷贝构造 Date ret = *this;//虽然用 = ,但仍是拷贝构造 //两个已经存在的对象才是赋值!!!,正在创建的是拷贝构造 ret += day;//复用+= return ret; } 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 ret = *this; ret -= day; return ret; } Date& Date::operator-=(int day) { _day -= day; while (_day <= 0) { --_month; if (_month == 0) { _year--; _month = 12; } _day += GetMonthDay(_year, _month); } return *this; }
至于为什么要先写+=和-=,再进行复用,如果反过来写的话,会增加拷贝构造的次数,效率较低!
- 前置++和后置++
//++d1; //d1++; //直接按特性重载,无法区分 //特殊处理,使用重载区分,后置++重载增加一个int参数跟前置++构成函数重载进行区分 Date& Date::operator++()//前置++ { *this += 1; return *this; } Date Date::operator++(int)//后置++ { Date tmp(*this); *this += 1; return tmp; }
- 前置--和后置--
为了区分前置和后置的区别,在定义重载时,后置统一增加一个int参数,这样可以使前置和后置构成函数重载,编译之后会形成不同的符号表,运行时再由编译器自动选择前置还是后置//--d1; //d1--; //直接按特性重载,无法区分 //特殊处理,使用重载区分,后置--重载增加一个int参数跟前置++构成函数重载进行区分 Date& Date::operator--()//前置-- { return *this -= 1; } Date Date::operator--(int)//后置-- { Date tmp(*this); *this -= 1; return tmp; }
- 日期相减
//d1 - d2 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 n = 0; while (min != max) { ++min; ++n; } return n * flag; }
4.2. 赋值运算符重载
经过前面运算符重载知识的铺垫,理解这里的赋值运算符重载应该会容易许多。
首先强调的是赋值运算符重载的操作数为两个已经创建好的对象,而在创建新对象过程中使用到该赋值运算符,实际调用的是拷贝构造函数
//Date ret(*this)//拷贝构造
Date ret = *this;//虽然用 = ,但仍是拷贝构造
//两个已经存在的对象才是赋值!!!,正在创建的是拷贝构造
赋值运算符的重载格式:
- 参数类型:const T&,引用传递可以提高效率(T为类名)
- 返回值类型:T&,返回使用引用传递可以提高返回效率,有返回值的目的是为了支持连续赋值
- 检测是否自己给自己赋值,如果是,那么不做任何处理
- 返回*this的引用:符合连续赋值的含义
Date& operator=(const Date& d) { if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; }
注意:
- 赋值运算符只能重载成类的成员函数不能重载成全局函数,因为重载成全局函数后(不在类体内定义),就没有this指针了,需要给两个参数,这时候会出现各种意想不到的结果!另外,赋值运算符重载是类的六个默认成员函数之一,在不显式定义的情况下,编译器也会默认生成一个默认的,若此时用户在类外自己定义了一个全局的赋值运算符重载,就会和默认生成的运算符重载冲突了,因此赋值运算符只能是类的成员函数!
- 如果我们没有显式定义赋值运算符重载时,编译器会生成一个默认的赋值运算符重载,并以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值,而自定义类型的成员变量需要调用对应类的赋值运算符重载完成赋值,这里也涉及深浅拷贝问题,像日期类中成员变量都是内置类型的类,可以直接使用默认生成的赋值运算符重载。
4.3. 流插入/流提取运算符重载
为了和普通数据的输入输出规范相对应,即满足操作数始终在操作符右侧,我们在定义流插入/流提取运算符重载时就不能在类体内定义,因为在类体内定义时,第一个操作数始终都是类对象的this指针,第一个操作数对应在操作符的左侧,不符合常用规范。
将其定义与类体外,定义如下:
//流插入重载
inline ostream& operator<<(ostream& out, const Date& d)
{
//out就是cout的别名
out << d._year << "-" << d._month << "-" << d._day << endl;//无法访问私有成员,利用友元解决
return out;
}
//流提取重载
inline istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
assert(d.CheckDate());
return in;
}
说明:
- 第一个参数传递的是iostream类的对象,out对应cout,in对应cin,在重载函数内部可进行正常的使用,同时保证了该参数是左操作数
- 第二个参数是要操作的类对象,是右操作数
- 由于输入输出是常用且较短的函数,所以将其定义为内联函数,提高访问效率
- 由于该运算符重载定义1在类体外,不能直接访问该类的私有成员属性,所以可以将这两个函数在类体内声明为友元函数
//友元函数 -- 这个函数内部可以使用Date对象访问私有保护成员 friend ostream& operator<<(ostream& out, const Date& d); friend istream& operator>>(istream& in, Date& d);
返回值为iostream是为了支持连续输入和输出,下面是一段测试代码
void TestDate5() { Date d1(2022, 7, 25); Date d2(2022, 7, 26); cout << d1 << d2; cin >> d1 >> d2; cout << d1 << d2; //对于定义在类体内的流插入,左操作数是类对象,用起来非常的奇怪啊! //d1.operator<<(cout); //d1 << cout; }
5. const成员
将const修饰的成员函数称为const成员函数,const修饰成员函数,实际修饰的是该成员函数的隐藏参数,即修饰this指针指向的内容,表明该成员函数中不能对该类的任何成员进行修改。
上面的运算符重载中,也有一些使用到了const去修饰this指针指向的内容
格式:返回类型 成员函数名(参数列表) const
this指针的改变:Date* const this -> const Date* const this
几个有意思的问题:
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其他非const成员函数吗?
- 非const成员函数内可以调用其他const成员函数吗?
答案:
- 不可以,因为const对象的值是不可以被修改的,而非const成员函数中this指针指向的内容可以被修改,传递过去属于权限的放大
- 可以,因为const成员函数中this指针指向的值不可以被修改,而非const对象传递过去后属于权限的缩小
- 不可以,因为const成员函数中this指针指向的内容不能被修改,调用其他非const成员函数时,传递过去的是const对象,属于权限的放大
- 可以,非const成员函数内的this指针指向的内容可以被修改,作为参数传递给其他const成员函数时,属于权限的缩小
小结:const对象和非const对象都可以调用const成员函数,所以一个类的成员函数能加const就加
6. 取地址运算符重载及const取地址运算符重载
这两个默认成员函数一般不需要重新定义,编译器会默认生成,使用默认生成的取地址重载即可。只有特殊情况,想让用户取到指定的内容时,才需要重新定义
本篇博客就到这了~😪😪😪,从下午开始写,码字码到了23点,早点睡吧
往期优质博客:
学习记录:
- 📆本篇博客整理于2022.9.6
- 🎓作者:如何写出最优雅的代码
- 📑如有错误,敬请指正🌹🌹
- 🥂关注一波不迷路!如果觉得写的不错,看完了别忘了点赞和收藏啊,感谢支持😏😏