一、拷贝构造函数
1、概念:
拷贝构造函数就是通过拷贝一个已经存在对象去初始化另一个对象;
2、特点:
(1)拷贝构造函数是构造函数的一个重载函数;
(2)拷贝构造函数可以有多个参数,但是第一个参数必须是类类型的引用;
(3)编译器自动生成的默认拷贝构造函数对被拷贝对象地内置类型进行浅拷贝(值拷贝)(一个一个字节地拷贝)给拷贝对象,对自定义类型则调用它自己的拷贝构造函数;
(4)当被拷贝对象中的成员函数中有申请资源的时,需要我们自己写拷贝构造函数,否则会有对同一块地址析构两次的问题;当拷贝对象中没有申请资源的属性时,可以不写拷贝构造函数,编译器的默认拷贝构造函数会进行浅拷贝。
3、代码示例:
(1)
#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;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);//写法一
Date d3 = d1;//写法二
}
拷贝构造函数和构造函数构成重载;成员函数默认有一个this指针,类中定义的拷贝构造函数的第一个参数是指向d2或者d3的指针(隐式),定义中的等式左边的成员变量都是this指针解引用了的;
对d1进行拷贝,这里虽然都是不申请资源的内置类型,但是我们自己写了拷贝构造函数,观察拷贝结果
当我们不写拷贝构造函数时,观察编译器的浅拷贝:
(2)
当把拷贝构造函数形参写成传值传参时,会出现无限递归的问题;
原因:
首先C++中规定传值传参时,会调用拷贝构造;
那么这样写拷贝构造函数时:
所以拷贝构造函数的形参要写自身类类型的引用。
(3) 当类中有申请资源的属性时,要写拷贝构造函数;
若是不写,代码示例:
class Stack
{
public:
Stack(int n = 4)//传参构造
{
_a = (int*)malloc(sizeof(int) * n);
if (_a == nullptr)
{
perror("malloc failed");
exit(-1);
}
_top = 0;
_capacity = n;
}
//析构函数//编译器默认的会对内置类型不做处理,所以属性中含资源申请的类一定要我们自己写,否则内存泄露
~Stack()
{
cout << "~Stack()" << endl;//表示调用了析构函数
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
}
//拷贝构造函数
//Stack(const Stack& s)
//{
// //有申请资源的所以要自己写
// _a = (int*)malloc(sizeof(int) * s._capacity);//给拷贝对象申请和被拷贝对象不一样的空间资源
// if (_a == nullptr)
// {
// perror("malloc failed");
// exit(-1);
// }
// //再让它们申请资源的空间存储的值相同
// memcpy(_a, s._a, sizeof(int) * s._top);
// //不申请资源的内置类型的拷贝
// _top = s._top;
// _capacity = s._capacity;
//}
//栈的插入
void Push(int x)
{
if (_top == _capacity)
{
int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
int* tmp = (int*)malloc(sizeof(int) * newcapacity);
if (tmp == nullptr)
{
perror("malloc failed");
return;
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top++] = x;
}
//数据打印
void Print()
{
for (int i = 0; i < _top; i++)
{
cout << _a[i] << " ";
}
cout << endl;
}
//private:
int* _a;
int _top;
int _capacity;
};
此时去拷贝构造一个对象,观察它们之间的差异:
int main()
{
Stack s1;
Stack s2(s1);
return 0;
}
调试:
看到 编译器的默认构造函数的浅拷贝(一个字节一个字节拷贝):s1和s2中的_top和_capacity是相同,这没有问题;但是_a是相同的就有问题了;
首先,程序结束,对象生命周期结束时,析构函数发挥作用,此时先析构s2,那么会把s2中的_a释放并置为空;再析构s1,会把s1中的_a也释放,置为空。但是s1、s2的_a是相同的地址,那么第二次析构时,会把空释放,这是不允许的;
其次,我们看到s1和s2插入数据之后_a存储的数据是怎样的:
int main()
{
Stack s1;
s1.Push(1);
Stack s2(s1);
s1.Push(2);
for (int i = 0; i < 4; i++)
{
cout << s1._a[i] << " ";
}
cout << endl;
for (int i = 0; i < 4; i++)
{
cout << s2._a[i] << " ";
}
return 0;
}
首先构造s1,并且插入数据1,再用s1去初始化s2,那么此时的s1、s2中存储的数据应该都是1,并且_top也都是1;再对s1插入数据2,s1会变化是当然的,s1中的_top也加1;但是因为s1和s2拷贝构造是浅拷贝,它们的_a是相同的,那么s2中的_a也会插入数据2(不符合预期);但是_top不变:并没有进入s2的插入函数。
后面再对s2插入数据,是在s2的_top为一的位置插入的,因为s1、s2 _a相同,那么s1中_top为1的位置也会是这个新插入的数据,那么s1原来的数据就被覆盖了。
所以当有申请资源的成员变量时,要我们自己写拷贝构造函数;
代码示例:
Stack(const Stack& s)
{
//有申请资源的所以要自己写
_a = (int*)malloc(sizeof(int) * s._capacity);//给拷贝对象申请和被拷贝对象不一样的空间资源
if (_a == nullptr)
{
perror("malloc failed");
exit(-1);
}
//再让它们申请资源的空间存储的值相同
memcpy(_a, s._a, sizeof(int) * s._top);
//不申请资源的内置类型的拷贝
_top = s._top;
_capacity = s._capacity;
}
拷贝的对象的成员变量_a应该和被拷贝对象的_a的类型大小都想同,至于地址要不同,开辟完成之后,在这个地址上面存储的数据也要相同,这里使用memcpy,把数据一个字节一个字节的拷贝过去。
二、运算符重载
1、定义和基本特点:
当运算符作用于类类型时,C++允许我们对运算符进行重载,对其赋予新的意义;当我们直接使用运算符去操作类类型时,编译器会报错,这个时候要使用运算符重载;
运算符重载是一个函数,它具有特殊的名称,它的名称是由operator加上普通的运算符构成的,和其他函数一样,这个函数具有类型和返回值以及参数;但是运算符重载这个函数规定必须有一个参数是类类型的参数;
运算符重载的运算规则和普通的运算符一样,在运算符被重载之后,它的优先级和结合性和内置类型的运算符保持一致;
不能重载语法中没有的运算符来创建新的运算符,比如:operator@;
有五个运算符不能被重载: .* :: sizeof ?: .
2、代码示例:
1、将运算符重载函数定义在类里面:
定义在类里面,因为成员函数第一个参数是指向类类型的this指针,那么操作数有两个时,只需要传一个,操作数为一个时,不需要传参。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
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(2024, 8, 10);
Date d2 = d1;
bool ret1 = d1.operator==(d2);//调用方式一
if (ret1 == true) cout << "日期相同" << endl;
else cout << "日期不同" << endl;
Date d3(2024, 8, 9);
bool ret2 = d1 == d3;//调用方式二
if (ret2 == true) cout << "日期相同" << endl;
else cout << "日期不同" << endl;
return 0;
}
2、将运算符重载定义在类外面:
重载在类外面,传参要传全,因为类外面的函数没有this指针;
若是涉及到类里的成员变量,还要使得类里面的成员函数可以被访问到;方法:
(1)成员变量公有化;
(2)在类里面添加函数,返回成员变量的值;
(3)将运算符重载函数就定义在类里面;
(4)友元函数。
一般来说,都是将其定义在类里面。
另外:后置加加和前置加加重载的区别是后置加加要在传参时多传一个0,并且在函数定义时,给形参加上int ,前置加加常规操作;
class A
{
public:
A(int x = 1, int y = 1)
{
_x = x;
_y = y;
}
//后置加加
A operator++(int)
{
A tmp=*this;
_x ++;
_y ++;
return tmp;//返回值是原本的,但是接收返回值之后原本的要加1
}
//前置加加
A& operator++()
{
_x++;
_y++;
return *this;
}
private:
int _x;
int _y;
};
int main()
{
A a;
//后置++
A x=a.operator++(0);//x的值是最开始的a,a的值加一
//前置++
A b;
A y=b.operator++();//y和b的值相同
return 0;
}
3、赋值重载:
赋值重载就是把一个已经存在的类赋值给另一个存在的类;
赋值重载函数的类型建议写成const加上类加上&,返回值类型写成类加&,这样可以减少拷贝次数,提高效率;
赋值重载和拷贝构造函数很相似;编译器默认的赋值重载函数对内置类型会进行浅拷贝,对自定义类型调用它自己的赋值重载函数;
当成员变量中有申请资源的时,我们要自己开辟,否则浅拷贝会出问题;
//拷贝构造函数
class Date
{
public:
//构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//赋值重载函数
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
void Print()
{
cout << _year <<'/'<<_month<<'/'<<_day<< endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(1,1,1);
d1.Print();
Date d2(2,2,2);
d1.operator=(d2);
d1.Print();
Date d3(3, 3, 3);
d1 = d3;
d1.Print();
}
它的调用方式和运算符重载的调用方式一样;
d1分别被赋值d2、d3之后的值发生变化。