介绍拷贝赋值运算符,先简单说下重载运算符的知识。重载运算符本质依然是函数的重载,重载运算符的函数名由operator关键字和运算符的符号组成,和其他函数类似,可以有形参和返回值。
重载运算符函数的参数表示运算符的运算对象,因为拷贝赋值运算符必须定义为成员函数,所以,拷贝赋值运算符的左侧运算对象的地址必须绑定到this,右侧对象作为参数初始化重载运算符函数的形参。为了和内置类型的赋值保持一致,拷贝赋值运算符通常返回一个左侧运算对象的引用(返回自身的引用,更新自身的值)
注意:如果一个类中有的成员是const的,那么编译器将不会提供默认生成的拷贝赋值运算符,因为不能对const对象重新赋值
此外,如果一个类中有引用成员,那么编译器也不会提供默认生成的构造函数,见博客https://blog.csdn.net/Master_Cui/article/details/106885137
class hasptr
{
public:
hasptr();
hasptr(const string &s);
hasptr(const hasptr &t);
hasptr &operator=(const hasptr &rval);
~hasptr();
string *ps;
int i;
};
hasptr::hasptr()
:ps(nullptr),
i(0)
{
cout<<__func__<<endl;
}
hasptr::hasptr(const string &s)
:ps(new string(s)),
i(0)
{
cout<<"hasptr(const string &s)"<<endl;
}
hasptr::hasptr(const hasptr &t)
{
cout<<"hasptr(const hasptr &t)"<<endl;
delete this->ps;
this->ps=new string(*t.ps);
this->i=t.i;
}
hasptr &hasptr::operator=(const hasptr &rval)
{
cout<<__func__<<endl;
delete this->ps;
this->ps=new string(*rval.ps);//和拷贝构造函数一样,要防止两个指针指向同一个对象
this->i=rval.i;
return *this;
}
hasptr::~hasptr()
{
cout<<__func__<<endl;
if (ps) {
delete ps;
ps=nullptr;
}
}
int main(int argc, char const *argv[])
{
hasptr hp("1234");
hasptr hp1;
hp1=hp;
cout<<*hp1.ps<<endl;
return 0;
}
与拷贝构造函数一样,如果自己没有实现,那么编译器会自动生成,自动生成的拷贝赋值运算符函数的功能和拷贝构造函数的功能也是类似的,将右侧对象的每个非static成员赋值给左侧对象的对应成员。如果成员有数组,那么会把数组中的元素逐个赋值到左侧对象的数组中。如果数组元素是自定义的类,那么还会调用自定义类的拷贝赋值运算符
hasptr &hasptr::operator=(const hasptr &rval)
{
cout<<__func__<<endl;
this->ps=rval.ps;
this->i=rval.i;
return *this;
}
如果hasptr不自定义拷贝赋值运算符,那么上述代码就相当于是编译器生成的拷贝赋值运算符
二、析构函数简述
1.概念
析构函数与构造函数的操作相反,构造函数初始化对象的每个非static的成员变量,而析构函数释放对象,并销毁对象的每个非static成员。析构函数的函数名是一个波浪号接类名,不接受形参,无返回值。因为析构函数无参,所以不能重载,类的析构函数只有一个。hasptr中的hasptr::~hasptr()就是析构函数
hasptr::~hasptr()
{
cout<<__func__<<endl;
if (ps) {
delete ps;
ps=nullptr;
}
}
2.析构函数都干了啥
构造函数有初始化部分和函数体,析构函数也有析构部分和函数体,构造函数的初始化部分在函数体之前执行,并按照成员在类中出现的顺序进行初始化。在析构函数中则相反,首先执行函数体,然后按照初始化顺序的逆序销毁成员。析构函数中的析构部分是看不见的,成员销毁时做啥完全依赖成员的类型,对于类类型成员,销毁时调用对应类的析构函数,内置类型没有析构函数,只是释放内存而已。所以,析构函数的函数体本身并不销毁成员,成员的销毁是在函数体之后的析构阶段隐式执行的
智能指针是类类型,有析构函数,所以智能指针成员在析构阶段会自动被销毁,而普通指针是内置类型,只会销毁指针本身,对于指针指向的对象,需要在函数体中显示delete。所以,推荐使用智能指针
3.析构函数的调用时机
只要对象被销毁,就会调用析构函数。对象被销毁有以下几种情况:1、对象离开作用域时。2、动态分配的对象被delete时。3、创建的临时对象的表达式结束时
示例
int main(int argc, char const *argv[])
{
hasptr hp("1234");
cout<<"-----------"<<endl;
vector<hasptr> vhp(2, hp);
cout<<"-----------"<<endl;
hasptr *hpp=new hasptr();
delete hpp;
cout<<"-----------"<<endl;
vhp.push_back(hasptr("temp"));
cout<<"-----------"<<endl;
return 0;
}
上述代码先创建了一个hp,然后创建了一个hasptr的容器,在容器中用hp对两个对象进行拷贝初始化,接着创建一个hasptr的指针并delete,然后想容器中push_back了一个临时对象,调用了一次构造函数和一次拷贝构造函数。但是此时vector重新分配内存了,将容器内的两个对象拷贝到新的内存区域并销毁原来的对象,因此有调用了两次拷贝构造函数和两次析构函数,当push_back表达式结束后,临时对象被销毁,又调用了一次析构函数。最后主函数作用域结束,释放四个hasptr对象和一个容器,又调用了四次hasptr的析构函数和vector<hasptr>的析构函数
4.编译器默认生成的析构函数
和拷贝构造函数。拷贝赋值运算符一样,如果没有自己实现析构函数,编译器也会自动生成,自动生成的析构函数的函数体是空,只是执行成员的销毁工作。以hasptr为例,编译器默认生成的析构函数的代码如下
hasptr::~hasptr() {}
三、何时自定义拷贝构造函数、拷贝赋值函数和析构函数?
如果一个类的成员中有指针,那么一定要自定义拷贝构造函数和拷贝赋值函数。而类的成员指针有可能指向动态对象,所以此时可能也需要自定义析构函数(因为有些类的对象只进行赋值或拷贝);但是如果一个类需要自定义析构函数,那么,说明,需要析构函数的函数体来释放动态对象,那么一定需要自定义拷贝构造函数和拷贝赋值函数来避免两个指针指向同一个对象
四、=default
可以用=default来显示地要求编译器生成默认的构造函数、拷贝构造函数、拷贝赋值函数和析构函数
示例
class test5
{
public:
test5()=default;
~test5()=default;
test5(const test5 &t)=default
test5 &operator=(const test5 &t);
};
test5 &test5::operator=(const test5 &t)=default;
如果在类内使用=default,那么默认的构造函数、拷贝构造函数、拷贝赋值函数和析构函数就是内联的,如果不希望默认的构造函数、拷贝构造函数、拷贝赋值函数和析构函数是内联的,那么需要在类外使用=default,代码如第9行
注意:=default只能对编译器生成的默认版本的成员函数使用
对于某些类来说,不需要拷贝和赋值操作,比如像博客https://blog.csdn.net/Master_Cui/article/details/107309990中IO类。C++中阻止类的对象进行拷贝或者赋值有两种方法:
1.将拷贝构造函数和拷贝赋值运算符的访问权限设置为private
class test6
{
public:
test6() {cout<<__func__<<endl;}
~test6() {cout<<__func__<<endl;}
void func1()
{
test6 t, t1;
test6 t2=t;
t1=t;
}
private:
test6(const test6 &t) {cout<<"test6(const test6 &t)"<<endl;}
test6 &operator=(const test6 &t) {cout<<__func__<<endl; return *this;}
};
int main(int argc, char const *argv[])
{
test6 t, t1;
t.func1();
return 0;
}
上述代码的输出结果如下
如果将func1的内容移动到主函数中,则无法通过编译
因为拷贝构造函数和拷贝赋值运算符是private,所以,访问范围被限制在类内,而在主函数中调用了拷贝构造函数和拷贝赋值运算符,所以无法编译通过,从而阻止了类test6的拷贝构造和赋值运算
如果想类test6的拷贝构造和赋值运算操作不能在类内或者友元中被调用,那么可以将类test6的拷贝构造和赋值运算函数声明在private区域,但是不定义,声明但不定义一个成员函数是合法的,但前提是声明但不定义的成员函数不能被调用,否则会引起链接错误,通过链接错误,防止了类test6的拷贝构造和赋值运算操作在类内或者友元中被调用
class test6
{
public:
test6() {cout<<__func__<<endl;}
~test6() {cout<<__func__<<endl;}
void func1()
{
test6 t, t1;
test6 t2=t;
t1=t;
}
private:
test6(const test6 &t);
test6 &operator=(const test6 &t);
};
int main(int argc, char const *argv[])
{
test6 t, t1;
t.func1();
return 0;
}
2.将拷贝构造函数和拷贝赋值运算符定义为删除的函数(=delete)
示例
class test7
{
public:
test7() {cout<<__func__<<endl;}
~test7() {cout<<__func__<<endl;}
private:
test7(const test7 &t)=delete;
test7 &operator=(const test7 &t)=delete;
};
int main(int argc, char const *argv[])
{
test7 t, t1;
return 0;
}
=delete的作用相比private拷贝构造函数和拷贝赋值运算符更强,=delete的明确告知编译器不定义对应的成员函数,与=default不同的是,=delete可以作用于任何成员函数
析构函数不能是删除的,如果析构函数是删除的,那么创建出来的对象离开作用域时将无法销毁,所以,如果析构函数是删除的,编译器将不允许定义该类的对象,如果一个类的成员的析构函数是删除的,那么也无法定义该类的对象,因为成员无法被销毁,自然整个对象也无法被销毁。虽然如果析构函数是删除的类的对象不能被定义,但是可以通过new动态创建
参考
《C++ Primer》
欢迎大家评论交流,作者水平有限,如有错误,欢迎指出