C++类和对象(中)

目录

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

2.构造函数 

2.1构造函数的概念

2.2构造函数的重载

2.3默认构造函数

2.4总结

3.析构函数

3.1析构函数的概念

3.2编译器自动生成的析构函数会做那些事情呢?

3.3析构函数的析构顺序

4.拷贝构造函数(复制构造函数)

4.1拷贝构造的概念

4.2拷贝构造的特征

4.3拷贝构造函数被调用的三种情况

4.4浅拷贝和深拷贝的区别

5.赋值运算符重载

5.1运算符的重载

5.2赋值运算符重载

5.3前置++和后置++(前置--和后置--)重载

六、const成员及const取地址运算符重载


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

如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
 

class Date {};

a5fbe7381df04b2989ff528e6bcb8bfc.png

2.构造函数 

#include<iostream>
using namespace std;
class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
int main()
{
	Date d1, d2;
	d1.Init(2023, 5, 2);
	d2.Init(2023, 5, 10);
	return 0;
}

 像上面的代码,可以发现我们每定义一个对象就得初始化一次,这样是不是觉得很麻烦呢,而且有时还会有遗忘的时候,这样就达不到我们预期的效果了。于是就有了构造函数

2.1构造函数的概念

在C++中,有一种特殊的成员函数,它的名字和类名相同,没有返回值,不需要用户显式调用,而是在创建对象时自动执行,用来初始化对象。这种特殊的成员函数叫构造函数

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year, int month, int day)//定义构造函数,注意前面不用加void
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void print()//定义普通成员函数
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
int main()
{    
	Date d1(2023, 5, 3);//创建时向构造函数传参
	Date d2(2023, 5, 7);
	d1.print();//调用普通成员函数
	d2.print();
	return 0;
}

上面的构造函数好虽然是好,但是当我们像数据结构定义栈的时候,就会发现我们并不知道我们需要多大的空间,所以这时我们就可以用缺省参数来指定一个默认值。如下:

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year=1, int month=1, int day=1)//定义构造函数,注意前面不用加void
	{
		_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 d2(2023, 5, 7);
	d1.print();
	d2.print();
	return 0;
}

b055064745b741848998accb603987d6.png

可以看到,当不传参的时候就使用了我们指定的默认值,传参时就是用传的实参

2.2构造函数的重载

和普通的成员函数一样,构造函数是允许重载的,一个对象可以有多个重载的构造函数,创建对象时根据实参,编译器会自动匹配最优的那一个构造函数。

#include<iostream>
using namespace std;
class Date
{
public:
	Date()
	{

	}
	Date(int year, int month, int day)//定义构造函数,注意前面不用加void
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void print()//定义成员函数
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year=2; //C++11支持的,此时表示声明,等创建对象时才是定义
	int _month=2; //没有传参的话,就用这个默认值
	int _day=2; //此时还未开辟空间,定义是才开
};
int main()
{
	Date d1;//使用默认值,不需要传参
	Date d2(2023, 5, 7);
	d1.print();
	d2.print();
	return 0;
}

94124ad474f049289532ff020a0453ea.png

 可以发现,创建第一个对象时调用了第一个构造函数,创建第二个对象时调用了第二个构造函数

2.3默认构造函数

①如果用户没有显示定义构造函数,那么编译器会自动生成一个构造函数,此时的这个构造函数就叫默认构造函数。只是这个构造函数的函数体是空的,也没有形参,也不执行任何操作。如:

#include<iostream>
using namespace std;
class Date
{
public:
	
private:
	int _year; 
	int _month; 
	int _day; 
};
int main()
{
	Date d1;
	return 0;
}

74e02df6a47743a195b3b38c1b4e7f17.png

 通过调试可以看见,关于编译器生成的默认成员函数,大家一定会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?

d1对象调用了编译器生成的默认构造函数,但是d1对象的成员变量_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??
原因:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型,看看
下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数

#include<iostream>
using namespace std;
class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d2;
	return 0;
}

49e5238252d547bb9f158fc7bc2456a6.png

 可以发现编译器自动生成的默认构造函数对自定义类型进行了处理(调用了自定义类型自己的构造函数),对内置类型不做处理

②调用默认构造函数时不能加括号

#include<iostream>
using namespace std;
class Date
{
public:
	
private:
	int _year=2; 
	int _month=2; 
	int _day=2; 
};
int main()
{
	Date d1();
	return 0;
}

a1a2228449d041df9da8ba65d1de368b.png

上面的代码,通过调试可以看到,我们的本意本来是调用构造函数创建对象,但现在却没都能创建对象和初始化对象,为什么会这样呢?

因为“Date d1()“语句可以解释为对函数的声明或对函数的调用,但C++分析程序时更偏向于声明,所以该语句被视为了函数的声明

2.4总结

 注:构造函数必须是public属性的,否则创建对象时无法调用,当然,设置成private、protected也可以,但是这样就没有意义了。

构造函数没有返回值,因为构造函数不需要变量来接受返回值,有了返回值也毫无意义

①不管是声明还是定义,函数名前面都不能出现返回值类型,即使是void也不允许;

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

③构造函数的调用是强制性的,一旦类中定义或声明了构造函数,那么创建对象时就一定要调用,不调用是错误。如果有多个重载的构造函数,那么创建对象时提供的实参必须和其中的一个构造函数匹配,也就是说创建一个对象只会调用一个构造函数。

④一个类必须有构造函数,要么用户自己定义,要么编译器自动生成。一旦用户自己定义或声明了构造函数,编译器就不会再自动生成。

⑥编译器自动生成的默认构造函数,对内置类型不做处理,对自定义类型会去调用自定义类型自己的构造函数

调用构造函数时不能加括号

3.析构函数

3.1析构函数的概念

创建对象时系统会自动调用构造函数进行初始化工作,同样,销毁对象时系统也会自动调用一个函数来进行清理工作,比如释放分配的内存,关闭打开的文件。

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由
编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数也是一种特殊的成员函数,没有返回值,不需要我们显示调用,在销毁对象时自动执行。

析构函数特征:

①析构函数是在类名前加上~符号。

②无参数无返回值类型

③析构函数不能被重载,所以一个类中有且只能有一个析构函数,如果用户没有显示定义,那么编译器会自动生成一个默认的析构函数

④对象声明周期结束时,C++编译系统自动调用析构函数

如下:

#include<iostream>
using namespace std;
typedef int SLtype;
class SL
{
public:
	SL(size_t capacity=4)
	{
		_a = (SLtype*)malloc(sizeof(SLtype)*capacity);
		if (!_a)
		{
			printf("malloc fail");
			exit(-1);
		}
		_size = 0;
		_capacity = capacity;
	}
	~SL()//定义析构函数
	{
		cout << "~SL()" << endl;//为了方便观察结果
		if (_a)
		{
			free(_a);
			_a = nullptr;
			_size = 0;
			_capacity = 0;
		}
	}
private:
	SLtype* _a;
	size_t _size;
	size_t _capacity;
	
};
int main()
{
	SL sl;
	return 0;
}

bca25f2ff38a41c7a5b80e6df780ce12.png

 当对象生命周期结束时,会自动调用析构函数,释放我们申请来的空间,如果我们没有申请空间可以不显示定义析构函数,让编译默认生成一个就好了

3.2编译器自动生成的析构函数会做那些事情呢?

#include<iostream>
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;
}

de91506966364d0db258354bbb1fc8e2.png

 通过上面的代码和结果可以发现,编译器调用Time对象的析构函数。

我们并没有在main函数中创建Time对象,为什么会调用呢?

因为main函数中创建Date 对象d,d中包含了四个成员变量,其中三个(_year,_month,_day)为内置类型,销毁时不需要资源的清理,最后系统直接将内存回收即可

而_t对象是自定义类型(Time类),所以销毁对象d时,要将其内部的Time类对象_t销毁,所以要调用Time类对象的析构函数。

但是:
main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁

3.3析构函数的析构顺序

#include<iostream>
using namespace std;
class Date
{
public:
	//构造函数
	Date(size_t year = 1, size_t month = 1, size_t day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	~Date()//析构函数
	{
		cout << (*this)._year << '-' << (*this)._month << '-' << (*this)._day << endl;
	}
private:
	size_t _year;
	size_t _month;
	size_t _day;
};
int main()
{
	Date d1;
	Date d2(2023,5,3);
    
	return 0;
}

39122fec1b5d476c8bda89a75720c73d.png

通过上面的程序和图可以发现先调用的d2的析构函数,然后才调用d1的析构函数,总结:

对象的销毁就像入栈出栈一样,和数据结构中栈的结构有点类似。

先定义的对象后销毁,后定义的对象先销毁
 注意:创建哪个类的对象则调用该类的构造函数,销毁那个类的对象则调用该类的析构函数

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


4.拷贝构造函数(复制构造函数)

4.1拷贝构造的概念

现实中有很多一模一样的陶瓷,我们称它们为复制品

那在创建对象的时候,能否创建一个和已存在对象一模一样的对象呢?显然这是可以的

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

4.2拷贝构造的特征

①拷贝构造函数的参数类型为本类类型,该参数可以是const修饰的也可以不是const修饰的。一般使用前者,这样既能用常量对象(初始化后值不能被改变的对象)作为参数,也能用非常量对象作为参数来初始化其他对象。当然也可以用函数重载的方法来处理。

#include<iostream>
using namespace std;
class Date
{
public:
	//构造函数
	Date(size_t year = 1, size_t month = 1, size_t day = 1)
	{
		cout << "构造Date()" << endl;
		_year = year;
		_month = month;
		_day = day;
	}
	//拷贝构造函数
	Date(const Date& D)
	{    
		cout << "拷贝构造Date(Date& D)" << endl;
		_year = D._year;
		_month = D._month;
		_day = D._day;
	}
private:
	size_t _year;
	size_t _month;
	size_t _day;
};
int main()
{
	Date d;//调用构造函数
	Date d1(d);//调用拷贝构造函数

	return 0;
}

14dfc9311d2243d894ba7cbfbd0a6872.png

可以发现,main函数中第一条语句调用的构造函数,第二条语句调用的拷贝构造函数

接着看下面的程序:

#include<iostream>
using namespace std;
class Date
{
public:
	//构造函数
	Date(size_t year = 1, size_t month = 1, size_t day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	size_t _year;
	size_t _month;
	size_t _day;
};
int main()
{
	Date d;//调用构造函数
	Date d1(d);//调用默认的拷贝构造

	return 0;
}

5e974666885c443e9a1d92a5b5a76571.png

 通过上面的程序可以发现,如果没有显式定义拷贝构造函数,编译就会自动生成一个拷贝构造函数。大多数情况下,其作用是实现从原对象到目标对象逐个字节的复制,即使得目标对象的每个成员变量都变得和原对象相等。编译器自动生成的拷贝构造函数称为默认拷贝构造函数

注:默认构造函数不一定存在,但是拷贝构造函数总是存在的。如果显示定义或声明了拷贝构造函数,那么编译器就不会再生成拷贝构造函数了。

实际上拷贝构造函数是特殊的构造函数,拷贝构造函数是构造函数的一份重载。

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

#include<iostream>
using namespace std;
class Date
{
public:
	//构造函数
	Date(size_t year = 1, size_t month = 1, size_t day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(Date D)//拷贝构造函数
	{
		_year = D._year;
		_month = D._month;
		_day = D._day;
	}
private:
	size_t _year;
	size_t _month;
	size_t _day;
};
int main()
{
	Date d;
	Date d1(d);

	return 0;
}

8da61d2c0cc24cb9b980361873afdc2e.png

 看上面的程序和结果就可以发现,拷贝构造函数的参数只能有一个且必须是类类型对象的引用,不能用传值传参的方式定义拷贝构造函数

4.3拷贝构造函数被调用的三种情况

当一个对象初始化同类的另一个对象时,会引发拷贝构造函数的调用,如:

Date d1(d);
Date d2 = d;//不是赋值语句,是调用拷贝构造函数初始化

注意:此时第二条语句不是赋值语句,是初始化语句,此时变量还不存在所以调用拷贝构造初始化。赋值语句是等号左边的 变量是一个早就存在(已定义)的变量时,才是赋值,赋值语句不会调用拷贝构造函数。

通过下边的语句进行比较:

date d;
Date d1(d);//该语句不是赋值语句,是初始化语句
date d2;
d2=d1;//该语句表示赋值语句

此时的第四条语句才是赋值语句,不会调用拷贝构造函数,因为d2已经存在,已经被初始化过了。

②如果函数的参数是类Date的对象,那么当该函数被调用时,类Date的拷贝构造函数就将被调用。就是说调用拷贝构造函数用来初始化形参。如:

#include<iostream>
using namespace std;
class Date
{
public:
	//构造函数
	Date(size_t year = 1, size_t month = 1, size_t day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date& After(Date D)//调用了拷贝构造来初始化形参D
	{
		++D._year;
		++D._month;
		++D._day;
		return D;
	}
private:
	size_t _year;
	size_t _month;
	size_t _day;
};
int main()
{
	Date d;
	d =d.After(d);//调用了拷贝构造来初始化形参D

	return 0;
}

向上面的程序就调用了拷贝构造函数来初始话形参D。

③如果函数的返回值是类Date的对象,则函数返回时,类Date的拷贝构造函数被调用。也就是说,作为函数返回值的对象是拷贝构造函数来初始化的。众所周知,如果函数返回值很小的话会保存在寄存器中,当返回的值很大时,就会产生一个临时变量,这个变量是Date类的对象时,就会调用拷贝构造函数初始化该临时变量。如:

Date Func()
	{
		(*this)._day += 1;
		return *this;
	}

上面的程序返回的就是一个Date类的对象,所以会调用拷贝构造函数初始化该函数的返回值

但一般都不会这样写,因为这样的返回效率太低了,传引用返回就好了,这样就不会调用拷贝构造函数了。

Date& Func()//传引用返回
	{
		
	}
void Func(Date& D)//传引用调用
	{
		
	}

以上两种情况都不会调用拷贝构造,这样的效率会比较高。但需要注意此时传回来和传过去的对象的值可以被改变,所以需要多加注意。

4.4浅拷贝和深拷贝的区别

浅拷贝,是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精准拷贝。如果属性是基本类型,那么拷贝的就是基本类型(int 、char等普通变量类型)的值;如果属性是内存地址(引用类型)(int*、char* 等指针类型),那么拷贝的就是内存地址,因此如果其中一个对象改变了,就会影响另一个对象。

编译器生成的默认拷贝构造函数属于浅拷贝

在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定
义类型是调用其拷贝构造函数完成拷贝的。
 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?
当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
 

#include<iostream>
using namespace std;
typedef int SLtype;
class SL
{
public:
	SL(size_t capacity=4)//构造函数
	{
		_a = (SLtype*)malloc(sizeof(SLtype)*capacity);
		if (!_a)
		{
			printf("malloc fail");
			exit(-1);
		}
		_size = 0;
		_capacity = capacity;
	}
	~SL()//析构函数
	{
		cout << "~SL()" << endl;
		if (_a)
		{
			free(_a);
			_a = nullptr;
			_size = 0;
			_capacity = 0;
		}
	}
private:
	SLtype* _a;
	size_t _size;
	size_t _capacity;
	
};
int main()
{
	SL sl;
	SL sl2(sl);//调用拷贝构造函数
	return 0;
}

结果: 

769b14f9778243c781ece45c6cb810c4.png

 可以发现它们的成员变量一模一样,且sl对象的成员变量_a和sl2对象的成员变量_a所指向的地址一样,这样程序结束时就会发生错误

因为当对象生命周期结束时,会调用析构函数,而sl对象的成员变量_a和sl2对象的成员变量_a所指向的空间是动态申请来的,最后该空间将会被释放两次,当该空间第二次被释放时属于非法访问了,因为第一次释放该空间就已经还给系统了,不能再被释放了。如:

a8d4e7988a9e472683069a8e85bc7d81.png

 通过上图就可知道,调用了两次析构函数,同一块空间被释放了两次,所以报错了。

那这样的问题应该怎么解决呢?这时就可以使用我们的深拷贝来实现了(需要我们显示定义)

深拷贝:将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新的对象,且修改新对象的值不会影响原对象。就是说如果拷贝的类型是内存地址(引用类型)那么就会申请另一块空间来存储该内存地址中的拷贝出来的内容。如:

#include<iostream>
using namespace std;
typedef int SLtype;
class SL
{
public:
	SL(size_t capacity=4)
	{
		_a = (SLtype*)malloc(sizeof(SLtype)*capacity);
		if (!_a)
		{
			printf("malloc fail");
			exit(-1);
		}
		_size = 0;
		_capacity = capacity;
	}
	SL(SL& sl)//拷贝构造
	{
		this->_a = (SLtype*)malloc(sizeof(int) * sl._capacity);
		if (!this->_a)
		{
			printf("malloc fail");
			exit(-1);
		}
		for (int i = 0; i < sl._size; ++i)//把数据拷贝一份到新的空间
		{
			this->_a[i] = sl._a[i];
		}
		this->_capacity = sl._capacity;
		this->_size = sl._size;
	}
	~SL()
	{
		cout << "~SL()" << endl;
		if (_a)
		{
			free(_a);
			_a = nullptr;
			_size = 0;
			_capacity = 0;
		}
	}
	SLtype* _a;
	size_t _size;
	size_t _capacity;
	
};
int main()
{
	SL sl;
	SL sl2(sl);//调用拷贝构造函数
	return 0;
}

dbf907da038a44ea8a7b3b70d1a95f4b.png

 通过上面的程序和图可以发现此时sl的成员变量_a和sl2的成员变量_a指向的不再是同一块空间,也析构了两次,且没有发生报错。 

注:引用类型表示内存地址存储在栈上,内存空间在堆区。就像sl的_a成员是在栈上开辟的,用来存储动态开辟出来的空间的地址,而该空间在堆上。引用类型和C++的引用不同,讲究的是一个解引用。解引用在栈上存储的地址,访问到堆区的空间。

5.赋值运算符重载

5.1运算符的重载

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

返回值类型和参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型返回值类型  operator 操作符 (参数列表)

#include<iostream>
using namespace std;
class Date
{
public:
	//构造函数
	Date(size_t year = 1, size_t month = 1, size_t day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
//需注意左操作数是隐参数this
//底层样式为:bool operator<(Date* this,const Date& D)
	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:
	size_t _year;
	size_t _month;
	size_t _day;
};
int main()
{
	Date d1(2023,5,3);
	Date d2(2023,5,4);
//第一种调用方式
	d1<d2;
//第二种调用方式
   d1.operator(d2);
    
	return 0;
}

操作符注意事项:

①该操作符有几个操作数就传几个参数,否则会报错,如:

//原因为‘<’操作数为2,我们却传了三个参数(this,d1,D)
	bool operator<(Date* d1,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;
	}

26620f37571e44e0a8ea34678ea41969.png

②不能通过连接其他符号来创建新的操作符:比如operator@
③重载操作符必须有一个类类型参数,用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this,如上面的程序
.*  ::  sizeof  ?:  . 注意以上5个运算符不能重载。


5.2赋值运算符重载

赋值运算符重载格式:

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

注:T表示类型,后面学到类模板的时候会重点述说。

①如果用户没有显示定义给出赋值运算符的重载,那么编译器会默认的去生成一份赋值运算符的重载函数,但是默认的赋值运算符的重载和默认的拷贝构造一样属于浅拷贝

void operator=(const Date& D)
	{
		_year = D._year;
		_month = D._month;
		_day = D._day;
	}

像上面的赋值运算符的重载看起来会发现没有什么问题,但是C++/C是支持连续的赋值语句的,如:

#include<iostream>
using namespace std;
class Date
{
public:
	//构造函数
	Date(size_t year = 1, size_t month = 1, size_t day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void operator=(const Date& D)//赋值运算符重载
	{
		_year = D._year;
		_month = D._month;
		_day = D._day;
	}
private:
	size_t _year;
	size_t _month;
	size_t _day;
};
int main()
{
	Date d1;
	Date d2;
	Date d3(2023, 5, 4);
	d1 = d2 = d3;//连续赋值
	return 0;
}

8a765e136ccd4d3888aa3a0d5623da45.png

 通过结果可以发现现在是不支持连续赋值的,所以该怎么解决呢?

很简单,了解了连续赋值的原理后,就会发现,每执行了一次赋值就会返回一个右操作数的值。所以我们可以这样:

Date& operator=(const Date& D)
	{
		_year = D._year;
		_month = D._month;
		_day = D._day;
		return *this;//返回右操作数的引用,提高效率
	}

该程序可以返回右操作数的值,但这样就属于传值返回了,效率低下,所以传引用返回提高效率。

②赋值运算符只能重载成类的成员函数,不能重载成全局函数

原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现
一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值
运算符重载只能是类的成员函数。

11b59f1654844a1fb113b07ecba3414f.png
 

 ③前面的①已经说了默认的赋值运算符重载属于浅拷贝,就像默认拷贝构造一样,当遇到像数据结构中顺序表、栈这样的类型时,就会发生一个地址空间没释放两次的错误。所以当遇到这种情况时需要我们显示定义一个赋值运算符的重载。

#include<iostream>
using namespace std;
typedef int SLtype;
class SL
{
public:
	SL(size_t capacity = 4)
	{
		_a = (SLtype*)malloc(sizeof(SLtype) * capacity);
		if (!_a)
		{
			printf("malloc fail");
			exit(-1);
		}
		_size = 0;
		_capacity = capacity;
	}
	SL& operator=(SL& sl)//赋值运算符的重载
	{
		this->_a = (SLtype*)malloc(sizeof(int) * sl._capacity);
		if (!this->_a)
		{
			printf("malloc fail");
			exit(-1);
		}
		for (int i = 0; i < sl._size; ++i)//把数据拷贝一份到新的空间
		{
			this->_a[i] = sl._a[i];
		}
		this->_capacity = sl._capacity;
		this->_size = sl._size;
		return *this;
	}
	~SL()
	{
		cout << "~SL()" << endl;
		if (_a)
		{
			free(_a);
			_a = nullptr;
			_size = 0;
			_capacity = 0;
		}
	}
	SLtype* _a;
	size_t _size;
	size_t _capacity;

};
int main()
{
	SL sl;
	SL sl2;
	SL sl3;
	sl3=sl2 = sl;//赋值
	return 0;
}

使用深拷贝的方式就可以解决这样的问题了。

注:如果类中未涉及到资源管理,那么赋值运算符是否实现都可以,否则必须要实现赋值运算符重载

5.3前置++和后置++(前置--和后置--)重载

①前置++和后置++

我们都知道前置++返回的是加一之后的值,所以直接加一即可

Date& operator++()
	{
		_day += 1;
		return *this;//传引用提高效率
	}

主要需要我们注意的是,后置++和前置++构成运算符重载的时候参数列表要加一个int

Date operator++(int)//传int构成重载,表示后置++
	{
		Date tmp(*this);//拷贝构造一个++前的对象,用来返回
		_day += 1;
		return tmp;//返回++前的值
	}

加int主要是为了构成重载,调用的时候可以不传参,加了int编译器就会默认为后置++的运算符重载

#include<iostream>
using namespace std;
class Date
{
public:
	//构造函数
	Date(size_t year = 1, size_t month = 1, size_t day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date& operator=(const Date& D)//赋值运算符重载
	{
		_year = D._year;
		_month = D._month;
		_day = D._day;
		return *this;
	}
	Date& operator++()
	{
		_day += 1;
		return *this;
	}
	Date operator++(int)//传int构成重载,表示后置++
	{
		Date tmp(*this);//拷贝构造一个++前的对象,用来返回
		_day += 1;
		return tmp;//返回++前的值
	}
	void print()
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
private:
	size_t _year;
	size_t _month;
	size_t _day;
};
int main()
{
	Date d(2023, 5, 4);
	++d;//++一次
	d.print();
	Date d1;
	d1=d++;//把++前的值赋给d
	d1.print();
	d.print();

	return 0;
}

结果:

通过结果可以看到,写的运算符重载没问题了,虽然定义的时候参数要加个int,但调用后置++ 时是可以不传参的,还要注意一点:后置++返回不能传引用,因为后置++传的是局部对象,函数调用完了就销毁了,得不到我们需要的值。所以只能传值返回

②前置--和后置--

这里和前置++与后置++的定义方法是一样的,就不多阐述了,直接上代码

//前置--
Date& operator--()
	{
		_day -= 1;
		return *this;
	}
//后置--
Date operator--(int)//不可传引用返回
	{
		Date tmp(*this);
		_day -= 1;
		return tmp;
	}

六、const成员及const取地址运算符重载

①const成员

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

 如:

#include<iostream>
using namespace std;
class Date
{
public:
	//构造函数
	Date(size_t year = 1, size_t month = 1, size_t day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void func()const
	{
		_day += 1;//修改_day
	}
private:
	size_t _year;
	size_t _month;
	size_t _day;
};
int main()
{
	Date d;
	d.func();

	return 0;
}

通过上面的程序和结果可以发现,被const修饰的成员函数不能对this指针指向的类的成员修改

注意点:

  •  const对象不可以调用非const成员函数
  • const成员函数内不可以调用其它的非const成员函数
  • 非const对象可以调用const成员函数
  • 非const成员函数内可以调用其它的const成员函数

②取地址重载及const取地址重载

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

class Date
{
public:
	Date* operator&() //取地址运算符重载
	{
		return this;
	}

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

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

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

今天的分享就到这里了,如果有错误的地方还望指出,感谢支持,886!

  • 14
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

冧轩在努力

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

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

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

打赏作者

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

抵扣说明:

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

余额充值