C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解

目录

类的6个默认成员函数

一.构造函数

1.概念

2.特征如下:

(1) 函数名与类名相同。

(2)无返回值。

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

(4)构造函数可以重载。

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

(6)默认构造函数:无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。

(7)默认构造函数可以不写吗?

(8)我们不写构造函数编译器默认生成的构造函数干了些什么事情?

(9)(了解)C++11打的补丁:

3.对构造函数的考察题

二.析构函数

1.概念

动态开辟的资源不用析构释放会永远内存泄漏吗?

2.特征

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

(2. 无参数无返回值。(所以析构函数不能构成函数重载)

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

(4. 对象生命周期结束时(最近那层大括号里面就是生命周期),C++编译系统系统自动调用析构函数。

(5. 我们不写析构函数编译器默认生成的析构函数干了些什么事情?

3.构造函数和析构函数自动调用的顺序

考察构造,析构顺序的题:

三.拷贝构造

1.大体的概念:创建一个新对象时,把一个已有对象完全拷贝给这个新对象:Date d2(d1)

2.特征

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

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

<3. 若未显示定义,系统生成默认的拷贝构造函数。

<4.我们不写拷贝构造函数编译器默认生成的拷贝构造函数干了些什么事情?

<5.日期类用默认,栈类需要自己写深拷贝

3.拷贝构造需要自己写的场景只有Stack这种类!而且是深拷贝。

4.拷贝构造的易错题

(1)题目1

(2)题目2:构造,析构,拷贝构造综合题

(3)题目2的变形——匿名对象

(4)继续变形

(5)题目4的变形

(6) 终极复合题

5.栈类,日期类,MyQueue类对于构造,析构,拷贝构造的需求

(1)日期类:

(2)栈类:

(3)MyQueue类(内置类型和自定义类型混合):

四.运算符重载

1.目的:

2.函数格式及细节:

3.探索如果正确使用运算符重载:

>:1 不能把 运算符重载operator写在类的外面

>:2 使用if (d1 == d2) 不是 if (operator==(d1, d2))

>:3 运算符重载operator写进类

>:4 传参用引用传参&,并加上const

>:5 运算符重载operator类外面和类里面都写了,优先调用类里面的

五.日期类的运算符重载

1.练习:写一下 if (d1 < d2) 的运算符重载

2.练习:写一下 d1 = d2 的运算符重载

(1)d1 = d2,if (d1 < d2),if (d1 == d2)运算符重载的代码

(2)赋值运算符重载和拷贝构造的区别

3.有意义的运算符才会重载,我们还可以重载很多日期类运算符。比如== , < , <= , > , >= , !=

Date.h

Date.cpp

test.cpp

(2)把定义写在类中(== , < , <= , > , >= , != 的运算符重载。)

4.+ ,+= 的运算符重载

Date.h

 Date.cpp

test.cpp

(1)此处的细节:const对于赋值运算符重载或拷贝构造可以防止权限被放大。

 (2)+和+=的互相复用,+复用+=更优一些!

5.- ,-= 的运算符重载

Date.cpp

test.cpp

6.前置,后置++

7.日期-日期 返回天数

8.const的细节

(1)权限放大问题

 (2)const修饰类的成员函数:

 (2)const修饰类的成员函数的格式:

(3)权限放大问题的解决方法:

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


类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6 个默认成员函数。
C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第1张图片

学习这几个函数分为两个大方面:
1、基本语法特性。 函数名、参数、返回值、什么时候调用...
2、我们不写编译器默认生成这个函数干了些什么事情!
 

一.构造函数

1.概念

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

2.特征如下:

(1) 函数名与类名相同。

(2)无返回值。

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

(4)构造函数可以重载。

比如写一个非默认构造函数和一个默认构造函数可形成重载:

#include
using namespace std;
class Date
{
public:
	Date()    //不传参写法(不建议这么写)
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}

	Date(int year, int month, int day)    //传参写法(不建议这么写)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

int main()
{

	Date d1;	//无参写法
	d1.Print();

	Date d2(2022, 5, 15);	//含参写法,全缺省函数传3个参数可以
	d2.Print();


	return 0;
}

class Date
{
public:
	//Date()    //不传参写法(不建议这么写)
	//{
	//	_year = 1;
	//	_month = 1;
	//	_day = 1;
	//}

	//Date(int year, int month, int day)    //传参写法(不建议这么写)
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//}

	Date(int year = 1, int month = 1, int day = 1)	//全缺省写法是最佳写法,相传几个参数都行
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

int main()
{
	//Date d1(); // 无参不能这么写,编译器无法区分函数声明还是类的定义
	Date d1;	//无参写法
	d1.Print();

	Date d2(2022, 5, 15);	//含参写法,全缺省函数传3个参数可以
	d2.Print();

	Date d3(2022);    //全缺省函数传1个参数可以
	d3.Print();

	Date d4(2022, 10);    //全缺省函数传2个参数可以
	d4.Print();

	return 0;
}

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

     我们不写,编译器会生成一个默认无参构造函数
     内置类型/基本类型:int/char/double/指针...
     自定义类型:class/struct去定义类型对象
     默认生成构造函数对于内置类型成员变量不做处理,对于自定义类型成员变量才会处理
class Date 不写构造函数时:d1中的内置类型 _year,int _month,int _day 都是随机值,不会初始化,但自定义类型 _aa 会去class A中调用类型A的构造函数, 如果A也不写构造函数 A() ,在类A中int _a 也是内置类型,那么_aa._a 也就是随机值了
class A
{
public:
	A()
	{
		cout << " A()" << endl;
		_a = 0;
	}
private:
	int _a;
};

class Date
{
public:
	// 我们不写,编译器会生成一个默认无参构造函数
	// 内置类型/基本类型:int/char/double/指针...
	// 自定义类型:class/struct去定义类型对象
	// 默认生成构造函数对于内置类型成员变量不做处理,对于自定义类型成员变量才会处理
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日

	A _aa;
};
int main()
{
	Date d1;
	d1.Print();

	return 0;
}

C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第2张图片总结:如果一个类中的成员全是自定义类型,我们就可以用默认生成的函数。如果有内置类型的成员,或者需要显示传参初始化,那么都要自己实现构造函数。90%都需要自己写构造函数

二次总结:一般情况一个C++类,都要自己写构造函数。一般只有少数情况可以让编译器默认生成。
 1、类里面成员都是自定义类型成员,并且这些成员都提供了默认构造函数
 2、如果还有内置类型成员,声明时给了缺省值(在private中给成员变量缺省值,这是C++11才有的操作)

举例:不需要自己写构造函数的情况详细代码:class MyQueue 这种只要自定义类型的类就可以不用写构造函数了

class Stack
{
public:
	Stack()
	{
		_a = nullptr;
		_top = _capacity = 0;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};

class MyQueue {
public:
	// 默认生成构造函数就可以用了
	void push(int x) {
	}

	int pop() {
	}

private:
	Stack _st1;
	Stack _st2;
};

int main()
{
	MyQueue q;
	Stack st;

	return 0;
}

(6)默认构造函数:无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。

注意:我们写的无参的构造函数  Stack()我们写的全缺省的构造函数  Stack(int capacity = 10)我们没写编译器默认生成的构造函数  (上面两个都不写,编译器就会自己生成一个),都可以认为是默认构造函数。——>不用传参就可以调用的 Stack(int capacity)这个需要传参,所以不是默认构造函数

class Stack
{
public:
	/*Stack()	//1.我们写的无参的构造函数
	{
		_a = nullptr;
		_top = _capacity = 0;
	}*/

	/*Stack(int capacity = 10)	//2.我们写的全缺省的构造函数
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		assert(_a);

		_top = 0;
		_capacity = capacity;
	}*/
	//3.或者上面两个都不写编译器默认生成的构造函数
	Stack(int capacity)	//但是这个需要传参数就,不是默认构造函数,就会导致默认构造函数生成失败
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		assert(_a);

		_top = 0;
		_capacity = capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

class MyQueue {
public:
	// 默认生成构造函数就可以用了
	void push(int x) {
	}

	int pop() {
	}

private:
	Stack _st1;
	Stack _st2;
};

int main()
{
	MyQueue q;
	Stack st;

	return 0;
}

用上面注释的那三种默认构造函数都可以正常运行,但是如果像上面这样用的是Stack(int capacity),这个带参的就不是默认构造函数,因为class MyQueue中我们没写默认构造函数所以编译器应该自己生成默认构造函数,但是MyQueue的默认构造函数无法生成,因为生成的条件是要先调用 Stack 的默认构造函数来初始化 _st1 和 _st2 ,因为我们写的不是默认构造函数,而是一个带参的函数,所以无法调用Stack 的默认构造函数,则MyQueue的默认构造函数无法生成,进而导致报错。

C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第3张图片

(7)默认构造函数可以不写吗?

一般是要写的,首先如果是下面这种非默认构造函数,初始化d1这样写是正确的,因为自己带实参了;初始化d2这样写是错误的,因为他自己没有带实参,而且也没有缺省形参,就错误。

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

int main()
{
    Date d1(2022,5,22);   //正确
    Date d2;  //错误
}    

结论:不写默认构造函数也是可以的,就看调用是否带参数,非默认构造函数->传参有实参->正确,非默认构造函数->传参无实参->错误。

为了在我们传参时有时调用写实参,有时调用不写实参,我们最好是用默认构造函数,默认构造函数d1和d2调用形式都对。

(8)我们不写构造函数编译器默认生成的构造函数干了些什么事情?

在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用? 对象调用了编译器生成的默认构造函数,但是 对象 year/ month/_day ,依旧是随机值。也就说在这里 编译器生成的默认构造函数并没有什么卵 用??
解答: C++ 把类型分成内置类型 ( 基本类型 ) 和自定义类型。内置类型就是语法已经定义好的类型:如int/char...,自定义类型就是我们使用 class/struct/union 自己定义的类型,编译器生成默认的构造函数会对自定类型成员 调用的它的默认成员函数( 默认生成构造函数对于内置类型成员变量不做处理,对于自定义类型成员变量才会处理

(9)(了解)C++11打的补丁:

针对编译器自己生成默认成员函数不初始化的问题:当既有内置类型也有自定义类型时,我们不写默认构造函数而用编译器自己生成的默认构造函数时内置类型就会是随机值, 为了使内置类型也初始化,C++11使成员变量声明时可以被赋值缺省参数,默认构造函数调用时会使用这个缺省参数

class Stack
{
public:
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		assert(_a);
	
		_top = 0;
		_capacity = capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
 
class MyQueue {
public:
	// 默认生成构造函数就可以用了

	void push(int x) {
	}
	
	int pop() {
	}
	
private:
	// C++11打的补丁,针对编译器自己生成默认成员函数不初始化的问题
	// 给的缺省值,编译器自己生成默认构造函数用
	int _size = 0;

	Stack _st1;
	Stack _st2;
};
	
int main()
{
	MyQueue q;
	
	return 0;
}

3.对构造函数的考察题

下列关于构造函数的描述正确的是( )

A.构造函数可以声明返回类型

B.构造函数不可以用private修饰

C.构造函数必须与类名相同

D.构造函数不能带参数

答案解析:

A.构造函数不能有返回值,包括void类型也不行

B.构造函数可以是私有的,只是这样之后就不能直接实例化对象

C.这是必须的

D.构造函数不光可以带参数,还可以有多个构造函数构成重载

所以选C

二.析构函数

1.概念

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

动态开辟的资源不用析构释放会永远内存泄漏吗?

所有动态开辟的资源,如果你不释放的话呢,因为它都是属于进程地址空间的。进程是正常结束的,那他这个时候进程会把这个虚拟地址和物理地址的这个页给解掉,正常进程会清理动态开辟的资源。所以说内存泄露是存在于一个进程持续运行的过程当中,那这个内存泄漏才会有持续的影响,如果比如说你一个你new了,你没释放,但是现在进程马上结束,并且是正常结束,那这个时候就不会有什么影响

2.特征

析构函数是特殊的成员函数。

特征:

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

(2. 无参数无返回值。(所以析构函数不能构成函数重载)

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

类比构造函数:类里面成员都是自定义类型成员,并且这些成员都提供了析构函数,就可以使用默认的析构函数

class Stack
{
public:
	// Init
	// Destroy

	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		assert(_a);

		_top = 0;
		_capacity = capacity;
	}

	~Stack()        //析构函数
	{
		cout << "~Stack():" << this << endl;

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

class MyQueue {
public:
	// 默认生成构造函数就可以用了
	// 默认生成析构函数就可以用了
	void push(int x) {
	}
	
	int pop() {
	}
	
private:
	Stack _st1;
	Stack _st2;
};

int main()
{
	/*Date d1;
	int i = 10;
	if (i > 0)
	{
		Stack st;
	}*/

	// 栈里面定义对象,析构顺序和构造顺序是反的
	Stack st1(1);
	Stack st2(2);

	MyQueue q;

	return 0;
}

C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第4张图片

(4. 对象生命周期结束时(最近那层大括号里面就是生命周期),C++编译系统系统自动调用析构函数。

(5. 我们不写析构函数编译器默认生成的析构函数干了些什么事情?

仍然和构造函数一样:编译器生成默认的析构函数会对自定义类型成员调用它的默认析构函数内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可

日期类没必要写析构函数,栈类这种malloc和new出新空间的才用写析构函数
class Stack
{
public:
	// Init
	// Destroy

	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		assert(_a);

		_top = 0;
		_capacity = capacity;
	}

	~Stack()
	{
		cout << "~Stack():" << this << endl;

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

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

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

	//~Date()
	//{
	//	cout << "~Date()" << endl;
	//}

private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

int main()
{
	Stack st1(1);
	Stack st2(2);
	return 0;
}

3.构造函数和析构函数自动调用的顺序

在下面这个栈里面定义对象,析构顺序和构造顺序是反的

class Stack
{
public:
	// Init
	// Destroy

	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		assert(_a);

		_top = 0;
		_capacity = capacity;
	}

	~Stack()
	{
		cout << "~Stack():" << this << endl;

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

class MyQueue {
public:
	// 默认生成构造函数就可以用了
	// 默认生成析构函数就可以用了
	void push(int x) {
	}
	
	int pop() {
	}
	
private:
	Stack _st1;
	Stack _st2;
};

int main()
{
	// 栈里面定义对象,析构顺序和构造顺序是反的
	Stack st1(1);
	Stack st2(2);

	MyQueue q;

	return 0;
}

构造函数调用:st1先,st2后

析构函数调用:st2先,st1后

考察构造,析构顺序的题:

设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为?( )

C c;
int main()
{

    A a;
    B b;
    static D d;
    return 0;
}

A.D B A C

B.B A D C

C.C D B A

D.A B D C

答案解析:

1、类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在, 因为static改变了对象的生命周期,需要等待程序结束时才会析构释放对象

   2、全局对象先于局部对象进行构造

   3、局部对象按照出现的顺序进行构造,无论是否为static

   4、所以构造的顺序为 c a b d

   5、析构的顺序按照构造的相反顺序析构,只需注意static改变对象的生命周期之后,会放在局部 对象之后进行析构。

总结来说:static修饰的局部变量的生命周期是到整个程序结束,他的生命周期肯定是出了大括号还存在的,当出了大括号肯定要先析构大括号里的局部变量,所以静态变量析构在局部变量之后;全局变量析构一定放在局部变量之后。

   6、因此析构顺序为B A D C

三.拷贝构造

1.大体的概念:创建一个新对象时,把一个已有对象完全拷贝给这个新对象:Date d2(d1)

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

2.特征

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

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

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

解释:

C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第5张图片
Date d2(d1); 对象调用类中的拷贝构造函数 Date(Date d) 时,先传参,实参d1传参给形参Date d,因为是 自定义类型对象,拷贝初始化规定要调用拷贝构造完成 ,因为这里传参就是把实参拷贝给形参,所以传参时还要调用一次拷贝构造,这次拷贝构造相当于是 Data data(d1) 去调用的(这里的data是第一次拷贝构造的形参,d1是第一次拷贝构造的实参) Date(Date d),调用这次拷贝构造又要传参,传参又要调用拷贝构造,所以形成无穷递归。所以务必加上引用, Date(Date& d)传参时就不用调用拷贝构造了,因为形参就是实参的别名(当实参,形参是内置类型时就直接赋值就行,没有内置类型的拷贝构造函数)
加const的意义
①防止写反,写成this的年月日赋值给d中年月日,如果加了const就会报错。
②const对于赋值运算符重载或拷贝构造可以防止权限被放大。详情见下文的四.7

<3. 若未显示定义,系统生成默认的拷贝构造函数。

<4.我们不写拷贝构造函数编译器默认生成的拷贝构造函数干了些什么事情?

(1)内置类型的成员会完成值拷贝,浅拷贝。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
(2)自定义类型的成员,默认生成的拷贝构造函数会 去调用这个成员的拷贝构造

<5.日期类用默认,栈类需要自己写深拷贝

那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像日期类这样的类是没必要的。栈类有必要深拷贝,深拷贝现在不在我们的学习范围。

浅拷贝:只适用于日期类这种,不适用栈类
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

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

		//d._year = _year;  //形参加上const如果写反了会报错,就可以轻松看出错误
		//d._month = _month;
		//d._day = _day;
	}

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

private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
//void Func(Date& d)
// 自定义类型对象,拷贝初始化规定要调用拷贝构造完成
void Func(Date d)
{}

int main()
{
	Date d1(2022, 5, 15);
	Func(d1);
	int x = 0;
	Func(x);

	Date d2(d1); // 拷贝构造
	d2.Print();
	d1.Print();

	return 0;
}

 错误例子:栈类用浅拷贝就会运行崩溃:

class Stack
{
public:
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		assert(_a);

		_top = 0;
		_capacity = capacity;
	}

	// st2(st1)
	// 只能深拷贝实现
	/*Stack(const Stack& st)
	{
	_a = st._a;
	_top = st._top;
	_capacity = st._capacity;
	}*/

	~Stack()
	{
		cout << "~Stack():" << this << endl;

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack st1(10);
	Stack st2(st1);
	return 0;
}

 把st1拷贝给st2,会导致st1和st2中的指针都指向同一个空间,这样有问题的,有2个问题:

①修改数据,增删查改会互相影响:当分别在st1和st2插入数据时,因为top是独立的,所以st1插入数据后 st2再插入时就会覆盖st1插入的数据,就会有问题。

②这块空间析构时会释放两次,程序会崩溃:st1和st2会分别调用析构函数,但是同一个空间不能free两次。

解决方案:要自己实现拷贝构造并且必须是深拷贝,深拷贝后面再讲。

把栈类进行浅拷贝运行结果必然会崩溃:

C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第6张图片

(但如果这里拷贝的指针变成数组就可以使用浅拷贝,指针malloc开辟的空间在堆上,数组在栈上,两个数组是独立的空间)

3.拷贝构造需要自己写的场景只有Stack这种类!而且是深拷贝。

一般的类,自己生成拷贝构造就够用了。只有像Stack这样类,自己直接管理资源,需要自己实现深拷贝,深拷贝的实现此处不提后面再说。无论是只有内置类型,还是只有自定义类型,还是内置类型和自定义类型混合MyQueue类,只要不是直接管理资源的,就不需要写拷贝构造函数。

内置类型利用默认生成的拷贝构造函数进行值拷贝,类里有自定义类型的话,类默认生成的拷贝构造函数会去调用自定义类型的拷贝构造函数,比如MyQueue类中有Stack,那么MyQueue类默认生成的拷贝构造函数就会去调用Stack类的深拷贝构造函数去拷贝这个对象的成员变量。

class MyQueue
{

private:
	int _size = 0;
	Stack _st1;
	Stack _st2;
};

int main()
{
	MyQueue mq1;
	MyQueue mq2(mq1);

	return 0;
}

4.拷贝构造的易错题

(1)题目1

C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第7张图片

 当时做这个题我一连串的疑问:B:选项说参数是对某个对象的引用,和答案不矛盾吧?C:自动生成的拷贝构造是缺省的吗?我们写的默认拷贝构造函数都没给缺省值,怎么证明自动生成的有缺省参数呢? D:选项的描述感觉在说浅拷贝,深拷贝也是把数据成员拷贝给另一个对象吗?

 答:实际上还是我的理解不够:

B说的是 "对某个对象的引用" 这个是不够准确的,必须是自身的类型的对象 才叫拷贝构造,B错。

C这里的缺省跟缺省参数没关系,缺省拷贝构造函数=默认拷贝构造函数。默认拷贝构造函数不能 保护private,而应该是共用public,C错。

D:深拷贝也要拷贝数据,D说的没问题。

所以选D。

官方解析:

A.拷贝构造函数也是一构造函数,因此不能有返回值

B.该函数参数是自身类型的对象的引用

C.自动生成的缺省拷贝构造函数,作为该类的公有成员,否则无法进行默认的拷贝构造

D.用对象初始化对象这是拷贝构造函数的使命,故正确

(2)题目2:构造,析构,拷贝构造综合题

下面分别调用了多少次构造函数,拷贝构造函数,析构函数?

答:调用1次构造,调用4次拷贝构造函数,调用5次析构函数

class Weight
{
public:
	Weight()
	{
		cout << "Weight()" << endl;
	}

	Weight(const Weight& w)
	{
		cout << "Weight(const Weight& w)" << endl;
	}

	Weight& operator=(const Weight& w)
	{
		cout << "Weight& operator=(const Weight& w)" << endl;
		return *this;
	}

	~Weight()
	{
		cout << "~Weight()" << endl;
	}

};
Weight f(Weight u)
{
	Weight v(u);
	Weight w = v;

	return w;
}

int main()
{
	Weight x;
	f(x);
	return 0;
}

调用1次构造:Weight x; 创建对象x时调用唯一 一次构造函数

调用4次拷贝构造函数:

①Weight f(Weight u) :调用f(x),把x传参给u时,因为是传值传参不是引用传参,所以调用一次拷贝构造。(若此处为Weight f(Weight& u) 引用传参,则会减少一次拷贝构造的调用)

②Weight v(u); :经典的拷贝构造初始化对象v 。

③Weight w = v; :如果是初始化就是调用拷贝构造,如果是前面已经初始化,现在是赋值 w = v 就是调用赋值运算符重载,这里显然是调用拷贝构造初始化对象w。

④return w; :返回值返回w,我们看返回值的类型是Weight ,说明是传值返回,则需要拷贝构造出一个临时对象返回,也调用了一次拷贝构造。(若此处为Weight& f(Weight u)返回引用,则会减少一次拷贝构造的调用)

总计调用了4次拷贝构造。

C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第8张图片

 调用5次析构函数:

①②③出函数f的作用域时(走完return w;后)会析构 函数内的三个对象u,v,w,调用3次析构函数。

④return w;时返回值是w拷贝构造出来的一个临时变量(临时对象),走完f(x)后,这个临时对象生命周期结束,所以析构了这个临时对象,这是第4次调用析构函数。

 ⑤走完return 0;时 析构main函数中的对象x,这是第5次调用析构函数。

总共调用了5次析构函数。

C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第9张图片

运行结果:

 C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第10张图片

(3)题目2的变形——匿名对象

匿名对象只存在于构造该对象的那行代码,离开构造匿名对象的那行代码后立即调用析构函数。

下面分别调用了多少次构造函数,拷贝构造函数,析构函数?

答:调用1次构造,调用3次拷贝构造函数,调用4次析构函数

class Weight
{
public:
	Weight()
	{
		cout << "Weight()" << endl;
	}

	Weight(const Weight& w)
	{
		cout << "Weight(const Weight& w)" << endl;
	}

	Weight& operator=(const Weight& w)
	{
		cout << "Weight& operator=(const Weight& w)" << endl;
		return *this;
	}

	~Weight()
	{
		cout << "~Weight()" << endl;
	}

};
Weight f(Weight u)
{
	Weight v(u);
	Weight w = v;

	return w;
}
int main()
{
	//Weight x;
	//f(x);
	f(Weight());   //  Weight(); 匿名对象,声明周期只在这一行

	return 0;
}

f(Weight());  中实参是匿名对象,本应该是先构造匿名对象,随后传值传参时把实参拷贝构造给形参u,但由于编译器优化,把先构造后立刻拷贝构造优化为直接构造,实参匿名对象和形参u合二为一,直接构造一个对象,所以相比题目2,这里是少调用了一次拷贝构造和一次析构函数。

C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第11张图片

 所以说 Weight x;  f(x); 和 f(Weight()); 相比,匿名对象这种传参效率更高。

(4)继续变形

下面分别调用了多少次构造函数,拷贝构造函数,析构函数?

答:调用1次构造,调用4次拷贝构造函数,调用5次析构函数

class Weight
{
public:
	Weight()
	{
		cout << "Weight()" << endl;
	}

	Weight(const Weight& w)
	{
		cout << "Weight(const Weight& w)" << endl;
	}

	Weight& operator=(const Weight& w)
	{
		cout << "Weight& operator=(const Weight& w)" << endl;
		return *this;
	}

	~Weight()
	{
		cout << "~Weight()" << endl;
	}

};
Weight f(Weight u)
{
	Weight v(u);
	Weight w = v;

	return w;
}
int main()
{
	Weight x;
    Weight ret=f(x);

	return 0;
}

 正常情况是调用5次拷贝构造: ①传值传参x->u 调用一次。②Weight v(u); 调用一次。③Weight w = v; 调用一次。④传值返回w->临时对象 调用一次。⑤临时对象给ret 调用一次。

但是由于编译器优化,一个表达式中,连续步骤的构造+拷贝构造,或者拷贝构造+拷贝构造,胆大的编译器可能就会优化,合二为一。因此④⑤合并为一次,所以一共调用4次拷贝构造

析构调用5次:分别是对象 u,v,w,ret,x。

C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第12张图片

(5)题目4的变形

答:调用2次构造,调用4次拷贝构造函数,调用6次析构函数,1次赋值运算符重载

class Weight
{
public:
	Weight()
	{
		cout << "Weight()" << endl;
	}

	Weight(const Weight& w)
	{
		cout << "Weight(const Weight& w)" << endl;
	}

	Weight& operator=(const Weight& w)
	{
		cout << "Weight& operator=(const Weight& w)" << endl;
		return *this;
	}

	~Weight()
	{
		cout << "~Weight()" << endl;
	}

};
Weight f(Weight u)
{
	Weight v(u);
	Weight w = v;

	return w;
}
int main()
{
	Weight x;
	Weight ret ;
	ret = f(x);
	return 0;
}

C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第13张图片

(6) 终极复合题

问调用了几次拷贝构造?

class Weight
{
public:
	Weight()
	{
		cout << "Weight()" << endl;
	}

	Weight(const Weight& w)
	{
		cout << "Weight(const Weight& w)" << endl;
	}

	Weight& operator=(const Weight& w)
	{
		cout << "Weight& operator=(const Weight& w)" << endl;
		return *this;
	}

	~Weight()
	{
		cout << "~Weight()" << endl;
	}

};
Weight f(Weight u)
{
	Weight v(u);
	Weight w = v;

	return w;
}
int main()
{
	Weight x;
	Weight y=f(f(x)) ;
	return 0;
}

拷贝构造图解分析:

步骤④和⑤:w拷贝构造一个临时对象,这个临时对象拷贝构造了u,然后编译器优化合二为一;相当于步骤⑧w拷贝构造一个临时对象,这个临时对象拷贝构造了y,然后编译器优化合二为一;

C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第14张图片

 C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第15张图片

5.栈类,日期类,MyQueue类对于构造,析构,拷贝构造的需求

(1)日期类:

需要自己写的:

        构造函数(全缺省构造函数 初始化年月日的值,默认生成的不会初始化内置类型)

不需要自己写的:

        析构函数(把年月日置0好像没什么意义,主要是日期类没有新开辟的空间)

        拷贝构造函数(内置类型值拷贝用默认生成的值拷贝就够用,没有指向的资源)

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)     //构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}
 
//  Date d2(d1);
//  Date(Date& d);
//	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; // 日
};

int main()
{
	Date d1(2022, 5, 15); 
	Date d2(d1);     //调用拷贝构造

	return 0;
}

(2)栈类:

都需要自己写:

        构造函数(需要初始化,默认生成的不会初始化内置类型)

        析构函数(清理指向资源)

        拷贝构造函数(必须是深拷贝,虽然都是内置类型,但是有指向的资源,浅拷贝会导致析构两次这种情况)

        赋值函数(后面会说)

class Stack
{
public:

	Stack(int capacity = 10)    //构造函数
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		assert(_a);
 
		_top = 0;
		_capacity = capacity;
	}
 
	~Stack()                    //析构函数
	{
		cout << "~Stack():" << this << endl;
 
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
                                //拷贝构造函数是深拷贝,这里暂时写不了
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
    Stack St;
    return 0;
}

(3)MyQueue类(内置类型和自定义类型混合):

class MyQueue
{

private:
	int _size = 0;   
//C++11使成员变量声明时可以被赋值缺省参数,默认构造函数调用时会使用这个缺省参数,使内置类型初始化
	Stack _st1;
	Stack _st2;
};

都不需要自己写:

        构造函数(默认生成的构造函数会去调用Stack类中的构造函数去初始化成员栈,内置类型有缺省参数可以初始化给值)

        析构函数(默认生成的析构函数会去调用Stack类中的析构函数去清理成员栈的指向资源,内置类型本身就不需要析构函数)

        拷贝构造函数(前提是里面的栈已经写了深拷贝,默认生成的拷贝构造函数会去调用Stack类中的拷贝构造函数去拷贝成员栈,默认拷贝构造也直接拷贝内置类型)

四.运算符重载

1.目的:

内置类型,可以直接用各种运算符;自定义类型,不能直接用各种运算符
为了自定义类型可以使用各种运算符,就使用运算符重载的规则(注意:这里的重载和函数重载无关,函数重载是函数名相同,参数不同,运算符重载:重新定义去控制运算符的行为和规则)

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

2.函数格式及细节:

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

函数原型:返回值类型 operator操作符(参数列表)
细节:
①不能通过连接其他符号来创建新的操作符:比如operator@
②重载操作符必须有一个类类型或者枚举类型的操作数(即只能对自定义类型进行运算符重载
③用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义让它去当-去用
④作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
.*   ::   sizeof   ?:   . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
*是可以重载的,.*不可以重载(点号.  三目运算?:  作用域访问符::  运算符sizeof   以及.*)
C++规定=,[ ],(),->这四个运算符只能被重载为类的非静态成员函数,其他的可以被友元重载(比如流提取运算符<<必须用友元),主要是因为其他的运算符重载函数都会根据参数类型或数目进行精确匹配,这四个不具有这种检查的功能,用友元定义就会出错

3.探索如果正确使用运算符重载:

判断日期类是否相等:可以写出运算符重载函数,这里没有const和引用,是未完善的版本
bool operator==(Date d1, Date d2)        //运算符重载未完善的版本
{
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}

>:1 不能把 运算符重载operator写在类的外面

类外无法访问类中的成员变量,如果非要访问,解决办法是:①把private去掉,让成员变量成为共有,显然这样是不安全的;②在类中写一个GetYear函数用于访问类中的成员变量,然后在外面利用这个函数访问成员变量,这样显然是很啰嗦很麻烦的。③利用友元才能写在外面,但是会破坏封装,这里仅做了解即可。
总结:这三种方法都有缺陷,所以不能把 运算符重载operate 写在类的外面!
错误的示例:
​​​​​​​错误的示例:
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

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

	int GetYear()
	{
		return _year;
	}

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

bool operator==(Date d1, Date d2)        //运算符重载最初始的版本
{
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}
//operator++()

int main()
{
	Date d1(2022, 5, 16);
	Date d2(2022, 5, 16);

	if (operator==(d1, d2))
	{
		cout << "==" << endl;
	}

	if (d1 == d2) // 编译器会处理成对应重载运算符调用 if (operator==(d1, d2))
	{
		cout << "==" << endl;
	}
	return 0;
}

>:2 使用if (d1 == d2) 不是 if (operator==(d1, d2))

如果我们还只是用 if (operator==(d1, d2))  的写法,那不如我们定义的时候把它写成一个普通函数得了:bool equate(Date d1, Date d2)  。这里使用if (d1 == d2) C++会做处理,编译器会处理成对应重载运算符调用 if (operator==(d1, d2))。

>:3 运算符重载operator写进类

operator写进类有this指针,所以应少写一个参数

bool operator==(Date d1, Date d2) 写成  bool operator==( Date d2) ,在编译器眼中就是:

bool operator==(Date* const this, Date d)

调用时用 if (d1 == d2) ,编译器会处理成 if ( d1.operator==(d2) ),在编译器眼中就是:d1.operator==(&d1,d2)


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

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

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

int main()
{
	Date d1(2022, 5, 16);
	Date d2(2022, 5, 16);

	if (d1.operator==(d2))
	{
		cout << "==" << endl;
	}

	if (d1 == d2) // 编译器会处理成对应重载运算符调用 if (d1.operator==(d2))
	{
		cout << "==" << endl;
	}
	return 0;
}

>:4 传参用引用传参&,并加上const

自定义类型传参会调用拷贝构造,没必要,我们用引用传参就能省去拷贝构造了,同时如果我们不改变形参,最好加上const进行保护,①防止你里面的赋值写反。②对于赋值运算符重载可以防止权限被放大,详情见 ——后面的 四.7


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

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

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

int main()
{
	Date d1(2022, 5, 16);
	Date d2(2022, 5, 16);

	if (d1.operator==(d2))
	{
		cout << "==" << endl;
	}

	if (d1 == d2) // 编译器会处理成对应重载运算符调用 if (d1.operator==(d2))
	{
		cout << "==" << endl;
	}
	return 0;
}

>:5 运算符重载operator类外面和类里面都写了,优先调用类里面的

运算符重载operator类外面和类里面都写了,编译器优先去类里面的查看是否有运算符重载,有就调用,没有就去类外面找。

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

	bool operator==(const Date& d)
	{
		cout << "里面" << endl;
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}

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

//private:
	int _year;
	int _month;
	int _day;
};
bool operator==(const Date& d1, const Date& d2)
{
	cout << "外面" << endl;
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}
int main()
{
	Date d1(2022, 5, 16);
	Date d2(2022, 5, 16);

	//if (d1.operator==(d2))
	//{
	//	cout << "==" << endl;
	//}

	if (d1 == d2) // 编译器会处理成对应重载运算符调用 if (d1.operator==(d2))
	{
		cout << "==" << endl;
	}
	return 0;
}

​​​​​​​

五.日期类的运算符重载

1.练习:写一下 if (d1 < d2) 的运算符重载

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//bool operator<(Date d)
	//{
	//	if (_year < d._year)
	//	{
	//		return true;
	//	}
	//	else if(_year == d._year)
	//	{
	//		if(_month < d._month)
	//		{
	//			return true;
	//		}
	//		else if (_month == d._month)
	//		{
	//			if (_day < d._day)
	//			{
	//				return true;
	//			}
	//		}
	//	}
	//	return false;
	//}
	bool operator<(const Date& d)
	{
		if ((_year < d._year)
			|| (_year == d._year && _month < d._month)
			|| (_year == d._year && _month == d._month && d._day < d._day))
		{
			return true;
		}
		else
		{
			return false;
		}
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

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

int main()
{
	Date d1(2022, 3, 16);
	Date d2(2022, 5, 16);

	if (d1 < d2) // 编译器会处理成对应重载运算符调用 if (d1.operator<(d2))
	{
		cout << "<" << endl;
	}
	return 0;
}

2.练习:写一下 d1 = d2 的运算符重载

(1)d1 = d2,if (d1 < d2),if (d1 == d2)运算符重载的代码

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

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

	bool operator<(const Date& d)
	{
		if ((_year < d._year)
			|| (_year == d._year && _month < d._month)
			|| (_year == d._year && _month == d._month && d._day < d._day))
		{
			return true;
		}
		else
		{
			return false;
		}
	}
	Date& operator=(const Date& d)        //d2 = d1的运算符重载
	{
		if (this != &d)     //自己赋给自己,当成员变量太多时浪费时间效率,防止自己赋给自己
//写成if (*this != d) 比较内容不行,因为!=没有运算符重载,只能通过比较指针判断;
//假设有运算符重载,那也是调用函数,调用函数肯定没原生指针比较效率高
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;          //为了满足连等d3 = d2 = d1;要给返回值 
	}

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

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

int main()
{
	Date d1(2022, 3, 16);
	Date d2(2022, 5, 16);
    Date d3(d1); // 拷贝构造 
	if (d1 == d2) // 编译器会处理成对应重载运算符调用 if (d1.operator==(d2))
	{
		cout << "==" << endl;
	}
	if (d1 < d2) // 编译器会处理成对应重载运算符调用 if (d1.operator<(d2))
	{
		cout << "<" << endl;
	}
    d3 = d2 = d1;     // 赋值重载/复制拷贝 -- 两个已经存在对象之间赋值
	//d2 = d1;
	d2.Print();

	return 0;
}

解释: d1 = d2 

①形参的const Date& d,const防止形参改变,引用传参减少拷贝构造函数调用

②if (this != &d)     防止自己赋给自己,当成员变量太多时浪费时间效率

③为了满足连等d3 = d2 = d1;的情况,赋值运算符重载要给返回值,比如d2=d1,赋值完后返回Date& *this,也就是d2的引用了,则可以继续进行赋值 d3=d2 了。

④返回值类型给Date的话是传值拷贝,这个返回值再赋值给main函数的一个变量会调用拷贝构造,所以改成引用,这里的引用是*this,*this就是调用的那个对象d2,返回的是d2的引用。

(2)赋值运算符重载和拷贝构造的区别

用一个对象初始化另一个对象时调用拷贝构造,如果是给已初始化对象赋值就调用运算符重载。

class Date中:

    Date& operator=(const Date& d)
	{
		if (this != &d)
		{
    		_year = d._year;
			_month = d._month;
			_day = d._day;
		}

		return *this;
	}
和
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
    
    void TestDate2()
    {
	    Date d1(2022, 5, 18);
	    Date d2 = d1 + 15;   //调用拷贝构造    
	    Date d3 = d1;        //调用拷贝构造 Date d3(d1);
	    d3 = d1 + 15;        //调用赋值运算符重载
    }

3.有意义的运算符才会重载,我们还可以重载很多日期类运算符。比如== , < , <= , > , >= , !=

(1)此处是声明写在类中,定义写在类外的形式(== , < , <= , > , >= , != 的运算符重载。)

(构造函数就必须改成下面格式,使输入的日期合法化。)

Date.h

#pragma once
#include
#include
using std::cin;    
using std::cout;
using std::endl;
class Date
{
public:
	bool isLeapYear(int year)
	{
		return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
	}
	// 获取某年某月的天数
	int GetMonthDay(int year, int month);
	
	// 全缺省的构造函数
	Date(int year = 1900, int month = 1, int day = 1);	

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

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	// 析构函数
	~Date()
	{
		_year = 0;
		_month = 0;
		_day = 0;
	}

	//运算符重载
	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);	// != 复用 ==

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

Date.cpp

#define _CRT_SECURE_NO_WARNINGS
#include"Date.h"
int Date::GetMonthDay(int year, int month)
{
	assert(year >= 0 && month > 0 && month < 13);
	const static int monthDayArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };  
//1.static细节:不改变数组内容时,加上static防止每次调用都开辟额外空间
	if (month == 2 && isLeapYear(year))
	{
		return 29;
	}
	return monthDayArray[month];
}

Date::Date(int year, int month , int day )	//2.声明给缺省,定义就不给
{
	if (year >= 1 && month >= 1 && month <= 12 && day <= GetMonthDay(year, month))
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		cout << "非法日期" << endl;
	}
}

// ==运算符重载	只写==和<,其他都复用
bool Date::operator==(const Date& d)
{
	return _year == d._year
		&& _month == d._month
		&& _day == d._day;
}

//
bool Date::operator < (const Date& d)
{
	if (_year < d._year 
		||(_year == d._year && _month < d._month)
		||(_year == d._year && _month == d._month && _day < d._day))
	{
		return true;
	}
	else
	{
		return false;
	}
}
// d1 <= d2 
bool Date::operator <= (const Date& d)
{
	return *this < d || *this == d;
}
bool Date::operator >(const Date& d)
{
	return !(*this <= d);
	//return d < *this;  
这样复用<也是可以的,但是d是const,传给<的非const this指针 
是权限的放大,所以务必在<的运算符重载后加上const,
原则:成员函数内只要不改成成员变量,建议都加const
}
bool Date::operator >= (const Date& d)
{
	return *this > d || *this == d;
}
bool Date::operator != (const Date& d)
{
	return !(*this == d);
}

test.cpp

void TestDate1()
{
	Date d1(2022, 5, 18);
	Date d2(2023, 3, 20);
	Date d3(2023, 3, 20);

	cout << (d1 < d2) << endl;
	cout << (d1 > d2) << endl;
	cout << (d1 == d3) << endl;
	cout << (d2 <= d3) << endl;
	cout << (d2 == d3) << endl;
}

int main()
{
	TestDate1();

	return 0;
}

(2)把定义写在类中(== , < , <= , > , >= , != 的运算符重载。

我们发现复用的那几个定义都比较短,所以我们可以把他们写成内联函数在调用处展开可以提高效率,复用的运算符重载函数把声明写在类中,定义写在类外的时候如果在定义处直接前面加上inline是不能内联的,原因是:内联函数不能声明和定义分离(即:inline不支持声明和定义分别放到.h 和.cpp),(Date.h 在Date.cpp中被展开,因为声明是inline,符号表不会生成函数地址,当test.cpp 中调用函数f时,call(函数名) 这个指令去符号表找函数名和地址映射关系时,找不到函数地址,则无法展开。)

如果想把复用函数变成内联,只需要利用类的特性:在类里面定义的函数默认是inline所以成员函数中要成为inline最好直接在类里面定义

(写完整过程的函数 == 和 < 函数比较长就不内联了)

Date.h

#pragma once
#include
#include
using std::cin;
using std::cout;
using std::endl;
class Date
{
public:
	bool isLeapYear(int year)
	{
		return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
	}
	// 获取某年某月的天数
	int GetMonthDay(int year, int month);
	
	// 全缺省的构造函数
	Date(int year = 1900, int month = 1, int day = 1);
	

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

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	// 析构函数
	~Date()
	{
		_year = 0;
		_month = 0;
		_day = 0;
	}

	//运算符重载
	bool operator ==(const Date& d);	//只写<和==,剩下的复用
	bool operator < (const Date& d);	
	bool operator <= (const Date& d)	// <= 复用 < 和 ==
	{
		return *this < d || *this == d;
	}
	bool operator >(const Date& d)	// > 复用 <=
	{
		return !(*this <= d);
		//return d< *this; 暂时不行,权限放大
	}
	bool operator >= (const Date& d)	// >= 复用 <
	{
		return *this > d || *this == d;
	}
	bool operator != (const Date& d)	// != 复用 ==
	{
		return !(*this == d);
	}
private:
	int _year;
	int _month;
	int _day;
};

4.+ ,+= 的运算符重载

Date.h

#pragma once
#include
#include
using std::cin;    
using std::cout;
using std::endl;
class Date
{
public:
	bool isLeapYear(int year)
	{
		return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
	}
	// 获取某年某月的天数
	int GetMonthDay(int year, int month);
	
	// 全缺省的构造函数
	Date(int year = 1900, int month = 1, int day = 1);	

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

	// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
	Date& operator=(const Date& d)		
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	// 析构函数
	~Date()
	{
		_year = 0;
		_month = 0;
		_day = 0;
	}

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

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

 Date.cpp

#define _CRT_SECURE_NO_WARNINGS
#include"Date.h"
int Date::GetMonthDay(int year, int month)
{
	assert(year >= 0 && month > 0 && month < 13);
	const static int monthDayArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };  
//1.static细节:不改变数组内容时,加上static防止每次调用都开辟额外空间
	if (month == 2 && isLeapYear(year))
	{
		return 29;
	}
	return monthDayArray[month];
}

Date::Date(int year, int month , int day )	//2.声明给缺省,定义就不给
{
	if (year >= 1 && month >= 1 && month <= 12 && day <= GetMonthDay(year, month))
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		cout << "非法日期" << endl;
	}
}

// 官方写法
// d1 += 100 日期+=天数
Date& Date::operator+=(int day)
{
	if (day < 0)                //+=负天数会有问题,直接取正复用下面的-=即可
		return *this -= -day;

	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		_month++;
		if (_month == 13)
		{
			++_year;
			_month = 1;
		}
	}

	return *this;
}

//个人的写法 日期+=天数 (仅仅是部分细节不同,思路相同)
//Date& Date::operator+=(int day)
//{
//	_day += day;
//	int daymax = GetMonthDay(_year, _month);
//	while (_day > daymax)
//	{
//		_day -= daymax;
//		++_month;
//		if (_month > 12)
//		{
//			_year++;
//			_month -= 12;
//		}
//		daymax = GetMonthDay(_year, _month);
//	}
//	return *this;
//}

// 官方复用写法
Date Date::operator+(int day)
{
	Date ret(*this);
	ret += day;

	return ret;
}

//官方常规写法
//Date Date::operator+(int day)
//{
//	Date ret(*this);
//
//	ret._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;
//}

//个人的写法:
//Date Date::operator+(int day)
//{
//	Date ret(*this);
//	ret._day += day;
//	int daymax = GetMonthDay(ret._year, ret._month);
//	while (ret._day > daymax)
//	{
//		ret._day -= daymax;
//		++ret._month;
//		if (ret._month > 12)
//		{
//			ret._year++;
//			ret._month -= 12;
//		}
//		daymax = GetMonthDay(ret._year, ret._month);
//	}
//	return ret;
//	//3.ret是临时变量,所以返回类型不能是引用(不能返回ret的引用,ret出作用域就销毁)
//}

test.cpp


void TestDate2()
{
	Date d1(2022, 5, 18);
	Date d2 = d1 + 15;
    Date d3;
	d3 = d1 + 15;
	d1.Print();
	d2.Print();

	d1 += 15;
	d1.Print();
}
int main()
{
	TestDate2();

	return 0;
}

 运行结果:

​​​​​​​C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第16张图片

(1)此处的细节:const对于赋值运算符重载或拷贝构造可以防止权限被放大。

比如这里 Date d2 = d1 + 15; 右边d1 + 15后类型是Date,他不是引用,而是函数内ret的一个临时拷贝,临时拷贝具有常性,自带const小权限,而 Date d2 = d1 + 15; 整体是拷贝构造Date(const Date& d),若不加const,形参就是非const大权限,实参给形参,小权限变成大权限属于权限放大,会报错

C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第17张图片

 (2)+和+=的互相复用,+复用+=更优一些!

C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第18张图片

 从细节上来讲:+复用+=更优一些,原因如下:

+复用+=:调用+=时没有产生临时拷贝,调用+时拷贝构造产生一次临时拷贝,返回值产生一次临时拷贝,一共2次;

+=复用+:调用+时拷贝构造产生一次临时拷贝,返回值产生一次临时拷贝,一共2次;调用+=时因为是复用了+,所以也会产生2次临时拷贝。相比之下还是+复用+=临时拷贝少,更优

5.- ,-= 的运算符重载

Date.cpp

Date Date::operator-(int day)
{
	Date ret = *this;
	ret -= day;
	return ret;
}

// d1 -= 100
Date& Date::operator-=(int day)
{
	if (day < 0)            //天数为负时,-=就会有问题,直接取正复用+=即可
		return *this += -day;

	_day -= day;
	while (_day <= 0)
	{
		--_month;
		if (_month == 0)
		{
			_month = 12;
			--_year;
		}

		_day += GetMonthDay(_year, _month);
	}

	return *this;
}

test.cpp

void TestDate3()
{
	Date d1(2022, 5, 18);
	Date d2 = d1 - 30;
	d2.Print();
	d1 -= 30;
	d1.Print();

}
int main()
{
	TestDate3();
	
	return 0;
}

运行结果:

6.前置,后置++

规定:因为参数名一样,为了区分需要写函数重载

Date& operator++() 是前置++ ;Date operator++(int ) 带参数的是后置++,(一般那个参数都不写,没必要写)

细节1:后置++的形参不能给全缺省参数,错误示例:Date operator++(int i=0),调用时传参如果带参肯定传给后置++,如果不带参就不知道传给前置还是后置++了,因为不带参既可以给没参数的前置++,也可以给全缺省的后置++。

细节2:Date operator++(int ) 后置++的形参规定必须是int型,别的类型不可以。

定义在类中:(类在头文件中)
    // 前置++
	Date& operator++()
	{
		*this += 1;
		return *this;
	}
	// 后置++
	Date operator++(int i)
	{
		Date tmp(*this);
		*this += 1;
		return tmp;
	}

——————————————————test.cpp
void TestDate4()
{
	Date d3(2022, 5, 18);
	d3.Print();
	Date ret1 = ++d3;	//d3.operator++()
	ret1.Print();
	d3.Print();

	Date ret2 = d3++;	//d3.operator++(0)
	ret2.Print();
	d3.Print();
}
int main()
{
	TestDate4();
	
	return 0;
}

C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第19张图片

7.日期-日期 返回天数

直接减需要考虑进位很麻烦,直接复用加法的进位

————————————————写在类中
// 日期-日期 返回天数
int Date::operator-(const Date& d)
{
	int flag = 1;
	Date max = *this;
	Date min = d;
	if (max < min)
	{
		max = d;
		min = *this;
		flag = -1;
	}
	int n = 0;
	while (min != max)
	{
		n++;
		min++;
	}
	return n * flag;
}

——————————————test.cpp
void TestDate5()
{
	Date d1(2022, 5, 18);
	Date d2(2020, 2, 4);
	cout << (d1 - d2) << endl;
	cout << (d2 - d1) << endl;
}
int main()
{
	TestDate5();
	
	return 0;
}

8.const的细节

(1)权限放大问题

在TestDate6函数中用非const修饰的对象d1调用Print函数没问题,但是在Func中调用就不行:由于Func中参数const,调用Func函数时,d1传参给d,d1的权限是非const,d的权限是const,在Func中用const修饰的对象d调用Print函数,&d的类型是const Date*,但是Print函数中隐含的this指针就是非const类型的,把const 的 &d 传给非const的 this指针 是权限的放大,会报错,应该想办法把Print函数的this指针改成const修饰,因此有 const修饰类的成员函数:

 (2)const修饰类的成员函数:

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

C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第20张图片

 (2)const修饰类的成员函数的格式:

在函数最后加const,如果声明定义分离,声明和定义后面都要加const

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


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

void Print(const Date* const this) 中, 红色的const是我们加的,蓝色的const是this指针自带的不能修改指针本身,

(3)权限放大问题的解决方法:

所以在.h文件 类 中的Print函数后加上const即可

.h文件 类 中的Print函数:

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

void Func(const Date& d)
{
	d.Print(); // d1.Print(&d); -> const Date*
}

test.cpp中:

void TestDate6()
{
	Date d1(2022, 5, 18);
	d1.Print(); // d1.Print(&d1); -> Date* 
	Func(d1);
}
int main()
{
	TestDate5();
	
	return 0;
}

其他的不用修改this指针指向内容的运算符重载函数我们都需要在后面加上const,==,<,>,+,-等等,+=和-=因为要改变this指针指向内容的不能加。

(4)const使用规则

建议成员函数中不修改成员变量的成员函数,都可以加上const,普通对象和const对象都可以调用
 

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

答:不可以,调用时实参是const修饰的,形参是非const,传参就是const给非const,权限放大,不可以。
2. 非const对象可以调用const成员函数吗?
答:可以,同理,权限缩小(可读可写改成只读)是可以的。
3. const成员函数内可以调用其它的非const成员函数吗?
答:不可以。下图所示当给+运算符重载函数用const修饰时,内部使用到了非const指针GetMonthDay函数就报错!!!,如果想正常运行还得给 GetMonthDay函数 加const,GetMonthDay函数 里面还有个非const的isLeapYear也得改成非const,使用就很麻烦。不如直接把+复用+=就很简单。
C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第21张图片

 +复用+=

 C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第22张图片

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

答:可以的,类似的是权限缩小。

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

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

.h的类中
	Date* operator&()    //取地址操作符重载
	{
		return this;
	}

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

test.cpp中:

void Func(const Date& d)
{
	d.Print(); // d1.Print(&d); -> const Date*

	cout << &d << endl;
}
void TestDate6()
{
	Date d1(2022, 5, 18);
	d1.Print(); // d1.Print(&d1); -> Date* 
	Func(d1);
}
int main()
{
	TestDate6();
	
	return 0;
}

C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_第23张图片

你可能感兴趣的:(C++前期,c++)