在讲解这个概念之前我先明确一下这边文章中的几个基本概念;当时写文章时因为这几个概念混淆了,导致大部分内容重写。
复制构造函数及调用复制构造函数的三种情况:
class A{ int x; public: A(int a):x(a) { }//构造函数 A(const A& a1) { }//复制构造函数 }; A Func(A a2) { }//1、类的对象作为形参,调用复制构造函数 A Func1() {A a3(4);return a3;}//2、类的对象作为函数的返回值,调用复制构造函数 int main(){ A a(1),a1(a);//3、用类的对象a初始化类的另一个对象a1,调用类的复制构造函数 }
浅拷贝(浅复制):将对象a内存中的数据按照二进制位(Bit)复制到对象b所在的内存(a和b指向同一内存空间)
深拷贝(深复制):将对象a的所有成员变量拷贝给新对象b,并为对象b分配一块新的内存,并将原有对象所持有的内存也拷贝过来(a和b指向不同内存空间)
运算符”=“如果没有被重载,默认执行的是浅拷贝;如果要让运算符”=“执行深拷贝,需要自己重载运算符。
标准格式
class A{
public:
A & operator = (line & b);
//返回值时引用,与类的复制(拷贝)构造函数有关
};
返回值为空类型
class A{
public:
void operator = (line & b);
//返回值为void类型
//当使用两个或两个以上“=”时,有时会类型不符
};
对象间的连续赋值
A a, b, c;
a=b=c;
//a=b=c等价于a=(b=c);
为了实现连等,要将返回值定义为该类的类型。
当执行到return *this;时,会调用赋值构造函数;若为定义复制构造函数,就会调用默认复制构造函数。但默认复制构造函数是浅拷贝,当类中有指针时就会出错。若不用引用做返回时,就必须自定义深拷贝构造函数。
接下来这个例子必须要重载运算符,且重载运算符时必须要使用引用。
#include
using namespace std;
class Myclass {
char* str;
public:
Myclass(const char* str1 = "default string") {
//构造函数,在对象生成时使用,用来初始化对象
str = new char[strlen(str1) + 1];//给成员变量str用new分配一片空间
strcpy(str, str1);//将参数值复制到成员对象str中
cout << "constructor called" << endl;
}
Myclass(const Myclass& a) {
cout << "copy construct" << endl;
}
~Myclass() {//析构函数,在对象消亡时使用
cout << "destrustor called" << endl;
}
void showChar() {//成员函数,用来显示该对象
cout << str << endl;
}
Myclass operator=(const Myclass& ele) {//重载“=”运算符
delete[] str;//str原来的内存空间大小可能不合适,需要释放掉重新分配
str = new char[strlen(ele.str) + 1];//给str重新分配空间
strcpy(str, ele.str);
return *this;//返回调用该函数的对象本身
}
};
int main() {
Myclass class1("string1"), class2;
class2 = class1;//由于“=”被重载了,故等价于:class2.operator=(class1)
// 若未重载“=”运算符;遇到对象用对象给对象赋值时,会调用复制构造函数
class1.showChar(); class2.showChar();
return 0;
}
/*
输出结果:
constructor called
constructor called
string1
string1
destrustor called
destrustor called
*/
由于未重载“=”运算符,故执行class2 = class1;时,“=”运算符执行的是浅拷贝的功能,class1和class2的str都指向同一内存空间。
当调用析构函数时,先调用class2的析构函数,把str所指向的内存给清除;然后调用class1的析构函数,此时就会对同一片内存delete两次(class2的析构函数已经清除过一次该内存),出现存泄漏的情况。
若参数不用引用,则有Myclass &operator=(const Myclass ele) { }
输出结果:
constructor called
constructor called
destrustor called
string1
string1
destrustor called
destrustor called
调用Myclass &operator=(const Myclass ele) { }是调用复制构造函数的一种情况:类的对象作为形参
实参传值给形参会创建一个临时变量,调用复制构造函数将实参复制给形参(此处进行的也是浅拷贝),实参形参的str都指向同一块内存。
当赋值函数执行完时,会清除函数的临时对象所占的内存;此时执行class1.showchar()访问的是刚被delete掉的内存,会发现内存泄漏。
备注:一片内存空间被delete后,其上存放的内容并不被立刻抹去,只是这块内存可以被再分配给其他。以下这段程序说明内存被delete掉后指针a1仍然可以指向原来的内存空间,此时的指针a1被称为野指针。
int main() { int *a1 = new int(3); cout << *a1 << endl;//输出:01464E40,3 delete a1; cout << *a1 << endl;//输出:01464E40,-572662307 //此结果和有些文章不同,建议自己动手运行一下 //有些文章说再delete语句后加a1=NULL或a1=nullptr可将a1的值清除掉,的确是这样,但是程序会报错并中止运行 return 0; }
重复delete一片空间危害:当new出来的内存空间a被delete掉后可以被再次分配,这片内存空间可以被再分配;此时b要申请一片内存空间,系统恰好将刚释放掉的a的空间分配给了他;如果后面的程序使用b的内容之前,由于重复delete这片内存空间,因此后面的程序运行出错。(虽然代码短了不会出现这种情况,但架不住代码几万行代码运行时会出现这种问题)
对已经被delete掉的指针我们不能对其进行任何操作,不过有争议的是delete之后是否要将指针置空(即ptr=NULL)。
参考文章https://cloud.tencent.com/developer/article/1879316
若参数使用普通引用,则有Myclass &operator=(Myclass& ele) { }
由于形参是引用,所以被调用的函数中执行时用的就是实参本身;由于引用不是const引用,所以一旦形参中的操作涉及到更改形参只,那么实参值也会被更改。
因此要使用const引用,禁止被调用的函数时,使得实参的值被改变。
若返回值不是引用,则有
Myclass operator=(const Myclass& ele)
{ ………………………………
return *this;//返回引用该函数的对象本身(对象本身类型为Myclass)
}
/*
返回值非引用的输出:
constructor called
constructor called
copy construct
destrustor called
string1
string1
destrustor called
destrustor called
……………………………………………………
返回值为引用的输出
constructor called
constructor called
string1
string1
destrustor called
destrustor called
*/
返回值为return *this;其作用是返回引用该函数的对象本身,即返回类的对象。
返回值为类的对象是调用复制构造函数的右一种情况:若返回值的类型不为引用,则返回对象时会创建一个临时对象,再调用复制构造函数,将返回的对象复制给临时对象,二者指向同一内存空间;函数结束后,临时对象被清除,其内存空间也被delete掉了;故calss2=class1的str指向一块随机内存,在调用class2.showchar()时会随机输出。
返回值为引用时,从输出结果可以看出并不生成临时对象,也就不存在上述问题;函数返回值就是调用该函数的对象本身的别名;同时使用引用返回的参数可以作为左值。
总结:
主要是为了防止浅拷贝(指向同一片内存空间,delete时会出问题)、调用复制构造函数和析构函数(多出来的操作都是对时间的消耗,令效率打折)
如有错误请在评论区指出,谢谢。