• 类和对象(中)【类的6个默认成员函数】 【零散知识点】 (万字)


    类和对象(中)

    1.类的6个默认成员函数

    如果一个类中什么成员都没有,简称为空类。

    空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。

    默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

    image-20240526225447180

    2.构造函数

    2.1概念

    对于以下Date类

    # include
    using namespace std;
    
    class Date
    {
    public:
    	void Init(int year, int month, int day)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    	void Print() // void Print(Date* this)  编译后
    	{
    		cout << _year << "-" << _month << "-" << _day << endl;
    
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    
    int main()
    {
    	Date d1,d2;
    	d1.Init(2024, 5, 26);// d1.Init(&d1, 2024, 5, 26); // 编译后
    	d2.Init(2077, 5, 26);// d2.Init(&d2, 2024, 5, 26);
    	d1.Print(); // 2024-5-26 
    	// d1.Print(&d1)
    	d2.Print(); // 2077-5-26 
    	// d2.Print(&d2);
    
    
    	return 0;
    }
    

    对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置

    信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?

    构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次

    2.2特性

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

    其特征如下:

    1. 函数名与类名相同。

    2. 无返回值。

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

    4. 构造函数可以重载。

    我们来看例子:

    # include
    using namespace std;
    
    class Date
    {
    public:
    	// 构造函数——在对象构建时调用的函数,这个函数完成初始化工作
    	// [要注意这个函数只完成实例对象的初始化,不参与对象的构造,那是编译器的工作]
    	Date(int year, int month, int day) // 构造函数的名和类名相同
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}// 构造函数不要返回值
    
    	// 构造函数也支持重载
    	Date() // 无参构造函数
    	{
    		_year = 0;
    		_month = 0;
    		_day = 0;
    	}
    
    	// 这个函数按需使用
    	void Init(int year, int month, int day)// void Init(Date* this, int year, int month, int day)
    	{
    		_year = year; // 如果我们设置的成员变量名称不加_或者m 这里就不好理解
    		_month = month;
    		_day = day;
    	}
    
    	void Print() // void Print(Date* this)  编译后
    	{
    		cout << _year << "-" << _month << "-" << _day << endl;
    		// cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    
    int main()
    {
    	Date d1(2024, 5, 26); // 直接给实例对象传参,由自动调用的构造函数完成初始化。
    	Date d2(2077, 5, 26);
    	Date d3; // 通过函数重载实现,调用无参构造函数
        // 注意这里不能Date d3();
    
    	d1.Print(); // 2024-5-26 
    	// d1.Print(&d1);
    
    	d2.Print(); // 2077-5-26 
    	// d2.Print(&d2);// 是通过隐含的this指针实现的
    	
    	d3.Print(); // 0 - 0 - 0
    
    
    	return 0;
    }
    
    1. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
    // 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
    # include
    using namespace std;
    
    class Time
    {
    public:
    	Time()
    	{
    		_hour = 0;
    		_min = 0;
    		_second = 0;
    
    		cout << "Time()" << endl;
    	}
    
    private:
    	int _hour;
    	int _min;
    	int _second;
    };
    
    class Date
    {
    public:
    	// 这里我们没有显示定义构造函数,编译器会生成无参默认构造函数
    
    	// 一旦用户定义了显式构造函数。那么编译器将不会生成无参构造函数
    	// 即使只定义了带参构造函数,编译器也不会生成无参构造函数
    	// 想要无参构造函数,要自己再定义一个
    	//Date(int year, int month, int day) 
    	//{
    	//	_year = year;
    	//	_month = month;
    	//	_day = day;
    	//}
    
    	void Init(int year, int month, int day)
    	{
    		_year = year; 
    		_month = month;
    		_day = day;
    	}
    
    	void Print() // void Print(Date* this)  编译后
    	{
    		cout << _year << "-" << _month << "-" << _day << endl;
    		// cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    
    	Time _t;
    };
    
    
    int main()
    {
    	Date d1; // 调用编译器生成的无参默认构造函数,
    	d1.Print();// -858993460--858993460--858993460
    	// 我们发现,即使d1调用了编译器的默认构造函数,打印出来还是随机值,看起来这个构造函数什么都没做一样
    	// 但是实际上,这个无参默认构造函数是有做事情的
    	// 我们给Date一个自定义的成员变量,_t
    	// d1.Print();的结果如下:
    	//Time()
    	//-858993460--858993460--858993460
    	// 这个说明了一个现象
    	// 默认生成的无参构造函数(语法坑:双标)
    	// 1. 针对内置类型的成员变量不会做处理
    	// 2. 针对自定义类型的成员变量,调用它的构造函数初始化
    
    	return 0;
    }
    
    1. 关于编译器生成的默认成员函数,很多人会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??

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

    总结:

    默认生成的无参构造函数语法坑:双标

    • 针对内置类型的成员变量不会做处理

    • 针对自定义类型的成员变量,调用它的构造函数初始化

    • 一旦用户定义了显式构造函数。那么编译器将不会生成无参构造函数
      即使只定义了带参构造函数,编译器也不会生成无参构造函数
      想要无参构造函数,要自己再定义一个

    注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值

    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 = 1970;
    	 int _month = 1;
    	 int _day = 1;
    	 // 自定义类型
    	 Time _t;
    };
    int main()
    {
     	Date d;
    	return 0;
    }
    

    但是这样也不够好,我们可以用之前学习的缺省参数——全缺省参数

    1. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数
    # include
    using namespace std;
    class Date
    {
    public:
    
    	 带参构造函数
    	//Date(int year, int month, int day) 
    	//{
    	//	_year = year;
    	//	_month = month;
    	//	_day = day;
    	//}// 构造函数不能有返回值
    
    	 构造函数也支持重载
    	//Date() // 无参构造函数
    	//{
    	//	_year = 0;
    	//	_month = 0;
    	//	_day = 0;
    	//}
    
    	// 更好的方式
    	// 构造函数——全缺省
    	Date(int year = 0, int month = 0, int day = 0)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    
    	 注意,有了全缺省的构造函数,不能再有无参构造函数,编译的时候会产生歧义
    	//Date() // 无参构造函数
    	//{
    	//	_year = 0;
    	//	_month = 0;
    	//	_day = 0;
    	//}
    
    	void Init(int year, int month, int day)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    
    	void Print() // void Print(Date* this)  编译后
    	{
    		cout << _year << "-" << _month << "-" << _day << endl;
    		// cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    
    };
    
    
    int main()
    {
    	Date d1; // 调用默认构造函数
    	// 1. 自己实现的无参构造函数
    	// 2. 自己实现的全缺省构造函数
    	// 3. 编译器自动生成的默认构造函数
    	// 这三个的特点都是无参,不传参数
    	// 因此这三个只能存在一个
    
    	Date d2(2024, 5, 27);
    
    	d1.Print();// 0-0-0
    	d2.Print();// 2024-5-27
    
    	return 0;
    }
    

    总结:
    默认构造函数一共有三种:

    • 自己实现的无参构造函数
    • 自己实现的全缺省构造函数
    • 编译器自动生成的默认构造函数

    这三种默认构造函数只能存在一种

    3.析构函数

    3.1概念

    通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?

    析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

    3.2特性

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

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

    2. 无参数无返回值类型。

    3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载

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

    来看一段代码:

    // 析构函数——在实例对象的生命周期结束的时候,会自动调用析构函数
    # include
    using namespace std;
    class Date
    {
    public:
    
    	// 构造函数——全缺省  [一种默认构造函数]
    	Date(int year = 0, int month = 0, int day = 0)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    
    	void Print() // void Print(Date* this)  编译后
    	{
    		cout << _year << "-" << _month << "-" << _day << endl;
    	}
    
    	~Date()
    	{
    		cout << "析构函数" << endl;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    
    };
    
    
    int main()
    {
    	// 析构函数,会在这两个对象生命周期结束之后自动调用,完成清理工作,不是完成对其的销毁
    	Date d1; // 调用默认构造函数
    	Date d2(2024, 5, 27);
    
    	// 执行代码,会执行两次析构函数
    	//析构函数
    	//析构函数
        //注意这个析构,先析构d2 在析构d1,因为是在栈区的变量
    	return 0;
    }
    

    在上述代码中要注意:析构,先析构d2 在析构d1,因为是在栈区的变量

    而有了析构函数和构造函数的存在,我们来感受一下它们的使用。

    这里我们来写一个Stack类,这个Stack类,不在需要初始化和销毁的接口,由构造函数和析构函数代替了。

    // 实现Stack类——使用构造和析构函数
    # include
    using namespace std;
    
    
    class Stack
    {
    public:
    	// 构造函数
    	Stack(int n = 10)
    	{
    		_a = (int*)malloc(sizeof(int) * n);
    		if (_a == NULL)
    		{
    			perror("Stack()malloc()");
    			exit(-1);
    		}
    		cout << "malloc:" << _a << endl; 
    		_size = 0;
    		_capacity = n;
    	}
    
    	// ....... 栈的接口
    
    	// 析构函数
    	~Stack()
    	{
    		free(_a);
    		cout << "free:" << _a << endl;
    		_a = NULL;
    		_size = _capacity = 0;
    	}
    
    private:
    	int* _a;
    	int _size;
    	int _capacity;
    };
    
    
    int main()
    {
    	Stack s1; 
    	Stack s2; 
    	// 注意先析构s2 在析构s1。因为两个局部变量在栈区上 ,后进先出
    	//malloc:000001FC0C62BEF0
    	//malloc : 000001FC0C636160
    	//free : 000001FC0C636160
    	//free : 000001FC0C62BEF0
    	return 0;
    }
    
    1. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
    # include
    using namespace std;
    
    class Time
    {
    public:
     ~Time()
     {
     	cout << "~Time()" << endl;
     }
    private:
     	int _hour;
     	int _minute;
     	int _second;
    };
    
    class Date
    {
    private:
       // 基本类型(内置类型)
       int _year = 1970;
       int _month = 1;
       int _day = 1;
       // 自定义类型
       Time _t;
    };
    int main()
    {
       Date d;
       return 0;
    }
    // 程序运行结束后输出:~Time()
    // 在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
    // 因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:
    // main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁
    
    // main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数
    // 注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
    

    总结:

    默认生成的析构函数(语法坑:双标

    • 针对内置类型的成员变量不会做处理

    • 针对自定义类型的成员变量,调用它的析构函数

    • 一旦用户定义了显式析构函数。那么编译器将不会生成默认析构函数

    1. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。

    因为默认析构函数,对内置类型的指针不会做处理,那假设这个指针指向堆区的内存空间的话,就会造成内存泄漏

    4.拷贝构造函数

    4.1概念

    在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎

    那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?

    拷贝构造函数只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用

    4.2特征

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

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

    2. 拷贝构造函数的参数只有一个必须是类 类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

    我们来看一个例子:

    // 拷贝构造函数
    # include
    using namespace std;
    
    class Date
    {
    public:
    
    	// 构造函数
    	Date(int year = 0, int month = 0, int day = 0)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    
    	// 这样无法编译通过,因为这里会造成递归拷贝,会无穷递归下去
    	Date(Date d) // 构造函数的重载函数
    	{
    		_year = d._year;
    		_month = d._month;
    		_day = d._day;
    	}
    
    	// 析构函数
    	~Date()
    	{
    		cout << "析构函数" << endl;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    
    };
    
    
    int main()
    {
    	Date d1(2024, 5, 27);
    	// 如果我们想创建一个跟d1一模一样的对象,我们可以怎么做呢?
    	//Date d2(2024, 5, 27);// 这样可以,但是肯定不好,因为d1一变,我们还要手动更改d2
    	// 这个时候我们就可以用到拷贝构造
    	Date d2(d1); // 传d1进去,// 这里会报错
    
    	return 0;
    }
    

    为什么会造成无穷递归呢?

    	// 这样无法编译通过,因为这里会造成递归拷贝,会无穷递归下去
    	Date(Date d) // 构造函数的重载函数
    	{
    		_year = d._year;
    		_month = d._month;
    		_day = d._day;
    	}
    int main()
    {
    	// Date d2(d1);
    }
    

    因为我们这里是传值给拷贝构造函数,我们知道,传值调用的形参就是实参的一个临时拷贝,既然是拷贝,我们就要创建一个跟实参一个类型的值,那实参是什么类型的值呢,是Date,相当于要把实参d1 拷贝给 形参d,那d的构造又需要调用拷贝构造函数,那又要传参,又要拷贝实参,又要构造形参,又要调用拷贝构造函数。无限循环下去。

    如图所示:

    image-20240527152533769

    解决方法是什么呢?

    其实就是让形参是实参的别名就好了。

    image-20240527162520756

    // 拷贝构造函数
    # include
    using namespace std;
    
    class Date
    {
    public:
    
    	// 构造函数
    	Date(int year = 0, int month = 0, int day = 0)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    
    	 这样无法编译通过,因为这里会造成递归拷贝,会无穷递归下去
    	//Date(Date d)
    	//{
    	//	_year = d._year;
    	//	_month = d._month;
    	//	_day = d._day;
    	//}
    
    	// 我们使用别名来解决这个问题
    	Date(const Date& d) // 传参的过程相当于 Date& d = d1;
    	{
    		_year = d._year;
    		_month = d._month;
    		_day = d._day;
    	}
    
    
    	void Print()
    	{
    		cout << _year << "-" << _month << "-" << _day << endl;
    	}
    
    	// 析构函数
    	~Date()
    	{
    		cout << "析构函数" << endl;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    
    };
    
    
    int main()
    {
    	Date d1(2024, 5, 27);
    	// 如果我们想创建一个跟d1一模一样的对象,我们可以怎么做呢?
    	//Date d2(2024, 5, 27);// 这样可以,但是肯定不好,因为d1一变,我们还要手动更改d2
    	// 这个时候我们就可以用到拷贝构造
    	Date d2(d1); // 传d1进去
    	Date d3 = d1; // 这个也是拷贝构造。
    
    	d2.Print();
    	d3.Print();
    
    	return 0;
    }
    

    上面这个代码的运行结果

    image-20240527164231057

    1. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
    class Time
    {
    public:
    	Time()
    	{
    		_hour = 1;
    		_minute = 1;
    		_second = 1;
    	}
    	Time(const Time& t)
    	{
    		_hour = t._hour;
    		_minute = t._minute;
    		_second = t._second;
    		cout << "Time::Time(const Time&)" << endl;
    	}
    private:
    	int _hour;
    	int _minute;
    	int _second;
    };
    class Date
    {
    private:
    	// 基本类型(内置类型)
    	int _year = 2024;
    	int _month = 5;
    	int _day = 28;
    	// 自定义类型
    	Time _t;
    };
    
    int main()
    {
    	Date d1;
    
    	// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
    	// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
    	Date d2(d1);
    	return 0;
    }
    
    1. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
    // 涉及内存资源管理的Stack的拷贝构造
    # include
    using namespace std;
    
    class Stack
    {
    public:
    	// 构造函数
    	Stack(int n = 10)
    	{
    		_a = (int*)malloc(sizeof(int) * n);
    		if (_a == NULL)
    		{
    			perror("Stack()malloc()");
    			exit(-1);
    		}
    		cout << "malloc:" << _a << endl;
    		_size = 0;
    		_capacity = n;
    	}
    
    	// ....... 栈的接口
    
    	// 析构函数
    	~Stack()
    	{
    		free(_a);
    		cout << "free:" << _a << endl;
    		_a = NULL;
    		_size = _capacity = 0;
    	}
    
    private:
    	int* _a;
    	int _size;
    	int _capacity;
    };
    
    int main()
    {
    	// 浅拷贝问题
    	Stack s1;
    	Stack s2(s1);
    
    	return 0;
    }
    

    上面我们的Stack类中,没有显式实现的拷贝构造,因此我们在main函数中的拷贝构造 的使用,用的都是编译器生成的默认的函数。这里用的是浅拷贝。因此造成了c++中比较经典的一个问题——浅拷贝问题

    我们来分析一下代码为何会崩溃。

    1. 首先就是我们在调用拷贝构造的时候,我们是将s1对象一个一个字节拷贝到s2中的,这就意味着我们两个对象所存储的数组指针_a是一样的,也就是说指向的数组都是同一个空间的数组
    2. 这个时候我们的代码还不会崩溃,并且也确实成功完成了拷贝的任务

    image-20240529004507107

    1. 但是问题会出现在析构函数上,我们知道在析构函数中,我们要完成对栈这个类对象的资源清理工作,我们要在析构函数中释放掉数组的空间,由于s1 s2存储在栈区,我们先释放掉s2的数组空间,但是紧接着我们要释放掉s1的数组空间,但是s1和s2指向的数组是同一个数组,这就会造成会同一个空间的重复释放,这就会导致代码崩溃

    image-20240529005943051

    image-20240529005527934

    因此对于这种涉及到内存资源管理的类,我们需要自己实现深拷贝的拷贝构造

    但是这里有人会说,那我不自己实现析构函数不就不会报错了吗?

    因为我们的析构函数中存在对数组空间的释放,但是不释放就会造成内存泄漏的问题,释放了就会造成浅拷贝问题。

    1. 拷贝构造函数典型调用场景:
    • 使用已存在对象创建新对象
    • 函数参数类型为类类型对象
    • 函数返回值类型为类类型对象
    class Date
    {
    public:
    	Date(int year, int minute, int day)
    	{
    		cout << "Date(int,int,int):" << this << endl;
    	}
    	Date(const Date& d)
    	{
    		cout << "Date(const Date& d):" << this << endl;
    	}
    	~Date()
    	{
    		cout << "~Date():" << this << endl;
    	}
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    Date Test(Date d) // 调用拷贝构造函数
    {
    	Date temp(d);
    	return temp; // 调用拷贝构造函数
    }
    int main()
    {
    	Date d1(2022, 1, 13); // 调用拷贝构造函数
    	Test(d1);
    	return 0;
    }
    

    5.赋值运算符重载

    5.1运算符重载

    为什么要搞运算符重载呢?

    其实很简单,就是因为内置类型,我们在使用运算符的时候。编译器知道怎么去比较,但是如果我们自己定义了一个类,在用内置的运算符,编译器就不知道怎么比较了,因此,为了能够方便的比较我们自己定义的类对象,我们需要对运算符重载

    在之前我们是通过自定义函数,来解决这个问题的,但是这个解决方式也没有运算符重载好。因为可读性不是很好。

    C++为了增强代码的可读性引入了运算符重载运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

    函数名字为:关键字operator后面接需要重载的运算符符号

    函数原型:返回值类型 operator操作符(参数列表)

    注意:

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

    • 重载操作符必须有一个类类型参数

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

    • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this指针形参

    • .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。注意是 **.**不能重载,不是 (解引用)

    我们来看一段代码:

    // 运算符重载
    # include
    using namespace std;
    
    class Date
    {
    public:
    
    	// 默认构造函数
    	Date(int year = 0, int month = 0, int day = 0)
    	{
    		_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;
    
    };
    
    
    // 运算符有几个操作数, operator重载的函数就有几个参数
    bool operator==(const Date& d1, const Date& d2)
    {
    	return d1._year == d2._year
    		&& d1._month == d2._month
    		&& d1._day == d2._day;
    }
    
    
    int main()
    {
    	Date d1(2024, 5, 27);
    	Date d2(d1);
    
    	// 如果我们相对Date类的 对象 d1 d2 进行比较
    	// 我们可以通过定义函数实现
    	//IsDateEqual(d1, d2); // 可读性不好
    	// 为了让可读性更好,c++推出了运算符重载
    	d1 == d2;// 编译过后会变成, operator==(d1, d2)
    	// 如果不对== 进行重载,那么这里就会编译出错
    	operator==(d1, d2); // 等价d1 == d2 ,但是不推荐这样写,这样可读性又下降了
    
    	if (d1 == d2)
    		cout << "相等" << endl;
    
    	return 0;
    }
    

    上述的写法不好,因为我们为了实现运算符重载,我们牺牲了Date类中成员变量的私有性。把成员变量变成公有的,才能让运算符重载函数访问到其成员变量。

    要如何解决这个问题呢?

    这里其实可以用友元解决,这个后面学习

    我们还可以通过把运算符重载函数写进类中来解决

    # include
    using namespace std;
    
    class Date
    {
    public:
    
    	// 默认构造函数
    	Date(int year = 0, int month = 0, int day = 0)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    
    	Date(const Date& d) // 拷贝构造函数
    	{
    		_year = d._year;
    		_month = d._month;
    		_day = d._day;
    	}
    
    	// 运算符重载
    	// 我们知道成员函数会自带this指针形参,因此这里要修改函数的形参个数。
    	// 我们知道谁调用这个运算符重载函数,那么this指针就指向谁
    	// d1 == d2;
    	// d1.operator==(d2);
    	bool operator==(const Date& d) // bool operator==(Date* this, const Date& d)
    	{
    		return _year == d._year
    			&& _month == d._month
    			&& _day == d._day;
    	}
    
    	// d1 > d2
    	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;
    		}
    		else
    			return false;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    
    };
    
    int main()
    {
    	Date d1(2024, 5, 27);
    	Date d2(d1);
    
    	d1 == d2; // 编译后变成  d1.operator==(d2)
    
    	if (d1 > d2)
    		cout << "大于" << endl;
    	else
    		cout << "其他" << endl;
    
    	return 0;
    }
    

    5.2赋值运算符重载

    1. 赋值运算符重载格式
    • 参数类型:const T&,传递引用可以提高传参效率
    • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
    • 检测是否自己给自己赋值
    • 返回*this :要复合连续赋值的含义

    来看代码:

    // 赋值运算符重载
    # include
    using namespace std;
    
    class Date
    {
    public:
    
    	// 默认构造函数
    	Date(int year = 0, int month = 0, int day = 0)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    
    	// 拷贝构造函数
    	Date(const Date& d) 
    	{
    		_year = d._year;
    		_month = d._month;
    		_day = d._day;
    	}
    
    	// 运算符重载
    	// d1 == d2 -> d1.operator==(d2);
    	bool operator==(const Date& d) // bool operator==(Date* this, const Date& d)
    	{
    		return _year == d._year
    			&& _month == d._month
    			&& _day == d._day;
    	}
    
    	// 赋值运算符重载
    	//d2 = d1  -> d2.operator(&d2, d1)
    	Date& operator=(const Date& d) // 其实这里不用引用传参也不会报错,但是使用引用传参效率更高
    	// void operator(Date* this, const Date d)
    	{
    		// 自己给自己赋值是没有意义的
    		if (this == &d) // d是引用,d的地址和this相同就说明是自己给自己赋值
    			return *this; 
    
    		_year = d._year;
    		_month = d._month;
    		_day = d._day;
    
    		return *this;
    	}
    
    	void Print()
    	{
    		cout << _year << "-" << _month << "-" << _day << endl;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    
    };
    
    
    int main()
    {
    	Date d1(2024, 5, 27);
    	Date d2 = d1; //注意这里这个=不是赋值运算符,而是拷贝构造
    
    	Date d3;
    	d3 = d1;
    	d3.Print();// 2024-5-27
    
    	d3 = d3; // 自己赋值给自己
    
    	//我们的=除了要实现单个的赋值,还要能实现连续的赋值
    	d2 = d3 = d1;// 先执行d3 = d1,也就是d3.operator=(d1), 其返回值得是d3 才可以
    	d2.Print();
    
    	return 0;
    }
    

    在来看看拷贝构造和 赋值运算符重载的区别:

    image-20240528215910346

    1. 赋值运算符只能重载成类的成员函数不能重载成全局函数
    class Date
    {
    public:
     Date(int year = 1, 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 =”必须是非静态成员
    

    编译出错的原因是:

    赋值运算符如果不显式实现,编译器会生成默认的赋值运算符重载。此时我们类外边在定义一个赋值运算符重载,就会和编译器的产生冲突。因此,赋值运算符重载只能是类的成员函数

    1. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
    // 如果没有显式实现 拷贝构造 和 赋值运算符重载,编译器会生成默认的
    # include
    using namespace std;
    
    class Date
    {
    public:
    
    	// 默认构造函数
    	Date(int year = 0, int month = 0, int day = 0)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    
    	 拷贝构造函数
    	//Date(const Date& d)
    	//{
    	//	_year = d._year;
    	//	_month = d._month;
    	//	_day = d._day;
    	//}
    
    	 赋值运算符重载
    	d2 = d1  -> d2.operator(&d2, d1)
    	//Date& operator=(const Date& d) // 其实这里不用引用传参也不会报错,但是使用引用传参效率更高
    	//	// void operator(Date* this, const Date d)
    	//{
    	//	// 自己给自己赋值是没有意义的
    	//	if (this == &d) // d是引用,d的地址和this相同就说明是自己给自己赋值
    	//		return *this;
    
    	//	_year = d._year;
    	//	_month = d._month;
    	//	_day = d._day;
    
    	//	return *this;
    	//}
    
    	void Print()
    	{
    		cout << _year << "-" << _month << "-" << _day << endl;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    int main()
    {
    	Date d1(2024, 5, 10);
    	Date d2(2024, 5, 28);
    	d1 = d2; // 即使我们类中屏蔽了赋值运算符重载,这里不会报错
    	// 因为我们不实现的时候,编译器会生成拷贝构造和 operator= 
    	// 会完成按字节的值拷贝(浅拷贝)。
    	// 也就是说有些类,比如这个日期类,我们不需要去实现拷贝构造和赋值运算符重载。
    	// 但是涉及到 内存管理 的类 我们就需要去实现拷贝构造和赋值运算符重载
    
    	d1.Print();// 2024-5-28
    	d2.Print();// 2024-5-28
    
    	Date d3(d1); // 屏蔽了拷贝构造函数,也不会报错,这里调用了编译器生成的默认拷贝构造函数
    	Date d4 = d3;
    	d3.Print();// 2024-5-28
    	d4.Print();// 2024-5-28
    
    	return 0;
    }
    
    • 值拷贝(浅拷贝):将对象按照一个字节一个字节的传过去。

    注意: 只有六个成员函数我们不显式实现,编译器会自动生成。

    拷贝构造函数和 赋值运算符函数就是其中之二,其他运算符编译器在没有显式实现的时候不会默认生成

    但是这样就产生了一个问题:

    • 我们还需要去自己实现拷贝构造函数和赋值运算符重载吗?编译器不是已经帮我们实现了吗?

    答案当时是需要的,因为除了我们的日期类,还有许多类我们会涉及到**(内存的资源管理)**。

    我们来看一段代码来感受一下:

    // 涉及内存资源管理的Stack的拷贝构造和赋值运算符重载
    # include
    using namespace std;
    
    class Stack
    {
    public:
    	// 构造函数
    	Stack(int n = 10)
    	{
    		_a = (int*)malloc(sizeof(int) * n);
    		if (_a == NULL)
    		{
    			perror("Stack()malloc()");
    			exit(-1);
    		}
    		cout << "malloc:" << _a << endl;
    		_size = 0;
    		_capacity = n;
    	}
    
    	// ....... 栈的接口
    
    	// 析构函数
    	~Stack()
    	{
    		free(_a);
    		cout << "free:" << _a << endl;
    		_a = NULL;
    		_size = _capacity = 0;
    	}
    
    private:
    	int* _a;
    	int _size;
    	int _capacity;
    };
    
    int main()
    {
    	// 浅拷贝问题
    	Stack s1;
    
    	Stack s3(30);
    	s1 = s3;
    
    
    	return 0;
    }
    

    上面我们的Stack类中,没有显式实现的赋值运算符重载,因此我们再main函数中的赋值运算符的使用,用的都是编译器生成的默认的函数。这里用的是浅拷贝。造成了浅拷贝问题

    前面我们已经知道了拷贝构造函数造成浅拷贝的分析和原理了

    对于赋值运算符也是同样的道理,我们将s3赋值给s1用的是浅拷贝,s1和s3的_a数组指针,指向的都是同一个数组空间,在调用析构函数的时候,也会造成对同一个空间的重复释放,代码崩溃.

    image-20240529004537914

    我们来看图片来更好的理解一下:

    image-20240529003926332

    image-20240529005154244

    因此对于这种涉及到内存资源管理的类,我们需要自己实现深拷贝的赋值运算符重载

    但是这里有人会说,那我不自己实现析构函数不就不会报错了吗?

    因为我们的析构函数中存在对数组空间的释放,但是不释放就会造成内存泄漏的问题,释放了就会造成浅拷贝问题。

    5.3前置++和后置++的重载

    直接来看代码:

    class Date
    {
    public:
    	Date(int year = 1900, int month = 1, int day = 1)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    	// 前置++:返回+1之后的结果
    	// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
    	Date& operator++()
    	{
    		_day += 1;
    		return *this;
    	}
    	// 后置++:
    	// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
    	// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
    	// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1
    	// 而temp是临时对象,因此只能以值的方式返回,不能返回引用
    	Date operator++(int)
    	{
    		Date temp(*this);
    		_day += 1;
    		return temp;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    int main()
    {
    	Date d;
    	Date d1(2022, 1, 13);
    	d = d1++;    // d: 2022,1,13   d1:2022,1,14
    	d = ++d1;    // d: 2022,1,15   d1:2022,1,15
    	return 0;
    }
    '
    运行

    6.日期类的实现

    实际上完整的完善的日期类,我们应该让声明和定义分离,并且还要优化代码。这里为了方便,我们就不让声明和定义分离了。

    // 实现一个完善的日期类
    # include
    using namespace std;
    
    class Date
    {
    public:
    	int GetMonthDay(int year, int month)
    	{
    		// 给13 的原因是为了刚好让下标对上月份
    		static int monthday[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
    		// 给static的原因是 每次访问的数组都不变并且都是这个数组,那每次访问都要开辟,不如直接放静态区去
    
    		// 闰年的2月份 是 29天
    		if (month == 2 && (year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
    			monthday[2] = 29;
    		else
    			monthday[2] = 28;
    
    		int day = monthday[month];
    
    		return day;
    	}
    
    	// 默认构造函数
    	Date(int year = 0, int month = 0, int day = 0)
    	{
    		// 对传进来的年 月 日 进行判断,是否合法,合法才构造
    		if (year >= 0 && month > 0 && month < 13 && day <= GetMonthDay(year, month))
    		{
    			_year = year;
    			_month = month;
    			_day = day;
    		}
    		else
    		{
    			cout << "非法日期" << endl;
    		}
    
    	}
    
    	// 拷贝构造函数 (其实日期类这种不涉及内存资源管理的类,不需要我们显式实现拷贝构造,但是实现了也没问题)
    	// Date(Date* this, const Date& d) 
    	Date(const Date& d) // 一定要有别名 & ,不然会无限递归
    	{
    		_year = d._year;
    		_month = d._month;
    		_day = d._day;
    	}
    
    	// 析构函数
    	~Date() // 其实类似日期类这种类,是不需要我们去编写显式析构函数的
    	{
    		cout << " 析构函数" << endl;
    	}
    
    	// 运算符重载
    	//d1 < d2  编译器处理后 d1.operator<(&d1, d2)
    	bool operator<(const Date& d)// bool operator(Date* this, 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;
    		else
    			return false;
    	}
    
    	// d1 == d2 编译器处理后  d1.operator==(&d1, d2)
    	bool operator==(const Date& d)// bool operator==(Date* this, const Date& d)
    	{
    		return _year == d._year
    			&& _month == d._month
    			&& _day == d._day;
    	}
    
    	// d1 <= d2  编译器处理  d1.operator<=(&d1, d2)
    	bool operator<=(const Date& d) // bool operator<=(Date* this, const Date& d)
    	{
    		// 这里我们采用复用上面的代码来实现,以减少代码重复
    		return *this < d || *this == d; // *this 解引用就是 d1
    		// (*this).operator<(&(*this), d) || (*this).operator==(&(*this), d);
    	}
    
    	// d1 > d2 编译器处理后 d1.operator>(&d1, d2)
    	bool operator>(const Date& d)
    	{
    		// 不再采取之前那种代码,代码重复性太强,并且一旦成员变量改变,代码也要跟着大量修改
    		// 我们采取函数复用
    		return !(*this <= d); // 只要*this 不<= d  那就是> d
    		// 编译器处理后  !((*this).operator<=(&(*this), d))
    	}
    
    	// d1 >= d2 编译器处理 d1.operator>=(&d1, d2)
    	bool operator>=(const Date& d)// bool operator>=(Date* this, const Date& d)
    	{
    		// 也采用函数复用。复用前面已经实现的 >  和 == 的运算符重载函数
    		return *this > d || *this == d;
    	}
    
    	// d1 != d2 ->  d1.operator!=(&d1, d2)
    	bool operator!=(const Date& d)// bool operator!=(Date* this, const Date& d)
    	{
    		// 函数复用
    		return !(*this == d);
    	}
    
    	 d1 + 10(天数) -> d1.operator+(&d1, 10)
    	//Date operator+(int day) // Date operator+(Date* this, int day)
    	 这里不能使用引用返回,因为拷贝构造的ret在函数结束之后就会销毁
    	//{
    	//	Date ret = *this; // 拷贝构造一个d1 ,这里等价于 ret(*this)
    	//	ret._day += day;
    	//	// 对day进行判断是否合法,合法就输出,不合法要进位
    	//	while (ret._day > GetMonthDay(ret._year, ret._month))// 有可能不止进一次位,所以给一个循环
    	//	{
    	//		ret._day -= GetMonthDay(ret._year, ret._month);
    	//		ret._month++;
    
    	//		// 要注意月份是否合法
    	//		if (ret._month == 13)
    	//		{
    	//			ret._year++;
    	//			ret._month = 1;
    	//		}
    	//	}
    
    	//	return ret;
    	//}
    
    	//d1 += 10 -> d1.operator+=(&d1, 10)
    	Date& operator+=(int day)// 这里可以使用引用返回,因为this指向的本来就是外面的d1,这个函数结束之后,d1不会销毁
    	{
    		// 如果day是负数。要处理
    		if (day < 0)
    		{
    			return *this -= -day; // 加负数 相当于 减正数
    		}
    
    		// += 和 + 的区别是, + 不改变d1本身,但是+=要改变d1本身
    		_day += day;
    		while (_day > GetMonthDay(_year, _month))
    		{
    			_day -= GetMonthDay(_year, _month);
    			_month++;
    
    			// 判断月份是否合法
    			if (_month == 13)
    			{
    				_year++;
    				_month = 1;
    			}
    		}
    
    		return *this; // 返回的是d1本身
    	}
    
    	// 上面我们对+的重载和 对+=的重载的代码重复性很高,那我们就可以考虑采用复用
    	// d1 + 10
    	Date operator+(int day)
    	{
    		Date ret(*this); // 拷贝构造d1
    		ret += day; // 复用我们实现的+=的重载
    		// ret.operator+=(day)
    
    		return ret;
    	}
    
    
    	 d1 - 10(天数) -> d1.operator-(&d1, 10)
    	//Date operator-(int day)
    	//{
    	//	Date ret = *this; // 拷贝构造一个d1
    	//	ret._day -= day;
    	//	// 判断-=day之后的 day是否合法,合法就返回,不合法就要退位
    	//	while (ret._day <= 0)
    	//	{
    	//		ret._month--; // 先让月份退到上一个月
    
    	//		// 判断月份是否合法
    	//		if (ret._month == 0)
    	//		{
    	//			ret._month = 12; 
    	//			ret._year--;
    	//		}
    
    	//		ret._day += GetMonthDay(ret._year, ret._month); // + 上一个月份的天数,看看是否_day是否合法
    	//	}
    
    	//	return ret;
    	//}
    
    	// d1 -= 10 -> d1.operator(&d1, 10)
    	Date& operator-=(int day) // Date& operator(Date* this, int day)
    	{
    		// 判断day是否是负数
    		if (day < 0)
    		{
    			return *this += -day; // 减负数 相当于 加正数
    		}
    
    		// -= 改变的是d1自己,也就是this指针指向的对象
    		_day -= day;
    		// 判断_day是否合法,不合法要退位,直至合法
    		while (_day <= 0)
    		{
    			// 先让月份退一位
    			_month--;
    
    			// 判断月份是否合法
    			if (_month == 0)
    			{
    				_month = 12;
    				_year--;
    			}
    
    			_day += GetMonthDay(_year, _month); // += 上一个月份的天数,看看是否_day是否合法
    		}
    
    		return *this; // 返回自身
    	}
    
    	// 对-的重载采用代码复用,减少对代码的重复性,提升维护性
    	// d1 - 10
    	Date operator-(int day)
    	{
    		Date ret(*this); // 拷贝构造d1
    		ret -= day; // 函数复用
    
    		return ret; // 返回ret的时候要创建一个临时变量, 会调用一次拷贝构造
    	}
    
    
    	// ++d1 -> d1.operator++(&d1)
    	Date& operator++() // Date& operator(Date* this)
    	{
    		// ++也可以像之前一样,每次调用+1天,+完之后判断day是否需要进位,月是否需要进位,年是否需要进位
    		// 但是为了提升类的维护性,和减少代码重复度,我们采取复用
    		*this += 1;// ++d1就是让自己去 + 1
    
    		return *this;
    	}
    
    	// d1++ -> d1.operator++(&d1, 0)
    	Date operator++(int) // 加int是为了代表是后置++,如果不加就是默认前置++
    	{
    		// 前置++和后置++的区别就是 前置++返回+之后的  后置++返回+之前的
    		Date tmp = *this;
    		*this += 1;
    
    		return tmp; // 返回+之前的
    	}
    
    	//--d1
    	Date& operator--()
    	{
    		*this -= 1; // 函数复用
    
    		return *this;
    	}
    
    	//d1--
    	Date operator--(int)
    	{
    		// 前置-- 要返回--之后的   后置-- 要返回-之前的
    		Date tmp(*this);
    		*this -= 1;
    
    		return tmp;
    	}
    
    	// 赋值运算符重载 operator=
    	// d1 = d2 -> d1.operator(&d1, d2)
    	Date& operator=(const Date& d)
    	{
    		// 防止自己给自己赋值
    		if (this != &d) // this指向的地址  d是别名,d的地址和this指针相等就说明是自己给自己赋值
    		{
    			_year = d._year;
    			_month = d._month;
    			_day = d._day;
    		}
    
    		return *this;
    	}
    
    	// 日期 - 日期   d1 - d2
    	// 我们让小的日期++  直至 == 大的日期, + 了多少次,就差了多少天
    	int operator-(const Date& d)
    	{
    		int flag = 1;
    		// 默认大的是 this指针指向的  小的是 d
    		Date max = *this; // 拷贝构造
    		Date min = d; // 拷贝构造
    		if (max < min)
    		{
    			// 走到这里说明  默认的情况是错的
    			min = *this;
    			max = d;
    			flag = -1; // 说明是小的 - 大的 最后应该是个负数
    		}
    
    		int n = 0;
    		while (max > min) // 让小日期一直加 直至 == 大日期
    		{
    			++min;
    			++n;
    		}
    
    		// 加了多少次,n就是多少,n就是相差的天数
    		return n * flag;
    	}
    
    
    	void Print()
    	{
    		cout << _year << "-" << _month << "-" << _day << endl;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    
    int main()
    {
    	Date d1(2024, 5, 28);
    	Date d2 = d1; // 等价于 d2(d1)
    
    	cout << (d1 < d2) << endl; // 0
    	cout << (d1 == d2) << endl;// 1
    	cout << (d1 > d2) << endl;// 0
    	cout << (d1 != d2) << endl;// 0
    	cout << (d1 <= d2) << endl;// 1
    	cout << (d1 >= d2) << endl;// 1
    
    	// 是否要重载一个运算符,看的是这个运算符是否对该类的对象有意义
    	// 比如日期 + 日期没有意义, 但是日期 - 日期有意义,是两日期相隔的天数
    	// 比如 日期 + 天数 有意义,是多少天之后, 日期 - 天数有意义 是多少天之前
    	// 而有+ 就有 += , 有-就有-=  , 并且日期 * 日期 和 日期 / 日期是没有意义的
    
    	Date d3 = d1 + 10; // 将d1 + 10后的日期 拷贝构造到d3对象
    	d3.Print();// 2024-6-7
    
    	Date d4(d1 + 100);
    	d4.Print();// 2024-9-5
    
    	Date d5(d1 + 1000);
    	d5.Print();// 2027-2-22
    
    	Date d6(d1 - 1000);
    	d6.Print();// 2021-9-1
    
    	Date d7(d1 -= 1000);
    	d7.Print();// 2021-9-1
    
    	Date d8(d1++);
    	d8.Print(); // 2021-9-1
    
    	Date d9(++d1);
    	d9.Print();//2021-9-3
    
    	Date d10(d1--);
    	d10.Print();// 2021-9-3
    
    	Date d11(--d1);
    	d11.Print();//2021-9-1
    
    	cout << d3 - d1 << endl; // 1010  
    	cout << d1 - d3 << endl;// -1010
    
    
    	return 0;
    }
    

    image-20240529153501815

    中间两次调用析构函数是因为,在日期 - 日期函数中,我们创建了两个Date类的局部变量,函数栈帧结束后要销毁变量。调用了析构函数

    7.const成员

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

    image-20240529212234519

    我们来看一段代码:

    // const成员
    # include
    using namespace std;
    
    class Date
    {
    public:
    
    	// 默认构造函数
    	Date(int year = 1, 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;
    	}
    	
    	// 由于this指针是隐含的 所以不能这样加  void Print(const Date* this)
    	// 因此我们使用const修饰成员函数, 实际上修饰的是 *this, 也就是this指针指向的对象
    	void Print() const // 编译后 -> void Print(const Date* this)
    	{
    		cout << _year << "-" << _month << "-" << _day << endl;
    	}
    private:
    	int _year;
    	int _month;
    	int _day;
    
    };
    
    
    void f(const Date& d)
    {
    	d.Print();// 编译无法通过 编译后 -> d.Print(&d)
    	// 因为这里的&d 是const Date*类型, 但是Print函数的this形参是 Date* 类型
    	// 涉及到了权限放大的问题,就无法编译通过
    }
    
    int main()
    {
    	Date d1;
    	f(d1);
    
    	return 0;
    }
    

    这里可以在回忆一下const修饰的用法

    1. const Date p1;*

    2. Date const p2;*

    3. Date const p3;*

    第一第二种const都在*的左边,修饰的都是指针指向的对象,也就是不能修改指针指向的对象。

    第三种 const在* 的右边,修饰的是指针本身,也就是不能让修改指针本身指向其他地方

    上面代码属于是对象调用成员函数,那我们再来看看成员函数之间的相互调用,是否也有const之间的关系

    来看代码:

    // 成员函数之间调用
    # include
    using namespace std;
    
    class Date
    {
    public:
    
    	// 默认构造函数
    	Date(int year = 1, 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;
    	}
    
    
    	void f1()// void f1(Date* this)
    	{
    		f2(); // 编译后 this->f2(this);
    		// 这里的*this是可读可写的,传进去的*this要变成const Date类型,只能读
    		// 属于权限缩小,不会报错
    	}
    
    	void f2() const // void f2(const Date* this)
    	{}
    
    	void f3()// void f3(Date* this)
    	{
    	}
    
    	void f4() const // void f4(const Date* this)
    	{
    		f3(); // this->f3(this)  会报错
    		// 这里的*this 只能读不能改, 但是传给f3之后会变成可读可写的
    		// 属于权限放大,因此会报错
    	}
    
    
    
    	void Print() const // 编译后 -> void Print(const Date* this)
    	{
    		cout << _year << "-" << _month << "-" << _day << endl;
    	}
    private:
    	int _year;
    	int _month;
    	int _day;
    
    };
    
    
    int main()
    {
    
    	return 0;
    }
    

    看了上面两段代码之后,请思考下面的几个问题:

    1. const对象可以调用非const成员函数吗?【不可以】

    2. 非const对象可以调用const成员函数吗?【可以】

    3. const成员函数内可以调用其它的非const成员函数吗?【不可以】

    4. 非const成员函数内可以调用其它的const成员函数吗?【可以】

    注意:

    成员函数之间调用,实际上就是this指针指向的对象在调用。

    总结:

    只要成员函数中不需要修改成员变量,最好都加上const修饰成const成员函数

    不然有const类型的对象传进来就会报错,因为权限缩小了。上面我们也有讲。

    如图所示:

    image-20240530002352630

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

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

    //取地址及const取地址操作符重载
    # include
    using namespace std;
    
    class Date
    {
    public:
    
    	// 默认构造函数
    	Date(int year = 1, int month = 1, int day = 1)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    
    	// 取地址操作符重载
    	Date* operator&()
    	{
    		cout << "取地址操作符重载" << endl;
    		return this;
    	}
    
    	// 取地址操作符重载
    	const Date* operator&() const
    	{
    		cout << "const取地址操作符重载" << endl;
    		return this;
    	}
    
    
    	void Print() const // 编译后 -> void Print(const Date* this)
    	{
    		cout << _year << "-" << _month << "-" << _day << endl;
    	}
    
    
    
    
    private:
    	int _year;
    	int _month;
    	int _day;
    
    };
    
    
    int main()
    {
    	Date d1, d2;
    	const Date d3, d4;
    	cout << &d1 << endl;
    	//取地址操作符重载
    	//00000008B33CF528
    	cout << &d2 << endl;
    	//取地址操作符重载
    	//00000008B33CF558
    	
    	//cout << &d3 << endl; // 这里不会调用我们实现的&重载函数,因为d3是const Date类 权限放大了。
    	 仍然获得了d3的地址是因为调用了编译器默认生成的const取地址操作符重载函数
    	000000A4BA3AF568
    
    	// const取地址操作符重载函数我们也可以自己实现
    	cout << &d4 << endl; // 这里调用的就是我们自己实现的
    	//const取地址操作符重载
    	//00000008B33CF5B8
    	return 0;
    }
    

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

  • 相关阅读:
    如何为开源项目和社区做贡献 -- 你应该知道的十件事
    QFramework引入Command
    MySQL常用脚本
    pytorch最后一步安装失败显示false怎么办?
    JavaScript基础知识-JS数据类型
    HTTPS协议原理
    docker简单实战
    鸿蒙原生应用元服务-访问控制(权限)开发校验环节
    对城乡居民消费结构数据AIDS模型进行参数估计时用SPSS软件的操作视频或操作流程图片
    canvas 和 webgl 有什么区别?
  • 原文地址:https://blog.csdn.net/aaa114514aaaa/article/details/139355971