👀樊梓慕:个人主页
🎥个人专栏:《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C++》
🌝每一个不曾起舞的日子,都是对生命的辜负
前言
我们继续学习默认成员函数,本篇文章博主带来的是拷贝构造函数与运算符、操作符重载的讲解,并且还有const成员所带来的一系列问题,最后博主会给大家贴出利用前面所学知识写出的日期类。
欢迎大家📂收藏📂以便未来做题时可以快速找到思路,巧妙的方法可以事半功倍。
=========================================================================
GITEE相关代码:🌟fanfei_c的仓库🌟
=========================================================================
1.拷贝构造函数
1.1概念
在介绍拷贝构造函数之前,我想先举一个例子来告诉大家为什么会有拷贝构造?
比如:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);//这里是什么行为?
return 0;
}
请观察我在代码中标识的部分,很明显这里是将d1作为实参传递给构造函数。
我们之前学习函数时,曾说过,形参是实参的一份临时拷贝。
那么好了,这就是拷贝构造函数存在的其中一个意义。
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
1.2特征
拷贝构造函数也是特殊的成员函数,其特征如下:
1.拷贝构造函数是构造函数的一个重载形式。
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发❓无穷递归调用❓。
🌀为什么这里会引发无穷递归调用🌀
前面对于拷贝构造函数的定义是这样说的:拷贝构造函数在用已存在的类类型对象创建新对象时由编译器自动调用。
我们观察下面的代码:
Date(const Date date)//你会发现这里报错了??
{
_year = date._year;
_month = date._month;
_day = date._day;
int main()
{
Date d1;
Date d2(d1);
}
当d1作为参数传递给拷贝构造函数时,是不是会再次调用拷贝构造函数??
那我们就找到为什么会引发无穷递归调用的答案了。
解决的方案就是特征所描述的那样:拷贝构造函数的参数只有一个且必须是类类型对象的引用。
也就是这样:
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
3.若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
4.编译器生成的默认拷贝构造函数(浅拷贝)已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
程序报错:
🔒那么对于日期类,他所做的拷贝就是浅拷贝,那浅拷贝会不会有风险存在呢🔒
对于日期类,浅拷贝没有风险。
但是对于自己开了空间的自定义类型来讲,浅拷贝可能会引发大麻烦。
大家还记得之前学习的析构函数么,通过前面的学习我们知道,析构函数会在对象销毁时时自动调用。
那么如果对于自己开了空间的自定义类型来讲,单纯的进行浅拷贝,就会有两个对象的两个指针指向相同的空间,那这两个对象在销毁时,自动调用析构函数,就会发生同一块空间释放两次的情况,显然程序会崩溃。
那如何避免这样的问题呢,很显然我们需要像构造函数那样,自己设计一个函数用来进行拷贝,这样的拷贝被称为深拷贝。
一般比如顺序表、链表、二叉树等开了空间的自定义类型都需要深拷贝。
那么我们可以得到结论:
- 内置类型成员完成值拷贝;
- 自定义类型调用该成员的拷贝构造。
那么都有什么样的场景用到了拷贝构造呢?
当用一个已初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用。也就是说,当类的对象需要拷贝时,拷贝构造函数将会被调用。
以下情况都会调用拷贝构造函数:
① 程序中需要新建立一个对象,并用另一个同类的对象对它初始化,如前面介绍的那样。
② 当函数的参数为类的对象时。在调用函数时需要将实参对象完整地传递给形参,也就是需要建立一个实参的拷贝,这就是按实参复制一个形参,系统是通过调用复制构造函数来实现的,这样能保证形参具有和实参完全相同的值。
③ 函数的返回值是类的对象。在函数调用完毕将返回值带回函数调用处时。此时需要将函数中的对象复制一个临时对象并传给该函数的调用处。
1.3补充的一些知识
首先再来给大家强化一下构造和拷贝构造的理解。
观察下面的代码:
int main()
{
A aa1(1); //构造
// 一个已经存在的对象拷贝初始化另一个要创建的对象,就是拷贝构造
A aa2(aa1); // 拷贝构造
A aa3 = aa1; // 拷贝构造 or 赋值拷贝 -> 拷贝构造
// 两个已经存在的对象拷贝是赋值拷贝
aa2 = aa3;
return 0;
}
本质上讲,A aa2(aa1)和A aa2=aa1两种写法是一样的,都属于拷贝构造。
博主还会在接下来的文章中,介绍更多有关构造和拷贝构造的内容,这里的内容比较琐碎,目前的知识支撑还不够,所以我们还要往后学几章。
2.运算符重载
2.1运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@。
- 重载操作符必须有一个类类型参数。
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义。
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this。
- .* :: sizeof ?:(三目操作符) . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
我们来实现一个运算符==的重载试试看:
大家同时思考一个问题:运算符重载函数是放在类内部,还是全局呢?
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//private: 这里是为了示例,一般成员变量为私有
int _year;
int _month;
int _day;
};
// 这里会发现运算符重载成全局的就需要成员变量是公有的,那么问题来了,封装性如何保证?
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
void Test()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
cout << (d1 == d2) << endl;
}
我们发现如果运算符重载成全局的,一般来讲类的成员变量都是私有的,那么如何访问成员变量进行运算呢?
这里其实可以用我们后面学习的友元解决,或者干脆重载成成员函数。
由于友元我们还没讲,那就先试试看将运算符重载函数放到类内部。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
bool operator==(const Date& d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
还记得我们前面学习的this指针是隐藏的参数吧。
我们知道==运算符需要两个操作数,所以运算符重载函数只需要一个显式参数,另一个为隐藏参数*this。
2.2赋值运算符重载
1. 赋值运算符重载格式
- 参数类型:const T&,传递引用可以提高传参效率;
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值;
- 检测是否自己给自己赋值;
- 返回*this :要符合连续赋值的含义。
class Date
{
public:
Date(int year = 1900, 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;
}
Date& operator=(const Date& d)
{
if (this != &d)//检测是否自己给自己赋值
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
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)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
3.用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝(浅拷贝)。
注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time& operator=(const Time& t)
{
if (this != &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;
}
有没有发现这里和构造函数仍然十分相似。
内置类型执行浅拷贝,自定义类型调用自己的重载赋值运算符。
为什么自定义类型不能简单的进行浅拷贝,而还要自己设计深拷贝?
相信前面讲解的拷贝函数的二次析构问题已经给了你答案, 我们再在这里举一个例子加深大家的印象。
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2;
s2 = s1;//这里是默认的赋值运算符,完成的是浅拷贝
return 0;
}
如果前面的二次析构你理解了的话,这里不是问题。
分析:
总结一下:
- 如果类中未涉及到资源管理,赋值运算符是否实现都可以;
- 一旦涉及到资源管理则必须要实现。
2.3流运算符重载
流运算符设计的初衷是为了解决自定义类型的输入和输出问题。
那么就一定会涉及到流运算符重载的相关内容。
但是流运算符又不能和其他运算符一样重载到类内部,相反,流运算符必须实现到全局,这是为什么呢?
我们知道成员函数有一个隐藏的参数this,流运算符也是一个双目操作符,一般流运算符我们写为cout<<a;
但假设我们将流运算符重载到类内部作成员函数的话,第一个参数为this,所以好像我们这样写才是对的a<<cout;
也就是说我们只能也必须将流运算符重载实现到全局,才能避免this作第一个参数。
我们试试看:
对啊,定义到全局,就访问不了私有的成员变量了呀。
那这里一般我们会采用友元函数解决成员变量私有的问题。(友元函数会在后续介绍)
比如:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1);
// 友元函数
friend void operator<<(ostream& out, const Date& d);
private:
int _year;
int _month;
int _day;
};
void operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}
这样就可以解决类外部的函数无法访问到私有成员变量的问题了。
可是我们在使用时发现,<<是需要返回值的,比如这种cout<<a<<b<<endl;
那我们修改一下函数实现:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1);
// 友元函数
friend ostream& operator<<(ostream& out, const Date& d);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
完整的输入输出流重载函数:
class Date
{
public:
Date(int year, int month, int day) // 构造函数
{
_year = year;
_month = month;
_day = day;
}
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
cout << d._year << "/" << d._month << "/" << d._day;
return out;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
int main()
{
int a = 0;
int b = 1;
cout << a << " " << b << endl;
Date d1(2022, 11, 27);
Date d2(2022, 11, 28);
cout << d1 << " " << d2 << endl;
cin >> d2;
cout << d2;
return 0;
}
2.4前置++与后置++重载
前置++与后置++的区别是什么呢?
一个是先加后用,一个则是先用后加。
还有另一个问题,他们的符号都是++,那怎么区分呢?
C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递。
我们直接看实现:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 前置++:返回+1之后的结果
// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
Date& operator++()
{
_day += 1;
return *this;
}
// 后置++:
// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1
// 而temp是临时对象,因此只能以值的方式返回,不能返回引用
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
Date d1(2022, 1, 13);
d = d1++; // d: 2022,1,13 d1:2022,1,14
d = ++d1; // d: 2022,1,15 d1:2022,1,15
return 0;
}
3.const成员函数
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
比如这样:
观察下面的代码:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
void Print() const
{
cout << "Print()const" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
void Test()
{
Date d1(2022, 1, 13);
d1.Print();
const Date d2(2022, 1, 13);
d2.Print();
}
请思考下面的几个问题:
1. const对象可以调用非const成员函数吗?
2. 非const对象可以调用const成员函数吗?
3. const成员函数内可以调用其它的非const成员函数吗?
4. 非const成员函数内可以调用其它的const成员函数吗?
之前在学习引用时,我们曾经讨论过权限缩小和放大的问题👉樊梓慕-引用
权限只能缩小,不能放大。
🔓解释一下🔓
也就是说const的对象在引用时,引用须加const,如果不加,此时引用的权限大于被引用的const对象,这就是所谓的权限不能放大。
但是不加const的对象在引用时,引用可加可不加const,因为权限是平级或是缩小了的,这就是所谓的权限只能缩小。
那搞清楚了这个概念,再看前面的题目有没有思路了呢?
答案:
- ❎权限被放大;
- ✅权限被缩小;
- ❎权限被放大;
- ✅权限被缩小;
📣📣📣成员函数定义的原则📣📣📣
- 能定义成const的成员函数都应该定义成const(这里的const是指参数列表后面的const,也就是修饰this的const),这样const对象和非const对象(这里的const是指修饰对象的类型为const)都可以调用;比如下面图片,属于const对象调用非const成员函数,我们刚做的小题已经告诉我们这样是权限放大的问题了,所以现在只需要将成员函数设计为const成员函数,将this的类型改为const Date* const this,就可以解决这一问题。
- 要修改成员变量的成员函数,不能定义成const。
4.取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义,编译器默认生成。
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
5.日期类的实现
class Date
{
// 获取某年某月的天数
int GetMonthDay(int year, int month)
{
static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int day = days[month];
if (month == 2
&& ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
day += 1;
}
return day;
}
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
if (year < 1900
|| month < 1 || month > 12
|| day < 1 || day > GetMonthDay(year, month))
{
cout << "非法日期" << endl;
}
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数
// d2(d1)
Date(const Date& d)
{
this->_year = d._year;
_month = d._month;
_day = d._day;
}
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d)
{
if (this != &d)
{
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}
return *this;
}
// 析构函数
~Date()
{
// 清理工作
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
// 日期+=天数
// d1 += 10
// d1 += -10
Date& operator+=(int day)
{
if (day < 0)
{
return *this -= -day;
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
}
// 日期+天数
// d + 10
Date operator+(int day)
{
Date ret(*this);
ret += day;
return ret;
}
// 日期-天数
Date operator-(int day)
{
Date ret(*this);
ret -= day;
return ret;
}
// 日期-=天数
// d -= 100
// d -= -100
Date& operator-=(int day)
{
if (day < 0)
{
return *this += -day;
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
// 前置++
// ++d -> d.operator++(&d)
Date& operator++()
{
*this += 1;
return *this;
}
// 后置++
// d++ -> d.operator++(&d, 0)
Date operator++(int)
{
Date ret(*this);
*this += 1;
return ret;
}
// // 后置--
Date operator--(int)
{
Date ret(*this);
*this -= 1;
return ret;
}
// 前置--
Date& operator--()
{
*this -= 1;
return *this;
}
// d1 > d2
// >运算符重载
bool operator>(const Date& d)
{
if (_year > d._year)
{
return true;
}
else if (_year == d._year)
{
if (_month > d._month)
{
return true;
}
else if (_month == d._month)
{
if (_day > d._day)
{
return true;
}
}
}
return false;
}
// ==运算符重载
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
// 下面复用上面两个的实现
// >=运算符重载
bool operator >= (const Date& d)
{
return *this > d || *this == d;
}
// <运算符重载
bool operator < (const Date& d)
{
return !(*this >= d);
}
// <=运算符重载
bool operator <= (const Date& d)
{
return !(*this > d);
}
// !=运算符重载
bool operator != (const Date& d)
{
return !(*this == d);
}
// d1 - d2
// 日期-日期 返回天数
int operator-(const Date& d)
{
int flag = 1;
Date max = *this;
Date min = d;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int day = 0;
while (min < max)
{
++(min);
++day;
}
return day * flag;
}
private:
int _year;
int _month;
int _day;
};
这里需要注意的点是:已经实现了的运算符重载可以复用,逻辑转化就好了,没必要每一个都要完全实现。
最近的内容很碎,大家只需要跟着博主一起慢慢学习就好,内容完全展开后就豁然开朗啦
=========================================================================
如果你对该系列文章有兴趣的话,欢迎持续关注博主动态,博主会持续输出优质内容
🍎博主很需要大家的支持,你的支持是我创作的不竭动力🍎
🌟~ 点赞收藏+关注 ~🌟
=========================================================================