目录
一、6个成员函数简要介绍
如果一个类中什么成员都没有,那么简称空类,那么空类中是什么都没有吗?
当然不是,因为任何一个类在不写的情况下,都会生成6个默认成员函数,分别如下:
1.初始化:构造函数主要完成初始化的工作,相当于C语言中的写的Init();
2.清理:析构函数主要完成清理工作,相当于C语言中写的Destory();
3.拷贝:拷贝构造是使用同类对象来初始化创建对象
4.赋值:赋值重载主要是把一个对象赋值给另一个对象
5.取地址和重载:主要是对普通对象和const对象取地址(很少自己实现)
二、构造函数
为什么会出现构造函数呢?
主要是因为假如你写了一个日期类,然后每次通过自己写的设置初始值的方法给对象设置内容,每次创建对象都去调用该方法,未免有些麻烦,所以出现了构造函数,在创建对象时,就将对象信息设置了进去(自动调用),保证对象被初始化。
那么什么叫做构造函数呢?
构造函数是一个特殊的成员函数,它的特性主要有以下几点:
1.函数名与类名相同2.对象实例化时编译器自动调用对应的构造函数
3.无返回值
4.构造函数可以重载
5.在对象的生命周期内只调用一次注意:构造函数虽然名称叫做构造,但是其主要任务并不是开辟空间创建对象,而是初始化对象
构造函数代码:
class Date
{
public:
//1.无参构造函数
Date()
{
_year = 0;
_month = 1;
_day = 1;
}
//2.带参构造函数
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//对象实例化后面如果没有参数会自动调用无参构造函数
Date d2(2022,5,25);//有参数就会自动调用带参构造函数
//会按顺序构造,如d1先构造,d2再构造
}
C++将类型分为俩类:
1.内置类型(基本类型):int / char / double / 指针 /数组等等
2.自定义类型: struct/calss等等
我们不写构造函数,编译器会自动生成一个无参的默认构造函数,一旦用户写了构造函数,编译器将不再生成.我们不写构造函数(自动生成的构造函数),对于内置类型的成员变量,编译器不做初始化处理,对于自定义类型的成员变量编译器自动生成的默认的构造函数会去调用它的默认构造函数(不用参数就可以掉的构造函数(1.不写构造函数,自动生成 2.无参 3.缺省))以进行初始化,如果没有默认构造函数就会报错,代码如下:
class A
{
public:
A()
{
_a = 0;
cout << _a << endl;
}
//缺省
//A(int a = 0)
//{
// _a = 0;
// cout << _a << endl;
//
//}
private:
int _a;
};
class Date
{
//Date没有写构造函数,将会对以下进行处理
private:
int _year;//内置类型,不做初始化处理
int _month;
int _day;
A _aa;//自定义类型,编译器生成的默认构造函数会调用自定义类型的默认构造函数
};
int main()
{
Date d1;
}
三、析构函数
析构函数: 与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的,而对象在销毁时会自动调用析构函数,完成对象的一些资源清理工作。
析构函数是特殊的成员函数,特点如下:
1.析构函数名在类名前加上字符~
2.无参数无返回值
3.一个类有且只有一个析构函数,若未显示定义,系统会自动生成默认的析构函数
4.对象声明周期结束时,C++编译系统自动调用析构函数
代码如下:
class Date
{
public:
// 推荐实现全缺省或者半缺省,因为比较好用
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
~Date()//析构函数
{
// Date类没有资源需要清理,所以Date不实现析构函数都是可以的
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int)*capacity);
if (_a == nullptr)
{
cout << "malloc fail\n" << endl;
exit(-1);
}
_top = 0;
_capacity = capacity;
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
size_t _top;
size_t _capacity;
};
int main()
{
Date d1;
Date d2(2022, 1, 15);
Stack s1;
Stack s2(20);//栈是后进先出,所以清理的顺序是s2,s1,d2,d1
//析构顺序:先局部,然后局部静态,再全局,同一个生命周期的先定义的,后析构
return 0;
}
如果我们不写,默认生成析构函数和构造函数类似,对于内置类型的成员变量不做处理,对于自定义类型的成员变量,编译器生成的默认析构函数,会去调用自定义类型的析构函数
代码如下:
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int)*capacity);
if (_a == nullptr)
{
cout << "malloc fail\n" << endl;
exit(-1);
}
_top = 0;
_capacity = capacity;
}
// 如果我们不写默认生成析构函数和构造函数类似
// 对于内置类型的成员变量不做处理
// 对于自定义类型的成员变量会去调用它的析构函数
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
size_t _top;
size_t _capacity;
};
// 两个栈实现一个队列,用来理解默认生成的析构函数调用自定义类型成员变量析构函数
class MyQueue {
public:
// 默认生成的构造函数和析构函数会对自定义类型成员变量调用他的构造和析构
void push(int x) {}
private:
Stack pushST;
Stack popST;
};
int main()
{
/*Date d1;
Date d2(2022, 1, 15);*/
Stack s1;
Stack s2(20);//栈是后进先出,所以清理的顺序是s2,s1,d2,d1
MyQueue mq;
return 0;
}
四、拷贝构造函数
只有单个形参 ,该形参是对本 类类型对象的引用 ( 一般常用 const 修饰 ) ,在用已存在的类类型对象创建新对象时由编译器自动调用
拷贝构造函数也是特殊的成员函数,特点有如下几点:
1.拷贝构造函数是构造函数的一个重载形式
2.拷贝构造函数的参数只有一个并且必须使用引用传参,使用传值方式会引发无穷递归调用
解决问题:为什么不加&会导致无穷递归调用?
首先我们了解一下什么叫做拷贝构造?
就是用已存在的类类型对象去初始化同类型的新的对象,比如这里用已存在的对象d1,去初始化新的对象d2,这时就会调用拷贝构造,而调用拷贝构造之前,需要先传参,也就是将d1传值给d,换句话说就是d1又是对d的初始化(自定义类型用一个同类型的对象来初始化自己,就是拷贝构造),又构成了拷贝构造,可以写成Date d(d1),而这是也同样,拷贝构造之前又需要先传参,以此向后执行,就构成了一个无穷递归调用。
那为什么引用传参(加&)就不会导致无穷递归调用呢?
因为加&,就相当于d是d1的别名,他们是一块空间的俩个名字,操作的是同一块空间的地址,所以就不会调用拷贝构造,所以可以得出一个总结,传值传参会再调用拷贝构造,而加&不会再调用拷贝构造,不会导致无穷递归的问题。所以拷贝构造函数的参数只有一个且必须使用引用传参
为什么一定要加const ?
const 的作用就是防止被修改,具体如下图所示:
若未显示定义,系统生成默认的拷贝构造函数。
1.内置类型成员会完成按字节的拷贝(浅拷贝)
那么如果对于日期类,生成默认的拷贝构造函数会有问题吗?
是没有什么问题的。
但是对于栈类会有问题吗?
会导致问题,为什么呢?
因为s2将s1的所有值都拷贝了过来,而指针空间也是指向的同一片空间,当s2析构函数释放了malloc开辟的空间后,s1又释放了那份空间,也就是说那份空间被释放了俩次,所以导致了报错
2.自定义类型成员,会调用它的拷贝构造(我们自己实现的拷贝构造函数)总结:
拷贝构造我们不写,生成的默认拷贝构造函数对于内置类型和自定义类型都会有拷贝处理,但是处理的细节是不同的,和构造函数和析构函数也是不一样的。
拷贝构造的优化:
1.
所以上面的代码应该是调用了三次拷贝构造,但是根据运行结果:
为什么只调用了俩次拷贝构造呢?
因为当u传值返回的时候,编译器会进行一次优化,即如果一次编译里面,连续的构造函数,(u->临时变量->y)会被优化成一次
但是当y变量先创建好,那么进行的应该是俩次拷贝构造和一次赋值拷贝
2.匿名对象又会进行几次拷贝构造呢?
根据上面的分析我们来进行一次练习:
解析:
优化一般是在传值传参和传值返回的时候,所以总共进行了7次拷贝构造
五、赋值运算符重载
5.1 运算符重载
C++ 为了增强代码的可读性引入了运算符重载 , 运算符重载是具有特殊函数名的函数 ,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。函数名字为:关键字 operator 后面接需要重载的运算符符号 。函数原型: 返回值类型 operator 操作符 ( 参数列表 )注意:1.不能通过连接其他符号来创建新的操作符:比如 operator@2.重载操作符必须有一个类类型(自定义类型)或者枚举类型的操作数3.用于内置类型的操作符,其含义不能改变,例如:内置的整型 + ,不能改变其含义4. .* 、 :: 、 sizeof 、 ?: 、 . 注意以上5个运算符不能重载(经常在笔试选择题中出现)
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//private:
int _year;
int _month;
int _day;
};
//函数名 operator 操作符
//返回类型 看操作符运算后返回值是什么
//参数 操作符有几个操作数,他就有几个参数
bool operator>(const Date& d1, const Date& d2)
{
if (d1._year > d2._year)
{
return true;
}
else if (d1._year == d2._year && d1._month > d2._month)
{
return true;
}
else if (d1._year == d2._year && d1._month == d2._month && d1._day > d2._day)
{
return true;
}
else//d1 <= d2
{
return false;
}
}
int main()
{
Date d1(2022,5,24);
Date d2(2022,12,24);
//默认C++是不支持自定义类型对象使用运算符
cout << (d1 > d2) << endl;//加()的原因是<<的优先级比>高,导致cout << d1先执行,并且d1>d2会转换成operator>(d1,d2)
cout << operator>(d1,d2) << endl;//函数名(参数);
//写d1 > d2;也可以增强程序的可读性
}
以上的代码是将成员变量改为公有的才能在类外调用类中的成员函数
那有没有什么方法可以不将私有成员函数改为公有就可以使用呢?
即将其在类中写为成员函数形式,就可以调用类中私有的成员变量
如下代码:
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//将其写为成员函数形式,就可以调用类中私有的成员变量
bool operator>(const Date& d2)//bool operator>(Date* const this,const Date& d2)
{
if (_year > d2._year)
{
return true;
}
else if (_year == d2._year && _month > d2._month)
{
return true;
}
else if (_year == d2._year && _month == d2._month && _day > d2._day)
{
return true;
}
else//d1 <= d2
{
return false;
}
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date s1(2023,2,1);
Date s2(2023,2,10);
cout << (s1 > s2) << endl;
cout << s1.operator>(s2) << endl;
}
5.2 赋值运算符重载
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d2)//拷贝构造
{
cout <<" Date(const Date & d2) "<< endl;
}
Date operator= (const Date& d2)
{
//极端情况下自己给自己赋值就可以不用处理了,直接判断一下跳过
if (this != &d2)
{
_year = d2._year;
_month = d2._month;
_day = d2._day;
}
return *this;//传值返回就会调用拷贝构造
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 5, 24);
Date d2(2022, 12, 24);
Date d3(2023, 1, 24);
d3 = d1 = d2;//d1.operator=(d2);
}
赋值运算符主要有四点:
1. 参数类型2. 返回值3. 检测是否自己给自己赋值4. 返回 *this5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。编译器默认生成赋值重载,跟拷贝构造做得事情完全类似
1.内置类型成员,会完成字节序值拷贝--浅拷贝2.自动类型成员变量,会调用它的operator=
总结:默认生成这四个默认成员函数
构造和析构处理机制是基本类似的拷贝构造和赋值重载处理机制是基本类似的
拷贝构造和赋值重载的区别:
拷贝构造:一个已经存在的对象拷贝初始化一个马上创建实例化的对象
Date d4(d1);
Date d5 = d1;
赋值重载:俩个已经存在的对象之间进行赋值拷贝
d2 = d1 = d3;
d1.operator=(d2);
六、日期类的实现
#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"
//获取一个月多少天
int Date::GetMonthDay(int year, int month)const
{
//因为数组每次都不变所以加static
static int monthDayArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
int day = monthDayArray[month];
//2月的闰年多一天,首先是2月,如果不是2月,即便是闰年也没有意义,所以先判断是否是2月
if (month == 2 && (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)))
{
day += 1;
}
return day;
}
//构造函数
Date::Date(int year,int month,int day)//成员函数前面回家this
{
_year = year;
_month = month;
_day = day;
if (!(year > 0
&& (month > 0 && month < 13)
&& (day > 0 && day <= GetMonthDay(year, month))))
{
cout << "非法日期->";
print();//相当于this->print();
}
}
//打印
void Date::print()const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
// >运算符重载
bool Date::operator>(const Date& d)const
{
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;
}
else//_year <= d._year
{
return false;
}
}
// ==运算符重载
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
{
return *this < d || *this == d;
}
// >=运算符重载
bool Date::operator>=(const Date& d)const
{
return *this > d || *this == d;//假如d1 >= d2,this表示d1的指针,*this表示d1
}
// !=运算符重载
bool Date::operator!=(const Date& d)const
{
return !(*this == d);
}
//+=
//d1 += 100
//天满往月进位,月满,往年进位
Date& Date::operator+=(int day)
{
//如果d1 += -100,就相当于d1 -= 100
if (day < 0)
{
return *this -= (-day);
}
_day += day;
while (_day > GetMonthDay(_year,_month))
{
_day -= GetMonthDay(_year,_month);
++_month;
if (_month == 13)
{
_month = 1;
++_year;
}
}
return *this;
}
//+
Date Date::operator+(int day)const
{
Date ret(*this);//拷贝构造
//就是d1不变,ret在变化,最后返回ret就表现出了+的作用
ret += day;//ret.operator+=(int day);
return ret;
}
//-=
//d1 -= 100
Date& Date::operator-=(int day)
{
//如果穿过来的day是-100,即d1 -= -100就相当于d1 += 100
if (day < 0)
{
return *this += -day;
}
//先将天数减掉,如果天数为负数,则向上一个月借天数加到天数上,如果月不够,那就向年借位
_day -= day;
while (_day <= 0)//每月没有0号
{
_month--;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year,_month);
}
return *this;
}
//d1 - 100
Date Date::operator-(int day)const
{
Date ret(*this);//拷贝函数
ret -= day;
return ret;
}
//++d1(推荐前置++) d1.operator++(&d1)
Date& Date::operator++()
{
//前置++
//返回++之后的值
*this += 1;//d1.operator+=(1);
return *this;
}
//d1++ d1.operator++(&d1,0)
Date Date::operator++(int)
{
//后置++,返回的是++前面的一个值所以要保留++前的值
Date ret(*this);//拷贝构造
*this += 1;
return ret;//进行俩次值拷贝
}
//--d1
Date Date::operator--()
{
//前置--
*this -= 1;
return *this;
}
//d1--
Date Date::operator--(int)
{
//后置--
//返回--前的值
Date ret(*this);//拷贝构造
*this -= 1;
return ret;
}
//日期-日期
int Date::operator-(const Date& d)const
{
//today offerday
//大 -> 小 为正
//小 -> 大 为负
//因为不知道谁大谁小,所以自己定义
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)//说明today小 offerday 大,所以小的和大的要变化
{
max = d;
min = *this;
flag = -1;//小->大为负
}
int count = 0;
while (min != max)
{
++min;
++count;
}
return count*flag;
}
//从公元一年1900.1.1(星期一)开始
void Date::PrintWeekDay()const
{
//指针数组--是一个指针,这个指针指向一个数组,这个数组中有7个元素
const char* arr[] = { "星期一","星期二", "星期三", "星期四", "星期五", "星期六", "星期天" };
/*Date start(1900, 1, 1);*/
int count = *this - Date(1900, 1, 1);//匿名对象,生命周期只在这一行
cout << (arr[count % 7]) << endl;//7天一循环
}
日期+-天数分析图:
七、const 成员
将 const 修饰的类成员函数称之为 const 成员函数 , const 修饰类成员函数,实际修饰该成员函数 隐含的 this指针 ,表明在该成员函数中 不能对类的任何成员进行修改。
总结:如果内容(*this)不需要修改,都加上const最好。如果内容(*this)需要修改,则不能加const,比如+=重载等
八、 取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如 想让别人获取到指定的内容!如果想定义可以为如下代码:
class AA
{
public:
AA* operator&()
{
return this;
}
const AA* operator&()const
{
return this;
}
};
int main()
{
AA a1;
cout << &a1 << endl;
const AA a2;
cout << &a2 << endl;
}