C++拷贝构造函数和运算符重载

目录

一,拷贝构造函数

二,运算符重载


一,拷贝构造函数

概念:在类的定义中,构造函数只是单纯将内置类型进行初始化,而拷贝构造函数是将整个类进行拷贝到另一个类中进行初始化。在定义拷贝构造函数时,只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

这里要说明的是,拷贝构造函数也是特殊的成员函数,其特征如下:

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

        2,拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为形参相当于实参的临时拷贝,在拷贝中会触发另一个拷贝构造函数的调用,从而引发无穷递归调用。传引用的话只会将源对象的别名进行拷贝,不会进行整体拷贝,从而避免了这种问题。

class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    //Date(const Date d)// 错误写法: 编译报错,会引发无穷递归
    Date(const Date& d)// 正确写法
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1;//调用构造函数
    Date d2(d1);//用类进行初始化,会调用拷贝构造函数
    return 0;
}

引发无穷递归的过程图

C++拷贝构造函数和运算符重载_第1张图片

        3,若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数只会进行浅拷贝,或者值拷贝。(浅拷贝是指只拷贝对象的基本数据类型和引用地址,不会拷贝引用地址所指向的对象,即原对象中的数据所指向的空间将与拷贝后的对象的数据所指向的空间相同)。

        这里要说明的是默认拷贝构造函数的处理对象不跟默认构造函数一样,但处理自定义类型的方式跟默认构造函数一样。默认拷贝构造函数会对自定义类型和内置类型都做处理,其中,内置类型成员完成浅拷贝,自定义类型成员将会调用这个成员的拷贝构造

#include 
using namespace std;
class Time
{
public:
    Time() {
        _hour = 1;
        _minute = 1;
        _second = 1;
    }
    Time(const Time& t) {
        _hour = t._hour;
        _minute = t._minute;
        _second = t._second;
        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 d1;
    Date d2(d1);
    return 0;
}

d1类运行的调试内部分析图(构造函数)

C++拷贝构造函数和运算符重载_第2张图片

d2类运行的调试内部分析图(拷贝构造函数)

C++拷贝构造函数和运算符重载_第3张图片

        下面问题来了,既然有默认拷贝构造函数,我们需要不需要再定义拷贝构造函数呢?

        请注意上面说的,默认拷贝构造只会进行浅拷贝,当我们在对象成员中开辟了动态空间时,使用默认拷贝构造将会出现问题。请看以下代码:

//此代码运行时将会崩溃
#include 
using namespace std;
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (!_array) {
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	~Stack()
	{
		if (_array) {
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};
int main()
{
	Stack s1;
	Stack s2(s1);
	return 0;
}

分析: 

        首先,s1先调用构造函数创建,在构造函数中开辟了10个元素的空间,然后,s2对象使用s1拷贝构造,Stack对象中没有自己定义,系统将生成一份默认的拷贝构造函数进行浅拷贝,这时,s1和s2栈结构将同时指向一块内存空间,当函数退出时,s2和s1会自动调用析构函数进行销毁,这就会造成s1和s2指向的同一块空间销毁两次,系统崩毁。

C++拷贝构造函数和运算符重载_第4张图片

        解决上面的问题不难,我们只需将s1和s2分别指向不同的空间即可,这时,需要我们自己定义拷贝构造函数。

#include 
using namespace std;
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (!_array) {
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	//定义拷贝构造
	Stack(const Stack& S)
	{
		_size = S._size;
		_capacity = S._capacity;
		_array = (DataType*)malloc(_capacity * sizeof(DataType));
		if (!_array) {
			perror("malloc申请空间失败");
			return;
		}
	}
	~Stack()
	{
		if (_array) {
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};
int main()
{
	Stack s1;
	Stack s2(s1);
	return 0;
}

总:如果类中没有涉及空间资源的申请时,拷贝构造函数可以不写,但是一旦涉及到资源空间的申请时,则拷贝构造函数是一定要写上的。

拷贝构造函数典型调用场景:

        1,使用已存在对象创建新对象。如同以上Stack类中s2的创建。

/*..........*/

int main()
{
    Stack s1;
    Stack s2(s1);//使用已存在的对象s1来创建对象s2
    return 0;
}

        2,函数参数类型为类类型对象——解析:因为在传参过程中,形参就相当于实参的临时拷贝,相当于用实参来创建形参。

        3,函数返回值类型为类类型对象——解析:当函数返回时,函数内局部对象的生命周期结束,但是临时变量的值会被拷贝到调用函数的栈帧中,或者通过引用传递给调用函数。当返回类类型对象时,直接将此类对象拷贝到调用函数栈帧中。

总结一句话,只要是运用了类对象与类对象直接赋值进行初始化的情况,系统就会调用拷贝构造。而为了提高效率,一般能用引用就用引用。


二,运算符重载

引入:

        在内置类型中,系统给我们自动定义了很大运算符,如:+、-、*、/、++、--、>等。这些内置类型和内置类型运算符的使用都是系统定义好的,可直接使用。现在问题来了,自定义类型要想使用这些运算符又当如何?由于自定义类型是我们自己定义的,系统不知道其结构,所以无法直接供我们使用,但C++为了增强代码的可读性引入了运算符重载的概念,可让自定义类型也使用这些运算符。运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

运算符重载的定义:

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

        函数原型:返回值类型 operator操作符(参数列表)。

这里有以下5个注意:

        1,不能通过连接其他符号来创建新的操作符:比如operator@

        2,重载操作符必须有一个类类型参数

        3,用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其+本身的含义

        4,作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this指针。

        5,“ .*    ::    sizeof    ?:    . ” 注意这5个运算符不能重载。这个经常在笔试选择题中出现。

在类的内部定义

第一个参数为this指针

// '='操作符,返回类型为类类型Date的引用
Date& operator=(const Date& d);

Date& Date::operator=(const Date& d);//此种情况是在类中声明,在外部实现,用限定符说明是类对象中的成员函数
// "+="操作符,返回类型为类类型Date的引用
Date& operator+=(int day);

Date& Date::operator+=(int day);//此种情况是在类中声明,在外部实现,用限定符说明是类对象中的成员函数
// '+'操作符,返回类型为类类型Date的引用
Date operator+(int day);

Date Date::operator+(int day);//此种情况是在类中声明,在外部实现,用限定符说明是类对象中的成员函数
// '-'操作符,返回类型为类类型Date的引用
Date operator-(int day);

Date Date::operator-(int day);//此种情况是在类中声明,在外部实现,用限定符说明是类对象中的成员函数
// "-="操作符,返回类型为类类型Date的引用
Date& operator-=(int day);

Date& Date::operator-=(int day);//此种情况是在类中声明,在外部实现,用限定符说明是类对象中的成员函数

在类的外部定义

没有默认参数,需要把类写入形参中

// "+="操作符,返回类型为类类型Date的引用
Date& operator+=(const Date& d1, int day);
// '+'操作符,返回类型为类类型Date的引用
Date operator+(const Date& d1, int day);
// '-'操作符,返回类型为类类型Date的引用
Date operator-(const Date& d1, int day);
// "-="操作符,返回类型为类类型Date的引用
Date& operator-=(const Date& d1, int day);

运用细节演示:

        (1)首先,我们先对“ >,<,>=,<=,==,!= ”这几个简单运算符在类对象内定义进行演示和解说。

#include 
#include 
using namespace std;
//再次提醒一下,类对象中的成员函数第一个默认参数为this,且指向此对象,下面运用时就不做说明
class Date
{
public:
	// 全缺省的构造函数
	Date(int year = 1900, 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;
	}
	// !=运算符重载
	bool operator != (const Date& d)
	{
		return !(*this == 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;
		return false;
	}
	// >=运算符重载
	bool operator >= (const Date& d)
	{
		return *this > d || *this == 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;
		return false;
	}
	// <=运算符重载
	bool operator <= (const Date& d)
	{
		return *this < d || *this == d;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2(2023, 10, 16);
	cout << (d1 > d2) << endl;//d1 > d2等效于d1.operator>(d2),这两个表达的意思一样
	cout << d1.operator>(d2) << endl;
	return 0;
}

        在上面代码中要说明的是在运算符表示过程中,我们即可用函数来表示运算符的含义,也可直使用运算符的形式表示含义。

在上面的运算符函数中:

d1 > d2等效于d1.operator(d2)

d1 == d2等效于d1.operator(d2)

上面的输出流中之所以(d1 > d2)用括号括起来是因为“ << ”操作符的运算级别高,会先与之运算。

        (2)接下来我们对“ +,-,+=,-= ”这几个简单运算符在类对象内定义进行演示和解说。

#include 
#include 
using namespace std;
class Date
{
public:
	// 获取某年某月的天数
	int GetMonthDay(int year, int month)
	{
		assert(year >= 1 && (month >= 1 && month <= 12));
		int MonthDays[] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
		if ((month == 2) && ((year % 400 == 0) || (year % 4 == 0 && year % 100 != 0)))
			return 29;
		return MonthDays[month];
	}
	// 全缺省的构造函数
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 日期-天数
	Date operator-(int day)
	{
		_day -= day;
		while (_day < 0) {
			if (_month == 1) {
				_year--;
				if (_year < 0) {
					perror("Date Count Error:");
					exit(-1);
				}
				_month = 12;
			}
			else {
				_month--;
			}
			_day += GetMonthDay(this->_year, this->_month);
		}
		return *this;
	}
	// 日期-=天数
	Date& operator-=(int day)
	{
		Date DateCopy(*this);
		//用临时对象返回,相当于函数的返回,需要有个对象去接收,而改变的只是临时对象
		//像直接使用类d1 -= 5,表达的只是此函数的功能,d1的值不会改变,若是d1 = d1 -= 5将会改变
		//DateCopy = DateCopy.operator-(day);
		//return DateCopy;

		//this指针直接指向的是此对象(即此类),用this指针来改变相当于直接改变了d1,类似于达到了改变d1的效果
		*this = DateCopy.operator-(day);
		return *this;
	}
	// 日期+=天数
	Date& operator+=(int day)
	{
		_day += day;
		while (_day > GetMonthDay(this->_year, this->_month)) {
			_day -= GetMonthDay(this->_year, this->_month);
			_month++;
			if (_month > 12) {
				_year++;
				_month -= 12;
			}
		}
		return *this;
	}
	// 日期+天数
	Date operator+(int day)
	{
		Date DateCopy(*this);
		DateCopy.operator+=(day);
		return DateCopy;
	}
	// 日期-日期 返回天数
	int operator-(const Date& d)
	{
		int YearDay = 0, MonthDay = 0, Day = _day - d._day;
		while (--_month) {
			MonthDay += GetMonthDay(_year, _month);
		}
		int month = d._month;
		while (--month) {
			MonthDay -= GetMonthDay(d._year, month);
		}
		_year--;
		int year = d._year - 1;
		for (int YearCount = _year - year; YearCount > 0; YearCount--) {
			YearDay += 365;
			if ((_year % 400 == 0) || ((_year % 4 == 0) && (_year % 100 != 0)))
				YearDay += 1;
			_year--;
		}
		return YearDay + MonthDay + Day;
	}
	void Print() {
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2023, 10, 16);
	Date d2;
	//+=和+运算符重载
	//这里要说明的是,由于"+="没有返回值,所以在函数对象内部改变的时候一定要用this指针来改变,this指针指向d1
	d1 += 365;
	cout << "d1重载d1+=365: ";
	d1.Print();
	d2 = d1 + 23;
	cout << "d2重载d2=d1+23: ";
	d2.Print();
	//-=和-的运算符重载
	//"-="运算符与"+="同理
	d1 -= 365;
	cout << "d1重载d1-=365: ";
	d1.Print();
	d2 = d1 - 365;
	cout << "d2重载d2=d1-365: ";
	d2.Print();
	//日期-日期的运用
	cout << "d2-d1: " << d2 - d1 << endl;
	return 0;
}

        这里要注意的是函数的返回类型,“ +=,-= ”运算一般是不需要返回类型的,所以在内部实现要用this访问,从而间接实现原对象的运算。“ +,- ”操作符要有返回类型去接收,可直接返回临时对象。

        (3)“ 前置++,后置++,前置--,后置-- ”在类对象内定义进行演示和解说。

    // 后置++,形参为int,C++专门规定,为了与前置++区分
    Date operator++(int) {
        Date ret(*this);
        *this += 1;
        return ret;
    }
    // 前置++
    Date& operator++() {
        *this += 1;
        return *this;
    }
    // 后置--,形参为int,C++专门规定,为了与前置--区分
    Date operator--(int) {
      //先将原本类进行拷贝,然后将此对象里的值--,返回此对象,实现了先赋值,再--
        Date ret(*this);
        *this -= 1;
        return ret;
    }
    // 前置--
    Date& operator--() {
      //直接返回*this,实现了先把对象的值--,然后将此对象返回,实现了先--,再赋值
        *this -= 1;
        return *this;
    }

        这里要注意的是前置++、前置--和后置++、后置--的写法和前置和后置的实现。如上代码中C++的专门规定前置与后置的写法,前置的实现是先进行运算,然后再进行赋值,所以要直接用this进行。后置的实现要先进行赋值,然后再运算。这里我们虽然实现了this的运算,但返回的是运算前的对象,进而实现了此原理。

        (4)赋值运算符。赋值运算符比较特殊,我们要区分它与拷贝构造。当类与类进行赋值时,若类还没有进行初始化,将会调用拷贝构造;若已进行了初始化,将会调用赋值运算符。

        1,赋值运算符一般也要设置返回类型,一般返回 *this,因为我们要保证连续赋值的情况。

#include 
#include 
using namespace std;
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 赋值运算符重载
	Date& operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2023, 11, 9);
	Date d2, d3, d4;
	d4 = d3 = d2 = d1;//调用赋值运算符重载
	return 0;
}

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

class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    int _year;
    int _month;
    int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)//出现错误,出现在全局中
{
    left._year = right._year;
    left._month = right._month;
    left._day = right._day;
    return left;
}

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

        3,用户自己如果没有定义赋值运算符,编译器会生成一个默认赋值运算符重载,以浅拷贝的形式实现拷贝。内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

#include 
using namespace std;
class Time
{
public:
	Time()
	{
		_hour = 1;
		_minute = 1;
		_second = 1;
	}
	Time& operator=(const Time& t)
	{		
		_hour = t._hour;
		_minute = t._minute;
		_second = t._second;
		return *this;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d1;
	Date d2;
	d1 = d2;
	return 0;
}

d1 = d2运行的调试解图

C++拷贝构造函数和运算符重载_第5张图片

        默认赋值运算符重载的原理跟默认拷贝构造一样,如果类中未涉及到资源空间的管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要自己定义实现。

        (5)流操作符的重载。在C++程序中,我们也可实现插入操作符(<<)和提取操作符(>>)的重载。

        我们首先要认识ostream和istream。ostream用于处理输出流的类,类中定义了插入操作符(<<)来将数据写入输出流;istream是用于处理输入流的类。它定义了提取操作符(>>)来从输入流中提取数据。

定义样例:

在类的内部定义

第一个参数默认this指针

class Date
{
private:
    int _year = 2023;
    int _month = 10;
    int _day = 16;
};

//没有返回类型的情况

void operator<<(ostream& out);

void operator>>(istream& in);

//在类内部声明,在外部进行定义,也相当于在内部定义

void Date::operator<<(ostream& out);

void Date::operator>>(istream& in);

//返回类型为ostream和istream,这种情况为了支持连续输入输出的情况,因为连续输入输出需要有返回值去接收

ostream& operator<<(ostream& out);

istream& operator>>(istream& in);

在类的外部定义

没有默认参数,需要把类写入形参中

class Date
{
private:
    int _year = 2023;
    int _month = 10;
    int _day = 16;
};

//没有返回类型的情况

void operator<<(ostream& out, const Date& d1);

void operator>>(istream& out, const Date& d1);

//返回类型为ostream和istream,这种情况为了支持连续输入输出的情况,因为连续输入输出需要有返回值去接收

ostream& operator<<(ostream& out, const Date& d1);

istream& operator>>(istream& out, const Date& d1);

首先我们先观察以下代码:

#include
using namespace std;
class Date
{
public:
	void operator<<(ostream& out) 
	{
		out << _year << "/" << _month << "/" << _day << endl;
	}
	void operator>>(istream& in)
	{
		in >> _year >> _month >> _day;
	}
private:
	int _year = 2023;
	int _month = 10;
	int _day = 16;
};
int main()
{
	Date d1;
	//cin >> d1;// 相当于cin.operator>>(d1);系统报错
	d1 >> cin;// 相当于d1.operator<<(cin);正常运行
	//cout << d1;// 相当于cout.operator<<(d1);系统报错
	d1 << cout;// 相当于d1.operator<<(cout);正常运行
	return 0;
}

        因为双操作数的运算符第一个参数是左操作数,第二个参数是右操作数,而在Date对象内部中,Date类对象默认占第一个位置,导致了以上情况的发生。

        要想解决以上的问题,保证代码的可读性,就改变参数的位置,显然这不能在类对象的内部定义,必须在外部定义。

无返回类型的使用(即不支持连续使用)

#include
using namespace std;
class Date
{
public:
	int _year = 2023;
	int _month = 10;
	int _day = 16;
};
void operator<<(ostream& out, const Date& d)
{
	out << d._year << "/" << d._month << "/" << d._day << endl;
}
void operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
}
int main()
{
	Date d1;
	cin >> d1;// 相当于cin.operator>>(d1);正常运行
	cout << d1;// 相当于cout.operator<<(d1);正常运行
	return 0;
}

运用图解

C++拷贝构造函数和运算符重载_第6张图片

有返回类型的使用(即支持连续使用)

#include
using namespace std;
class Date
{
public:
	int _year = 2023;
	int _month = 10;
	int _day = 16;
};
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "/" << d._month << "/" << d._day << endl;
	return out;
}
istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}
int main()
{
	Date d1, d2;
	cin >> d1 >> d2;// 从左到右连续进行,返回类型为输入流类的引用
	cout << d1 << endl << d2;// 从左到右连续进行,返回类型为输出流类的引用
	return 0;
}

这里要注意几个问题:

        1,当在外部定义时,因为是在外面访问成员对象的,因此权限必须设为public。

        2,在使用 “ >> ” 运算符时,因为是要对成员对象进行输入流给值的操作,所以类对象不能加用const。

        3,连续使用运算符操作相当于连续不断的赋值操作,这里需注意函数的返回类型。

总结一下:除流操作符以外的操作符一般是实现成员函数,“ >>,<< ”流运算要在外部实现,只有这样才能让流对象做第一个参数,实现可读性。还有就是要注意函数的返回类型,因为有了返回类型,才能实现连续使用运算符的操作。

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