上一篇我们谈到了类和对象及默认成员函数,对其有了基本的认识。这一篇我们完善剩下的默认成员函数并补充一些周边知识
拷贝构造与赋值重载函数
什么是赋值重载函数
//对于内置类型,我们经常进行赋值操作:
int a;
int b = 10;
a = b;
//而对于自定义类型,需要进行这样的操作则需要我们重载赋值运算符:
Date d1;
Date d2(2022, 10, 9);
Date d3(d2); //拷贝构造
d1 = d2; //赋值重载
拷贝构造与赋值重载函数之间的关系:
最直观的地方就是——二者共同行为都在进行拷贝,只是发生的时机不同:
对于拷贝构造函数来说:以前面d2、d3来举例,我们希望创建出d3变量的时候,能够同时用d2的值完成对d3的初始化操作;而对于赋值重载函数来说:我们已经创建好了d2变量并完成了初始化操作后,但是我们需要对d2进行修改,这时候可以用赋值操作符来对d2进行修改;所以说赋值重载函数发生在拷贝构造函数之后且二者都是默认成员函数
//编译器支持如下写法:
Date d4 = d2;
/*思考:这是拷贝构造还是赋值重载?
从意义上来说:该场景是创建出一个d4变量,同时用d2对d4进行初始化操作,所以更符合拷贝构造函数的意义*/
//类比于:
int a = 10;
int b = a;
//所以这是一种拷贝构造行为
如何写赋值重载函数
//如何写赋值重载函数,还是以日期类为例
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
this->_year = year;
_month = month;
_day = day;
}
//重载运算符 "=",使其实现赋值功能
void operator=(const Date& d)
{
this->_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
对于需要两个操作数的运算符来说,左右顺序有时候很重要。对于加法来说,加法交换律不影响最终结果,但是对于减法来说,左右顺序会影响结果,所以这时候我们需要严格控制具有两个操作数的运算符
//函数调用过程:
int main()
{
Date d2(2022, 10, 14);
Date d4;
d4 = d2;
return 0;
}
//对于d4 = d2,我们需要格外注意,赋值操作符需要两个操作数
//而这里是d2赋给d4,所以d4在左,d2在右,所以我们在写函数的时候需要注意左右顺序
//d4 = d2 ==> d4.operator=(d2),这里d4被this指针所接收,d2作为参数传入函数
值得注意的是:虽然赋值的基本功能已经完成,但是我们还需要支持链式赋值的场景
int i, j;
i = j = 10;
//从意义上来说,从右往左执行,规定j = 10之后返回一个值,然后作为右操作数再赋给i从而实现连续赋值
Date d0;
Date d1;
Date d2(2022, 10, 9);
d0 = d1 = d2; //d1 = d2的返回值应该是d2或者d1,二者都行,但是严格来说需要返回左操作数
//所以我们重新修改赋值重载函数:
Date& operator=(const Date& d) //我们本身不需要修改d的内容,所以使用const来缩小权限
{
this->_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
//因为*this的内容不随栈帧而销毁,所以使用传引用返回减少拷贝次数
//对比:
Date& operator=(const Date& d) //我们本身不需要修改d的内容,所以使用const来缩小权限
{
this->_year = d._year;
_month = d._month;
_day = d._day;
return d; //此处报错,因为返回d会扩大权限,原本d的权限被const的修饰,
//若想返回则需要使用const Date&返回,但是一般规定返回的值是可以被修改的,
//所以这里建议返回*this,而不返回d
}
既然赋值运算符重载函数时默认成员函数,那么我们不主动实现的时候,编译器是如何自行实现?
对于内置类型编译器完成值拷贝,类似于memcpy;而对于自定义类型编译器则会去调用其自身的赋值运算符重载函数,所以大多数情况下拷贝构造和赋值重载都不需要我们主动实现。
但是对于需要写析构函数的类,则需要我们主动实现拷贝构造和赋值重载函数,类似于Stack
typedef int STDatatype;
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (STDatatype*)malloc(sizeof(STDatatype) * capacity);
_capacity = capacity;
_top = 0;
}
Stack(Stack& d)
{
_a = (STDatatype*)malloc(sizeof(STDatatype) * d._capacity);
memcpy(_a, d._a, sizeof(STDatatype) * d._capacity);
_top = d._top;
_capacity = d._capacity;
}
void Push(STDatatype x);
void Pop();
STDatatype Top();
bool Empty();
int Size();
void Print();
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDatatype* _a;
int _top;
int _capacity;
};
//所以这里一样要进行深拷贝:
//操作如下:1.直接先释放掉st1的空间 2.重新开辟空间 3.再进行数据拷贝
//实现如下:
Stack& operator=(const Stack& st)
{
free(this->_a);
_a = (STDatatype*)malloc(sizeof(STDatatype) * st._top);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(_a, st._a, sizeof(int) * sizeof(_top));
_top = st._top;
_capacity = st._capacity;
return *this;
}
//考虑如下情况:
int main()
{
Stack st1;
st1 = st1; //将自己赋值给自己会发生什么?如下图所示:
}
如图所示会发现,st1变成了随机值,为什么?
因为this指针和形参指向同一块空间,上来先将this指针所指向的空间释放,再去开辟空间进行拷贝,所以是随机值,存在越界问题
//我们对代码进行改善:
Stack& operator=(const Stack& st)
{
//首先对地址进行判断,如果不相等在执行原本逻辑
if(this != &st)
{
free(this->_a);
_a = (STDatatype*)malloc(sizeof(STDatatype) * st._top);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(_a, st._a, sizeof(int) * sizeof(_top));
_top = st._top;
_capacity = st._capacity;
}
//最后不管this == &st || this != &st,都正常返回*this即可
return *this;
}
重载流插入和流提取操作符
//我们有这样的使用场景:
int main()
{
int a = 10;
cout << a << endl; //内置类型的输出
//定义一个日期类对象:
Date d1;
cout << d1 << endl //该如何实现日期类对象的输出?
return 0;
}
//这时候我们需要重载流插入和流提取运算符:
//cin 是 istream 类型对象,cout 是 ostream 类型对象
int i = 1;
double d = 1.11;
int j;
cout << i; //流插入
cout << d;
cin >> j //流提取
/*为什么cout 和 cin 能自动识别类型?
本质是使用函数重载和运算符重载来实现
i、d是内置类型,cout、cin是自定义类型,只不过是由库来实现,默认支持内置类型;但是识别不了自定义类型,所以需要我们自行实现*/
//和之前一样,我们将流插入、流提取运算符定义在类中:
class Date
{
void operator<<(ostream& out)
{
out << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
}
int main()
{
Date d1;
cout << d1;
return 0;
}
//但是如果我们定义在类中,会面临如下图问题:
//所以我们将<< 和 >> 定义在全局
void operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}
注意:一般情况下全局函数不定义在头文件中,因为头文件如果被多处包含,编译的时候展开就会生成多处定义,会报链接错误。在学习C语言中我们就知道:同一个函数,可以多次声明 ,但是只能有一个定义,所以这里有两种解决方案:1、声明和定义分离 2、非要在头文件定义则可以加上static
//但是定义在全局,又会面临无法访问私有成员的问题,这里通过友元来解决:
//友元仅仅是打破了封装,类中声明普通函数加上friend,即可使用该函数访问私有成员
class Date
{
//在头文件的类中声明友元函数(可以在类中任意位置):
friend void operator<<(ostream& out, const Date& d);
friend void operator>>(istream& in, Date& d);
private:
int _year;
int _month;
int _day;
}
//在其他文件定义:
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 >>;
}
** 同样地我们还要支持链式流插入,流提取**
//我们还会遇到如下使用情形:
Date d1, d2;
cin >> d1 >> d2;
cout << d1 << d2 << endl;
//为了支持链式形式,我们需要定义函数的时候具有返回值:
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
//返回左值(左操作数):
return out;
}
instream& operator<<(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day >>;
return in;
}
const成员函数
class Date
{
public
void Print()
{
if ((_year >= 1)
&& (_month >= 1 && _month <= 12)
&& _day <= getMonthDay(_year, _month))
cout << _year << "/" << _month << "/" << _day << endl;
else
cout << "日期非法" << endl;
}
private:
int _year;
int _month;
int _day;
}
//或许会有这样的使用场景:
int main()
{
Date d1;
d1.Print();
const Date d2; //定义一个const对象
d2.Print();
return 0;
}
//但是这样的程序会报错,如下图所示:
//我们还原类中Print函数的真实原形(实际并不能这样写):
void Print(Date* const this) //假设还原出被隐藏的this指针
{
if ((_year >= 1)
&& (_month >= 1 && _month <= 12)
&& _day <= getMonthDay(_year, _month))
cout << _year << "/" << _month << "/" << _day << endl;
else
cout << "日期非法" << endl;
}
/*
this指针最开始被const修饰,仅仅只是保证了this指针的指向不会被恶意修改,保证了传入对象地址的准确性;但是*this并没有被const修饰,所以this指针指向的内容是允许被修改的,这就是this指针正常情况下可读可写的权限。而
当我们外部传入的对象其内容不可被修改时,正常this指针的权限就过大了,所以我们需要额外使用const来修饰*this
*/
//书写方法如下图:
//我们对比两个const成员函数的使用例子:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
this->_year = year;
_month = month;
_day = day;
}
//d1 > d2
bool operator>(const Date& d);
//d1 < d2
bool operator<(const Date& d);
//d1 - d2
int operator-(const Date& d)
{
Date longDay = *this, shortDay = d;
int flag = 1;
//这里接下来分为两种情况:
if (*this < d) //或者d > *this ?
{
longDay = d;
shortDay = *this;
flag = -1;
}
int count = 0;
while (longDay != shortDay)
{
++count;
++shortDay;
}
return count * flag;
}
void Print() const
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
private:
int _year;
int _month;
int _day;
};
所以为了解决这种权限放大情况,我们在重载函数后面加上const:
//这里是声明和定义分离
bool Date::operator>(const Date& d) const
{
//...
}
总结:凡是在成员函数内部不改变成员变量(即*this指向的内容),这些成员函数都应该加上const
构造函数与初始化列表
//我们知道对于日期类可以采用如下方法初始化对象:
class Date
{
public:
Date(int year, int month, int day)
{
this->_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
}
//接下来引入初始化列表:
class Date
{
public:
//对于初始化列表书写格式如下:
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
}
//栈的初始化列表如何写?
typedef int STDatatype;
class Stack
{
public:
Stack(int capacity = 4)
:_top(0)
, _capacity(capacity)
{
_a = (STDatatype*)malloc(sizeof(STDatatype) * capacity);
if (nullptr == _a)
{
perror("malloc fail");
exit(-1);
}
memset(_a, 0, sizeof(int) * capacity);
}
Stack(Stack& d)
{
_a = (STDatatype*)malloc(sizeof(STDatatype) * d._capacity);
memcpy(_a, d._a, sizeof(STDatatype) * d._capacity);
_top = d._top;
_capacity = d._capacity;
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDatatype* _a;
int _top;
int _capacity;
};
看到这里应该对初始化列表有初步认识,那么初始化列表到底有什么特别之处,或者不可缺少之处?
//必须使用初始化列表场景之一 :const成员变量,以如下一段代码为例:
//情况1:没有初始化列表,在构造函数体内赋值
class B
{
public:
B(int a, int ref)
{
_n = 10;
}
private:
const int _n;
};
//情况2:没有初始化列表,且构造函数体内不进行相关操作
class B
{
public:
B(int a, int ref)
{}
private:
const int _n;
};
//情况3:在声明时给定缺省值
class B
{
public:
B(int a, int ref)
{}
private:
const int _n = 10;
int m = 10;
};
//情况4:给定缺省值,并且给定初始化列表
class B
{
public:
B(int a, int ref)
:_n(5)
,_m(3)
{}
private:
const int _n = 10;
int _m = 10;
};
//这种情况下并不会采用缺省值
//必须使用初始化列表场景之二:当成员变量为自定义类型且该自定义类型没有合适的默认构造函数
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int ref)
:_n(a)
,_m(ref)
,_aa(3)
{}
private:
int _m = 10;
A _aa;
};
//必须使用初始化列表场景之三:成员变量为引用
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int ref)
:_n(a)
,_m(ref)
,_aa(3)
,_ref(ref);
{}
private:
int _m = 10;
A _aObject;
int& _ref; //成员变量为引用
};
//引用与const一样,唯一且必须在定义的时候初始化,所以这时候只能使用初始化列表
总结:引用,const,自定义类型成员(且该自定义类型成员没有合适的默认构造),必须使用初始化列表进行初始化,所以以后不管什么场景尽量使用初始化列表,且尽量对一个类提供全缺省默认构造函数
版权归原作者 Lin_zhiyouyu 所有, 如有侵权,请联系我们删除。