c++之类和对象详解 拷贝构造,赋值运算符重载
拷贝构造
那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
==拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用==
构造——初始化
拷贝构造——拷贝初始化
class Date { //.... }; int main() { Date d1(2000, 1, 1); Date d2(d1);//以d1的数据来初始化d2 return 0; }
拷贝构造特征
拷贝构造函数也是特殊的成员函数,其特征如下:
拷贝构造函数是构造函数的一个重载形式。
拷贝构造函数的参数只有一个且必须是==类类型对象的引用==,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
为什么会引发无穷递归引用呢?
class Date { public: Date(const Date& d) { cout << "拷贝构造成功!" << endl; _year = d._day; _month = d._month; _day = d._day; } Date(int year, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year = 0; int _month = 0; int _day = 0; }; void func1(Date d)//形参是实参的拷贝 { cout << "func1" << endl; } void func2(Date& d)//形参是实参的别名 { cout << "func2" << endl; } int main() { Date d1(2000, 1, 1); func1(d1); func2(d1); return 0; }
我们可以看到==传值引用导致产生的临时变量进行拷贝时候会引发拷贝构造==!
假如我们使用的是传值引用来当拷贝构造的参数:
拷贝构造函数接收值需要创建临时变量!创建临时变量引发拷贝构造,然后拷贝构造又需要创建临时变量去接收值,那么临时变量的临时变量又引发拷贝构造......以此不断的递归下去!
所以只能使用引用,因为引用是取别名实际用的仍然是那个变量!
为什么要使用拷贝构造这个东西呢?
因为自定义类型可能很复杂!编译器不清楚应该按什么方式拷贝!
像是内置类型编译器可以直接按字节一个个拷贝!
但是像是链表或者树呢?
像是链表我们就要一个个拷贝!
像是树我们就得递归拷贝!
先是拷贝根再拷贝左子树,然后拷贝右子树
然后一直递归下去!
那能不呢使用指针呢?答案是可以的!==因为指针的类型是date 产生的临时变量不会引发拷贝构造,但是这时候叫做构造函数,不是拷贝构造函数!==,==因为拷贝构造的定义就是参数类型要是类的引用!==但是那样子很不方便还要解引用等操作,不如使用引用!*
拷贝构造的注意
为了防止反向拷贝的情况发生我们一般都会加上const缩小权限!
Date(Date& d) { _year = d._day; _month = d._month; d._day = _day; }//没有加上const会导致反向拷贝使原先数据丢失! Date(const Date& d) { _year = d._day; _month = d._month; d._day = _day; }//这样的话一旦发生反向拷贝就会直接报错!
若未显式定义,编译器会生成==默认的拷贝构造函数==。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做==浅拷贝==,或者值拷贝。
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定 义类型是调用其拷贝构造函数完成拷贝的。
class Date { public: //Date(Date& d) //{ // _year = d._day; // _month = d._month; // _day = d._day; //} Date(int year, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year = 0; int _month = 0; int _day = 0; }; int main() { Date d1(2000, 1, 1); Date d2(d1); return 0; }
我们可以看到对于日期类这种只含内置类型的,编译器自动生成的默认拷贝构造函数已经够用了!
但是像是更复杂就会出问题
class stack { public: stack(int newcapcacity = 4) { int* temp = (int*)malloc(sizeof(int) * newcapcacity); if (temp == nullptr) { perror("malloc fail"); exit(-1); } _a = temp; _top = 0; _capacity = newcapcacity; } ~stack()//这就是栈的析构函数! { free(_a); _a = nullptr; _top = 0; _capacity = 0; } void Push(int x) { if (_top == _capacity) { int newcapacity = 2 * _capacity; int* temp = (int*)realloc(_a, sizeof(int) * newcapacity); if (temp == nullptr) { perror("realloc fail"); exit(-1); } _a = temp; _capacity = newcapacity; } _a[_top++] = x; } private: int* _a; int _top; int _capacity; }; int main() { stack st1; st1.Push(1); st1.Push(2); stack st2(st1); return 0; }
看上去好像是完成了拷贝!但是有没有发现st1和st2的指针指向了==同一块内存空间==,也就是意味着我 改变st1就会改变st2!
这个程序会导致崩溃!因为当st2的指针被析构函数释放掉之后,st1的析构函数就会导致访问野指针!
所以此时我们需要使用到深拷贝!
class stack { public: stack(int newcapcacity = 4) { int* temp = (int*)malloc(sizeof(int) * newcapcacity); if (temp == nullptr) { perror("malloc fail"); exit(-1); } _a = temp; _top = 0; _capacity = newcapcacity; } ~stack() { free(_a); _a = nullptr; _top = 0; _capacity = 0; } stack(stack& st)//深拷贝 { _a = (int*)malloc(sizeof(int) * st._capacity); if (_a == nullptr) { perror("malloc fail"); exit(-1); } memcpy(_a, st._a, st._top * sizeof(int)); _capacity = st._capacity; _top = st._top; } void Push(int x) { if (_top == _capacity) { int newcapacity = 2 * _capacity; int* temp = (int*)realloc(_a, sizeof(int) * newcapacity); if (temp == nullptr) { perror("realloc fail"); exit(-1); } _a = temp; _capacity = newcapacity; } _a[_top++] = x; } private: int* _a; int _top; int _capacity; }; int main() { stack st1; st1.Push(1); st1.Push(2); stack st2(st1); st2.Push(3); return 0; }
这下就成功完成了深拷贝
需要写析构函数的类的都要写拷贝构造!
不需要写析构的类都不需要自己写!
像是栈,链表,顺序表,二叉树....这些要动态开辟的都需要写析构函数,也都需要写拷贝构造!
但是像是日期类的这些就不需要!
class stack { //.... }; class MyQuene { public: void Push(int x) { _PushST.Push(x); } private: stack _PopST; stack _PushST; };
像是myqueue这种也都不需要写拷贝构造!因为它也不需要写析构函数!
拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
赋值运算符重载
class Date
{
public:
Date(Date& d)
{
_year = d._day;
_month = d._month;
_day = d._day;
}
Date(int year, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year = 0;
int _month = 0;
int _day = 0;
};
int main()
{
Date d1(2000, 1, 1);
Date d2(2000, 2, 28);
d1 > d2;
d1 == d2;
d1 + 100;
d1 - d2;
return 0;
}
像是上面的d1与d2我们如何简洁的进行比较呢?如何像是以前一样使用运算符?这时候就引入了运算符重载!
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
为了让自定义类型可以使用运算符!
函数名字为:关键字operator后面接需要重载的运算符符号。 函数原型:返回值类型 operator操作符(参数列表)
class Date { public: Date(Date& d) { _year = d._day; _month = d._month; _day = d._day; } Date(int year, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } bool operator==(const Date& d2) { return _year == d2._year && _day == d2._day && _month == d2._month; } private: int _year = 0; int _month = 0; int _day = 0; }; int main() { Date d1(2000, 1, 1); Date d2(2000, 2, 28); d1 == d2;//此时编译器会转换成调用d1.operator==(d2); d1.operator==(d2);//直接这样调用也行! //编译器很聪明如果是放在全局的那么会优先调用全局,如果全局没有就回去类里面找! //如果是全局就会被转换成operator==(d1,d2) cout << (d1 == d2) << endl;//要加括号因为运算符优先级问题,因为我们并没有重载<< 所以<< 没哟办法输出自定义类型! return 0; }
为什么运算符重载在要放在类里面呢?
因为如果我们放在外面会导致一个问题,那就是因为类里面的数据都是私有的!
我们无法访问,如果想要访问我们只能再写一个函数用于获取类里面的数据!
所以不如直接放在里面!
bool operator==(const Date& d1,const Date& d2) { return d1._year == d2._year && d1._day == d2._day && d1._month == d2._month; }//这样写会显示参数哦多为什么? //因为类里面的非静态成员函数都会加上一个this指针! //也就是说是实际上的样子为 bool operator==(cosnt* Date this,const Date& d1,const Date& d2) { return d1._year == d2._year && d1._day == d2._day && d1._month == d2._month; }//所以实际上我们只需要自己写一个参数就足够了! bool operator==(const Date& d2) { return _year == d2._year && _day == d2._day && _month == d2._month; }
赋值重载
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
};//这就是赋值重载
赋值运算符的写法注意
void operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
如果怎么写的话会出现一个问题就是在无法进行链式访问 d1 = d2 = d3;
但是也最好不要使用Date作为返回类型因为这样子会产生一个临时变量!
Date operator=(const Date& d) { _year = d._year; _month = d._month; _day = d._day; return *this; };
因为d在出了作用域后不会被销毁可以使用引用来作为返回值!
使用引用作为传参的类型 也是为了防止产生临时变量!const是为了防止对传参对象进行修改!
以引用作为返回值的时候要注意不可以传参对象作为返回值
Date& operator=(const Date& d) { _year = d._year; _month = d._month; _day = d._day; return d; }
这样就会出现经典的权限放大!
解决方法
cosnt Date& operator=(const Date& d) { _year = d._year; _month = d._month; _day = d._day; return d; } // or Date& operator=(Date& d) { _year = d._year; _month = d._month; _day = d._day; return d; }
但是这样也要求我们得使用const的类型去接收,我们一般要求变量都是可以修改的!
所以还是使用Date&
如果使用第二种修改方式这样也会导致我们无法对传参对象进行保护!
Date& operator=(Date& d) { d._year = _year;//万一写反了! _month = d._month; _day = d._day; return d; }
赋值重载的默认性
如果我们不写一个赋值重载,类中会自己生成一个赋值重载!
class Date { public: Date(int year = 10,int month = 10,int day = 10) { _year = year; _month = month; _day = day; } //Date& operator=(const Date& d) //{ // _year = d._year; // _month = d._month; // _day = d._day; // return *this; //} private: int _year; int _month; int _day; }; int main() { Date d1(100,10,100); Date d2; d2 = d1; return 0; }
默认的赋值重载会完成一次直拷贝!按字节一个个的拷贝过去!
==默认赋值重载和默认拷贝构造很相似==
- 对于内置类型都是进行值拷贝!
- 对于自定义类型都是调用自定义类型的默认成员函数!
默认赋值重载调用自定义类型的默认赋值重载
默认拷贝构造调用自定义类型的默认拷贝构造!
所以对于默认拷贝构造的问题也会出现在默认赋值重载上面
像是对于要动态开辟的类型 例如stack/queue/list.....
class stack { public: stack(int newcapcacity = 4) { int* temp = (int*)malloc(sizeof(int) * newcapcacity); if (temp == nullptr) { perror("malloc fail"); exit(-1); } _a = temp; _top = 0; _capacity = newcapcacity; } ~stack()//这就是栈的析构函数! { free(_a); _a = nullptr; _top = 0; _capacity = 0; } stack(stack& st) { _a = (int*)malloc(sizeof(int) * st._capacity); if (_a == nullptr) { perror("malloc fail"); exit(-1); } memcpy(_a, st._a, st._top * sizeof(int)); _capacity = st._capacity; _top = st._top; } void Push(int x) { if (_top == _capacity) { int newcapacity = 2 * _capacity; int* temp = (int*)realloc(_a, sizeof(int) * newcapacity); if (temp == nullptr) { perror("realloc fail"); exit(-1); } _a = temp; _capacity = newcapacity; } _a[_top++] = x; } private: int* _a; int _top; int _capacity; }; int main() { stack st1; st1.Push(1); st1.Push(2); stack st2; st2.Push(3); st2.Push(4); st1 = st2; return 0; }
==然后这个程序会发生崩溃!==
因为两个指针指向了同一块的内存地址!
st2的析构函数释放了这一块内存空间
然后st1的析构函数又一次的释放了这一块内存空间!
释放野指针导致了崩溃!
而且还会导致==内存泄漏!==
因为st的指针指向了st2的空间,但是却没有释放自己原有的空间!
所以要自己去写赋值重载!
stack& operator=(const stack& st) { free(_a); _a = (int*)malloc(sizeof(int) * st._capacity); if (_a == nullptr) { perror("malloc fail"); exit; } memcpy(_a, st._a, sizeof(int) * _top); _top = st._top; _capacity = st._capacity; return *this; }
这样子就可以完成对于栈这种类型的赋值
但是这样写还是有缺陷!比如遇到自己赋值给自己的时候!
我们会发现原本的值竟然变成的随机值!
因为我们一开始就free掉了原来的空间!所以导致了一旦自己赋值给自己的时候,一开始的free就会导致数据的丢失!
所以我们要进一步的修改
stack& operator=(stack& st) { if (this != &st) { _a = (int*)malloc(sizeof(int) * st._capacity); if (_a == nullptr) { perror("malloc fail"); exit(-1); } memcpy(_a, st._a, st._top * sizeof(int)); _capacity = st._capacity; _top = st._top; } return *this; }
为什么不使用realloc去改变原来的数组大小,反倒是使用先free再malloc的形式呢?
答:==因为要考虑的情况太多了,数组比原来大,数组比原来小,数组和原来相同==
数组比原来的大我们可以正常使用realloc。
数组比原来小,我们不使用realloc(realloc一般不用于缩小数组!)
但是如果不对数组进行缩小而是正常的进行拷贝,万一出现原先数组有100000个 赋值数组只有10个这种情况的话就会导致极大的空间浪费!
所以基于以上的几种情况我们选择采用先free在malloc的方式!
我们从上面的特征可以看出来拷贝构造和赋值重载具有很多的相似之处!
我们也可以得出一个相似的结论:
==需要写析构函数的就要写显性赋值重载,否则就不需要!==
像是类似myquene的情况因为不用谢析构所以自然也不用谢赋值重载都是显性的都够用了!
因为myqueue的默认赋值重载会自动的去调用stack类型的赋值重载来使用!
class stack { //.... } class MyQuene { public: void Push(int x) { _PushST.Push(x); } private: stack _PopST; stack _PushST; }; int main() { MyQuene q1; q1.Push(1); q1.Push(2); MyQuene q2; q2.Push(3); q2.Push(4); q1 = q2; return 0; }
赋值重载和拷贝赋值的区别在哪里?
class Date
{
public:
Date(int year = 10,int month = 10,int day = 10)
{
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(100,10,100);
Date d2(d1);//拷贝构造 是初始化另一个要马上创建的对象!
d2 = d1;//赋值重载(赋值拷贝!)已经存在的两个对象之间的拷贝!
Date d3 = d1;//这看上去好像是赋值重载!
//但是其实拷贝构造!因为从定义上看它更符合 初始化一个要创建的对象!
return 0;
}