如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员 函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
特殊成员函数,我们不写,编译器会自己生成一个;我们自己写了,编译器就不会生成
class Date();
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的构造函数需要自己写,因为要按照自己的意愿进行初始化
构造函数让我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
即与我们常用的Destory函数功能相似
其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4. 对象生命周期结束时(出作用域),C++编译系统系统自动调用析构函数。
//析构函数:类名前加上字符 ~
~Stack()
{free(_a);_capacity=0;_size=0;
}
因此总结一下:这里的Stack就需要自己写析构函数,释放对应空间
面向需求:编译器默认生成就可以满足,就不用自己写,不满足就需要自己写
C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值
例如:
class MyQueue {
public:void push(int x){_pushST.Push(x);}
public:Stack _pushST; //自定义类型Stack _popST; //自定义类型size_t _size; //内置类型;unsigned int
};
初始化为0:
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
例如:
//无参的构造函数
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)构造函数需要传递参数,由上述定义可知,我们自定义了构造函数------>系统不会生成构造函数------>需要传递参数------>不是默认构造函数,因此会出现这类错误
引入:
int main()
{Date d1(2023,2,26); //构造//拷贝一份d1Date 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两个函数,分别是传值传参(形参是实参的拷贝)和引用传参(形参是实参的别名),因此在每次传值传参时会调用拷贝构造,会引起无穷递归;
拷贝构造------>调用该函数------>传参------->传参又是一个拷贝构造------>…(无穷递归)
形参加const,防止写反,即:
Date(const Date& d) // 正确写法:编译报错,会引发无穷递归{_year = d._year;_month = d._month;_day = d._day;}
在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗? 当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
①对于自定义类型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)
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号
函数原型: 返回值类型 operator操作符(参数列表)
例如我们在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;
}
我们再来尝试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;
}
Date类日期间的大于等于:
bool operator>=(const Date& d)
{//this代表d1的地址,因此对this解引用即指向d1//复用上述已经书写的大于和等于的判定return *this>d||*this==d;
}
日期加天数怎么办呢?(例如算某天过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个运算符不能重载
区分:
Date d2(d1); //拷贝构造(本质是初始化) 一个初始化另一个马上要创建的对象
d3=d1; //赋值重载(复制拷贝) 已经存在两个对象直接的拷贝
在上述的例子中,日期类出现的频率极高,可见它是一个非常经典的例子,因此我们在此将日期类的全部用法实现,具体如下:
✍首先,由于日期类所涉及到的函数接口较多,因此我们定义Date.h头文件包含日期类的接口,而Date.cpp文件为我们具体实现日期类的方式
为什么这里我们只在Date.h中提供接口呢(声明)?
答:全局函数在.h文件中会导致重定义,是由于Date.cpp和Test.cpp文件包含了Date.h,因此定义在.h中会在符号表拷贝多次,自己与自己发生重定义,因此才将声明与实现分离;或者置为静态的(static)
.h文件中尽量不含全局变量和函数,否则会出现链接问题
#define _CRT_SECURE_NO_WARNINGS 1
#include
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;
}
凡是内部不改变成员变量,其实也是*this对象数据的,这些成员函数都应该加const
这两个默认成员函数一般不用重新定义,编译器默认会生成
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;
}