类与对象(中)
概述六个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。
构造函数
概念
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时编译器会自动调用,用来初始化类的成员变量,每个对象在生命周期内只会调用一次。
构造函数是完成对象的初始化的,与Init函数做相同的工作,但是Init函数需要自主调用,而构造函数不用,避免了忘记调用Init函数而造成对象随机值的情况。
特性
构造函数是特殊的成员函数,需注意的是,构造函数并不是开空间创建对象,而是初始化对象。其具有以下特性
-
函数名与类名相同
-
无返回值
-
对象实例化时编译期自动调用对应的构造函数。
-
构造函数可以重载
-
如果类中没有显示定义构造函数,则编译器会自动生成一个无参的默认构造函数,一旦用户显示定义编译器就不会生成
-
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认的构造函数只能有一个。注意无参构造函数、全缺省构造函数、编译器默认生成的构造函数,都是默认的成员函数。一般推荐使用全缺省的。
析构函数
概念
与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作
特性
-
析构函数的函数名是在类名前加~。
-
无参数无返回值。
-
一个类有且只有一个析构函数(也就是不能重载)。和构造函数一样若未显示定义系统会自动生成默认的析构函数。
-
对象生命周期结束时,C++编译器会自动调用析构函数。
-
不写析构函数时编译器自动生成的析构函数会处理自定义类型成员,不处理内置类型成员。
先后析构顺序
int main()
{
Stack st;
st.Push(1);
st.Push(2);
st.Push(3);
Stack st2;
st2.Push(4);
st2.Push(5);
st2.Push(6);
return 0;
}
对象定义在主函数中,函数调用会建立栈帧,栈帧中的对象构造和析构也要符合栈的特征(先进后出)所以上面代码时st先构造,st2再构造,st2析构,st再析构。
拷贝构造函数
概念
只有单个形参,该形参是对本类类型对象的引用(一般用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
特征
拷贝构造函数也是特殊的成员函数其特征如下:
-
拷贝构造函数是构造函数的一种重载形式。
-
拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
-
若未显示定义,系统默认生成的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或值拷贝。
-
那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?日期类这样的是没有必要。那么下面的类呢。
-
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。 class String { public: String(const char* str = "jack") { _str = (char*)malloc(strlen(str) + 1); strcpy(_str, str); } ~String() { cout << "~String()" << endl; free(_str); } private: char* _str; }; int main() { String s1("hello"); String s2(s1); } //这里会出现同一个空间被析构两次,,造成内存泄漏。原因是编译器自动生成的拷贝只实现了简单的值拷贝造成s1和s2的_str指向同一块空间,s2析构时空间释放,s1析构时释放已经释放的空间造成内存泄漏。
赋值运算符重载
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。函数名字为:关键字operator后面接需要重载的运算符符号。函数原型:返回值类型 operator操作符(参数列表)注意:
-
不能通过连接其他符号来创建新的操作符:比如operator@
-
重载操作符必须有一个类类型或者枚举类型的操作数
-
用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
-
作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
-
.* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
赋值运算符主要有四点
-
参数类型
-
返回值
-
检测是否自己给自己赋值
-
返回*this
-
一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。
具体应用(日期类的实现)
#include<iostream>
#include<cassert>
using namespace std;
class Data
{
public:
//构造函数
Data(int year = 1900, int month = 1, int day = 1) {
if (!IsInvalib()) {
_year = year;
_month = month;
_day = day;
}
else {
assert(false);
}
}
//判断日期是否为合法日期
bool IsInvalib() {
return _year >= 0 && _month > 0 && _month < 13
&& _day>0 && _day <= GetMonthDay(_year, _month);
}
//判断是否为闰年
bool IsLeepYear(int year) {
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
return true;
}
return false;
}
//根据年和月获取该月的天数
//内联函数(建议编译器在编译时将该函数在调用处展开,以提高效率)
inline int GetMonthDay(int year, int month) { //inline内联函数关键字,只是一个建议,采用不采用取决于编译器,以及函数行数。
int arry[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
int day = arry[month];
if (month == 2 && IsLeepYear(year)) {
day = 29;
}
return day;
}
//拷贝构造
Data(const Data& d) //必须使用引用,否则会无穷递归
{ //如果没有定义,系统会默认的缺省的拷贝构造
_year = d._year;
_month = d._month;
_day = d._day;
}
void print() const {
cout << _year << "-" << _month << "-" << _day << endl;
}
//~Data //析构函数,日期类没有需要清理的资源,所以使用系统默认的析构
Data& operator=(const Data& d) //=重载 建议在此处使用引用,以免去调用拷贝构造的消耗
{
if (this != &d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
bool operator==(const Data& d) const //==重载
{
return _year == d._year //隐含的this指针(左边为:this->_year,this省去)
&& _month == d._month
&& _day == d._day;
}
bool operator!=(const Data& d) //!=重载
{
return !(*this == d);
}
bool operator>=(const Data& d)
{
return !(*this < d);
}
bool operator<=(const Data& d)
{
return *this < d || *this == d;
}
bool operator>(const Data& d)
{
return !(*this <= d);
}
bool operator<(const Data& d)
{
if ((_year > d._year)
|| (_year == d._year) && (_month > d._month)
|| (_year == d._year) && (_month == d._month) && (_day > d._day))
return true;
return false;
}
Data operator+(int day)
{
Data ret = *this;
if (day < 0) {
ret -= -day;
}
else {
ret._day += day;
while (ret._day > GetMonthDay(ret._year, ret._month))
{
int monthday = GetMonthDay(ret._year, ret._month);
ret._day -= monthday;
ret._month++;
if (ret._month==12) {
ret._year++;
ret._month = 1;
}
}
return ret;
}
}
Data operator+=(int day)
{
*this = *this + day;
return* this;
}
Data operator-(int day)
{
Data ret = *this;
if (day < 0) {
ret += -day;
}
else {
ret._day -= day;
while (ret._day <= 0) {
if (ret._month == 1) {
ret._month = 12;
ret._year--;
}
else {
ret._month--;
}
int monthday = GetMonthDay(ret._year, ret._month);
ret._day += monthday;
}
return ret;
}
}
Data operator-=(int day)
{
*this = *this - day;
return *this;
}
//++d
Data operator++()
{
*this += 1;
return *this;
}
//d++
Data operator++(int)
{
Data tmp = *this;
*this += 1;
return tmp;
}
//--d
Data operator--()
{
*this -= 1;
return *this;
}
//d--
Data operator--(int)
{
Data tmp = *this;
*this -= 1;
return tmp;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2001,3,12);
d1 -= 100;
d1.print();
Data d2(2018, 2, 28);
d2.print();
++d1; // d1.operator++(&d1);
d1.print();
d2++; // d2.operator++(&d1, 0);
d2.print();
Data d3(2001, 1, 1);
d3.print();
d3--;
d3.print();
return 0;
}
const成员
const修饰类的成员函数
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
注意:
-
const对象不可以调用非const成员函数,设及权限放大
-
非const成员可以调用const成员函数,权限缩小。
取地址及const取地址运算符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!