摘要:类的6个默认成员函数,日期类
如果一个类中什么成员都没有,简称为空类。然而,空类并不是什么成员都没有,任何类在什么都不写时,编译器会自动生成6个默认成员函数。默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
(ps.下文中标题右上角有数字标识的为6个默认成员函数的内容,以示区分)
1. 构造函数_Constructor¹
构造函数:创建对象?❌ → 初始化 ✔
features
- 函数名同类名相同
- 没有返回值(
void) - 对象实例化时编译器自动调用
- 可以实现重载
- 构造函数不写编译器会默认生成
注意 warning:
- 无参和全缺省会存在歧义,调用的时候不明确,建议实现全缺省构造函数。
- 调用无参构造函数时,不要加括号,这样写同样会引起歧义,编译器分不清是对象还是函数声明 ( e.g.Date d2(); 函数声明——Type function_name(parameter list(可为空))。
- 构造函数不可以显式调用!
默认构造函数:可以不用传参的构造函数(包括自己没有显式写构造函数而编译器自动生成的,默认构造函数有且只有一个)即无参或全缺省构造函数。
自动生成的构造函数
- 自己不写编译器才会自动生成,写了编译器就不会再自动生成其他构造函数。
- 自动生成的构造函数对 内置类型 不做处理(对于自定义类型会因具体的编译器而异,语法上规定不做处理,但可能有的编译器会处理),针对这个内置类型不处理的问题,C++11支持在声明处给缺省值,其他地方都没有给初始化值就会用缺省值初始化。(注意:所有的指针都是内置类型,自定义类型的指针也是内置类型!)
- 对于 自定义类型 会自动调用其自己的默认构造函数。
sum.真正便捷的是自动调用
2. 析构函数_Destructor²
析构函数:销毁对象?❌(出作用域生命结束编译器自动销毁) → 完成对象中的资源清理工作 ✔
features
- 函数名为:~类名
- 没有返回值(
void),没有参数 - 有且只有一个,不可重载(都没有参数肯定无法构成函数重载)
- 对象生命周期结束自动调用,调用顺序遵循栈后进先出的原则,即后定义的先析构
- 同构造函数一样,自己不写编译器会自动生成
什么情况下需要自己写析构函数?
3. 拷贝构造_Copy Constructor³
(注:拷贝构造函数也是构造函数)
值拷贝/浅拷贝
值拷贝:将数据内容完全一样地拷贝一份。但在有些场景中,值拷贝会导致一些问题,如下图所示。
如何解决两次调用析构函数的问题?
传值传参→ 传引用传参- 实现拷贝构造函数实现深拷贝,如下图(传值传参时,对于自定义类型会调用拷贝构造函数)
features
- 拷贝构造是构造函数的一个函数重载——函数名为类名,没有返回值。
- 拷贝构造参数必须传引用传参(这里的引用推荐加 const 以免将要被拷贝的对象被修改)。否则会导致无穷调用:调用拷贝构造函数 → 传值传参 → 值拷贝 → 调用拷贝构造 →…(示例如下图)
正确的拷贝构造函数示例:
class Date
{
public:
Date(const int year = 1, const int month = 1, const int day = 1)//全缺省构造函数
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)//copy constructor
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
自动生成的默认拷贝构造
- 对于 内置类型 值拷贝(例如对于上述日期类不用自己写拷贝构造,值拷贝已经能够满足需求)
- 对于 自定义类型 自动调用拷贝构造函数
4.操作符重载_operator
class Date
{
public:
Date(const int year = 1, const int month = 1, const int day = 1)//全缺省构造函数
{
_year = year;
_month = month;
_day = day;
}
bool operator<(const Date& d)//操作符重载
{
if (_year < d._year)
return true;
else if (_year == d._day && _month < d._month)
return true;
else if (_year == d._day && _month == d._month && _day < d._day)
return true;
else
return false;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 2, 3);
Date d2(2022, 1, 4);
cout << (d1 < d2)<<endl;
return 0;
}
注:上述代码中 d1<d2 → d1.operator<(d2) (编译器会自动转化为这样显式的调用,所以这与调用其他成员函数是一样的),隐藏的参数 this指针 指向该重载操作符的左操作数 d1,传递的参数 d2 为右操作数。
Rules
- 不能创造原本没有的操作符。(e.g.operator@)
- 参数中必须有一个自定义类型。
- 不能改变操作符原本的含义。
- 对于成员函数,第一个参数为 this 指针。即 this 指针所指向的对象一定是操作符的左操作数。
- 5个不能重载的操作符::: ?: .* sizeof .
A major design goal of C++ is to let programmers define their own types that are as easy to use as the built-in types.
——《C++ Primer》
操作符重载使得使用自定义类型就像使用内置类型一样便捷。
5.class Date
1)成员函数声明和定义分离:
类内声明,内外定义。
- 缺省值应在函数声明时给出,定义处不能再次定义缺省值,会引发重定义错误。
- 定义成员函数要指明类域(在返回值类型之后,函数名之前)——Type classname::function_name(parameter list)
2)constructor:
- 注意检查日期是否合法(传参可能传递非法日期 )→ 年份没有限制,月份必须在 [1,12] 的区间内,日期受到年份(闰年)和月份的限制。
(下述代码补充说明:GetMonthDay 函数中的 array 用 static 修饰是因为考虑到会频繁调用该函数,导致频繁开辟 array 空间,所以选择将 array 的数据放置在静态区,减少重复开空间,warning:不要对该数组里的数据进行修改!数据存储在静态区,每次调用该函数,修改的行为会被不断累积!!出于这样的考虑,array 可以加 const 保护)
int GetMonthDay(const int year, const int month)
{
static int array[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2)
{
if (((year % 100 != 0) && (year % 4 == 0)) || (year % 400 == 0))
///++array[2];//error!!!!!!!!!!!!!
return 29;
}
return array[month];
}
Date::Date(const int year, const int month, const int day)
{
if (month <= 12 && day <= GetMonthDay(month))
{
_year = year;
_month = month;
_day = day;
}
else
{
_year = _month = _day = 0;
cout << "日期非法" << endl;
}
}
3)赋值重载_operator=⁴
①赋值与拷贝构造的区别:
赋值重载:将一个变量中的数据 赋 给另一个变量
拷贝构造:用一个已经存在的对象来创建一个新的对象(构造函数主要是完成初始化工作,定义一个自定义类型的变量时会自动调用构造函数!)
②连续赋值
赋值的结果返回Date对象(推荐传引用返回)
③ 自己给自己赋值
自己给自己赋值,即 this 指针与传引用传参的参数表示的是同一个存储空间,所以不用继续进行赋值直接返回。综合上述所说,示例代码如下:
const Date& Date::operator=(const Date& d)
{
if (this == &d)
return *this;
_year = d._year;
_month = d._month;
_day = d._day;
return d;
}
④自动生成默认赋值重载函数
同构造函数和析构函数一样,自己不写会自动生成。自动生成的默认赋值重载函数会完成值拷贝。对于日期类,值拷贝已经可以满足需求,因此可以不用自己写赋值重载函数。
4)其他常用操作符重载:
①+= 与 +
- 复用的问题:①operator+ 复用 operator+= 两次调用拷贝构造;②operator+= 复用 operator+ 四次调用拷贝构造,一次赋值。所以选择operator+ 复用 operator+= 更好。
- += 负数:注意传参可能为负值的情况
示例代码:
Date& Date::operator+=(int days)
{
if (days < 0)
days = -days;
_day += days;
int _month_max_day = GetMonthDay(_year, _month);
while (_day > _month_max_day)
{
_day -= _month_max_day;
//处理月份
++_month;
if (_month > 12)
{
++_year;
_month = 1;
}
//更新月最大天数
_month_max_day = GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator+(const int days) const
{
Date tmp(*this);
tmp += days;
return tmp;
}
②前置++ 与 后置++
后置++ 的操作符重载中,语法规定通过参数 int 与前置++构成函数重载,int 只是用来占位,具体传什么内容由编译器处理。例如如下代码的函数声明:(再次提醒只是返回值不同不能构成函数重载)
Date& operator++();//前置++
Date operator++(int);//后置++
5)日期 - 日期 运算
- 方式一:直接算
- 方式二:让 小日期++ 直到 等于 大日期,++多少次就相差多少天(其中要用到比较大小的操作符,关于这些操作符的重载在本文第4部分——操作符重载——有一处示例,实现其中一个操作符,其他可以复用,思路简单,在此不多做赘述。以下提供一个实现日期相减的函数示例)
int Date::operator-(const Date& d) const { int count = 0, flag = 1; Date Max_date = *this; Date Min_date = d; if (d > *this) { flag = -1; Max_date = d; Min_date = *this; } while (Min_date < Max_date) { ++count; ++Min_date; } return count*flag; }
6. const 对象
在 C++ 入门一文中在介绍常引用(const 引用)时,讨论了关于权限放大的问题,同样,对于 const对象 来说,它们无法调用 非const成员函数——因为会导致权限放大的问题。
由上图不难看出,一个函数 const 版本和 非const版本 可以构成函数重载——参数类型不同,this指针的类型分别是 const Type* const 和 Type* const (例如,对于operator[]的操作符重载就很有必要实现 const 和 非const 两个版本)
- 建议:不做修改的成员函数都加 const 修饰,注意互相复用的函数之间会相互影响!
7. 取地址重载⁵ 及 const 对象取地址⁶
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
class Date
{
public:
const Date* operator&()const
{
return nullptr;
}
private:
int _year;
int _month;
int _day;
};
8. 补充:流插入和流提取操作符重载
以上,我们可以了解到,流提取 >> 的左操作数为 cin —— 一个类型为 istream 的对象,右操作数为一个内置类型的变量,同理,流插入 << 的左操作数为 cout —— 一个类型为 ostream 的对象,右操作数为一个内置类型的变量。
接下来,我们尝试对日期类的对象实现流插入和流提取的操作符重载(以流提取 >> 为例,流插入同理):因为日期类的成员变量私有,在类外无法访问,所以我们尝试在类内实现。同时,考虑到连续流插入/流提取,返回值类型应为ostream/istream。如下代码。
class Date
{
public:
ostream& operator<<(ostream& out)
{
out << _year << "/" << _month << "/" << _day << endl;
return out;
}
private:
int _year;
int _month;
int _day;
};
在上述代码中, 可以看到,第一个参数为 隐藏的this指针 ,第二个参数为 ostream对象的引用。则调用该函数即为:d(Type:Date)<<cout(Type:ostream),显然,这样的实现是符合该操作符原本的使用习惯的,在类内实现第一个参数必为 隐藏的this指针,所以考虑在类外实现该函数,为了能够访问内部成员变量,需要用到友元声明(详见类和对象下)。
class Date
{
public:
friend ostream& operator<<(ostream& out, Date d);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, Date d)
{
out << d._year << "/" << d._month << "/" << d._day << endl;
return out;
}
cin/cout 的意义:针对自定义类型可以实现重载(scanf/printf 不能很好地支持自定义类型);能够自动识别类型(例如对于 printf 函数如果数据类型改变,相应地也要改变打印格式)
END