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!

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

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

相关文章

用ChatGPT三分钟免费做出数字人视频- 提升自媒体魅力

用ChatGPT三分钟免费做出数字人视频- 提升自媒体魅力 一、ChatGPT产生文案二、腾讯智影网站三、选择一个2D数字人四、粘贴文本五、编辑自定义&#xff0c;合成六、资源七、其他数字人平台推荐八、生成视频预览 本教程收集于&#xff1a;AIGC从入门到精通教程汇总 操作指引 Ch…

哪个牌子的电视盒子好用?罗鹏数码盘点2023电视盒子排名

电视机资源少、卡顿&#xff0c;配置不足的时候只需要安装一台电视盒子就可以解决这些问题&#xff0c;不需要花费大价钱更换电视机。那么&#xff0c;你知道哪个牌子的电视盒子好用吗&#xff1f;今天罗鹏就来详细聊聊这个话题&#xff0c;分享2023最新电视盒子排名。 一&…

gitlab服务器发送邮件配置

1.修改gitlab的配置文件&#xff1a; vim /etc/gitlab/gitlab.rb 这里具体的gitlab.rb文件所在路径需要根据实际的来 找到如下图所示的部分&#xff0c;放开注释&#xff0c;修改配置&#xff0c;此处我用的发件邮箱是QQ邮箱&#xff0c;所以域名配置都是qq.com&#xff0c;…

图表控件Stimulsoft 2023.2 带来极致深色主题, 一起来看看还有哪些亮点?

Stimulsoft Reports 是一款报告编写器&#xff0c;主要用于在桌面和Web上从头开始创建任何复杂的报告。可以在大多数平台上轻松实现部署&#xff0c;如ASP.NET, WinForms, .NET Core, JavaScript, WPF, Angular, Blazor, PHP, Java等&#xff0c;在你的应用程序中嵌入报告设计器…

clickhouse的嵌套数据结构Tuple、Array与Nested类型介绍和使用示例

文章目录 Tuple类型Array类型Nested类型使用示例单独使用Tuple数组嵌套 Array(Tuple)Nested类型 生产使用&#xff1a;分组查询 Tuple类型 Tuple是ClickHouse数据库中的一种数据类型&#xff0c;它允许在一个字段中存储由不同数据类型组成的元组(tuple)。元组可以包含任意数量…

快速了解C语言的基本元素

C语言是一种编程语言&#xff0c;和其它语言一样&#xff0c;也定义了自己的语法和词汇。学习C语言&#xff0c;首先要学习C语言的词汇&#xff0c;再学习C语言的语法规则&#xff0c;然后由词汇构成语句&#xff0c;由语句构成源程序&#xff0c;源程序也称为源代码或代码&…

ChatGPT :国内免费可用 ChatGPT +Midjourney绘图

前言 ChatGPT&#xff08;全名&#xff1a;Chat Generative Pre-trained Transformer&#xff09;&#xff0c;美国OpenAI 研发的聊天机器人程序 &#xff0c;于2022年11月30日发布 。ChatGPT是人工智能技术驱动的自然语言处理工具&#xff0c;它能够通过理解和学习人类的语言来…

【MySQL】绪论 MySQL工作环境

文章目录 实验内容实验步骤实验内容 MySQL命令MySQL 的启动与关闭MySQL 管理备份和还原数据库navicat工具使用实验步骤 1. MySQL命令 (1)查看MySQL基本命令 (2)查看MySQL版本信息 2. MySQL的启动与关闭 (1)启动MySQL服务器 (2)测试服务器启动成功 (3)合法用

stream笔记

1、 创建流stream 1.1、 Stream 的操作三个步骤 1.2、 stream中间操作 1.2.1 、 limit、skip、distinct 1.2.2、 map and flatMap 1.2.3、 sort 自然排序和定制排序 1.3、 add and andAll difference: 1.4、 终止操作 1.4.1、 allmatch、anyMatch、noneMatch、max、min…

前端开发中,定位bug的几种常用方法

目录 第一章 前言 第二章 解决bug的方法 2.1 百度 2.2 有道翻译 2.3 debugger 2.4 console.log 日志打印 2.5 请求体是否携带参数 2.6 注释页面渲染代码 2.7 其他 第三章 尾声 备注&#xff1a;该文章只是本人在工作/学习中常用的几种方法&#xff0c;如果有不对大家…

朋友去华为面试,轻松拿到30K的Offer,羡慕了......

最近有朋友去华为面试&#xff0c;面试前后进行了20天左右&#xff0c;包含4轮电话面试、1轮笔试、1轮主管视频面试、1轮hr视频面试。 据他所说&#xff0c;80%的人都会栽在第一轮面试&#xff0c;要不是他面试前做足准备&#xff0c;估计都坚持不完后面几轮面试。 其实&…

第四十六章 Unity 布局(上)

学习了UI元素的使用&#xff0c;并不能构建出一个完整的UI界面&#xff0c;我们需要使用一些方法将这些UI元素按照“设计稿”的效果&#xff0c;将其摆放到对应的位置上。如何摆放这些UI元素&#xff0c;就是我们需要讲的“布局”&#xff0c;当然这需要借助一些布局组件来完成…

毕业论文相关

毕业论文参考文献和Word保存 一、Word中出现[7-9]多个文献的引用 在正文中选中参考文献角标&#xff0c;右击选择“切换域代码”&#xff0c;参考文献角标[7][8][9]变为{ REF _Ref98345319 \r \h * MERGEFORMAT }{ REF _Ref98345321 \r \h * MERGEFORMAT }{ REF _Ref99390603…

第5章 负载均衡

第5章 负载均衡 5.1 proxy_pass详解 在nginx中配置proxy_pass代理转发时&#xff0c;如果在proxy_pass后面的url加/&#xff0c;表示绝对根路径&#xff1b;如果没有/&#xff0c;表示相对路径&#xff0c;把匹配的路径部分也给代理走。 假设下面四种情况分别用 http://192.…

Java并发编程实践学习笔记(三)——共享对象之发布和异常

目录 1 公共静态变量逸出 2 非私有方法逸出私有变量 3 this引用逸出 4 构造函数中的可覆盖方法调用逸出 发布&#xff08;publishing&#xff09;一个对象的意思是&#xff1a;使对象能够在当前作用域之外的代码中使用。例如&#xff0c;将一个指向该对象的引用保存到其他代…

InnoDB线程模型

新版本结构演变 MySQL 5.7 版本 将 Undo日志表空间从共享表空间 ibdata 文件中分离出来&#xff0c;可以在安装 MySQL 时由用户自行指定文件大小和数量增加了 temporary 临时表空间&#xff0c;里面存储着临时表或临时查询结果集的数据Buffer Pool 大小可以动态修改&#xff0…

你不知道的自动化?使用自动化测试在项目中创造高业务价值...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 脱离数据支撑谈价…

GRPC - JAVA笔记

GRPC - JAVA笔记 gRPC简介 由google开源的一个高性能的RPc框架&#xff0c;由google内部的Stubby框架演化而来。2015年正式开源。云原生时代的RPC标准&#xff0c;由Go语言开发 gRPC的核心设计思路 网络通信 ------> gRPC 自己封装了网络通信的部分&#xff0c;提供了多种…

VS2022编译libiconv-1.17

需求概述 获得最新版本的windows下可用的libiconv静态库。 解决方案 概述 使用VS2022编译libiconv-1.17。需要对源码手动进行配置。 本文所述的方法同样适用于动态库&#xff0c;并且理论上适用于VS2010~2022所有版本。 如果你不在乎libiconv的版本&#xff0c;可以参考 …

Redis缓存

就先不连接数据库了 我们测试缓存 实体类&#xff1a; Data AllArgsConstructor NoArgsConstructor public class User implements Serializable {private int id;private String name;private String sex;private String addr; } service&#xff1a; Service public…