C++类与对象(中)

类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情
况下,都会自动生成下面6个默认成员函数。

class Date {};

image-20210906182045855

构造函数

概念

对于以下的日期类:

class Date
{
public:
	void SetDate(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
	void Display()
	{
		cout <<_year<< "-" <<_month << "-"<< _day <<endl;
	}

private:
int _year;
int _month;
int _day;
};

int main()
{
	Date d1,d2;
	d1.SetDate(2018,5,1);
	d1.Display();
	
	Date d2;
	d2.SetDate(2018,7,1);
	d2.Display();
	
	return 0;
}    

对于Date类,可以通过SetDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。

特性

构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:

  1. 函数名与类名相同。

  2. 无返回值。

  3. 对象实例化时编译器自动调用对应的构造函数。

  4. 构造函数可以重载。

    class Date
    {
    public :
        // 1.无参构造函数
        Date ()
        {}
        
        // 2.带参构造函数
        Date (int year, int month , int day )
        {
     	   _year = year ;
     	   _month = month ;
     	   _day = day ;
    }
    
    private :
    int _year ;
    int _month ;
    int _day ;
    };
    
    void TestDate()
    {
        Date d1; // 调用无参构造函数
        Date d2 (2015, 1, 1); // 调用带参的构造函数
    // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
    // 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
    
    Date d3();
    }
    

    对于Date d3();

    image-20210906191814295

  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成

    class Date
    {
    public:
    /*
        // 如果用户显式定义了构造函数,编译器将不再生成
        Date (int year, int month, int day)
        {
     	   _year = year;
     	   _month = month;
     	   _day = day;
        }
    */
    private:
    int _year;
    int _month;
    int _day;
    };
    
    void Test()
    {
    // 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数
    	Date d;
    }
    
  6. 无参的构造函数全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参
    构造函数、全缺省构造函数、我们没写时编译器默认生成的构造函数,都可以认为是默认成员函数。尽量使用全缺省构造函数

    // 默认构造函数
    class Date
    {
    public:
        Date()
        {
     	   _year = 1900 ;
     	   _month = 1 ;
     	   _day = 1;
        }
        
        Date (int year = 1900, int month = 1, int day = 1)
        {
     	   _year = year;
     	   _month = month;
     	   _day = day;
        }
    private :
    int _year ;
    int _month ;
    int _day ;
    };
    
    // 以下测试函数能通过编译吗?
    void Test()
    {
        Date d1;
    }
    

    编译结果:

    image-20210906193837000

    我们可以看到编译器对其报错:Date含有多个默认构造函数。说明一个类只能包含一个默认构造函数。

  7. 关于编译器生成的默认成员函数,很多童鞋会有疑惑:在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d1对象调用了编译器生成的默认构造函数,但是d对象_ year/_ month/_ day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??

    解答:

    C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如int/char…,自定义类型就是我们使class/struct/union自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数

    class Time
    {
    public:
    
        //自定义的构造函数    
        Time()
        {
     	   cout << "Time()" << endl;
     	       _hour = 0;
     	   _minute = 0;
     	   _second = 0;
        }
        
    private:
    int _hour;
    int _minute;
    int _second;
    };
    
    class Date
    {
    private:
    // 基本类型(内置类型)
    int _year;
    int _month;
    int _day;
    // 自定义类型
    Time _t;
    };
    
    int main()
    {
        Date d;
        return 0;
    }
    

    上述代码就是编译器使用了自身的默认构造函数,使Date的_t成员调用了它自己的默认构造函数。

  8. 成员变量的命名风格

    // 我们看看这个函数,是不是很僵硬?
    class Date
    {
    public:
        Date(int year)
        {
            // 这里的year到底是成员变量,还是函数形参?
            year = year;
        }
    private:
    int year;
    };
    
    // 所以我们一般都建议这样
    class Date
    {
    public:
        Date(int year)
        {
        _year = year;
        }
    private:
    int _year;
    };
    
    // 或者这样。
    class Date
    {
    public:
        Date(int year)
        {
        	m_year = year;
        }
    private:
    int m_year;
    };
    

    看第一种命名方式的结果:

    image-20211009202919356

d1的year最后没有被初始化为1,而是随机值,因为在Date构造函数中,等号左边的year代表的是局部变量year,即参数,所以实际上是参数year自己在给自己赋值,并没有对成员变量year赋值,编译器使用了就近原则。

所以成员变量最好加标识

析构函数

概念

前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。

这里的资源清理函数并不是指将数据从栈上pop掉,而是对已经申请的堆空间进行销毁,关闭已经打开的文件,将成员变量置为合适的值等。

特性

析构函数是特殊的成员函数。
其特征如下:

  1. 析构函数名是在类名前加上字符 ~。

  2. 无参数无返回值。

  3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。

  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

    typedef int DataType;
    class SeqList
    {
    public :
        
        //构造函数
        SeqList (int capacity = 10)
        {
            _pData = (DataType*)malloc(capacity * sizeof(DataType));
            assert(_pData);
            _size = 0;
            _capacity = capacity;
        }
    
        //自己定义的析构函数 
        ~SeqList()
        {
            if (_pData)
            {
                free(_pData ); // 释放堆上的空间
                _pData = NULL; // 将指针置为空
                _capacity = 0;
                _size = 0;
            }
        }
        
    private :
    
    int* _pData ;
    size_t _size;
    size_t _capacity;
    };
    
  5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,与构造函数相同,编译器生成的默认析构函数,会对自定类型成员调用它的析构函数。

    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;
    };
    
    class Person
    {
    private:
    	String _name;
    	int _age;
    };
    
    int main()
    {
    	Person p;
    	return 0;
    }
    

    结果:

image-20210906194513804

说明编译器自动生成的析构函数是会使自定义类型成员调用它自己的默认析构函数的。

当我们在一个类里面,将其他类作为成员时,在该类里我们就不用再写这些类的构造函数和析构函数,因为编译器会帮我们调用它们自己的。比如题目:用两个栈实现一个队列。当然,内置类型还是需要进行构造和初始化的。

拷贝构造

概念

拷贝构造就像构造函数的一个双胞胎兄弟。

image-20210906225511800

在创建对象时,可否创建一个与一个对象一某一样的新对象呢?
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象
创建新对象时由编译器自动调用。

特征

拷贝构造函数也是特殊的成员函数,其特征如下:

  1. 拷贝构造函数是构造函数的一个重载形式。

  2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。

    示例:当我们想把d1拷贝给d2时

    class Date
    {
    public:
        
        //构造函数
    Date(int year = 1900, int month = 1, int day = 1)
    {
    	_year = year;
    	_month = month;
    	_day = day;
    }
        
        //拷贝构造函数
    Date(const Date& d)
    {
        _year = d._year;
    	_month = d._month;
    	_day = d._day;
    }
        
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    int main()
    {
        Date d1;
        Date d2(d1);
        return 0;
    }
    

    Date d2(d1) 等价于 Date d2 = d1

    为什么要用引用的方式传参?

    如果我们把拷贝构造函数的传参方式写为:

    Date(Date d)
    {
        _year = d._year;
    	_month = d._month;
    	_day = d._day;
    }
    

    则会引发无穷的递归:

    形参d是实参d1的一份拷贝,所以,它也要调用拷贝构造函数:

    image-20210906230704076

    所以参数的传递要改成引用的形式

    当然,最好在形参的类型前加上const,防止使用时失误修改了形参,进而修改了在外面的实参。

  3. 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷
    贝,这种拷贝我们叫做浅拷贝,或者值拷贝

    class Date
    {
        
    public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
    	_year = year;
    	_month = month;
    	_day = day;
    }
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    int main()
    {
    	Date d1;
       	// 这里d2调用的编译器的默认拷贝构造完成拷贝,d2和d1的值也是一样的。
    	Date d2(d1);
    	return 0;
    }
    
  4. 那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像
    日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

    // 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
    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);
    }
    

    结果是:

    image-20210906233427172

    为什么会出现这样的结果呢?

    因为浅拷贝实际上就是执行了类似 String s2 = s1 的指令,所以s1将自己的值,也就是"hello\0"的首字符地址赋给了s2,s2也指向了这块空间,指向这块空间没有错,错的是执行析构函数时,对同一块空间free了两次,导致了错误。

    如果我们把析构函数中的free删除,结果就不会报错了

    image-20210906234029165

这么写虽然不会报错,但这个做法并不好,析构函数就是为了节省资源,这么写就达不到我们想要的目的了,所以在特殊情况下,还是要自己写拷贝构造函数。

运算符重载

运算符重载

自定义类型是不能用运算符的,要用就得实现重载函数,自定义类型用的时候等价于调用这个重载函数 。

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字及参数列表,其返回值类型与参数列表与普通的函数类似。函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意 :

  • 不能通过连接其他符号来创建新的操作符:比如operator@

  • 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义

  • 作为类成员的重载函数时,其形参看起来比操作数数目少1成员,函数的操作符有一个默认的形参this,限定为第一个形参。

  • ..*::sizeof?: 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。 重载操作符必须有一个类类型或者枚举类型的操作数

// 全局的operator==
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
	_year = year;
	_month = month;
	_day = day;
}
//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;
}

void Test ()
{
	Date d1(2018, 9, 26);
	Date d2(2018, 9, 27);
	cout<<(d1 == d2)<<endl;
}

d1 == d2就相当于operator==(d1, d2)

但为了保证我们成员变量的私有性,不能以这种全局形式的函数比较成员变量。

下面就是将运算符重载函数放在类里面,这样就保护了我们成员变量的私有性。

class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
	_year = year;
	_month = month;
	_day = day;
}
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this指向的调用函数的对象
bool operator==(const Date& d2)
{
    return _year == d2._year;
	&& _month == d2._month
	&& _day == d2._day;
}
private:
	int _year;
	int _month;
	int _day;
};
void Test ()
{
	Date d1(2018, 9, 26);
	Date d2(2018, 9, 27);
	cout<<(d1 == d2)<<endl;
}

关于这一段函数:

bool operator==(const Date& d2)
{
    return _year == d2._year;
	&& _month == d2._month
	&& _day == d2._day;
}

函数的参数,bool operator==(const Date& d2) 就相当于 bool operator==(const Data* this, const Date& d2)

d1 == d2 就相当于 operator==(&d1, d2)

赋值运算符重载

class Date
{
public:
    
    //构造函数
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

        //拷贝构造函数
    Date (const Date& d)
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

        //赋值运算符重载
    Date& operator=(const Date& d)
    {
        if(this != &d)//如果失误写成对自身赋值,就可以加个判断省去赋值过程
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }

    }
    
private:
	int _year ;
	int _month ;
	int _day ;
};

赋值运算符主要有四点:

  1. 参数类型

  2. 返回值

  3. 检测是否自己给自己赋值

  4. 返回 * this

    在类的函数里就已经可以完成赋值了,为什么要返回* this呢?

    我们先看赋值运算符=,可以实现a = b = c的连续赋值,连续赋值的原理是,b=c会有一个返回值,大小为b,所以a = b,可以将这个返回值b赋给a。

    既然这样,我们返回类型是写成Date还是Date&呢?

    最好是写成Date&,这样相比写成Date可以少一次拷贝调用,之前就说到过,函数生成的返回值是一个临时拷贝的常变量,所以实际上会先进行一次拷贝构造,再将拷贝构造的结果给返回。

    而如果是引用返回,就不用进行拷贝构造,因为创造的临时变量并不会开辟空间,只是返回值的一个别名。

  5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝

class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    
private:
int _year;
int _month;
int _day;
};

int main()
{
    Date d1;
    Date d2(2018101);

    // 这里d1调用的编译器生成operator=完成拷贝,d2和d1的值也是一样的。
    d1 = d2;
    return 0;
}

Date(d4) = d1 (d4未创建)是运算符赋值重载还是拷贝构造呢?

答案是:拷贝构造。正因为d4未创建,所以要用拷贝构造来实现这个赋值功能。所以Date(d4) = d1就等价于Date d4(d1)。

那么编译器生成的默认赋值重载函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像
日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
class String
{
public:
    
    String(const char* str = "")
    {
        _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("world");
    s1 = s2;
}

与前面编译器自己生成的拷贝构造函数一样,s1与s2指向的是同一块空间,所以调用析构函数时会导致free同一块空间两次,引发错误。

我们再举一个栈的例子,栈的功能没有实现完全,只是为了体现编译器自己生成的赋值运算符重载的缺点。

这里我们没有创建operator=,s2 = s1使用的是编译自己生成的赋值运算符重载,通过调试我们看到s2的_a和s1的 _a指向的空间是一样的

  • 这样的话,在栈的Push、Pop等操作时,s1和s2的数据是存放在一块空间的,会造成混乱。
  • 并且在调用析构时也会free同一块空间两次

image-20210907154924729

因此,对于像Stack、String等这些数据结构的类,构造函数、析构函数、拷贝构造、赋值重载都需要我们自己写。默认生成的拷贝构造和赋值重载都会有问题

而像Date类这种不牵扯到采用数据结构存放数据的类,我们只要写构造函数就行(析构没什么意义,不用释放空间),其它的使用编译器默认生成的就可以。

日期类的实现

class Date
{
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month)
{
static int days[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int day = days[month];
if (month == 2
&&((year % 4 == 0 && year % 100 != 0) || (year%400 == 0)))
{
day += 1;
}
return day;
}
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1);
// 拷贝构造函数
// d2(d1)
Date(const Date& d);
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d);
// 析构函数
~Date();
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day);
// 日期-天数
Date operator-(int day);
    // 日期-=天数
Date& operator-=(int day);
// 前置++
Date& operator++();
// 后置++
Date operator++(int);
// 后置--
Date operator--(int);
// 前置--
Date& operator--();
// >运算符重载
bool operator>(const Date& d);
// ==运算符重载
bool operator==(const Date& d);
// >=运算符重载
inline bool operator >= (const Date& d);
// <运算符重载
bool operator < (const Date& d);
// <=运算符重载
bool operator <= (const Date& d);
// !=运算符重载
bool operator != (const Date& d);
// 日期-日期 返回天数
int operator-(const Date& d);
private:
int _year;
int _month;
int _day;
};

有几个注意的地方:

  1. +=、-=这样的操作符是对this指针指向的Date类本身进行操作,所以返回类型是引用,可以节省一个拷贝构造的开销。

    而+、-则是对另一个Date类进行操作,所以要先拷贝一份this指针指向的Date,再对拷贝的Date进行操作并返回它,返回类型是Date,而不是引用,因为函数调用完拷贝的Date就销毁了,使用引用会造成内存的非法访问。

  2. +、-的day小于0时,意味着执行-或+,所以要进行判断day是否小于0,如果小于,则调用对应的+或-的重载函数(代码中没有实现)。下面以operator+=为例:

    Date& operator+=(int day)//Date& operator+=(Date* this, int day)
    	{
        	if(day < 0)//判断day是否小于0,小于的话就z
            {
                return *this -= -day;
            }
        
    		_day += day;
    
    		//获取对应年月的最大天数
    		int Monthday = GetMonthdays(_year, _month);
    		//如果相加后的天数大于最大天数,对应的月份或者年份需要进位
    		while (_day > Monthday)
    		{
    			//月份进位
    			++_month;
    			_day -= Monthday;
    
    			//年份进位
    			if (_month > 12)
    			{
    				++_year;
    				_month = 1;
    			}
    			Monthday = GetMonthdays(_year, _month);
    		}
    
    		return *this;
    	}
    

具体实现如下:

//实现一个日期类

class Date {
public:

	//获取每个月的天数
	int GetMonthdays(int year, int month)
	{
		if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))
			return 29;

		//把0空出来
		int months[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };

		return months[month];
	}


	//默认构造函数
	Date(int year = 0, int month = 1, int day = 1)
	{
		//筛选非法日期
		if (year > 0 && month > 0 && month <= 12 && day > 0 && day <= GetMonthdays(year, month))
		{
			
			_year = year;
			_month = month;
			_day = day;
		}
		else
		{
			cout << "非法日期" << endl;
		}
		
		
	}

	//==操作符重载
	inline bool operator==(const Date& d2)
	{
		return _year == d2._year && _month == d2._month && _day == d2._day;
	}

	//<操作符重载
	inline bool operator<(const Date& d2)//operator<(Date* 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;

		return false;
	}

	//<=操作符重载方法一
	/*bool operator<=(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;

		return false;
	}*/

	//<=操作符重载方法e
	bool operator<=(const Date& d2)//operator<=(Date* this, const Date& d2)
	{
		return (*this) == d2 || (*this) < d2;
	}

	//>操作符重载
	bool operator>(const Date& d2)//operator>(Date* this, const Date& d2)
	{
		return !((*this) <= d2);
	}

	//>=操作符重载
	bool operator>=(const Date& d2)//operator>=(Date* this, const Date& d2)
	{
		return !((*this) < d2);
	}

	//!=操作符重载
	bool operator!=(const Date& d2)//operator!=(Date* this, const Date& d2)
	{
		return !((*this) == d2);
	}

	//+操作符重载
	Date operator+(int day)//Date operator+(Date* this, int day)
	{
		//不能修改this指向的Date,返回的是一个新的Date
		Date ret(*this);
		ret += day;//复用+=代码
		
		return ret;
	}

	//+=操作符重载,对this指针进行操作,返回类型就为引用
	Date& operator+=(int day)//Date& operator+=(Date* this, int day)
	{
		if (day < 0)
		{

			return *this -= -day;
		}
		_day += day;

		//获取对应年月的最大天数
		int Monthday = GetMonthdays(_year, _month);
		//如果相加后的天数大于最大天数,对应的月份或者年份需要进位
		while (_day > Monthday)
		{
			//月份进位
			++_month;
			_day -= Monthday;

			//年份进位
			if (_month > 12)
			{
				++_year;
				_month = 1;
			}
			Monthday = GetMonthdays(_year, _month);
		}

		return *this;
	}

	//-操作符重载
	Date operator-(int day)//Date operator-(Date* this, int day)
	{
		Date ret(*this);
		ret -= day;//复用-=代码
		
		return ret;
	}

	//-=操作符重载,对this指针进行操作,返回类型就为引用
	Date& operator-=(int day)//Date operator-=(Date* this, int day)
	{

		_day -= day;

		while (_day <= 0)//小于0就向月份借位
		{
			--_month;
			if (_month <= 0)//月份<1时向年份借位
			{
				--_year;
				_month = 12;
			}
			_day += GetMonthdays(_year, _month);
		}

		return *this;
	}

	//前置++操作符重载,对this指针进行操作,返回类型就为引用
	Date& operator++()//Date& operator++(Date* this)
	{
		*this += 1;//复用+=代码

		return *this;
	}

	//前置--操作符重载,对this指针进行操作,返回类型就为引用
	Date& operator--()//Date& operator--(Date* this)
	{
		*this -= 1;//复用-=代码

		return *this;
	}

	//后置++操作符重载
	Date& operator++(int)//强行加一个int参数构成重载。可不传实参
	{
		//返回的是++前的数据
		Date ret(*this);
		*this += 1;//复用+=代码

		return ret;
	}

	//后置--操作符重载
	Date& operator--(int)//强行加一个int参数构成重载。可不传实参
	{
		//返回的是--前的数据
		Date ret(*this);
		*this -= 1;//复用+=代码

		return ret;
	}

	//日期减日期
    //让小的日期一天一天的加到等于大的日期,同时对加的次数进行计数,计数的值就是两者的差值
	int operator-(const Date& d)
	{
        //flag用来判断结果的正u
		int flag = 1;
		Date max = *this;
		Date min = d;

		if (min > max)
		{
			max = d;
			min = *this;
			flag = -1;
		}

		int count = 0;
		while (min != max)
		{
			++min;
			++count;
		}

		return flag * count;
	}

	//拷贝构造函数
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}


private:
	int _year;
	int _month;
	int _day;
};

void Test()
{
	Date d1(2021, 9, 7);
	d1.Print();

	Date d2(2021, 9, 6);
	d2.Print();

	Date d3(2020, 2, 29);
	d3.Print();

	int a = d1 - d2;
	int b = d2 - d1;
	cout << a << endl;
	cout << b << endl;
	/*d1.Print();*/
	
    //测试
	/*d1--;
	d1.Print();

	++d1;
	d1.Print();

	d2++;
	d2.Print();

	--d2;
	d2.Print();

	d2 = d2 + 100;
	d2.Print();

	d1 += 10000;
	d1.Print();

	d1 -= 10000;
	d1.Print();*/
	//cout << (d1 == d2) << endl;
	//cout << (d1 > d2) << endl;
	//cout << (d1 >= d2) << endl;
	//cout << (d1 < d2) << endl;
	//cout << (d1 <= d2) << endl;
	//cout << (d1 != d2) << endl;
}

int main()
{
	Test();

	return 0;
}

const成员

const修饰类的成员函数

将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this
指针,表明在该成员函数中不能对类的任何成员进行修改。

image-20210906225212653

什么时候给成员函数加const呢?只要成员函数中不需要修改成员变量最好都加上const

我们来看看下面的代码

class Date
{
public :
	void Display ()
	{
		cout<<"Display ()" <<endl;
		cout<<"year:" <<_year<< endl;
		cout<<"month:" <<_month<< endl;
		cout<<"day:" <<_day<< endl<<endl ;
	}
	void Display () const
	{
		cout<<"Display () const" <<endl;
		cout<<"year:" <<_year<< endl;
		cout<<"month:" <<_month<< endl;
		cout<<"day:" <<_day<< endl<<endl;
	}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
void Test ()
{
	Date d1 ;
	d1.Display ();
	const Date d2;
	d2.Display ();
}

d1既可以调用void Display ()又可以调用void Display ()const,而d2只能调用void Display ()const

请思考下面的几个问题 :

  1. const对象可以调用非const成员函数吗?不行,权限被放大了
  2. 非const对象可以调用const成员函数吗?可以,权限缩小是允许的
  3. const成员函数内可以调用其它的非const成员函数吗?不行,权限被放大了
  4. 非const成员函数内可以调用其它的const成员函数吗? 可以,权限缩小是允许的

关于3.4两个问题,我们举一个例子:

class Person {
public :
	Person(const char* name = "Jack", int age = 10)
	{
		_name = name;
		_age = age;
	}    
    
	void f1()//等价于void f1(Person* this)
	{
		f2();
	}

	void f2()const//等价于void f2(const Person* this)
	{

	}

	void f3()const//等价于void f3(const Person* this)
	{
		f4();
	}

	void f4()//等价于void f4(Person* this)
	{

	}


private:
	const char* _name;
	int _age;
};

f1没有const修饰,调用有const修饰的f2,由可读和可写的权限变成了只可读,是可以的

f3有const修饰,调用没有const修饰的f4,由只可读的权限变成了可读和可写,是不允许的

我们看编译器的报错

image-20210908121116626

取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成

class Date
{
public :
    
Date(int year = 0, int month = 1, int day = 1)
{	
	_year = year;
    _month = month;
    _day = day;
}
    
Date* operator&()
{
return this ;
}
    
const Date* operator&()const//地址不被修改
{
return this ;
}
    
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};


这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比
如想让别人获取到指定的内容!

当我们不创建operator&的重载函数时,编译器会调用自己生成的默认取地址的重载函数:

image-20210908121739600

当我们创建了后,结果也是一样的,地址不一样是因为每次函数栈帧的开辟位置都是随机的

image-20210908121811768

创不创建结果都是一样的,除非我们不想让别人知道对象的地址,就可以在operator&的内部返回一个nullptr

const Date* operator&()const//地址不被修改
{
	return nullptr ;
}
  • 9
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WoLannnnn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值