【c++篇】:解析c++类--优化编程的关键所在(二)

在这里插入图片描述

文章目录

  • 一.默认成员函数
  • 二.构造函数
    • 2.1.构造函数的概念
    • 2.2构造函数的特性
  • 三.析构函数
    • 3.1析构函数的概念
    • 3.2析构函数的特性
  • 四.拷贝构造函数
    • 4.1拷贝构造函数的概念
    • 4.2拷贝构造函数的特性
  • 五. 赋值运算符重载
    • 5.1运算符重载
    • 5.2赋值运算符重载
    • 5.3前置++和后置++重载
  • 六.`const`成员
  • 七.取地址和`const`取地址操作符重载
  • 八.日期类`Date`完整实现
    • 头文件`Date.h`
    • 函数定义文件`Date.cpp`

一.默认成员函数

如果一个类什么也没有就是空类。但是空类中就什么也没有吗?事实上并非如此。

在c++中,当一个类被定义时,编译器会自动为这个类生成一些特殊的成员函数,这些函数被称为默认成员函数。虽然这些函数在类中可能没有被显示定义,但在默写情况编译器会隐式的为他们生成实现。类的六个默认成员函数如图所示:

在这里插入图片描述
在上面的六个默认成员函数中,构造,析构,拷贝赋值,赋值运算符重载这四个函数最为重要,也是本篇文章将要重点讲解的。

二.构造函数

2.1.构造函数的概念

首先我们来看一下关于日期类Data:

class Data {
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(){
    Data d1;
    d1.Init(2024, 10, 20);
    d1.Print();
    return 0;
}

在上面这段代码中,通过成员函数Init给创建的对象初始化,但如果每次创建对象都是调用该函数,就会有点麻烦。

编译器无法预期一个程序在执行过程中会在何时创建一些什么对象,而只能根据当时的上下文要求来创建。对象的初始化最好能够通过运行时执行一个函数来完成,而且是在对象创建的同时完成初始化,这个函数就是构造函数

2.2构造函数的特性

构造函数是特殊的成员函数,构造函数虽然名字叫做构造,但是它的主要任务是完成对对象的初始化,并不是开空间创建对象。

  • 构造函数函数名和类名相同。

  • 构造函数无返回值。

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

  • 构造函数可以重载。比如:

    class Data {
    public:
        //无参构造函数
    	Data() {
    	}
        //带参构造函数
    	Data(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;
    };
    void test1() {
        //调用带参构造函数创建对象
    	Data d1(2024, 10, 20);
        //调用无参构造函数创建对象
    	Data d2;
        //无参调用时对象后面不能带括号,否则就像函数声明一样
    	Data d3();
    }
    
  • 如果类中没有显示定义构造函数时,c++编译器会自动生成一个无参的默认构造函数,而当用户显示定义时,编译器就不再生成。还是这个日期类Data

    class Data {
    public:
    	//将构造函数屏蔽时,代码可以通过编译,因为编译器自动生成一个无参的默认构造函数
    	/*Data(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() {
    	Data d1;
    
    	return 0;
    }
    

    我们再来看一下如果当前对象d1调用成员函数Print会有什么结果:

    在这里插入图片描述

    在这里插入图片描述

    这时我们可能会有疑问,不是编译器会自动生成默认构造函数完成初始化吗?为什么d1对象依旧是随机值?这时候我们就需要了解到什么是内置类型,什么是自定义类型。

    c++把类型分成内置类型自定义类型

    内置类型:就是语言提供的数据类型,比如常用的int,char,double以及指针类型等等。

    自定义类型:就是我们使用的class/struct/union等自己定义的类型。

    当我们没有写构造函数时,编译器会自动生成一个无参的默认构造函数,但是:内置类型不做处理,自定义类型会去调用它的默认构造。比如下面这段代码:

    //一个成绩类,包含语数英三个成员变量
    class Scores {
    public:
    	Scores() {
    		_chian = 0;
    		_math = 0;
    		_english = 0;
    	}
    	int _chian;
    	int _math;
    	int _english;
    };
    class Data {
    public:
    	void Print() {
    		cout << _year << " " << _month << " " << _day << endl;
    		cout << _s._chian << " " << _s._math << " " << _s._english << endl;
    	}
    private:
    	int _year;
    	int _month;
    	int _day;
        //假设日期类成员变量包含一个成绩类对象
    	Scores _s;
    };
    int main() {
    	Data d1;
    	d1.Print();
    	return 0;
    }
    

    在这里插入图片描述

    这时候我们就可以看到,我们在Data类中定义了一个成员变量Scores类对象,当我们创建Data类对象d1时,打印结果就会如上图所示,我们并没有写构造函数,而是使用编译器自动生成的默认构造函数,内置类型year,month,day没有处理,而自定义类型s则是调用它对应类的默认构造函数完成初始化。

    注意:在c++11中针对内置类型不初始化的情况,又增加了新的解决方法:内置类型成员变量在类中声明时可以给默认值。比如下面这段代码:

    class Scores {
    public:
    	Scores() {
    		_chian = 0;
    		_math = 0;
    		_english = 0;
    	}
    	int _chian;
    	int _math;
    	int _english;
    };
    class Data {
    public:
    	void Print() {
    		cout << _year << " " << _month << " " << _day << endl;
    		cout << _s._chian << " " << _s._math << " " << _s._english << endl;
    	}
    private:
        //内置类型
        //给成员变量设置默认值
    	int _year=1;
    	int _month=1;
    	int _day=1;
        //自定义类型
    	Scores _s;
    };
    int main() {
    	Data d1;
    	d1.Print();
    	return 0;
    }
    

    在这里插入图片描述

  • 无参的构造函数和全缺省的构造函数都称为默认构造函数,但是只能有一个。比如下面这段代码,就会出现错误

    class Data {
    public:
    	Data() {
    		_year = 1;
    		_month = 1;
    		_day = 1;
    	}
    	Data(int year=0, int month=0, int day=0) {
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    	void Print() {
    		cout << _year << " " << _month << " " << _day << endl;
    	}
    private:
    	int _year=1;
    	int _month=1;
    	int _day=1;
    };
    int main() {
    	Data d1;
    	d1.Print();
    	return 0;
    }
    

    在这里插入图片描述

三.析构函数

3.1析构函数的概念

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

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

把对象的初始化工作放到构造函数中,把清理工作放到析构函数中,当对象创建时,自动调用构造函数,当对象消亡时,自动调用析构函数,这样就不用担心对象的初始化和清理了,即使忘记,也能自动调用。

3.2析构函数的特性

  • 析构函数名是在类名前加上字符~

  • 无参数无返回类型。

  • 一个类只能有一个析构函数,所以析构函数不能重载。如果没有写时,编译器会自动生成默认析构函数。

  • 当对象生命周期结束时,编译器会自动调用析构函数。以下面这个栈为例:

    class Stack {
    public:
        //构造函数
    	Stack(int capacity=4) {
    		cout << "Stack" << endl;
    		_a = (int*)malloc(sizeof(int) * capacity);
    		if (_a == nullptr) {
    			perror("malloc fail");
    			return;
    		}
    		_top = 0;
    		_capacity = capacity;
    	}
    	void PushStack(int x) {
    		//checkcapacity();
    		_a[_top++] = x;
    	}
    	//......
        //析构函数
    	~Stack() {
    		cout << "~Stack" << endl;
    		free(_a);
    		_a = nullptr;
    		_top = 0;
    		_capacity = 0;
    	}
    private:
    	int* _a;
    	int _top;
    	int _capacity;
    };
    
    int main() {
    	Stack st1;
    	st1.PushStack(1);
    	st1.PushStack(2);
    	return 0;
    }
    

    在这里插入图片描述

    从上面的结果中我们可以看到,我们只是创建了一个对象st1,但并没有显示调用它的构造和析构,而是编译器自动调用完成初始化和清理。

  • 对于系统自动生成的默认析构函数,内置类型不做处理,自定义类型会调用它的默认析构函数。以两个栈实现一个队列为例:

    class Stack {
    public:
        //构造函数
    	Stack(int capacity=4) {
    		cout << "Stack" << endl;
    		_a = (int*)malloc(sizeof(int) * capacity);
    		if (_a == nullptr) {
    			perror("malloc fail");
    			return;
    		}
    		_top = 0;
    		_capacity = capacity;
    	}
    	void PushStack(int x) {
    		//checkcapacity();
    		_a[_top++] = x;
    	}
    	//......
        //析构函数
    	~Stack() {
    		cout << "~Stack" << endl;
    		free(_a);
    		_a = nullptr;
    		_top = 0;
    		_capacity = 0;
    	}
    private:
    	int* _a;
    	int _top;
    	int _capacity;
    };
    
    class Queue {
    public:
        //自定义类型Stack 
        //两个对象pushst,popst
    	Stack pushst;
    	Stack popst;
    };
    int main() {
        //创建一个类Queue对象q
    	Queue q;
    	return 0;
    }
    

    在这里插入图片描述

    从上面的结果中我们可以看到,类Queue中只有两个自定义类型成员变量pushst,popst,当我们创建一个类Queue对象q时,自定义类型成员变量pushst,popst会自动调用他们对应类的默认构造和析构函数完成初始化和清理工作。

  • 一般情况下,如果类中有动态申请资源时,需要显示写析构函数来释放资源;如果没有动态申请的资源或者全是自定义类型时,不需要写析构函数。

四.拷贝构造函数

4.1拷贝构造函数的概念

在现实生活中我们一定经常遇到拷贝的情况,比如将一个文件新复制一份用来备用,或者说一对双胞胎,这些都是常见的拷贝。

而在前面了解到对象的初始化和清理后,我们再来试想一下能不能将一个已经创建的对象拷贝给一个新的对象?

这时候就要引出我们要学的拷贝构造函数

拷贝构造函数,又称复制构造函数,是c++中的一种特殊的构造函数,用于创建对象的副本。他通过使用已有对象的属性值来初始化一个新的对象,实现对象的复制操作。

4.2拷贝构造函数的特性

  • 拷贝构造函数是构造函数的一个重载形式。(也就是说函数名和类名相同)。

  • 拷贝构造函数的参数只能有一个且必须是类类型对象的引用,如果是传值方式编译器会报错,因为会引发无限递归调用拷贝构造函数。

    class Date {
    public:
    	Date(int year, int month, int day) {
    		_year = year;
    		_month = month;
    		_day = day;
    	}
        //拷贝构造函数
    	Date(const Date& d) {
    		_year = d._year;
    		_month = d._month;
    		_day = d._day;
    	}
    
    	void Print() {
    		cout << _year << " " << _month << " " << _day << endl;
    	}
    private:
    	int _year = 1;
    	int _month = 1;
    	int _day = 1;
    };
    
    int main() {
    	Date d1(2024, 10, 21);
    	Date d2 = (d1);
    	d1.Print();
    	d2.Print();
    
    	return 0;
    }
    

    在这里插入图片描述

    上面所示样例是使用引用方式,结果是正确的。

    如果拷贝构造函数是传值方式时,就会出现以下错误:

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

    在这里插入图片描述

  • 如果没有显示定义拷贝构造函数时,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对对象按内存存储按字节序完成拷贝,这种拷贝方式为值拷贝或者浅拷贝。

    在前面的构造函数和析构函数我们知道,这两个对于内置类型不做处理,自定义类型会调用它的默认构造或析构。而拷贝构造函数则是有所不同,对于内置类型会完成值拷贝,而自定义类型会去调用它的拷贝构造。以下面这个为例:

    class Time {
    public:
    	Time(int hour=1, int minute=1, int second=1) {
    		_hour = hour;
    		_minute = minute;
    		_second = second;
    	}
    	Time(const Time& t) {
    		_hour = t._hour;
    		_minute =t._minute;
    		_second =t._second;
    		cout << "const Time& t" << endl;
    	}
    
    
    //private:
    	int _hour;
    	int _minute;
    	int _second;
    };
    
    class Date {
    public:
    	void Print() {
    		cout << _year << "-" << _month << "-" << _day;
    		cout << "-" << t._hour <<"-" << t._minute <<"-"<< t._second << endl;
    	}
    	
    private:
        //内置类型
    	int _year=0;
    	int _month=0;
    	int _day=0;
        //自定义类型
    	Time t;
    };
    
    int main() {
    	Date d1;
        //用已经存在的对象d1拷贝构造对象d2
    	Date d2(d1);
    	d1.Print();
    	d2.Print();
    	return 0;
    }
    

    在这里插入图片描述

    在上面的这一段代码中,Data类中有内置类型和自定义类型Time对象t,用已经存在的对象d1拷贝构造对象d2,在类Data中,我们并没有写拷贝构造函数,而是编译器默认生成并调用,内置类型成员变量year,month,day完成值拷贝,自定义类型t调用了他自己的拷贝构造函数完成拷贝。

  • 编译器生成的默认拷贝构造函数只能完成字节序的值拷贝,对于使用动态内存开辟的对象需要使用深拷贝,这时候就需要自己显示实现拷贝构造函数。还是以栈为例:

    如果按照下面的代码运行,程序会发生崩溃而中断。这是因为用对象st1拷贝构造一个新的对象st2,st1使用了动态内存开辟空间,如果直接使用编译器生成的默认拷贝构造,只能完成值拷贝,并不能实现深拷贝,所以就会出现如下错误:

    class Stack {
    public:
    	Stack(int capacity=4) {
    		cout << "Stack" << endl;
    		_a = (int*)malloc(sizeof(int) * capacity);
    		if (_a == nullptr) {
    			perror("malloc fail");
    			return;
    		}
    		_top = 0;
    		_capacity = capacity;
    	}
    	void PushStack(int x) {
    		//checkcapacity();
    		_a[_top++] = x;
    	}
    	//......
    	~Stack() {
    		cout << "~Stack" << endl;
    		free(_a);
    		_a = nullptr;
    		_top = 0;
    		_capacity = 0;
    	}
    private:
    	int* _a;
    	int _top;
    	int _capacity;
    };
    int main() {
    	Stack s1;
    	s1.PushStack(1);
    	s1.PushStack(2);
        s1.PushStack(3);
    	s1.PushStack(4);
    	Stack s2(s1);
    	return 0;
    }
    

    在这里插入图片描述

    在这里插入图片描述

    注意:如果类中没有涉及到动态资源申请时,拷贝构造函数可以写也可以不写,而一旦涉及到有动态资源申请时,就一定要写拷贝构造函数,否则就是浅拷贝。

五. 赋值运算符重载

5.1运算符重载

对于运算符我们一定不会陌生,先以常见的为例,比较运算符<,>我们一定经常用吧,如果现在有一个日期类的对象d1和d2:

class Date {
public:
	Date(int year,int month,int day) {
		_year = year;
		_month = month;
		_day = day;
	}
	void Print() {
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	int _year = 0;
	int _month = 0;
	int _day = 0;
	Time t;
};
int main() {
	Date d1(2024, 10, 21);
	Date d2(2023, 10, 21);
	bool ret = d1._year < d2._year;
	cout << ret << endl;
	return 0;
}

我们想要比较这两个对象的成员变量年份时,我们可以这样直接比较:

bool ret = d1._year < d2._year;

在这里插入图片描述

但是如果我们想要直接比较对象d1和d2,还可以直接使用比较运算符吗?结果就会如下所示:

bool ret = d1 < d2;

在这里插入图片描述

前面这两个不同点在于,年份是内置类型,可以直接使用运算符比较,而两个对象则是自定义类型,不能直接使用。

c++为了方便这一点引入了运算符重载。

运算符重载是对已有运算符的重新定义,使其能够处理自定义类型的数据。这实际上是通过定义一个特殊的函数来实现的。该函数也是具有其返回值类型,函数名字以及参数列表。

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

注意:

  • 运算符重载并不是创建新的运算符,比如:operator@
  • 重载操作符必须有一个类类型参数
  • 不能改变内置类型的运算符含义
  • 作为类成员函数重载时,形参比操作数目少一,这是因为,成员函数的第一个参数为隐藏的this指针
  • .*,::,sizeof,?:,.这五个运算符不能重载

明白了这个概念后,我们再来看刚才对于两个对象的比较,用运算符重载来实现:

全局函数operator<

bool operator<(const Date& d1, const Date& d2) {
	if (d1._year < d2._year) {
		return true;
	}
	if (d1._year == d2._year && d1._month < d2._month) {
		return true;
	}
	if (d1._year == d2._year && d1._day == d2._month && d1._day < d2._day) {
		return true;
	}
	return false;
}
int main() {
	Date d1(2022, 10, 21);
	Date d2(2023, 10, 21);
	cout << (d1<d2) << endl;
	return 0;
}

在这里插入图片描述
作为类成员函数operator<,结果依然和上图一样

class Date {
public:
	Date(int year,int month,int day) {
		_year = year;
		_month = month;
		_day = day;
	}
	void Print() {
		cout << _year << "-" << _month << "-" << _day << endl;
	}
    //作为类成员函数
    //bool operator<(Date*this,const Date& d)
	bool operator<(const Date& d) {
		if (_year < d._year) {
			return true;
		}
		if (_year == d._year && _month < d._month) {
			return true;
		}
		if (_year == d._year && _day == d._month && _day < d._day) {
			return true;
		}
		return false;
	}
private:
	int _year = 0;
	int _month = 0;
	int _day = 0;
	Time t;
};
int main() {
	Date d1(2022, 10, 21);
	Date d2(2023, 10, 21);
	cout << (d1<d2) << endl;
	return 0;
}

5.2赋值运算符重载

注意:赋值运算符重载是将一个自定义类型赋值给另一个,和拷贝构造函数的区别就是:拷贝构造是用一个已经存在的对象初始化另一个对象;而赋值运算符重载是已经存在的两个对象之间赋值拷贝。

  • 赋值运算符重载格式
    • 参数类型:const T&,传递引用可以提高效率
    • 返回值:类型是T&,返回引用可以提高效率,返回的是*this,因为要符合连续赋值的含义
    • 检查是否自己给自己赋值
class Date {
public:
    //构造函数
	Date(int year,int month,int day) {
		_year = year;
		_month = month;
		_day = day;
	}
    //遍历打印函数
	void Print() {
		cout << _year << "-" << _month << "-" << _day << endl;
	}
    //=重载函数
	Date& operator=(const Date& d) {
        //先判断一下是否自己给自己赋值
		if (this !=& d) {
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
private:
	int _year = 0;
	int _month = 0;
	int _day = 0;
};

int main() {
	Date d1(2022, 10, 21);
	Date d2(2023, 10, 21);
    //将对象d1赋值给d2
	d2 = d1;
	d1.Print();
	d2.Print();
	return 0;
}

上面代码结果:

在这里插入图片描述

  • 如果用户没有显示时,编译器会自动生成一个默认赋值运算符重载函数,这里就和拷贝构造函数一样,实现的是值拷贝,如果是内置类型成员变量可以直接复制,自定义类型成员变量就去调用对应类的的赋值运算符重载完成赋值。

    还是以Date类和Time类为例:

    class Time {
    public:
    	Time(int hour=1, int minute=1, int second=1) {
    		_hour = hour;
    		_minute = minute;
    		_second = second;
    	}
        //Time类的赋值运算符重载
    	Time& operator=(const Time& t) {
    		if (this != &t) {
    			_hour = t._hour;
    			_minute =t._minute;
    			_second =t._second;
    		}
    		return *this;
    	}
    	int _hour;
    	int _minute;
    	int _second;
    };
    class Date {
    public:
    	Date(int year, int month, int day) {
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    	void Print() {
    		cout << "内置类型成员变量:";
    		cout << _year << "-" << _month << "-" << _day << endl;
    		cout << "自定义类型成员变量:";
    		cout << t._hour << "-" << t._minute << "-" << t._second << endl;
    	}
    	
    private:
        //内置类型
        //Date类中没有赋值运算符重载,默认生成并调用
    	int _year=0;
    	int _month=0;
    	int _day=0;
        //自定义类型
        //调用对应类的赋值运算符重载
    	Time t;
    };
    
    int main() {
    	Date d1(2024, 10, 21);
    	Date d2(2023, 10, 21);
    	d2 = d1;
    	d1.Print();
    	d2.Print();
    	return 0;
    }
    

    在这里插入图片描述

  • 和拷贝构造函数一样,如果类中没有申请动态资源的,可以不写赋值运算符重载,编译器默认生成的就可以完成赋值,而如果类中含有申请动态资源的,就需要用户自己显示实现。

    还是以栈为例,以下代码运行会报错,和拷贝构造函数错误原因一样。

    class Stack {
    public:
    	Stack(int capacity=4) {
    		_a = (int*)malloc(sizeof(int) * capacity);
    		if (_a == nullptr) {
    			perror("malloc fail");
    			return;
    		}
    		_top = 0;
    		_capacity = capacity;
    	}
    	void PushStack(int x) {
    		//checkcapacity();
    		_a[_top++] = x;
    	}
    	......
    	~Stack() {
    		free(_a);
    		_a = nullptr;
    		_top = 0;
    		_capacity = 0;
    	}
    private:
    	int* _a;
    	int _top;
    	int _capacity;
    };
    int main() {
    	Stack s1;
    	s1.PushStack(1);
    	s1.PushStack(2);
    	Stack s2;
    	s2 = s1;
    	return 0;
    }
    

    在这里插入图片描述

  • 赋值运算符重载只能重载成类的成员函数不能重载成全局函数。因为如果类中没有赋值运算符重载时,编译器会默认生成,如果在全局中重载,就会和默认生成的产生冲突。

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

  • 前置++:

    • 前置++返回加一后的结果
    • 因为this指针指向的对象在函数调用结束时不会销毁,所以用引用做返回值提高效率
  • 后置++:

    • 为了让前置++和后置++构成重载,c++规定,后置++重载时多加上一个int类型的参数,但调用时不用传参,只是为了区分
    • 后置++返回加一之前的值,所以要先将this指针指向的对象保存一份,然后this+1
    • 因为用来保存的为临时变量,所以只能用传值方式返回,不能用引用

完整代码如下:

class Date {
public:
	Date(int year,int month,int day) {
		_year = year;
		_month = month;
		_day = day;
	}
	void Print() {
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	//前置++
	Date& operator++() {
		_day ++;
		return *this;
	}
    //后置++
	Date operator++(int) {
		Date tmp(*this);
		_day++;
		return tmp;
	}

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

int main() {
	Date d1(2022, 10, 21);
	Date d2(2023, 10, 21);
	d1++;
	d2++;
	d1.Print();
	d2.Print();
	return 0;
}

结果如下:

在这里插入图片描述

六.const成员

我们知道被const修饰的将具有常性,不能再进行修改,如果以下面这种情况为例,用const修饰对象d1,然后调用Print成员函数就会报错:

class Date {
public:
	Date(int year,int month,int day) {
		_year = year;
		_month = month;
		_day = day;
	}
    //void Print(Date* this)
	void Print() {
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year = 0;
	int _month = 0;
	int _day = 0;
};
int main(){
    //d1.Print(&d1)
    const Date d1(2024, 10, 21);
    d1.Print();
    return 0;
}

在这里插入图片描述

这里报错的原因是因为,被const修饰的对象d1调用成员函数Print时,实际上是这样d1.Print(&d1),而成员函数则是void Print(Date* this),从const Date*Date*权限放大,所以会出现错误,为了解决此问题,就要在成员函数后面加const修饰:

//void Print(const Date* this)
	void Print() const {
		cout << _year << "-" << _month << "-" << _day << endl;
	}

加上const修饰后,this指针就会变成const Date*,从const Date*const Date*权限平移,可以运行。

如果创建一个对象d2Date类型,从Date*const Date*权限缩小,依然可以运行。所以在成员函数后加上const修饰后,对于非const修饰的对象和const修饰的对象都可以调用。

class Date {
public:
	Date(int year,int month,int day) {
		_year = year;
		_month = month;
		_day = day;
	}
    //void Print(const Date* this)
	void Print() const {
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year = 0;
	int _month = 0;
	int _day = 0;
};
int main(){
    //d2.Print(&d2)
    Date d2(2024, 10, 20);
    d2.Print();
    return 0;
}

注意:

  • 允许权限平移和权限缩小,但是不能权限放大。
  • 只要成员函数内部不修改成员变量,都应该加const
  • 要修改成员变量的成员函数不能加const

七.取地址和const取地址操作符重载

  • 普通对象取地址操作符重载:

    Date* operator&(){
        return this;
    }
    
  • const对象取地址操作符重载:

    const Date* operator&() const {
        return this;
    }
    

    这两默认成员函数一般不用重新定义,编译器会默认生成。使用默认生成的就可以,一般特殊情况会需要重载,比如需要获取到指定的内容。

    class Date {
    public:
    	Date(int year,int month,int day) {
    		_year = year;
    		_month = month;
    		_day = day;
    	}
        //普通对象取地址操作符重载
    	Date* operator&() {
    		cout << "Date* operator&()" << endl;
    		return this;
    	}
        //const对象取地址操作符重载
    	const Date* operator&()const {
    		cout << "const Date* operator&()" << endl;
    		return this;
    	}
    
    private:
    	int _year = 0;
    	int _month = 0;
    	int _day = 0;
    };
    int main(){
        Date d1(2024, 10, 22);
        const Date d2(2024, 10, 21);
        cout << &d1 << endl;
        cout << &d2 << endl;
        return 0;
    }
    

    自己显示的取地址操作符重载函数结果:

    在这里插入图片描述

编译器默认生成的取地址操作符重载结果:

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

private:
	int _year = 0;
	int _month = 0;
	int _day = 0;
};
int main(){
    Date d1(2024, 10, 22);
    const Date d2(2024, 10, 21);
    cout << &d1 << endl;
    cout << &d2 << endl;
    return 0;
}

在这里插入图片描述

八.日期类Date完整实现

这里主要提供头文件和函数定义文件,测试文件就不再展示,可以自己尝试创建几个对象进行检验。

头文件Date.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<iostream>
using namespace std;

class Date {
public:
	//获取当月天数
	int GetDay(int year, int month);
	//构造函数
	Date(int year,int month,int day);
	//拷贝构造函数
	Date(const Date& d);
	//赋值运算符重载
	Date& operator=(const Date& d);

	//日期+=天数
	Date& operator+=(int day);
	//日期+天数
	Date operator+(int day);
	//日期-=天数
	Date& operator-=(int day);
	//日期-天数
	Date operator-(int day);

	//前置++
	Date& operator++();
	//后置++
	Date operator++(int);
	//前置--
	Date& operator--();
	//后置--
	Date operator--(int);

	//比较运算符重载
	bool operator<(const Date& d);
	bool operator==(const Date& d);
	bool operator>(const Date& d);
	bool operator<=(const Date& d);
	bool operator>=(const Date& d);
	bool operator!=(const Date& d);
	//打印
	void Print();

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

函数定义文件Date.cpp

#include"Date.h"
int Date::GetDay(int year, int month) {
	int Day[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	int day = Day[month];
	if (month == 2 && ((year % 4 == 0 && year % 100 != 0) ||( year % 400 == 0))) {
		day++;
	}
	return day;
}

Date::Date(int year,int month,int day)
{
	_year = year;
	_month = month;
	_day = day;
}

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

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

Date& Date::operator+= (int day) {
	_day += day;
	while (_day > GetDay(_year, _month)) {
		_day -= GetDay(_year, _month);
		_month++;
		if (_month == 13) {
			_year++;
			_month = 1;
		}
	}
	return *this;
}

Date Date::operator+(int day) {
	Date tmp(*this);
	tmp._day += day;
	while (tmp._day > GetDay(tmp._year, tmp._month)) {
		tmp._day -= GetDay(tmp._year, tmp._month);
		tmp._month++;
		if (tmp._month == 13) {
			tmp._year++;
			tmp._month = 1;
		}
	}
	return tmp;
}

Date& Date::operator-=(int day) {
	_day -= day;
	while ( _day < 0) {
		_month--;
		if (_month == 0) {
			_year--;
			_month = 12;
		}
		_day += GetDay(_year, _month);
	}
	return *this;
}

Date Date::operator-(int day) {
	Date tmp(*this);
	tmp._day -= day;
	while (tmp._day < 0) {
		tmp._month--;
		if (tmp._month == 0) {
			tmp._year--;
			tmp._month = 12;
		}
		tmp._day += GetDay(tmp._year, tmp._month);
	}
	return tmp;
}

Date& Date::operator++() {
	if (_month == 12 && _day == GetDay(_year, _month)) {
		_year++;
		_month = 1;
		_day = 1;
	}
	else if (_day == GetDay(_year, _month)) {
		_month++;
		_day = 1;
	}
	else {
		_day++;
	}
	return *this;
}

Date Date::operator++(int) {
	Date tmp(*this);
	if (_month == 12 && _day == GetDay(_year, _month)) {
		_year++;
		_month = 1;
		_day = 1;
	}
	else if (_day == GetDay(_year, _month)) {
		_month++;
		_day = 1;
	}
	else {
		_day++;
	}
	return tmp;
}

Date& Date::operator--() {
	if (_month == 1 && _day ==1) {
		_year--;
		_month = 12;
		_day = GetDay(_year,_month);
	}
	else if (_day == 1) {
		_month--;
		_day = GetDay(_year, _month);
	}
	else {
		_day--;
	}
	return *this;
}

Date Date::operator--(int) {
	Date tmp(*this);
	if (_month == 1 && _day == 1) {
		_year--;
		_month = 12;
		_day = GetDay(_year, _month);
	}
	else if (_day == 1) {
		_month--;
		_day = GetDay(_year, _month);
	}
	else {
		_day--;
	}
	return tmp;
}

Date& Date::operator=(const Date& d) {
	_year = d._year;
	_month = d._month;
	_day = d._day;
	return *this;
}

bool Date::operator<(const Date& d) {
	if (_year <d._year) {
		return true;
	}
	if (_year == d._year && _month < d._month) {
		return true;
	}
	if (_year == d._year && _month == d._month && _day < d._day) {
		return true;
	}
	return false;
}

bool Date::operator==(const Date& d) {
	if (_year == d._year && _month == d._month && _day == d._day) {
		return true;
	}
	return false;
}

bool Date::operator>(const Date& d) {
	return !(*this < d) && !(*this == d);
}

bool Date::operator<=(const Date& d) {
	return (*this < d) || (*this == d);
}

bool Date::operator>=(const Date& d) {
	return (*this > d) || (*this == d);
}

bool Date::operator!=(const Date& d) {
	return !(*this == d);
}

以上就是关于c++类六个默认成员函数的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/901105.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【业务】群组服务功能重构测试总结

背景&#xff1a; 群组微服务重构想法上半年就开始了&#xff0c;目前老群组服务除了代码设计不合理&#xff0c;服务部署无法启动也是痛点。研发侧发起技术重构。 测试角度来说我这边痛点有三个&#xff1a; 业务不熟悉也没有完整有效case->有哪些功能点&#xff0c;那些…

docker 单节点arm架构服务器安装zookeeper、kafka并测试通信

kafka、zookeeper常用镜像介绍 kafka和zookeeper常见的镜像有以下三个&#xff1a;wurstmeister/zookeeper、kafka、confluentinc/cp-zookeeper、cp-kafka 和 bitnami/zookeeper、kafka。 wurstmeister/xxx: 由wurstmeister团队维护&#xff0c;提供的镜像适用于开发和测试环…

Mac apache配置cgi环境-修改httpd.conf文件、启动apache

Mac自带Apache&#xff0c;配置CGI&#xff0c;分以下几步&#xff1a; 找到httpd.conf。打开终端&#xff0c;编辑以下几处&#xff0c;去掉#或补充内容。在这个路径下写一个测试文件.py格式的&#xff0c;/Library/WebServer/CGI-Executables&#xff0c;注意第一行的python…

矩阵概念 和 性质

目录 一、矩阵因式分解 二、矩阵在图形学的运用 一、矩阵因式分解 1、先将矩阵化为上三角阵&#xff0c;得到U 2、每个主元列以下元素 主元 得到下三角阵 二、矩阵在图形学的运用 二维移动&#xff1a; 子空间H&#xff1a; 零向量属于H 对H中任意向量u、v&#xff0c;uv…

2024-10-25 算法学习及论文辅导(每日更新,随时联系)

看看学习小群的学习氛围&#x1f447;&#x1f3fb; 很多同学自己学习遇到问题没人解决&#xff0c;最终消耗了时间&#xff0c;精力同时大大消耗了自己对学习的信心&#x1f627; &#x1f973;来看看跟班学习&#xff0c;大家遇到问题的时候是怎么解决的&#xff1a; 首先…

idea安装visualVm插件

idea 安装visualVM插件用于分析java程序&#xff0c; 1.在插件市场安装visualvm launcher 2.安装成功后&#xff0c;重启idea&#xff0c;此时启动按钮旁边有这两个按钮 3.需要在这里配置插件的visualvm位置 4.配置完后&#xff0c;点击启动

ArcGIS计算落入面图层中的线的长度或面的面积

本文介绍在ArcMap软件中&#xff0c;计算落入某个指定矢量面图层中的另一个线图层的长度、面图层的面积等指标的方法。 如下图所示&#xff0c;现在有2个矢量要素集&#xff0c;其中一个为面要素&#xff0c;表示某些区域&#xff1b;另一个为线要素&#xff0c;表示道路路网。…

Linux相关概念和易错知识点(16)(Shell原理、进程属性和环境变量表的联系)

Shell原理及其模拟实现 在认识进程exec系列函数、命令行参数列表、环境变量之后&#xff0c;我们可以尝试理解一下Shell的原理&#xff0c;将各方知识串联起来&#xff0c;让Shell跑起来才能真正理解这些概念。我会以模拟Shell执行的原理模拟一个Shell。途中配上相关讲解。 1…

InnoDB 存储引擎<一>InnoDB简介与MySQL存储架构及相关数据结构

目录 回顾MySQL架构 InnoDB简介 ​MySQL存储结构 回顾MySQL架构 对MySQL架构图的总结: MySQL服务器是以网络服务的方式对外提供数据库服务的&#xff0c;我们使用的应用程序以及客户端统称为外部程序。 外部程序通过发送网络请求的方式来连接MySQL服务器&#xff0c;这时首先每…

Leetcode239. 滑动窗口最大值

问题描述&#xff1a; 给你一个整数数组 nums&#xff0c;有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回 滑动窗口中的最大值 。 示例 1&#xff1a; 输入&#xff1a;nums [1,3,…

Python爬虫教程:从入门到精通

Python爬虫教程&#xff1a;从入门到精通 前言 在信息爆炸的时代&#xff0c;数据是最宝贵的资源之一。Python作为一种简洁而强大的编程语言&#xff0c;因其丰富的库和框架&#xff0c;成为了数据爬取的首选工具。本文将带您深入了解Python爬虫的基本概念、实用技巧以及应用…

文件下载漏洞

文件安全 文件下载 常见敏感信息路径 Windows C:\boot.ini //查看系统版本 C:\Windows\System32\inetsrv\MetaBase.xml //IIS配置文件 C:\Windows\repair\sam //存储系统初次安装的密码 C:\Program Files\mysql\my.ini //Mysql配置 C:\Program Files\mysql\data\mysql\user.…

Python小游戏13——植物大战僵尸

代码 import random import time # 植物类 class Plant: def __init__(self, name, health): self.name name self.health health def is_alive(self): return self.health > 0 # 僵尸类 class Zombie: def __init__(self, name, health): self.name name self.health h…

Tornado简单使用

Tornado简单使用 1 介绍 Tornado 是一个基于Python的Web服务框架和 异步网络库&#xff0c;它最初由 FriendFeed 开发&#xff0c;后来被 Facebook 收购并开源&#xff0c;通过利用非阻塞网络 I/O, Tornado 可以承载成千上万的活动连接&#xff0c;完美的实现了 长连接、WebS…

关于 Linux 内核“合规要求”与俄罗斯制裁的一些澄清

原文&#xff1a;Michael Larabel - 2024.10.24 当 一些俄罗斯的 Linux 开发者被从内核的 MAINTAINERS 文件中移除 时&#xff0c;原因被描述为“合规要求”&#xff0c;但并未明确这些要求具体涉及什么内容。随后&#xff0c;Linus Torvalds 对此发表了评论&#xff0c;明确指…

SIP 业务举例之 三方通话:邀请第三方加入的信令流程

目录 1. 3-Way Conference - Third Party Is Added 简介 2. RFC5359 的 3-Way Conference - Third Party Is Added 信令流程 3. 3-Way Conference - Third Party Is Added 总结 博主wx:yuanlai45_csdn 博主qq:2777137742 想要 深入学习 5GC IMS 等通信知识(加入 51学通信)…

[bug] vllm 0.6.1 RuntimeError: operator torchvision::nms does not exist

[bug] vllm 0.6.1 RuntimeError: operator torchvision::nms does not exist 环境 python 3.10 torch 2.4.0cu118 torchvision 0.19.0cu118 vllm 0.6.1.post2cu118问题详情 if torch._C._d…

Spring Boot框架中小企业设备监控系统开发

3系统分析 3.1可行性分析 通过对本中小企业设备管理系统实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本中小企业设备管理系统采用Spring Boot框架&#xff0…

ubuntu20.04上使用 Verdaccio 搭建 npm 私有仓库

安装nvm 首先安装必要的工具&#xff1a; apt update apt install curl下载并执行nvm安装脚本&#xff1a; curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash添加环境变量&#xff08;如果安装脚本没有自动添加&#xff09;。编辑 ~/.bash…

java项目之基于web的智慧社区设计与实现(springboot)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的基于web的智慧社区设计与实现。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 基于web的智…