C++基础(4)——类与对象(默认成员函数)

       

目录

1.拷贝构造函数:

1.1 为什么要引入拷贝构造:

1.2 拷贝构造函数的定义及特性:

1.3 什么类可以不用编写拷贝构造:

2. 赋值运算符重载:

2.1 为社么要引入运算符重载:

2.2运算符重载的定义以及特性:


       在前面的文章中,引入了C++中类的概念,对于一个类而言,如果其中不存在成员,则该类被称之为空类。但是空类中并不是不存在任何内容,而是编译器会自动生成以下6默认成员函数,即:用户没有显性显示,编译器会生成的函数。

      6个函数大致可以分为以下三类,分别是用于初始化和清理的构造函数析构函数,用于拷贝赋值的拷贝构造函数赋值运算符重载,以及         

     在上一篇文章中,对于用于初始化构造函数的定义其特点进行介绍,即:函数名和类名相同、没有返回值、对象实例化时编译器会自动调用构造函数,并且针对于自定义类型和内置类型的作用不同、可以构成重载。并且介绍了用于清理的析构函数的定义及其特点,即:函数名是类名之前加~,无参数无返回值类型、一个类中只能由一个析构函数(所以析构函数不能构成重载)并且在未显性显示的情况下,编译器会自动生成析构函数、编译器会自动调用析构函数。

     在本文中,将继续介绍默认成员函数中的其他函数:

1.拷贝构造函数:

1.1 为什么要引入拷贝构造:

       在正式介绍拷贝构造函数的定义以及性质之前,需要先说明为什么要引入拷贝构造,为了解释此问题,首先提及数据结构中,对于函数的传参方式。例如在栈中,向各个功能函数传递栈这个数据结构的参数时,一般采用传址调用而非传值调用,这是因为传址调用在速度和大小方面都优于传值调用。但是,这并不意味着传值调用不可以使用,例如在下面的代码中:

#include
using namespace std;

class Date
{
public:
	//构造函数
	Date(int year = 2023, int month = 11, int day = 21)
	{
		_year = year;
		_month = month;
		_day = day;
	}

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

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

void func(Date dd)
{
    cout << "func(Date dd)" << endl;
	dd.Print();
}
int main()
{
	//类的实例化
	Date d1;
	func(d1);

	return 0;
}

运行结果如下:
C++基础(4)——类与对象(默认成员函数)_第1张图片

      不难发现,向函数func传递参数时,并没有传递指针或者采用引用,而是直接将类作为参数传递。对于这种直接传值的方式,可以称为浅拷贝或者值拷贝。对于上述代码所给出的日期类,浅拷贝并不会造成程序的错误。

      但是在不同的情况下,浅拷贝可能会造成程序的错误,例如上篇文章中提到的栈:

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 3)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_top = 0;
	}
	~Stack()
	{
		_array = nullptr;
		_capacity = 0;
		_top = 0;
	}
private:
	DataType* _array;
	int _capacity;
	int _top;
};

为了方便演示,这里专门创建一个函数func2用于检测,具体如下:
 

void func2(Stack s1)
{
	//......
}
int main()
{
	//类的实例化
	Date d1;
	func(d1);

	Stack s;
	func(s);

	return 0;
}

运行上述程序,编译器会显示错误:
C++基础(4)——类与对象(默认成员函数)_第2张图片

     通过对上述日期类和栈类的调用,会发现,在日期类进行传值调用或者说进行浅拷贝时,并不会出现错误,而对于栈这个类则会报错。导致两者不同的原因,就在于栈这个类中,有一个成员变量是指针_array。 对于传值调用,是直接将变量的值进行传递,对于指针也不例外,通过监视窗口,可以观察SS1中指针_array的地址。

C++基础(4)——类与对象(默认成员函数)_第3张图片

C++基础(4)——类与对象(默认成员函数)_第4张图片

        通过图片不难发现,再向函数func2传递参数时,直接将对象S作为参数传递, 因此,对象S中的成员变量的值也传给了形参S1。所以,SS1中的指针_array指向同一块地址,具体可以有下面的图片表示:

C++基础(4)——类与对象(默认成员函数)_第5张图片

       在上一篇文章及文章开头,提及了析构函数的一个特点:对象生命周期结束时,会自动调用析构函数。因此,当函数func2调用结束后,此时对象S1的生命周期结束,因此,析构函数会清理对象S1中指针_array指向的空间。

      当函数运行结束后,当主函数main运行结束时,此时对象S的生命周期结束,编译器会再次调用析构函数,清理对象S中指针_array指向的空间。上面提到,两个对象中的指针指向了同一块空间,因此,本次清理时,会造成错误,因为指针_array指向的空间被清理了两次。

      在C++中,为了解决浅拷贝这种方式在上述情况下会引起错误的问题,因此,C++规定自定义对象在进行拷贝时,需要调用拷贝构造函数

1.2 拷贝构造函数的定义及特性:

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

      特性如下:

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

      为了便于解释特性2中,为什么采用传值方式会引发无穷递归,文章首先给定下面一个构造函数:

Date da(d1);
//拷贝构造函数:
	Date(Date dd)
	{
		_year = dd._year;
		_month = dd._month;
		_day = dd._day;
	}

       在采用上述拷贝方式的情况下,首先传递参数d1,由于C++规定自定义类型传值或者值拷贝需要调用拷贝构造,因此在第一次传参后,并没有直接去调用拷贝构造,而是编译器额外新生成一个拷贝构造函数,并且去调用新生成的拷贝构造函。为了调用拷贝构造函数,首先需要传递参数,但是在传递参数时,又会生成一个新的拷贝构造函数。。。。。。因此会引发无限递归。

C++基础(4)——类与对象(默认成员函数)_第6张图片

 

在特性2中提到,拷贝构造函数的参数只有一个,并且必须是引用的方式,即:

//拷贝构造函数:
	Date(Date& dd)
	{
		_year = dd._year;
		_month = dd._month;
		_day = dd._day;
	}

在这种情况下,再次运行上面的代码,就不会造成无穷递归,具体原理如下:

void func(Date d3)
{
	cout << "func(Date dd)" << endl;
	d3.Print();
}


func(d1);

       在调用func函数时,首先需要传递参数,此时传递参数的方式为传值拷贝,因为,会调用拷贝构造函数,由于拷贝构造函数的参数是类型对象的引用,因此参数dd就是d1的别名,此时的this指针指向d3,所以,在拷贝构造函数对日期类进行赋值时,通过this指针,直接将对象dd的成员变量赋值给this指针指向的对象d3的成员变量,完成赋值。

     并且,由于拷贝构造函数的参数是类型对象的引用,不是传值调用,所以,在向拷贝构造函数传递参数时,不会引发无穷递归(同理,传递指针也可以避免无穷递归)。

    在基本了解了拷贝构造函数的定义以及特性后,可以利用拷贝构造函数来解决上面栈类的问题,即:开辟的空间会被释放两次。解决问题的方法就是通过拷贝构造函数来实现深拷贝,即在拷贝时不只拷贝值,还将被拷贝对象的资源一起进行拷贝。代码如下:

Stack s2(s);
//拷贝构造函数:
	Stack(Stack& stt)
	{
		_array =(int*)malloc(sizeof(int)*stt._capacity);
		if (_array == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		memcpy(_array, stt._array, sizeof(int) * stt._capacity);
		_capacity = stt._capacity;
		_top = stt._top;
	}

对于上述代码,this指针指向对象S2stt是对象s的引用,所以,根据深拷贝,需要将被拷贝对象的资源一起拷贝的原则,对对象S2中的指针_array再开辟一块空间,大小和对象stt中的指针指向的空间大小相同,但是两块空间的地址不同,即:
C++基础(4)——类与对象(默认成员函数)_第7张图片

由于两块空间的地址不同,因此,不会出现析构函数将同一块空间释放两次的情况。

1.3 什么类可以不用编写拷贝构造:

针对这个问题,可以通过一个例子进行说明:
首先,将日期类中的拷贝构造删除,即:
 

class Date
{
public:
	//构造函数
	Date(int year = 2023, int month = 11, int day = 21)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	

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

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

void func(Date d3)
{
	cout << "func(Date dd)" << endl;
	d3.Print();
}

int main()
{
	//类的实例化
	Date d1;
	func(d1);

	return 0;
}

运行上述程序,通过监视窗口观察对象d1\, d3

C++基础(4)——类与对象(默认成员函数)_第8张图片

不难发现,即使没有人为编写拷贝构造函数,两个对象依然完成了值拷贝。这是因为拷贝构造函数属于默认成员函数,在没有人为编写的情况下,针对内置类型会自动完成值拷贝。针对自定义类型会去调用此类型的拷贝构造,如果没有人为编写或者显性显示的拷贝构造,则编译器会自动生成。

例如,对于下面的自定义类型violent,类中并没有人为给出拷贝构造函数

int main()
{
	violent p1;
	violent p2(p1);

	return 0;
}
class violent
{
	Stack pp1;
	Stack pp2;
	int size;
};

此时运行程序,通过监视窗口观察类violent中的成员变量pp1\, \, pp2\, \, size

C++基础(4)——类与对象(默认成员函数)_第9张图片

      可以发现,主函数中对象p1\, \, p2中的成员变量都被进行了拷贝,并且还进行了深拷贝。由于成员pp1 \, \, \, pp2的类型是Stack,因此编译器自动调用了成员相对类型的拷贝函数,这一点,可以通过下面的代码进行验证。

       即在拷贝构造函数的开头加上一行打印,如果编译器会自动调用成员相对类型的拷贝函数,即调用Stack中的拷贝函数。则会打印一次。

	//拷贝构造函数:
	Stack(Stack& stt)
	{
		cout << "Stack(Stack& stt)" << endl;
		_array =(int*)malloc(sizeof(int)*stt._capacity);
		if (_array == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		memcpy(_array, stt._array, sizeof(int) * stt._capacity);
		_capacity = stt._capacity;
		_top = stt._top;
	}

运行结果如下:
C++基础(4)——类与对象(默认成员函数)_第10张图片

    通过上面的例子,不难看出, 针对Dateviolent这两种类,使用编译器默认生成的拷贝函数即可。不过二者有稍有差别。因为Date中所有成员的类型都是内置类型,编译器默认生成的拷贝构造函数来完成值拷贝已经满足了Date的需求。针对violent这种类的成员变量的类型是自定义类型,需要调用该成员的拷贝构造函数,即StackStack种已经存在了人为编写的拷贝构造函数,编译器直接调用即可。

2. 赋值运算符重载:

2.1 为社么要引入运算符重载:

       在C++中,针对内置类型的变量,可以通过> ,= ,<等运算符来判断他们之间的关系。但是针对类这种这种较为复杂的类型,却不能通过运算符来判断他们之间的大小关系,例如:

bool ret = d1 > d2;

在C++中,如果需要使用运算符来判断类之间的关系,需要利用函数来完成,即:
 

bool Compare(Date x, Date y)
{
	return x._year == y._year &&
		x._month == y._month &&
		x._day == y._day;
		   
}
bool Comparebig(Date x, Date y)
{
	if (x._year > y._year)
	{
		return true;
	}
	else if (x._year == y._year && x._month > y._month)
	{
		return true;
	}
	else if (x._year == y._year && x._month == y._month && x._day > y._day)
	{
		return true;
	}

	else
	{
		return false;
	}

}

不过由于不同用户的使用及命名习惯不同,会导致函数的函数名可读性及规范性差。因此,在C++中,为了规范性以及可读性,引入了运算符重载

2.2运算符重载的定义以及特性:

定义如下:运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。函数名字为:关键字operator后面接需要重载的运算符符号。函数原型:返回值类型 operator操作符(参数列表)

例如,将上面给出的用于比较两个类较大的函数进行改写:
 

bool operator>(Date x, Date y)
{
	if (x._year > y._year)
	{
		return true;
	}
	else if (x._year == y._year && x._month > y._month)
	{
		return true;
	}
	else if (x._year == y._year && x._month == y._month && x._day > y._day)
	{
		return true;
	}

	else
	{
		return false;
	}

}

判断两个类是否相等的函数改写为:

bool operator==(Date x, Date y)
{
	return x._year == y._year &&
		x._month == y._month &&
		x._day == y._day;
		   
}

       虽然利用关键字 operator规范了函数名的书写方式后,使得代码的可读性变高,但是在接收函数判断的结果时,例如:

	bool ret = operator>(d1, d2);
	bool ret1 = operator==(d1, d2);

	cout << operator>(d1, d2) << endl;
	cout << operator==(d1, d2) << endl;

代码的可读性仍然不高,因此,C++在此时再次进行了优化,即:

	bool ret = d1 > d2;
	bool ret1 = d1 == d2;

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

在这种情况下,编译器会去寻找,代码中是否存在相应的函数,即:operator>,operator==,如果存在则会自动调用,不存在则会报错。

虽然代码的可读性再一次提高,但是针对上述函数依旧存在两个问题:

1. 调用日期类的成员变量时,需要将类的访问限定符由private改为public

2. 函数的参数在传参时,由于传递的参数是自定义类型,并且传参的方式是传值(浅拷贝),因此需要调用拷贝构造函数。

针对问题一,只需要将函数都放在类种便可以解决,针对第二个问题,将函数的传参方式由传值拷贝改为传引用即可,即:
 

class Date
{
public:
	//构造函数
	Date(int year = 2023, int month = 11, int day = 21)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//拷贝构造函数:
	Date(Date& dd)
	{
		_year = dd._year;
		_month = dd._month;
		_day = dd._day;
	}

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

	bool operator==(Date& x, Date& y)
	{
		return x._year == y._year &&
			x._month == y._month &&
			x._day == y._day;

	}

	bool operator>(Date& x, Date& y)
	{
		if (x._year > y._year)
		{
			return true;
		}
		else if (x._year == y._year && x._month > y._month)
		{
			return true;
		}
		else if (x._year == y._year && x._month == y._month && x._day > y._day)
		{
			return true;
		}

		else
		{
			return false;
		}

	}

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

不过此时运行代码,编译器会显示如下错误:

这是因为,对于成员函数的参数,都会有一个隐藏的参数,即this指针,因此,需要将上述函数的参数改为:
 

bool operator==(Date& y)
bool operator>(Date& y)

再进行函数的调用时,即

bool ret = d1 > d2;
	bool ret1 = d1 == d2;

编译器会自动将上述调用的形式进行转换,转换为:

bool ret = d1 > d2;
	//d1.operator>(&d1,d2)
	bool ret1 = d1 == d2;
	//d1.operaotr(&d1,d2);

对于上述形式,可以理解为,函数内部的参数由两个,一个是指向d1this指针,另一个则是上述函数中传递的参数Datey

在函数调用时,也可以用上述方式进行调用,即:
 

bool ret3 = d1.operator>(d2);

因此,对于上述函数,其正确写法为:
 

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

	}

	bool operator>(Date& y)
	{
		if (_year > y._year)
		{
			return true;
		}
		else if (_year == y._year && _month > y._month)
		{
			return true;
		}
		else if (_year == y._year && _month == y._month && _day > y._day)
		{
			return true;
		}

		else
		{
			return false;
		}

	}

此时,两个函数内部均有两个参数,即上面所说的传递的参数y和一个指向xthis指针。编译器会通过this指针自动完成函数的整个运行过程。

你可能感兴趣的:(C++,c++,开发语言)