【C++】拷贝构造函数与运算符重载
写在前面
拷贝构造函数与赋值运算符重载都是属于类的默认成员函数!
默认成员函数是程序猿不显示声明定义,编译器会中生成。
在程序编写中,我们也经常使用拷贝的方式来获取到对应的值,例如整形变量拷贝int a = 0; int b = a;
等等。在程序的编写中,我们也会需要进行对象的拷贝,这样来获取到对应对象的值。
文章目录
- 写在前面
- 一、拷贝构造函数
- 1.1、若未显式定义,编译器会生成默认的拷贝构造函数。
- 二、赋值运算符重载
- 2.1、赋值运算符重载(类的默认成员函数)
- 2.2、重载运算符中的特殊:前置++和后置++的重载
- 2.3、流插入与流提取的重载运算符
一、拷贝构造函数
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。是一个构造函数
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
我们先创建一个标准的拷贝构造函数
class Date
{
public:
Date() {
cout << "Date()" << endl;
}
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
Date(const Date& d1) { //拷贝构造函数
_year = d1._year;
_month = d1._month;
_day = d1._day;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main() {
Date d1(2024,1,1);
printf("d1:>");
d1.Print();
Date d2(d1);
printf("d2:>");
d2.Print();
return 0;
}
程序运行结果:
如果我们不使用类引用作为参数,而是使用传值的方式作为参数的话,会导致无穷递归调用
class Date
{
public:
Date() {
cout << "Date()" << endl;
}
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
Date(Date d1) { //拷贝构造函数,使用传值方式
_year = d1._year;
_month = d1._month;
_day = d1._day;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main() {
Date d1(2024,1,1);
printf("d1:>");
d1.Print();
Date d2(d1);
printf("d2:>");
d2.Print();
return 0;
}
- 我们知道,函数参数使用传值,那么形参就是实参的一份临时拷贝。
C++规定的拷贝:
- 内置类型直接拷贝
- 自定义类型必须调用该自定义类型对应的拷贝构造函数完成拷贝。
- 此时,形参是
stack
类的对象,我们实参也是stack
类的对象d1
,形参接收就需要进行拷贝,而这个时候又涉及到了类的拷贝,那么也是调用stack
类的拷贝构造函数,但是拷贝构造函数又时需要传值……无限套娃,如下图
程序运行结果:
- 编译器会检查拷贝构造函数是否正确的编写。
在拷贝构造中,参数要加上const
,加上const
的好处:
- 防止在拷贝构造中写反,导致原对象被修改为其他值。
- 如果原对象是
const
修饰的对象,也可以进行拷贝构造,不会造成权限放大。
1.1、若未显式定义,编译器会生成默认的拷贝构造函数。
默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
拷贝构造对类型的处理:
- 对内置类型成员进行值拷贝/浅拷贝
- 对自定义类型成员会调用它的拷贝构造函数
当类中的属性全部都是内置类型成员,我们可以使用默认生成的拷贝构造函数完成拷贝操作
我们把Date
类自己编写的拷贝构造删除后,尝试使用默认的拷贝构造函数。
class Date
{
public:
Date() {
cout << "Date()" << endl;
}
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main() {
Date d1(2024,1,1);
printf("d1:>");
d1.Print();
Date d2(d1);
printf("d2:>");
d2.Print();
return 0;
}
程序运行结果:
在上图中,我们也可以看出,程序没有编写拷贝构造函数,使用默认拷贝构造函数也完成了任务。但这并不代表可以一直使用默认构造函数。
我们使用stack
类来尝试一下,默认拷贝构造函数是否可以完成我们预想的结果。
class stack {
public:
stack(int defintCapacity = 4) {
_arr = (int*)calloc(defintCapacity, sizeof(int));
if (nullptr == _arr)
{
perror("malloc申请空间失败");
return;
}
_capacity = defintCapacity;
_size = 0;
}
stack(int* arr, int defintCapacity) {
if (nullptr == arr) {
perror("malloc申请空间失败");
return;
}
_arr = arr;
_capacity = defintCapacity;
_size = 0;
}
void push(int x) {
//....扩容等
_arr[_size++] = x;
}
~stack() {
free(_arr);
_arr = nullptr;
}
private:
int* _arr;
int _size;
int _capacity;
};
int main() {
stack s1;
stack s2(s1);
return 0;
}
程序运行结果:
- 因为在默认的拷贝构造函数对象按内存存储按字节序完成拷贝,在拷贝结束后发现
s2
对象的_arr
数组地址和s1
对象的_arr
数组地址一样。 - 这时候我们如果我们往
s2
存储内容,也会改变s1
对象的内容,这不是我们想要的,而且在对象结束生命周期之后,对象会自动调用自己对应的析构函数。这时候析构函数就多次释放同一个空间程序崩溃,如下图。
使用浅拷贝的内存图布局如下图:
要解决这种问题,就需要涉及深拷贝(不才后面会专门写一篇笔记),下面代码是针对这种情况的特殊解决办法。
class stack {
public:
stack(int defintCapacity = 4) {
_arr = (int*)calloc(defintCapacity, sizeof(int));
if (nullptr == _arr)
{
perror("malloc申请空间失败");
return;
}
_capacity = defintCapacity;
_size = 0;
}
stack(int* arr, int defintCapacity) {
if (nullptr == arr) {
perror("malloc申请空间失败");
return;
}
_arr = arr;
_capacity = defintCapacity;
_size = 0;
}
stack(stack& s) { //需要程序猿自己编写拷贝构造函数(深拷贝)
_arr = (int*)calloc(s._capacity, sizeof(int));
if (nullptr == _arr)
{
perror("malloc申请空间失败");
return;
}
memcpy(_arr, s._arr, s._capacity * sizeof(int));
_capacity = s._capacity;
_size = s._size;
}
void push(int x) {
//....扩容等
_arr[_size++] = x;
}
~stack() {
free(_arr);
_arr = nullptr;
}
private:
int* _arr;
int _size;
int _capacity;
};
我们解决了内置类型拷贝构造的问题后,我们自定义类型是否需要每个都写拷贝构造函数呢?
我们使用栈实现队列
class stack {
public:
stack(int defintCapacity = 4) {
_arr = (int*)calloc(defintCapacity, sizeof(int));
if (nullptr == _arr)
{
perror("malloc申请空间失败");
return;
}
_capacity = defintCapacity;
_size = 0;
}
stack(int* arr, int defintCapacity) {
if (nullptr == arr) {
perror("malloc申请空间失败");
return;
}
_arr = arr;
_capacity = defintCapacity;
_size = 0;
}
void push(int x) {
//....扩容等
_arr[_size++] = x;
}
~stack() {
free(_arr);
_arr = nullptr;
}
stack(stack& s) {
_arr = (int*)calloc(s._capacity, sizeof(int));
if (nullptr == _arr)
{
perror("malloc申请空间失败");
return;
}
memcpy(_arr, s._arr, s._capacity * sizeof(int));
_capacity = s._capacity;
_size = s._size;
}
private:
int* _arr;
int _size;
int _capacity;
};
class MyQueue{
private:
stack s1;
stack s2;
};
int main() {
stack s1;
s1.push(1);
s1.push(2);
s1.push(3);
stack s2(s1);
return 0;
}
程序运行结果:
- 这时,
MyQueue
类就不需要编写拷贝构造,因为在MyQueue
对象进行拷贝时,会自动调用stack
类的拷贝构造。 - 不写拷贝构造函数与不需要写构造函数与析构函数的逻辑是一样的。
- 拷贝构造函数的形参也尽可能的加上
const
修饰权限范围,这样可以防止形参对象的属性被修改,而且也可以防止实参的权限放大的问题
二、赋值运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数
日期类比较大小
class Date
{
public:
Date() {
cout << "Date()" << endl;
}
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
public:
int _year = 1;
int _month = 1;
int _day = 1;
};
bool Less(const Date& s1, const Date& s2) {
if (s1._year < s2._year) {
return true;
}
else if (s1._year == s2._year && s1._month < s2._month) {
return true;
}
else if (s1._year == s2._year && s1._month == s2._month && s1._day < s2._day) {
return true;
}
return false;
}
bool Larger(const Date& s1, const Date& s2) {
if (s1._year > s2._year) {
return true;
}
else if (s1._year == s2._year && s1._month > s2._month) {
return true;
}
else if (s1._year == s2._year && s1._month == s2._month && s1._day > s2._day) {
return true;
}
return false;
}
int main() {
Date d1(2022,5,4);
Date d2(2022,5,5);
cout << Less(d1, d2) << endl;
cout << Larger(d1, d2) << endl;
return 0;
}
- 在我们自定义的日期类中,我们想比较两个类的大小只能通过函数的形式来比较
- 如果函数命名不规范时,我们难以比较
所以在C++中增加了赋值运算符重载,方便程序猿的使用
运算符重载关键字:
operator
后面接需要重载的运算符符号。
我们使用运算符重载来改善上述代码:
class Date
{
public:
Date() {
cout << "Date()" << endl;
}
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
public:
int _year = 1;
int _month = 1;
int _day = 1;
};
bool operator<(const Date& s1, const Date& s2) {
if (s1._year < s2._year) {
return true;
}
else if (s1._year == s2._year && s1._month < s2._month) {
return true;
}
else if (s1._year == s2._year && s1._month == s2._month && s1._day < s2._day) {
return true;
}
return false;
}
bool operator>(const Date& s1, const Date& s2) {
if (s1._year > s2._year) {
return true;
}
else if (s1._year == s2._year && s1._month > s2._month) {
return true;
}
else if (s1._year == s2._year && s1._month == s2._month && s1._day > s2._day) {
return true;
}
return false;
}
int main() {
Date d1(2022, 5, 4);
Date d2(2022, 5, 5);
cout << operator<(d1, d2) << endl;
cout << operator>(d1, d2) << endl;
return 0;
}
我们只需要把函数名修改为:operator<
和operator>
。这样就完成了 <
和>
的运算符重载。我们在main
函数中调用也是如此,使用operator<
和operator>
来调用此函数。但是这样就和我们编写函数没什么区别。
运算符重载的作用是可以直接在main
函数中使用<
和>
来进行比较。如下:
int main() {
Date d1(2022, 5, 4);
Date d2(2022, 5, 5);
cout << operator<(d1, d2) << endl;
cout << operator>(d1, d2) << endl;
printf("\n");
cout << (d1 < d2) << endl;
cout << (d1 > d2) << endl;
return 0;
}
程序运行结果:
-
虽然我们是使用了
<
和>
来进行比较,但是我们通过operater
来重载<
和>
后,我们就可以直接使用<
和>
来进行比较。本质上还是使用operator<
和operator>
来调用此函数,编译器会自己处理的过程。如下图: -
这时,
operater
重载的<
和>
是全局的,对类中的属性要求就一定是public
类型,如果是私有的就无法访问,所以我们要重载类对象的<
和>
时,可以把operater
函数作为类的成员函数。
运算符重载必须使用满足C++对运算符重载的规定:
- 不能通过连接其他符号来创建新的操作符:比如
operator@
- 重载操作符必须有一个类类型参数,即不能全部都是内置类型,比如
bool operator+(int& a ,int& b){...}
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型
+
,不能改变其含义,如改为减… - 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的
this
.*
::
(域作用限定符)sizeof
?:
(三目).
注意以上5个运算符不能重载。
我们把上述例子的全局运算符重载函数改为Date
类的内置成员函数。
class Date
{
public:
Date() {
cout << "Date()" << endl;
}
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
bool operator<(const Date& s2) {
if (_year < s2._year) {
return true;
}
else if (_year == s2._year && _month < s2._month) {
return true;
}
else if (_year == s2._year && _month == s2._month && _day < s2._day) {
return true;
}
return false;
}
bool operator>( const Date& s2) {
if (_year > s2._year) {
return true;
}
else if (_year == s2._year && _month > s2._month) {
return true;
}
else if (_year == s2._year && _month == s2._month && _day > s2._day) {
return true;
}
return false;
}
public:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main() {
Date d1(2022, 5, 4);
Date d2(2022, 5, 5);
cout << (d1 < d2) << endl;
cout << (d1 > d2) << endl;
printf("\n");
cout << d1.operator<(d2) << endl;
cout << d1.operator>(d2) << endl;
return 0;
}
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的
this
- 而且作为类成员函数重载时,就算属性是私有的,我们也可以进行比较。
- 类成员函数重载时,在
main
函数中,也可以直接使用<
和>
来进行比较,编译器最终也是会转换为d1.operator<(d2)
2.1、赋值运算符重载(类的默认成员函数)
赋值运算符重载与 拷贝构造函数不同处:
- 拷贝构造函数适用于:用一个已经存在的对象初始化另外一个对象。
- 赋值运算符重载函数适用于:已经存在的两个对象之间赋值拷贝。
class Date
{
public:
Date() {
cout << "Date()" << endl;
}
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
Date(const Date& d1) {
cout << "Date(const Date& d1)" << endl;
_year = d1._year;
_month = d1._month;
_day = d1._day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
bool operator<(const Date& s2) {
if (_year < s2._year) {
return true;
}
else if (_year == s2._year && _month < s2._month) {
return true;
}
else if (_year == s2._year && _month == s2._month && _day < s2._day) {
return true;
}
return false;
}
bool operator>(const Date& s2) {
if (_year > s2._year) {
return true;
}
else if (_year == s2._year && _month > s2._month) {
return true;
}
else if (_year == s2._year && _month == s2._month && _day > s2._day) {
return true;
}
return false;
}
void operator=(const Date& s2) {
_year = s2._year;
_month = s2._month;
_day = s2._day;
}
public:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main() {
Date d1(2022, 5, 4);
Date d2(2000, 1, 1);
d1 = d2;
return 0;
}
程序运行结果
- 程序运行结果没有问题,但是我们在内置类型赋值的时候可以连续赋值的,如
int a,b; a=b=12;
,但是我们把赋值运算符重载
的返回值设置了void
,程序运行不了连续赋值的结果。如下图:
我们可以把返回值设置为Date
类作为返回值。如下程序:
Date operator=(const Date& s2) {
_year = s2._year;
_month = s2._month;
_day = s2._day;
return *this;
}
- 这样我们就可以完成连续赋值的处理,如下图
但是,我们直接使用传值返回,会造成大量无用的拷贝构造,这样会对性能造成一点影响:
我们就可以把传值返回改为传引用返回。虽然this
指针是形参,会随着函数的生命周期结束而销毁,但是我们可以把*this
作为返回,这样我们返回的是对象的地址,对象的生命周期不会随着函数的生命周期结束而销毁。
Date& operator=(const Date& s2) {
_year = s2._year;
_month = s2._month;
_day = s2._day;
return *this;
}
程序运行结果:
d1 = d2
其底层与d1.operator=(d2)
是一样的- 当然,在使用重载赋值运算符时,程序猿会出现自己与自己赋值的情况。这时候我们可以在重载运算符中加入自己给自己赋值的条件判断。这样可以避免自己给自己赋值。
Date& operator=(const Date& s2) {
if (this != &s2) {
_year = s2._year;
_month = s2._month;
_day = s2._day;
}
return *this;
}
赋值运算符重载格式:
- 参数类型:
const T&
,传递引用可以提高传参效率 - **返回值类型:**T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回
*this
: 要复合连续赋值的含义
因为赋值运算符重载是类的默认成员函数,即用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
注意编译器默认生成的赋值运算符重载函数:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。与拷贝构造的行为一样。
需要注意的是,编译器生成的赋值运算符拷贝是逐字节拷贝,即浅拷贝的方式。 遇到指针等相关拷贝的情况,需要程序猿编写深拷贝。
赋值运算符只能重载成类的成员函数不能重载成全局函数
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
2.2、重载运算符中的特殊:前置++和后置++的重载
在重载运算符中有前置++
和后置++
两个特殊的运算符重载。因为在程序中这两个运算符的函数名都是一样的。需要函数重载来区别前置++
和后置++
。(重载运算符--
同理)
我们先实现前置++
,还是使用上述的Date
类为例。
class Date
{
public:
Date() {
//cout << "Date()" << endl;
}
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
Date(const Date& d1) {
cout << "Date(const Date& d1)" << endl;
_year = d1._year;
_month = d1._month;
_day = d1._day;
}
void Print(){
cout << _year << "-" << _month << "-" << _day << endl;
}
bool operator<(const Date& s2) {
if (_year < s2._year) {
return true;
}
else if (_year == s2._year && _month < s2._month) {
return true;
}
else if (_year == s2._year && _month == s2._month && _day < s2._day) {
return true;
}
return false;
}
bool operator>(const Date& s2) {
if (_year > s2._year) {
return true;
}
else if (_year == s2._year && _month > s2._month) {
return true;
}
else if (_year == s2._year && _month == s2._month && _day > s2._day) {
return true;
}
return false;
}
Date& operator=(const Date& s2) {
_year = s2._year;
_month = s2._month;
_day = s2._day;
return *this;
}
Date& operator++() {//前置++
_day++;
return *this;//因为是前置++,可以把加完后的结果直接当做返回值。
}
public:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main() {
Date d1(2022, 5, 4);
++d1;
d1.Print();
return 0;
}
程序运行结果:
operator++()
:因为++
是自增不需要参数,所以我们把形参列表设为空。
我们把operator++()
当做为前置++
,但后置++
也是相同的函数名operator++
,所以在C++中我们在后置++
的形参中增加一个参数,让其形成函数重载,以达到前置++
和后置++
的区别。
但是 ++
是自增,不需要接收任何形参,但是需要完成函数重载,所以我们只需要在形参列表中增加一个int
类型即可完成重载,该类型可以不添加变量名称。如下
Date& operator++() {//前置++
_day++;
return *this;//因为是前置++,可以把加完后的结果直接当做返回值。
}
Date operator++(int) {//后置++
Date d1 = *this;//因为是后置加加,所以要把原来的结果先储存为一个临时变量。之后,我们才可以进行自增处理
_day++;
return d1;//返回的是临时变量的值。
}
- 后置
++
中,operator++(int)
的int
参数不是为了接收具体的值。仅仅是占位,跟前置++
构成函数重载。
其中前置++
和后置++
的调用会由编译器区分。我们程序员不需要理会如何进行调用。
在我们程序员自己编写的前置++
和后置++
中。会有性能的区别。因为 后置++
需要进行拷贝构造,创建一个临时变量来作为返回值,而且也是传值返回。但是在内置类型的++
或--
中性能区别不大。
2.3、流插入与流提取的重载运算符
在上面例中,我们Date
类对象的内容打印是使用print
函数来进行Date
类属性的显示,为了更好的打印,我们可以使用流的重载运算符来进行自定义类型的打印。
不才在文档查询网站中,查询到cin
是istream
类的对象和cout
是osterem
类的对象,那么我们在Date
类中,就可以通过istream
类和osterem
类来自定义引用对象来进行标准输入输出。
我们先以打印为例,即重载<<
运算符。
class Date
{
public:
Date() {
//cout << "Date()" << endl;
}
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
Date(const Date& d1) {
cout << "Date(const Date& d1)" << endl;
_year = d1._year;
_month = d1._month;
_day = d1._day;
}
void operator<<(ostream& out) {
out << _year << "年" << _month << "月" << _day << "日";
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main() {
Date d1(2022, 5, 4);
//d1.Print();
cout << d1;
Date n1 = d1;
//n1.Print();
cout << d1 << endl << n1 << endl;
return 0;
}
运行结果:
- 因为我们是把重载运算符写入了类中,成为了类的方法,所以我们直接使用
cout << d1
是无法访问的 cout << d1
其本质是cout.operator(d1)
,我们在库中是没有自定义类型重载的。如下图- 所以我们在
Date
类中重载的<<
运算符,需要d1 << cout
这样来使用,因为这才符合d1.operator(cout)
的函数调用
但是d1.operator(cout)
明显不符合我们是使用习惯,但是在类中,第一个形参默认是this
指针,无法改变的。所以我们就把operator<<
定义在类外面,作为全局函数。
作为全局重载运算符函数时,我们久可以把第一个参数设置为ostrem
,第二次参数设置为我们自定义类型Date
,这样久可以完成cout << d1
的使用。如下图
- 此时虽然,
cout << d1
没有报错,但是我们类中的属性时私有的,为了访问到我们私有的属性,我们在类中可以把全局重载运算符函数operater<<
声明为友元函数。(不才后面会专门出一篇笔记讲解)
class Date
{
friend void operator<<(ostream& out, const Date& d1);//把operator<<函数声明为友元函数
public:
Date() {
//cout << "Date()" << endl;
}
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
Date(const Date& d1) {
//cout << "Date(const Date& d1)" << endl;
_year = d1._year;
_month = d1._month;
_day = d1._day;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
void operator<<(ostream& out, const Date& d1) {
out << d1._year << "年" << d1._month << "月" << d1._day << "日";
}
- 在类中声明前加上
friend
关键字,声明void operator<<(ostream& out, const Date& d1)
函数时友元函数。
运行结果:
和之前的运算符重载函数相同,我们为了多次运用<<
,我们的返回值久设置为ostrem
,这样我们就可以实现复合使用cout << d1 << endl
。又因为cout
是iostream
创建的对象,而且cout
的生命周期是程序的声明周期,所以我们可以把ostream
设置为ostream&
引用返回
ostream& operator<<(ostream& out, const Date& d1) {
out << d1._year << "年" << d1._month << "月" << d1._day << "日";
return out;
}
运行结果:
同理,流插入也是如此
istream& operator>>(istream& in, Date& d1) {
in >> d1._year >> d1._month >> d1._day;
return in;
}
- 也需要在类中声明为友元函数。
以上就是本章所有内容。若有勘误请私信不才。万分感激💖💖 如果对大家有用的话,就请多多为我点赞收藏吧~~~💖💖
ps:表情包来自网络,侵删🌹