Welcome to 9ilk's Code World
(๑•́ ₃ •̀๑) 个人主页: 9ilk
(๑•́ ₃ •̀๑) 文章专栏: 与C++的邂逅
🏠 类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6 个默认成员函数。
class Date {}; //空类
注:这里的“默认”与之前讲的“缺省”意思类似,意思是用户没有显示实现,编译器会自动生成。
🏠 构造函数
📌 构造函数概念
对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置
信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?如果有时忘记先调用Init初始化,这时会导致日期是随机值,可能会导致其他麻烦,这时C++提供了构造函数这个解决方案。
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2022, 7, 5);
d1.Print();
Date d2;
d2.Init(2022, 7, 6);
d2.Print();
return 0;
}
注:构造函数类似Init函数的功能,它是特殊的成员函数,它的主要人物不是开空间创建对象,而是跟Init功能一样初始化对象。
📌 构造函数特性
-
构造函数函数的函数名与类名相同。
-
构造函数无返回值,但不是说返回值是void,不需要写void。
-
对象实例化时编译器自动调用对应的构造函数。(与C语言的Init函数不同,这里是自动调用)。
- 构造函数可以进行函数重载。也就是说可以写多个构造函数,多种初始化方式,需要几个写几个。
class Date
{
public:
Date() //无参构造
{
_year = 1;
_month = 1;
_day = 1;
}
Date(int year,int month,int day) //有参构造
{
_year = year;
_month = month;
_day = day;
}
Date(int year = 2, int month = 3, int day = 4) /全缺省
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d; //无参构造 无参调用
Date d1(2, 1, 1); //有参构造 对象+参数列表 有参调用
return 0;
}
注:对于无参构造来初始化对象,规定是不能带括号,这样与函数声明分割开;而有参构造要带括号+参数列表。
- 默认构造:不传参就可以调用的构造并且默认构造函数只能有一个。
-
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
用户显式定义编译器将不再生成。
我们可以看到,当我们未显示定义构造函数时,编译器会自动生成一个构造函数,它对成员的处理如下:
注意: C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即: 内置类型成员变量在
类中声明时可以给默认值 。
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
};
这种默认构造有什么意义呢?
class MyQueue
{
private:
Stack _pushst;
Stack _popst;
};
对于这种成员都是自定义类型的成员,此时自动生成的构造就可以对自定义类型成员调用默认构造,不需要我们自己动手。
🏠 析构函数
📌 析构函数概念
📌 析构函数特性
- 析构函数名是在类名前面加上字符~。
class Date
{
public:
~Date(); //析构
};
- 析构函数无参数无返回值。
- 对象生命周期结束时,C++编译系统自动调用析构函数,当然他也能显示调用。
- 一个类只能有一个析构函数(没有参数也就不支持重载)。若未显示定义,系统会自动生成默认的析构函数。
我们可以看到当用户为显示定义析构函数时,会对它的成员做以下处理:
- 不是所有的类都需要显示写析构函数,若类中有不属于对象的资源(如在堆上申请空间)则需要写析构进行清理。
//没有资源需要清理
class Date
{
private:
int _year;
int _month;
int _day;
}
//有资源需要清理
class MyQueue
{
private:
Stack pushst;
Stack popst;
}
- 关于构造和析构的顺序
🏠拷贝构造函数
📌 拷贝构造函数概念
拷贝构造两种写法:
class Date
{
public:
private:
int _year;
int _month;
int _day;
};
Date(const Date& d) // 错误写法:编译报错,会引发无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
int main()
{
Date d1;
//拷贝构造
Date d2(d1);
Date d3 = d1;
return 0;
}
📌 拷贝构造函数特性
- 拷贝构造函数是构造函数的一个重载形式,它的函数名也与类名相同,显示实现拷贝构造后编译器就不生成默认构造了。
-
拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,
因为会引发无穷递归调用。
调用拷贝构造就需要传参,我们这里传参是传值传参,C++规定自定义类型传值传参要调用拷贝构造,语法逻辑上循环往复造成无穷递归,但编译器会进行强制检查不允许这样用。
Date(Date* d)
{
_year = d->_year;
_month = d->_month;
_day = d->_day;
}
若要完成拷贝用指针实现的构造也是可以实现的,但是规定拷贝构造参数是引用,而且引用比指针使用起来更方便。
Date(Date& d)
{
d._year = _year;
d._month = _month;
d._day = _day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
-
若未显式定义,编译器会生成默认的拷贝构造函数 。 默认的拷贝构造函数对象按内存存储按
字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
- 编译器自动生成的拷贝构造实现的是浅拷贝而不是深拷贝。
解决方法:深拷贝
此时我们可以开一个跟s1指向空间一样大小的空间,再将里面的值一个一个拷贝到新开空间,再让s2指向这块空间。
- 拷贝构造函数典型调用场景
🏠 赋值运算符重载
📌 运算符重载
我们之前C语言对两个自定义类型的比较需要我们自己写一个函数,但是这样代码的可读性较差,为此C++引入了运算符重载,让自定义类型可以像内置类型一样可以直接使用运算符进行操作。
class Date
{
public:
int _year;
int _month;
int _day;
};
//比较日期的小于
bool compare(const Date& d1,const Date& d2)
{
if(d1._year < d2._year)
return true;
else if(d1._year == d2._year)
{
if(d1._month < d2._month)
return true;
else if(d1._month == d2._month)
{
if(d1._year < d2._year)
return true;
return false;
}
else
return false;
}
return false;
}
//运算符重载
bool opertaor<(const Date& d1,const Date& d2)
{
if(d1._year < d2._year)
return true;
else if(d1._year == d2._year)
{
if(d1._month < d2._month)
return true;
else if(d1._month == d2._month)
{
if(d1._year < d2._year)
return true;
return false;
}
else
return false;
}
return false;
}
cout << Compare(d1,d2)<<endl; //可读性差
cout << (d1 < d2) <<endl; //易于理解可读性强
函数名 : 关键字operator + 需要重载的运算符符号。
函数原型 :返回值类型 operator操作符 (参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 运算符重载的返回值与要重载的运算符有关,一个类要重载哪些运算符是看需求,看重载有没有价值和意义。
-
重载操作符必须有一个类类型参数,运算符重载是为了自定义类型而生的参数是自定义类型就没意义了。
int operator-(int,int);//错误
-
用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义。
-
.* :: sizeof ?: . 这五个运算符是不能被重载的 。
class ob { void fcun(){} };//类ob内有函数func; typedef void(ob::*pobfunc)(); //成员函数指针类型 int main() { pobfunc fp = &ob::func;//定义成员函数指针p指向函数func ob temp;//定义ob类对象temp (temp.*fp)();使用对象temp加上.*运算符调用fp指向的成员函数 }
- 运算符重载的两种调用方式:显示调用和转换调用
Date d1;
Date d2;
cout << operator==(d3,d4) <<endl;//显示调用
cout << (d3 == d4) <<endl; //转换调用
- 运算符重载成全局函数,无法访问私有成员的问题
class Date
{
private:
int _year;
int _month;
int _day;
};
bool operator==(const Date& d1,const Date& d2)
{
return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
由于operator==是在全局域重载的,但成员是私有的,在类外不能访问因此我们需要其他解决方法。
a. 提供这些成员的get或set
class Date
{
public:
int Getyear()
{
return _year;
}
//Getmonth() Getday()....
private:
int _year;
int _month;
int _day;
};
b.使用友元
c.重载成成员函数(一般用这种)
-
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this。毕竟运算符重载 参数个数要与重载的操作符的操作数一致。
class Date
{
public:
bool operator==(const Date& d1,const Date& d2);//参数个数为3 但==操作数应该为2
bool operator==(const Date& d) //隐含的this 参数个数实际为2
{
//....
}
private:
int _year;
int _month;
int _day;
};
重载成成员函数后的调用,我们常用转换调用。
Date d1;
Date d2;
cout << d1.operator==(d4)<<endl; //显示调用成员函数
cout <<( d1 == d2 ) << endl;//本质还是转换成显示调用
注:如果此时又在全局重载还是会优先调用在类里面重载的,这也说明此时在全局重载没有意义。
- 运算符重载中,参数顺序和操作顺序是一致的。
📌 赋值运算符重载
Date d1;
Date d2;
d2 = d1;
对于这段代码,d2==d1并不是调用拷贝构造,而是调用赋值运算符重载。对于赋值运算符重载我们需要注意以下几点:
- 拷贝构造 vs 赋值重载
-
参数类型 : const T&,传递引用可以提高传参效率。
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值。
- 传值返回 vs 传引用返回
传值返回
传引用返回
//这里没返回局部或临时对象 传引用返回效率高
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
- 检测是否自己给自己赋值
Date& operator=(const Date& d)
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
-
返回*this :要符合连续赋值的含义。
-
赋值运算符只能重载成类的成员函数不能重载成全局函数(规定),因为它是默认的成员函数。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员
- 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
默认赋值重载意义:
- 对于内含引用成员的class,重载赋值操作符并无意义,因为引用不可改变指向,指向其他对象。
🏠 const成员函数
📌 const成员函数
class Date
{
public:
void Print()const
{
cout << _year << " " << _month << " " << _day << endl;
}
void Print(const Date* this) //编译器处理
{
cout << this->_year << " " << this->_month << " " << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
📌 const 与 非const比较
【面试题】
1. const对象可以调用非const成员函数吗?
2. 非const对象可以调用const成员函数吗?
3. const成员函数内可以调用其它的非const成员函数吗?
4. 非const成员函数内可以调用其它的const成员函数吗?
注:const对象要在定义时就给他值。
🏠 取地址以及const取地址操作符重载
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
class Date
{
public :
Date* operator&()
{
return nullptr ;
}
const Date* operator&()const
{
return (const Date*)0xffffffff ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
🏠 Date类的实现
📌 日期类大小比较
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;
-
operator <
参考代码:
bool Date::operator<(const Date& d)
{
if (_year < d._year)
{ //年小直接日期小
return true;
}
else if (_year == d._year)
{//年等再比月
if (_month < d._month)
{
return true;
}
else if (_month == d._month) //月等再比日
{
return _day < d._day;
}
}
return false;
}
-
operator ==
判断两个日期相等只需要判断他们的年月日是不是都相等。
bool Date::operator==(const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
-
operator <=
小于等于只要判断这个日期是否是小于或等于另一个日期即可,我们直接复用前两个实现的小于和等于。
bool Date::operator<=(const Date& d)
{
return (*this) < d || (*this) == d;
}
-
operator >
我们知道大于的对立事件,所以取<=的逻辑反就是大于,此时可以复用上一个实现的<=。
bool Date::operator>(const Date& d)
{
return !((*this) <= d);
}
- operator >=
大于等于的对立事件是小于,所以我们直接复用<即可。
bool Date::operator>=(const Date& d)
{
return !((*this) < d) ;
}
-
operator !=
不等于的对立事件是等于,我们直接复用==即可。
bool Date::operator!=(const Date& d)
{
return !((*this) == d);
}
📌 日期 += 天数
参考代码:
class Date
{
//...
int GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
static int monthDayArray[13] = { -1, 31, 28, 31,30,31,30,31,31,30,31,30,31};//下标从0开始
// 不好处理
// 闰年
if (month == 2 && (year % 4 == 0 && year % 100 !=0) || (year % 400 == 0))
{
return 29;
}
else
return monthDayArray[month];
}
//...
};
Date& Date::operator+=(int dat)
{
_day += day;
while(_day > GetMonthDay(_year,_month))
{
_day -= GetMonthDay(_day,_month); //满足当前月后剩下的天数
++_month; //超出当前月
if(_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
📌 日期 + 天数
Date Date::operator+(int day) const
{
Date temp(*this);//拷贝
temp += day; //复用
return temp;
}
既然逻辑一样,那是否可以先实现+,再让+=复用+呢?
Date Date::operator+(int day) const
{
Date temp(*this);//拷贝
temp._day += day;
while(temp._day> GetMonthDay(temp._year,temp._month))
{
temp._day-= GetMonthDay(temp._day,temp._month); //满足当前月后剩下的天数
++temp._month; //超出当前月
if(temp._month == 13)
{
++temp._year;
temp._month = 1;
}
}
return temp;
}
Date& Date::operator+=(int day)
{
*this = *this + day;
return *this;
}
📌 日期 -= 天数
参考代码:
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+=(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)
{
if (day < 0)
{
return (*this) += -day;
}
_day -= day;
//此时该月的减完了
while (_day < 0)
{
//用上一个月的来补
--_month;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthday(_year, _month);
}
return *this;
}
📌 日期 - 天数
跟日期+天数一样复用即可。
Date Date::operator-(int day)
{
Date temp(*this);
temp -= day;
return temp;
}
📌 前置++
//前置++ 先加
Date& Date::operator++()
{
(*this) += 1;
return *this;
}
📌 后置++
重载前置++之前我们需要理清函数重载与运算符重载之间的关系。
Date operator-(int day);
int operator-(const Date& d);
对于前置++和后置++,他们用的都是同一运算符所以为了区分他们构成重载,设计者强行给后置++的重载增加了一个int形参,只是单纯为了区分。
// 这里不需要写形参名,因为接收值是多少不重要,也不需要用
// 这个参数仅仅是为了跟前置++构成重载区分
Date Date::operator++(int)
{
Date temp(*this);
*this += 1;
return temp;
}
📌 日期-日期
int Date::operator-(const Date& d) const
{
int flag = 1;
Date max = *this;
Date min = d;
int n = 0;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
while (min != max)
{
min++;
n++;
}
return n*flag;
}
📌 流插入重载
- 内置类型使用流插入
int i = 0 ;
double d = 1.1;
cout << i << " "<< d <<endl;
我们平常使用的cou其实是个ostream对象,为什么可以直接cout << 内置类型对象呢?
- 自定义类型
void Date::operator<<(ostream& out)
{
out << _year << "年" << _month << "月" << _day << "日"<<endl;
}
d1.operator<<(cout);//传ostream对象
d1 << cout;
有同学可能会好奇为什么转换调用时,Date类是做左操作数?
在重载成全局前我们需要知道我们成员变量一般是设置成私有的,我们若想在全局访问他们,除了写一个Get或Set,我们可以将流插入重载设置为友元。友元即这个类的朋友,也可以访问这个类成员。
class Date
{
//...
friend void operator<<(ostream& out, const Date& d);
}
void operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}
还有不足的是,上面返回值是void的话,如果我们想连续打印多个日期类对象,由于是从左向右结合,所以我们需要返回一个ostream对象继续进行打印。
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
📌 流提取重载
bool Date::CheckDate()
{
if (_month < 1 || _month > 12 || _day <1 || _day > GetMonthday(_year, _month))
{
return false;
}
return true;
}
istream& operator>>(istream& in, Date& d)
{
cout << "请依次输入年月日:>";
in >> d._year >> d._month >> d._day;
if (!d.CheckDate())
{
cout << "日期非法" << endl;
}
return in;
}
📌 构造函数
Date::Date(int year, int month, int day)
{
// 检查日期的合法性
if(CheckDate())
{
_year = year;
_month = month;
_day = day;
}
else
{
cout << "非法日期" << endl;
cout << year << "年" << month << "月" << day << "日" << endl;
}
}
📌 何时设置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;