类与对象(中)
目录
类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员 函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
特殊成员函数,我们不写,编译器会自己生成一个;我们自己写了,编译器就不会生成
class Date();
Type One:初始化和清理
1.构造函数:初始化
C语言使用时,经常忘记Init(初始化)就使用,忘记Destroy
//原来我们需要使用下面方式来初始化
Class Date
{
public:
void Init(int year,int month,int day)
{
_year=year;
_month=month;
_day=day;
}
}
能不能在对象定义的时候就将其初始化呢?—— 构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
其特征如下:
1. 函数名与类名相同。
2. 无返回值。(void都不用写)
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载:提供多种构造函数,多种初始化方式
构造函数的作用是初始化,具体表现如下:
Class Date
{
private:
int _year;
int _month;
int _day;
public:
//初始化
Date(int year,int month,int day)
{
_year=year;
_month=month;
_day=day;
}
}
int main()
{
Date d1(2023,1,28);
Date d2(2023,1,29);
Date d3;
}
当然,由于初始化代表的是日期,所以在初始化时我们检验输入的日期的合法性:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
//检查日期是否合法
if (!(year >= 1
&& (month >= 1 && month <= 12)
&& (day >= 1 && day <= GetMonthDay(year, month))))
{
cout << "非法日期" << endl;
}
}
因此当我们输入两个非法日期后,会出现:
再例如我们使用构造函数来实现栈时:
class Stack
{
public:
//栈的初始化(构造函数)
Stack(int capacity=4)
{
_a=malloc(sizeof(int)*capacity);
if(a==nullptr)
{
perror("malloc fail");
exit(-1);
}
_top=0;
_capacity=capacity;
}
//...
}
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
假如我们未定义构造函数,那么编译器自动生成结果如下:
关于编译器生成的默认成员函数,对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用?
解答: C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如: int/char… ,自定义类型就是我们使用class/struct/union等自己定义的类型,对于默认生成的构造函数,不会对内置类型处理,对于自定义类型会调用它的默认构造。
因此总结一下:上述的Stack和Date的构造函数需要自己写,因为要按照自己的意愿进行初始化
2.析构函数:销毁
构造函数让我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
即与我们常用的Destory函数功能相似
其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4. 对象生命周期结束时(出作用域),C++编译系统系统自动调用析构函数。
//析构函数:类名前加上字符 ~
~Stack()
{
free(_a);
_capacity=0;
_size=0;
}
因此总结一下:这里的Stack就需要自己写析构函数,释放对应空间
面向需求:编译器默认生成就可以满足,就不用自己写,不满足就需要自己写
注意1:
C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值
例如:
class MyQueue {
public:
void push(int x)
{
_pushST.Push(x);
}
public:
Stack _pushST; //自定义类型
Stack _popST; //自定义类型
size_t _size; //内置类型;unsigned int
};
初始化为0:
注意2:
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
例如:
//无参的构造函数
Date()
{
_year = 1;
_month = 2;
_day = 3;
}
//全缺省的构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
指定了多个默认构造:
不传参数就可以调用的构造函数就叫默认构造
我们将无参构造函数去掉:
这里的Date(int year , int month , int day)构造函数需要传递参数,由上述定义可知,我们自定义了构造函数------>系统不会生成构造函数------>需要传递参数------>不是默认构造函数,因此会出现这类错误
Type Two:拷贝复制
3.拷贝构造函数
引入:
int main()
{
Date d1(2023,2,26); //构造
//拷贝一份d1
Date d2(d1); //拷贝构造 -- 拷贝初始化
}
拷贝构造函数也是特殊的成员函数,其特征如下:
1.拷贝构造函数是构造函数的一个重载形式
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
例如,我们定义Date类,通过拷贝构造函数实现如下:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date(Date d) // 错误写法
Date(Date& d) // 正确写法:编译报错,会引发无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
public:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2023, 2, 27);
Date d2(d1); //拷贝构造 -- 拷贝初始化
cout << d1._year << "," << d1._month << "," << d1._day << endl;
cout << d2._year << "," << d2._month << "," << d2._day << endl;
return 0;
}
理解:拷贝构造函数的参数只有一个且必须是类类型对象的引用
如上述程序,我们定义func1和func2两个函数,分别是传值传参(形参是实参的拷贝)和引用传参(形参是实参的别名),因此在每次传值传参时会调用拷贝构造,会引起无穷递归;
拷贝构造------>调用该函数------>传参------->传参又是一个拷贝构造------>…(无穷递归)
注意1:
形参加const,防止写反,即:
Date(const Date& d) // 正确写法:编译报错,会引发无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
注意2:
在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗? 当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
①对于自定义类型Date,默认拷贝构造如下:
该默认拷贝构造函数实现了我们的目标
②对于自定义类型Stack(栈),默认拷贝构造如下:
这里为什么报错呢,是因为在st1结束后会自动调用析构函数,而在调用完析构函数后,由于st2是st1的拷贝它们指向同一块空间,因此此时st2所指向的区域已被销毁形成野指针,此时默认生成的拷贝构造函数不能实现了我们的目标
编译器默认生成的拷贝构造函数为:浅拷贝
深拷贝
为了解决浅拷贝在某类场景下不适用的情况,我们引用的深拷贝,以Stack为例,如下:
//st2(st1)
Stack(const Stack& st)
{
//开一个与st1相同大小的空间
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
//拷贝该空间上的值
memcpy(_a, st._a, sizeof(int) * st._top);
_top = st._top;
_capacity = st._capacity;
}
可表示为下图:
总结:
需要些析构函数的类,都需要写深拷贝的拷贝构造(Stack)
不需要写析构函数的类,默认生成的浅拷贝的拷贝构造就可以用(date)
4.赋值运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号
函数原型: 返回值类型 operator操作符(参数列表)
示例1:
例如我们在Date类中比较两个日期是否相等:
bool operator==(const Date& d1,const Date& d2)
{
return d1._year==d2._year
&&d1._month==d2._month
&&d1._day==d2._day;
}
即:
这里的:
d1==d2; //编译器会转换成operator==(d1,d2);
上述对于比较的结果,可更改为:(注意运算符的优先级)
问题:若属性是私有的,怎么访问呢?
方式一:设置一个共有的get方法
方式二:放到类里面
//更改如下:
bool operator==(const Date& d)
{
return _year==d._year
&&_month==d._month
&&_day==d._day;
}
示例2:
我们再来尝试Date类日期间大于小于的比较:
//放在类里面的
bool operator>(const Date& d)
{
if (_year > d._year)
{
return true;
}
else if (_year == d._year && _month > d._month)
{
return true;
}
else if (_year == d._year && _month == d._month && _day > d._day)
{
return true;
}
return false;
}
示例3:
Date类日期间的大于等于:
bool operator>=(const Date& d)
{
//this代表d1的地址,因此对this解引用即指向d1
//复用上述已经书写的大于和等于的判定
return *this>d||*this==d;
}
示例4:
日期加天数怎么办呢?(例如算某天过100天之后的日期)
函数名:operator+=
参数:(int day)
返回类型:Date
得到每个月的天数;天满了进月,月满了进年
Step1:得到每个月的天数(判定是平年还是闰年)
int GetMonthDay(int year,int month)
{
static int monthDayArray[13]={
0,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;
}
else
{
return monthDayArray[month];
}
}
Step2:加上天数
Date operator+=(int day)
{
_day+=day;
//日进
while(_day>GetMonthDay(year,month))
{
_day-=GetMonthDay(year,month);
_month++;
//月进
if(_month==13)
{
_year++;
_month=1;
}
}
return *this;
}
注意这里*this的作用
因此结果如下:
很巧妙的是,在编写这篇文章时(2023年2月27日),再加100天就是6月7日,恰好是今年高考的百日誓师!
我们上述所书写的operator+、operator>等函数功能,我们依然可以通过定义普通的函数名实现,那么我们定义运算符重载的意思是什么呢?
运算符重载的真正意义是增加可读性!
注意:
1.不能通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个类类型参数(针对自定义类型的)
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
5.注意以上5个运算符不能重载
5.赋值重载
区分:
Date d2(d1); //拷贝构造(本质是初始化) 一个初始化另一个马上要创建的对象
d3=d1; //赋值重载(复制拷贝) 已经存在两个对象直接的拷贝
6.日期类的实现
在上述的例子中,日期类出现的频率极高,可见它是一个非常经典的例子,因此我们在此将日期类的全部用法实现,具体如下:
✍首先,由于日期类所涉及到的函数接口较多,因此我们定义Date.h头文件包含日期类的接口,而Date.cpp文件为我们具体实现日期类的方式
为什么这里我们只在Date.h中提供接口呢(声明)?
答:全局函数在.h文件中会导致重定义,是由于Date.cpp和Test.cpp文件包含了Date.h,因此定义在.h中会在符号表拷贝多次,自己与自己发生重定义,因此才将声明与实现分离;或者置为静态的(static)
.h文件中尽量不含全局变量和函数,否则会出现链接问题
声明:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Date
{
//友元声明(类中的任意位置)
friend inline ostream& operator<<(ostream& out, const Date& d);
friend inline istream& operator>>(istream& in, Date& d);
private:
int _year;
int _month;
int _day;
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month);
// 全缺省的构造函数
Date(int year = 2000, int month = 1, int day = 1);
// 拷贝构造函数
// d2(d1)
Date(const Date& d);
//日期类打印
void Print() const;
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d);
// 析构函数
~Date();
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day) const;
// 日期-天数
Date operator-(int day) const;
// 日期-=天数
Date& operator-=(int day);
// 前置++
Date& operator++();
// 后置++
Date operator++(int);
// 前置--
Date& operator--();
// 后置--
Date operator--(int);
// >运算符重载
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;
// 日期-日期 返回天数
int operator-(const Date& d) const;
};
// 流插入的重载
inline ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
// 流提取的重载
inline istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
实现:
#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"
using namespace std;
//1.月份内的天数
int Date::GetMonthDay(int year, int month)
{
int monthDay[] = {
0, 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;
}
else
{
return monthDay[month];
}
}
//2.构造函数
//这里最好写一个全缺省的构造函数,这样即使不传参对象也会被初始化
Date::Date(int year, int month, int day)
{
//这里的缺省值已经在声明的时候给出了
_year = year;
_month = month;
_day = day;
}
//3.拷贝构造函数
//拷贝构造函数必须是传值引用传参,并且最好加上const防止拷贝的时候实参被改变
//这里的日期类可以不用书写,编译器会自动给出
Date::Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//4.析构函数
Date::~Date()
{
_year = 0;
_month = 0;
_day = 0;
}
//5.打印日期
void Date::Print() const
{
cout << _year << "/" << _month << "/" << _day << endl;
}
//6.赋值重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& Date::operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
//7.日期+=天数
//这个属于赋值运算符重载,由内置类型+=符号运算,会改变左操作数的值;
//因此在返回时我们直接返回运算之后的对象即可
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 > 12)
{
_month = 1;
_year++;
}
}
return *this;
}
//8.日期+天数
//复用上述日期+=天数即可
//需要注意这里不会改变日期原本的值
//而是返回一个临时产生的对象
Date Date::operator+(int day) const
{
Date tmpObject(*this); //产生临时对象
return tmpObject += day; //复用
}
//9.日期-=天数
//同理,会改变日期,直接返回即可
Date& Date::operator-=(int day)
{
if (day < 0)
{
return *this += -day;
}
_day -= day;
while (_day <= 0)
{
_month--;
_day += GetMonthDay(_year, _month);
//月份减完年减一
if (_month < 1)
{
_month = 12;
_year--;
}
}
return *this;
}
//10日期-天数
//同理,复用日期-=天数即可
Date Date::operator-(int day) const
{
Date tmpObject(*this);
return tmpObject -= day;
}
//11.==运算符重载
//意思是判断日期是否相等,返回的是0/1(bool型)
bool Date::operator==(const Date& d) const
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
//12.>运算符重载
//比较日期的大小:先比年-年相等比月-年月相等比日
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)
{
if (_day > d._day)
{
return true;
}
}
}
return false;
}
//13.>=、<、<=等运算符重载
//均可以通过复用>、==来实现
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 || *this == d);
}
bool Date::operator!=(const Date& d) const
{
return !(*this == d);
}
//14.日期-日期
//日期-日期是有意义的
//首先需要区分日期间的大小日期,我们直接小的日期每次循环+1天
//循环次数即为间隔天数
int Date::operator-(const Date& d) const
{
//默认是当前对象日期大
//很经典的找大的方法:先假设
Date min = d;
Date max = *this;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
//计算相差天数
int count = 0;
while (min != max)
{
min++;
count++;
}
return count * flag;
}
//15.前置和后置++/--
//前置++,先加再用,因此直接对该对象加减即可
Date& Date::operator++()
{
//引用返回
return *this += 1;
}
//后置++,先用再加,返回加之前的值
//规定:后置++多一个参数,是为了与前置++做出区分,构成重载,无其它作用
Date Date::operator++(int)
{
Date tmpObject(*this);
*this += 1;
//传值返回
return tmpObject;
}
// 前置--:返回减后的值
Date& Date::operator--()
{
return *this -= 1;
}
//后置--:后置会多两次拷贝
Date Date::operator--(int)
{
Date tmpObject(*this);
*this -= 1;
return tmpObject;
}
7.const成员
凡是内部不改变成员变量,其实也是*this对象数据的,这些成员函数都应该加const
Type Three:取地址重载
8.取地址重载
这两个默认成员函数一般不用重新定义,编译器默认会生成
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
特殊需求:要求这个类的对象不让取地址
Date* operator&()
{
return nullptr;
}
const Date* operator&() const
{
return nullptr;
}