
🫧个人主页:小年糕是糕手
💫个人专栏:《C++》《C++同步练习》《数据结构》《C语言》
🎨你不能左右天气,但你可以改变心情;你不能改变过去,但你可以决定未来!
目录
一、拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数。
拷贝构造的特点:
- 拷贝构造函数是构造函数的一个重载。
- 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。拷贝构造函数也可以多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须有缺省值。
- C++ 规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
- 若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝 / 浅拷贝 (一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。
- 像 Date 这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显式实现拷贝构造。像 Stack 这样的类,虽然也都是内置类型,但是_a 指向了资源,编译器自动生成的拷贝构造完成的值拷贝 / 浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝 (对指向的资源也进行拷贝)。像 MyQueue 这样的类型内部主要是自定义类型 Stack 成员,编译器自动生成的拷贝构造会调用 Stack 的拷贝构造,也不需要我们显式实现 MyQueue 的拷贝构造。这里还有一个小技巧,如果一个类显示实现了析构并释放资源,那么他就需要显示写拷贝构造,否则就不需要。
- 传值返回会产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名 (引用),没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回。
无穷递归:
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、赋值运算符重载
赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。
赋值运算符重载的特点:
- 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成 const 当前类类型引用,否则会传值传参会有拷贝
- 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
- 没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝 / 浅拷贝 (一个字节一个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数。
- 像 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();
//}
