【C++】类和对象(三) -- 拷贝构造函数、赋值运算符重载

🫧个人主页:小年糕是糕手

💫个人专栏:《C++》《C++同步练习》《数据结构》《C语言》

🎨你不能左右天气,但你可以改变心情;你不能改变过去,但你可以决定未来!



目录

一、拷贝构造函数

二、赋值运算符重载

2.1、运算符重载

2.1.1、解决成员函数私有不能调用问题

plan1:

plan2:

2.1.2、.*运算符

2.2、赋值运算符重载

2.2.1、代码一:

2.2.2、代码二:

2.3、简单日期类的实现

2.3.1、Date.h

2.3.2、Date.cpp

2.3.3、test.cpp


一、拷贝构造函数

如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数。

拷贝构造的特点:

  1. 拷贝构造函数是构造函数的一个重载。
  2. 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。拷贝构造函数也可以多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须有缺省值。
  3. C++ 规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
  4. 若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝 / 浅拷贝 (一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。
  5. 像 Date 这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显式实现拷贝构造。像 Stack 这样的类,虽然也都是内置类型,但是_a 指向了资源,编译器自动生成的拷贝构造完成的值拷贝 / 浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝 (对指向的资源也进行拷贝)。像 MyQueue 这样的类型内部主要是自定义类型 Stack 成员,编译器自动生成的拷贝构造会调用 Stack 的拷贝构造,也不需要我们显式实现 MyQueue 的拷贝构造。这里还有一个小技巧,如果一个类显示实现了析构并释放资源,那么他就需要显示写拷贝构造,否则就不需要。
  6. 传值返回会产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名 (引用),没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回。

无穷递归:

cpp 复制代码
//拷贝构造
#include<iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//拷⻉构造函数的第⼀个参数必须是当前类类型对象的引⽤
	//使⽤传值⽅式编译器直接报错,因为语法逻辑上会引发⽆穷递归调⽤
	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;
};

//自定义类型,传值传参要调用拷贝构造(这里要用引用传参不能直接传值传参)
//我们调用会先完成d1拷贝构造给d,然后进入函数
//建议引用传参加上const(只要被引用对象不改变)
// void func(const Date &d)
void func(Date& d)//这里不能写成void func(Date d)
{
	//...
}

int main()
{
	Date d1(2025, 8, 1);
	//拷贝构造 -- 拷贝同类型的对象来初始化
	Date d2(d1);

	//权限可以平移或缩小
	const Date d3(2025, 8, 1);
	Date d4(d3);
	//这也是拷贝构造
	Date d5 = d3;

	//我们调用函数先完成传参再进入函数
	func(d1);

	return 0;
}

但是如果是栈的话,需要自己实现拷贝构造,因为他需要的是深拷贝

对于日期类我们就是单纯的拷贝即可(浅拷贝/值拷贝)
浅拷贝的特点:1)一个对象修改,会影响另一个对象
2)析构时,释放俩次空间(同一块空间)

对于栈来说他不能浅拷贝,我们修改一个栈不能影响第二个
深拷贝的特点:1)不仅仅对成员拷贝,还要对指向资源空间数据进行处理(各自有各自的空间)
2)析构时,各自释放各自的

二、赋值运算符重载

2.1、运算符重载
  • 当运算符被用于类类型的对象时,C++ 语言允许我们通过运算符重载的形式指定新的含义。C++ 规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。

  • 运算符重载是具有特殊名字的函数,他的名字是由 operator 和后面要定义的运算符共同构成。和其他函数一样,它也具有其返回类型和参数列表以及函数体。

  • 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。

  • 如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传递隐式的 this 指针,因此运算符重载作为成员函数时,参数比运算对象少一个。

  • 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。

  • 不能通过连接语法中没有的符号来创建新的操作符:比如 operator@。

  • ..*::sizeof?:,注意以上 5 个运算符不能重载。(选择题里面常考,大家要记一下)

  • 重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如:int operator+(int x, int y)

  • 一个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如 Date 类重载 operator - 就有意义,但是重载 operator * 就没有意义。

  • 重载 ++ 运算符时,有前置 ++ 和后置 ++,运算符重载函数名都是 operator++,无法很好的区分。C++ 规定,后置 ++ 重载时,增加一个 int 形参,跟前置 ++ 构成函数重载,方便区分。

  • 重载 <<和>> 时,需要重载为全局函数,因为重载为成员函数,this 指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了 对象 <<cout,不符合使用习惯和可读性。重载为全局函数把 ostream/istream 放到第一个形参位置就可以了,第二个形参位置当类类型对象。

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
//这里最好写成(我们这里不用更改参数)
//bool operator==(const Date& x1,const Date& x2)
bool operator==(Date x1, Date x2)
{
	//这里只是一个简单演示不是实现内容的写法
	return true;
}

//这里最好写成(我们这里不用更改参数)
//bool operator-(const Date& x1,const Date& x2)
int operator - (Date x1, Date x2)
{
	return 0;
}

int main()
{
	Date d1(2025, 11, 29);
	Date d2(2025, 11, 30);
	//⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数
	//顺序是不能换的
	d1 == d2;
	//等价于
	operator==(d1, d2);
	
	d1 - d2;
	//等价于
	operator-(d1, d2);

	return 0;
}

这里仅为简单展示一下,下面我们去详细实现:

2.1.1、解决成员函数私有不能调用问题
cpp 复制代码
//运行符顺序是对应的不能换
//但是我们成员函数是私有的,这里不能直接用该怎么解决?
bool operator==(const Date& x1, const Date& x2)
{
	return x1._year == x2._year
		&& x1._month == x2._month
		&& x1._day == x2._day;
}
plan1:
cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//plan 1
	//我们去调用公有的成员函数
	int GetYear()
	{
		return _year;
	}
	int GetMonth()
	{
		return _month;
	}
	int GetDay()
	{
		return _day;
	}
	//我们将下面的_year、_month、_day均改成调用成员函数即可(java中常用)

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

//运行符顺序是对应的不能换
//但是我们成员函数是私有的,这里不能直接用该怎么解决?
bool operator==(const Date& x1, const Date& x2)
{
	return x1._year == x2._year
		&& x1._month == x2._month
		&& x1._day == x2._day;
}

int operator-(const Date& x1, const Date& x2)
{
	//这里我们先不实现了,比较复杂
	return 0;
}

int main()
{
	Date d1(2025, 8, 1);
	Date d2(2025, 10, 1);
	cout << (d1 == d2) << endl;
	//可以写成
	operator==(d1, d2);
	//同上
	d1 - d2;
	operator-(d1, d2);

	return 0;
}
plan2:
cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//plan 2
	//我们在类外面不能访问,在类里面可以访问

	//运行符顺序是对应的不能换
    //但是我们成员函数是私有的,这里不能直接用该怎么解决?
	//成员函数有一个隐含的this指针
	//参数个数要和运算符的运算对象数量一样多,这样我们将operator函数变为成员函数多了一个隐含的this指针,这里实际上有三个参数
	bool operator==(const Date& d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}

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


int main()
{
	Date d1(2025, 8, 1);
	Date d2(2025, 10, 1);
	cout << (d1 == d2) << endl;
	//可以写成
	d1.operator==(d2);

	return 0;
}
2.1.2、.*运算符
cpp 复制代码
//介绍一个新的运算符 -- .*
//.*主要用来访问成员函数的指针
#include<iostream>
using namespace std;

void func1()
{
	cout << "void func()" << endl;
}

class A
{
public:
	void func2()
	{
		cout << "A::func()" << endl;
	}
};

int main()
{
	//函数指针的调用
	void(*pf1)() = func1;
	(*pf1)();

	//成员函数指针也要指定类域
	//A类型成员函数的指针(成员函数有个隐含的this指针)
	//成员函数的指针前还要加&(语法规定)
	void(A::*pf2)() = &A::func2;
	A aa;
	//this指针在形参和实参的位置均不能显示传递
	//不能直接调用
	(aa.*pf2)();

	return 0;
}
2.2、赋值运算符重载

赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。

赋值运算符重载的特点:

  1. 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成 const 当前类类型引用,否则会传值传参会有拷贝
  2. 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
  3. 没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝 / 浅拷贝 (一个字节一个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数。
  4. 像 Date 这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载。像 Stack 这样的类,虽然也都是内置类型,但是_a 指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝 / 浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝 (对指向的资源也进行拷贝)。像 MyQueue 这样的类型内部主要是自定义类型 Stack 成员,编译器自动生成的赋值运算符重载会调用 Stack 的赋值运算符重载,也不需要我们显示实现 MyQueue 的赋值运算符重载。这里还有一个小技巧,如果一个类显示实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要。
2.2.1、代码一:
cpp 复制代码
//重载操作符⾄少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义
//如: int operator+(int x, int y)
//⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义
//⽐如Date类重载operator - 就有意义,但是重载operator + 就没有意义。
//日期相加是没有意义,但是日期加天数是有意义的
#include<iostream>
using namespace std;

Date operator+(const Date& d, int x);

int main()
{

	return 0;
}
2.2.2、代码二:
cpp 复制代码
//赋值运算符重载(赋值拷贝)
//拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象。
//赋值运算符重载是用于完成俩个已经存在的对象直接的拷贝赋值
#include<iostream>
using namespace std;

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

	//这可以解决大部分的赋值,但是连续赋值不能解决(没有返回值)
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	//赋值运算符重载
	//d3 = d5
	//我们将d5赋值给了this指针,但是我们拿不到d3
	//这里实际前面还有一个Date* const this
	//this就是d3的地址,*this就是d3
	//this指针在形参和实参的位置不能显示定义,但是在类里可以使用
	//如果这里使用传值传参(特点就是不返回*this,返回的是*this的拷贝)
	Date& operator =(const Date& d)
	{
		//防止自己赋值给自己
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}

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

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


int main()
{
	Date d1(2025, 11, 25);
	//拷贝构造 -- 一个已经存在的对象初始化另一个对象
	Date d2(d1);
	//一定注意,这是拷贝构造
	Date d4 = d1;

	Date d3(2025, 11, 26);
	//赋值运算符重载(俩个已经存在的对象)
	d1 = d3;

	Date d5(2025, 11, 27);
	d1 = d3 = d5;
	//赋值支持连续赋值,从右往左
	//首先是d3 = d5,d3作为表达式的返回值再赋值给d1
	//所以这里是俩次函数调用

	return 0;
}

我们需要注意这段代码中的++Date& operator =(const Date& d)++ 这里为什么不用Date operator =(const Date &d):

原因:避免拷贝开销 + 支持连续赋值

使用Date& operator=(返回引用),而不是Date operator=(返回值),主要有两个核心原因:

1. 避免不必要的拷贝,提升效率

如果返回Date(值),函数会在返回时拷贝当前对象 (调用拷贝构造函数),产生额外的性能开销;而返回Date&(引用),直接返回当前对象本身,无拷贝操作,更高效。

2. 支持连续赋值(如d1 = d3 = d5

C++ 中连续赋值(a = b = c)的执行逻辑是从右到左 :先计算b = c,再将结果赋值给a

  • operator=返回Date&(引用),b = c的结果是b的引用,可直接参与后续赋值(a = (b = c));
  • 若返回Date(值),b = c的结果是一个临时对象,虽然也能完成赋值,但临时对象会被销毁,且存在拷贝开销。

这是 C++ 中赋值运算符重载的标准写法,既高效又符合语法习惯。(AI生成)

2.3、简单日期类的实现

这里我们只是简单实现一些日期类的基本功能,下一篇博客我会为大家讲解取地址运算符重载,当我们学完之后我会为大家带来更详细的日期类的实现。

2.3.1、Date.h
cpp 复制代码
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
using namespace std;

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1);
	void Print();

	//给我一个年份和月份我们要获取这个月的天数
	//高频调用的小函数最好使用内联,类里面的函数本身就内联/
	int GetMonthDay(int year, int month)
	{
		assert(month > 0 && month < 13);
		static int monthDayArray[13] = { -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
		//闰年与平年2月天数
		if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
		{
			return 29;
		}
		else
		{
			return monthDayArray[month];
		}

	}

	/*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);*/

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

	// d1 -= 天数 
	Date& operator-=(int day);
	Date operator-(int day);

	//// d1 - d2
	//int operator-(const Date& d);

	// ++d1 -> d1.operator++()
	Date& operator++();

	// d1++ -> d1.operator++(0)
	// 为了区分,构成重载,给后置++,强⾏增加了⼀个int形参 
	// 这⾥不需要写形参名,因为接收值是多少不重要,也不需要⽤ 
	// 这个参数仅仅是为了跟前置++构成重载区分 
	Date operator++(int);

	Date& operator--();
	Date operator--(int);
private:
	int _year;
	int _month;
	int _day;
};
2.3.2、Date.cpp
cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1

#include"Date.h"

Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}
void Date::Print()
{
	cout << _year << "-" << _month << "-" << _day << endl;
}

//日期+天数
//d1 += 100
//+=改变自己返回自己,传引用返回
Date& Date::operator+=(int day)
{
	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day = _day - GetMonthDay(_year, _month);
		++_month;
		if (_month == 13)
		{
			++_year;
			_month = 1;
		}
	}

	return *this;
}

//不能改变自己
//d1 + 100
Date Date:: operator+(int day)
{
	//拷贝构造
	Date tmp(*this);
	////我们去改变拷贝构造不去改变自己
	//tmp._day += day;
	//while (tmp._day > GetMonthDay(tmp._year, tmp._month))
	//{
	//	tmp._day = tmp._day - GetMonthDay(tmp._year, tmp._month);
	//	++tmp._month;
	//	if (tmp._month == 13)
	//	{
	//		++tmp._year;
	//		tmp._month = 1;
	//	}
	//}
	tmp += day;

	return tmp;
}


// d1 -= 天数 
Date& Date::operator-=(int day)
{
	if (day < 0) 
	{ 
		// 处理负天数(等价于 += 绝对值)
		return *this += -day;
	}

	_day -= day;
	// 当日期≤0时,向前借月/年
	while (_day <= 0) 
	{
		--_month; // 月份减1
		if (_month == 0)
		{
			// 月份减到0,切换到上一年的12月
			--_year;
			_month = 12;
		}
		// 日期 += 当前月份的天数(向前借月,用当月天数补)
		_day += GetMonthDay(_year, _month);
	}

	return *this; // 返回自身引用,支持链式操作(如 d1 -= 5 -= 3)
}

//d1 - 天数
Date Date::operator-(int day)
{
	Date tmp(*this); // 拷贝原对象(不修改自身)
	tmp -= day;      // 调用 -= 完成计算(复用逻辑,避免冗余)
	return tmp;      // 返回新对象
}


//前置++
//++d1 -> d1.operator++( );
//调用完成后d1还在,所以用引用返回
Date& Date::operator++()
{
	*this += 1;
	return *this;
}

//后置++
//d1++ -> d1.operator++(0);
//返回的是一个局部对象不能用传引用返回
Date Date::operator++(int)
{
	Date tmp(*this);
	*this += 1;

	return tmp;
}

//前置--
Date& Date::operator--()
{
	*this -= 1;
	return *this;
}

//后置--
Date Date::operator--(int)
{
	Date tmp(*this);
	*this -= 1;

	return tmp;
}
2.3.3、test.cpp
cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1

#include"Date.h"

//d1+=天数/d1+天数/d1-=天数/d1-天数
int main()
{
	//+=
	Date d1(2025, 11, 29);
	Date d2 = d1 += 100;
	d1.Print();
	d2.Print();

	//+
	Date d3(2025, 11, 29);
	Date d4 = d3 + 100;
	d3.Print();
	d4.Print();

	//-=
	Date d5(2025, 11, 29);
	Date d6 = d5 - 100;
	d5.Print();
	d6.Print();

	//-
	Date d7(2025, 11, 29);
	Date d8 = d7 - 100;
	d7.Print();
	d8.Print();

	return 0;
}

////前置++与后置++
////前置--与后置--
//int main()
//{
//	Date d1(2025, 11, 29);
//	Date ret1 = d1++;
//	ret1.Print();
//	d1.Print();
//
//	Date d2(2025, 11, 29);
//	Date ret2 = ++d2;
//	ret2.Print();
//	d2.Print();
//
//	Date d3(2025, 11, 29);
//	Date ret3 = d3--;
//	ret3.Print();
//	d3.Print();
//
//	Date d4(2025, 11, 29);
//	Date ret4 = --d4;
//	ret4.Print();
//	d4.Print();
//}

相关推荐
繁华似锦respect1 小时前
C++ 设计模式之单例模式详细介绍
服务器·开发语言·c++·windows·visualstudio·单例模式·设计模式
艾莉丝努力练剑1 小时前
【C++:C++11收尾】解构C++可调用对象:从入门到精通,掌握function包装器与bind适配器包装器详解
java·开发语言·c++·人工智能·c++11·右值引用
卿雪1 小时前
MySQL【索引】篇:索引的分类、B+树、创建索引的原则、索引失效的情况...
java·开发语言·数据结构·数据库·b树·mysql·golang
CNRio1 小时前
第七章-DockerSwarm:容器集群的‘指挥官‘
java·开发语言·容器
徐新帅1 小时前
C++ 竞赛训练营第三课:STL 核心容器之 priority_queue
开发语言·c++
m0_740043731 小时前
JavaScript
开发语言·javascript·ecmascript
八月的雨季 最後的冰吻1 小时前
FFmepg--29- C++ 音频混音器实现
开发语言·c++·音视频
wjm0410061 小时前
秋招ios面试 -- 真题篇(一)
开发语言·ios·面试
拾光Ծ1 小时前
“异常”处理机制 与 C++11中超实用的 “智能指针”
java·开发语言·c++·安全