目录
前言
这次没前言,直接看!
1.拷贝构造函数💨
如果需要使用一个对象去复制出来和它一样的对象时,此时就需要拷贝构造函数!
先给出本篇博客通篇使用的类例:
class Date
{
private:
int _year;
int _month;
int _day;
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
};
1.1拷贝构造函数的引入
拷贝构造函数是属于构造函数的一种,先给出其基本使用方法:
int main()
{
Date d1(2022, 10, 10);
Date d2(d1);
d2.Print();
}
由上可以看出,调用默认的拷贝构造函数:
类名 对象2 (对象1) 用对象1复制对象2
既然上述调用的是默认的拷贝构造函数,那么拷贝构造函数的显示定义应该是什么呢?
Date(const Date& d)
{
cout << "Date(const Date & d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
上述就是拷贝构造函数的显式定义,可以轻易的看出来:
拷贝构造函数是构造函数的重载,这一点在前面也提到过!
参数此时也只有一个,是本类的对象的引用并且用const修饰,那为什么要用引用的方式传参呢?
1.2拷贝构造函数参数的探究
C++要求拷贝构造函数必须是类对象的引用,那么传值传参为什么不可以呢?
当函数参数为类的对象时,在调用的时候需要将实参完整送给形参,也就是建立1个实参的拷贝,此时就需要按实参复制一个形参,就需要调用拷贝构造函数!
同上述一样,传值传参时,形参是实参的一份临时拷贝,需要调用拷贝构造函数,此时也就造成了上图的这种无限递归的问题(无限套娃🤔)!
所以,当显式实现拷贝构造函数的时候传引用也就得以解释了!而使用const修饰的原因无他,就是保护参数值不被修改
以上,就是拷贝构造函数参数问题,当然编译器比较高级的话,传值传参压根编译不起来!
1.3何时应该定义拷贝构造函数
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
下面给出简易栈类的各成员函数:
如果此类中没有显式的拷贝构造函数的话:
class Stack
{
private:
int* _arr;
int _top;
int _capacity;
public:
Stack(int capacity = 4)
{
_arr = (int*)malloc(sizeof(int) * capacity);
if (_arr == NULL)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
~Stack()
{
free(_arr);
_arr = NULL;
_top = _capacity = 0;
}
void Push(int x)
{
_arr[_top++] = x;
}
void Display()
{
for (int i = 0; i < _top; i++)
{
cout << _arr[i] << " ";
}
cout << endl;
}
};
//....
//用d1去复制d2,调用默认生成的拷贝构造函数!
Stack d2(d1);
此时会产生以上这种问题,当函数结束后调用析构函数时就会触发断点,引起中断!
所以如果要避免这种情况,就应该自己写一个拷贝构造函数:
class Stack
{
private:
int* _arr;
int _top;
int _capacity;
public:
Stack(int capacity = 4)
{
_arr = (int*)malloc(sizeof(int) * capacity);
if (_arr == NULL)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
~Stack()
{
cout << "~Stack" << endl;
free(_arr);
_arr = NULL;
_top = _capacity = 0;
}
Stack(const Stack& s)
{
cout << "Stack(const Stack & s)" << endl;
_arr = (int*)malloc(sizeof(int) * s._capacity);
if (_arr == NULL)
{
perror("malloc fail");
exit(-1);
}
memcpy(_arr, s._arr, sizeof(int) * s._top);
_top = s._top;
_capacity = s._capacity;
}
void Push(int x)
{
_arr[_top++] = x;
}
void Display()
{
for (int i = 0; i < _top; i++)
{
cout << _arr[i] << " ";
}
cout << endl;
}
};
因此如果存在生成资源的话,就需要我们自己生成拷贝构造函数,从而避免free一块空间多次的情况!最后,在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的!
1.4构造函数的调用情形
- 程序中建立1个新的对象,并用另1个同类的对象对他初始化。
- 当函数参数为类的对象时,如果是传值传参,实参需要拷贝1份临时对象给形参,此时需要调用拷贝构造函数
- 函数返回值是类的对象,在函数调用完毕时将返回值带回函数调用处时,此时需要将函数中的对象复制1个临时对象并传给函数调用处,此时仍然需要调用拷贝构造函数!
2.浅谈运算符重载
2.1什么是运算符重载
运算符重载和函数重载的含义是相同的。所谓重载,就是重新赋予含义。函数重载是对一个已有的函数赋予新的含义,使之可以实现新的功能;同样的,运算符重载也应是如此,使1个运算符拥有操纵多种数据类型的能力!
运算符重载的方法就是定义1个重载运算符的函数,使指定的运算符不仅能实现原有的功能,而且能实现函数中指定的功能。也就是说运算符重载是通过定义函数实现的,其本质就是函数的重载!
重载运算符的格式如下:
函数类型 operator 运算符名称(形参表)
{
//......
}
紧接着上面的日期类,我们来实现判断两个日期类型的对象的相等:
bool Date::operator==(const Date& d)
{
return (this->_year == d._year)
&& (this->_month == d._month)
&& (this->_day == d._day);
}
函数名由operator和运算符组成
Date d1;
Date d2;
if(d1==d2)
{
//.....
}
其调用过程就是以d2为实参调用d1的运算符重载函数operator,然后判断2个类型是否相等!
其实上述判断相等的情况我们可以使用1个函数去判断,那么使用运算符重载意义在哪呢?
使用运算符重载对于用户是更加友好的,便于用户去阅读、编写、维护!在使用过程中,只有该类型重载了+ - * / == != 等运算符,就可以直接去使用,而不用关心函数内部如何实现,也不用进行参数的传递!
❗❗❗
需要注意的是,运算符重载后其原有的功能依旧得以保留,没有丧失和改变,前面提到运算符重载其实本质上就是函数重载,根据运算符前后的数据类型的不同,编译器会自动调用识别的!
2.2重载运算符的特性
不能重载的运算符:
.(成员访问运算符)
*(成员指针访问运算符)
::(域运算符)
sizeof(长度运算符)
?:(条件运算符)
前两个运算符不能重载的原因是为了保证访问成员的功能不被改变,域运算符和sizeof运算符的运算对象是类型不是变量或一般表达式因此也不能重载!
- 重载运算符的参数必须有1个类类型的参数。
- 用于内置类型的运算符,其含义不能改变。
- 作为类成员函数重载时,其形参看起来比操作数的数目少1,因为第1个参数是隐藏的this指针
t4. 重载不能改变运算符的操作数的个数
2.3运算符重载的意义
本来,c++提供的运算符只能用于C++的内置类型,但C++的重要基础就是类和对象,如果C++的运算无法运用于类和对象的话,那么类和对象就会受很大的限制!
为了解决这个问题,使类和对象有更加强大的生命力,C++采取的方法不是为类对象另外定义一批运算符,而是允许对运算符也进行重载,通过运算符的重载,扩大了C++已有运算符的作用范围!从而很方便的使用新的数据类型,例如复数的加减乘除等运算!
3.赋值运算符重载
赋值运算符的重载对于类也是十分有必要的,同之前一样,当我们并不显示定义的话,编译器会自动生成一个默认的赋值运算符重载!
3.1赋值运算符重载的基本特性
下面给出日期类的运算符重载:
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
对于日期类的赋值运算符重载是有如下的几个问题的:
- 参数传const &是提高效率,避免调用拷贝构造函数,并且保护数据不被修改;
- 带有返回值的原因是为了应对连续赋值的,而传引用返回同样是为了减少调用拷贝构造函数提高效率;
- if条件判断的目的是防止多个相同的对象自身连续赋值对数据进行错误操作!(这一点会在下面论述)
3.2赋值重载的进一步探究
下面存在Stack类:
class Stack
{
private:
int* _arr;
int _top;
int _capacity;
public:
Stack(int capacity = 4)
{
free(_arr);
_arr = (int*)malloc(sizeof(int) * capacity);
if (_arr == NULL)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
Stack& operator=(const Stack& s)
{
_arr=s._arr;
_top = s._top;
_capacity = s._capacity;
return *this;
}
~Stack()
{
free(_arr);
_arr = NULL;
_top = _capacity = 0;
}
};
上述代码存在什么问题呢?
毫无疑问是会触发端点的,如图所示,当调用赋值重载的时候2个arr数组一模一样,在函数结束后,调用析构函数会对arr析构两次,从而触发断点!并且_arr存储的那块空间也找不到了,造成了内存泄漏!
所以为了修正以上这种情况该运算符重载就变成了:
Stack& operator=(const Stack& s)
{
if (this != &s)
{
free(_arr);
_arr = (int*)malloc(s._capacity * sizeof(int));
if (_arr == NULL)
{
perror("malloc fail");
exit(-1);
}
memcpy(_arr, s._arr, sizeof(int) * s._top);
_arr = s._arr;
_top = s._top;
_capacity = s._capacity;
}
}
如果将if条件判断屏蔽掉的话,此时按main函数运行的话,就会出现一下情况:
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
s1.Display();
s1 = s1;
s1.Display();
}
所以前面的第三点也就得以解释了!
3.3关于赋值重载的补充
1.赋值运算符只能重载成类的成员函数,不能重载成全局函数!这是因为类中的赋值重载不显式定义的话,编译器会产生一个默认的,这就和全局的赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数!
2.当用户没有显式实现时,编译器会生成1个默认的赋值运算符重载,以值的方式逐字节拷贝;对于内置类型是直接赋值的,而自定义类型成员变量需要调用对应的赋值运算符进行重载完成赋值!
4.拷贝构造函数和赋值重载的区别
- 如果不主动编写拷贝构造函数和赋值重载函数,编译器会自动生成,并且以"按成员拷贝"的方式自动生成相应的默认函数,如果类中的数据成员含有指针成员或者引用成员的时候,默认的函数可能就会引起错误!
- 拷贝构造函数是在对象创建的时候用一个已经存在的对象去对该对象初始化时调用的;而赋值重载是使用定义出来的对象,用一个已经存在的对象赋值给另一个已经存在的对象,使得两个对象具有相同的状态,总之对象的赋值是已经定义了的对象,而对象的拷贝是从无到有建立1个对象!
Ending
关于类和对象的拷贝构造函数和赋值重载就介绍完了,这部分的内容也是相对而言比较枯燥的,因为其中要点确实琐碎,还是应该写出来多调试几遍!